Skip to content
12 changes: 8 additions & 4 deletions docs/setup/local-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ func main() {

session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"})
response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"})
if d, ok := response.Data.(*copilot.AssistantMessageData); ok {
fmt.Println(d.Content)
if response != nil {
if d, ok := response.Data.(*copilot.AssistantMessageData); ok {
fmt.Println(d.Content)
}
}
}
```
Expand All @@ -117,8 +119,10 @@ defer client.Stop()

session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"})
response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"})
if d, ok := response.Data.(*copilot.AssistantMessageData); ok {
fmt.Println(d.Content)
if response != nil {
if d, ok := response.Data.(*copilot.AssistantMessageData); ok {
fmt.Println(d.Content)
}
}
```

Expand Down
85 changes: 85 additions & 0 deletions nodejs/test/python-codegen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { JSONSchema7 } from "json-schema";
import { describe, expect, it } from "vitest";

import { generatePythonSessionEventsCode } from "../../scripts/codegen/python.ts";

describe("python session event codegen", () => {
it("maps special schema formats to the expected Python types", () => {
const schema: JSONSchema7 = {
definitions: {
SessionEvent: {
anyOf: [
{
type: "object",
required: ["type", "data"],
properties: {
type: { const: "session.synthetic" },
data: {
type: "object",
required: [
"at",
"identifier",
"duration",
"integerDuration",
"uri",
"pattern",
"payload",
"encoded",
"count",
],
properties: {
at: { type: "string", format: "date-time" },
identifier: { type: "string", format: "uuid" },
duration: { type: "number", format: "duration" },
integerDuration: { type: "integer", format: "duration" },
optionalDuration: {
type: ["number", "null"],
format: "duration",
},
action: {
type: "string",
enum: ["store", "vote"],
default: "store",
},
summary: { type: "string", default: "" },
uri: { type: "string", format: "uri" },
pattern: { type: "string", format: "regex" },
payload: { type: "string", format: "byte" },
encoded: { type: "string", contentEncoding: "base64" },
count: { type: "integer" },
},
},
},
},
],
},
},
};

const code = generatePythonSessionEventsCode(schema);

expect(code).toContain("from datetime import datetime, timedelta");
expect(code).toContain("at: datetime");
expect(code).toContain("identifier: UUID");
expect(code).toContain("duration: timedelta");
expect(code).toContain("integer_duration: timedelta");
expect(code).toContain("optional_duration: timedelta | None = None");
expect(code).toContain('duration = from_timedelta(obj.get("duration"))');
expect(code).toContain('result["duration"] = to_timedelta(self.duration)');
expect(code).toContain(
'result["integerDuration"] = to_timedelta_int(self.integer_duration)'
);
expect(code).toContain("def to_timedelta_int(x: timedelta) -> int:");
expect(code).toContain(
'action = from_union([from_none, lambda x: parse_enum(SessionSyntheticDataAction, x)], obj.get("action", "store"))'
);
expect(code).toContain(
'summary = from_union([from_none, lambda x: from_str(x)], obj.get("summary", ""))'
);
expect(code).toContain("uri: str");
expect(code).toContain("pattern: str");
expect(code).toContain("payload: str");
expect(code).toContain("encoded: str");
expect(code).toContain("count: int");
});
});
61 changes: 39 additions & 22 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ python chat.py

```python
import asyncio

from copilot import CopilotClient
from copilot.session import PermissionHandler
from copilot.generated.session_events import AssistantMessageData, SessionIdleData

async def main():
# Client automatically starts on enter and cleans up on exit
Expand All @@ -37,10 +38,11 @@ async def main():
done = asyncio.Event()

def on_event(event):
if event.type.value == "assistant.message":
print(event.data.content)
elif event.type.value == "session.idle":
done.set()
match event.data:
case AssistantMessageData() as data:
print(data.content)
case SessionIdleData():
done.set()

session.on(on_event)

Expand All @@ -57,7 +59,9 @@ If you need more control over the lifecycle, you can call `start()`, `stop()`, a

```python
import asyncio

from copilot import CopilotClient
from copilot.generated.session_events import AssistantMessageData, SessionIdleData
from copilot.session import PermissionHandler

async def main():
Expand All @@ -73,10 +77,11 @@ async def main():
done = asyncio.Event()

def on_event(event):
if event.type.value == "assistant.message":
print(event.data.content)
elif event.type.value == "session.idle":
done.set()
match event.data:
case AssistantMessageData() as data:
print(data.content)
case SessionIdleData():
done.set()

session.on(on_event)
await session.send("What is 2+2?")
Expand Down Expand Up @@ -333,7 +338,15 @@ Enable streaming to receive assistant response chunks as they're generated:

```python
import asyncio

from copilot import CopilotClient
from copilot.generated.session_events import (
AssistantMessageData,
AssistantMessageDeltaData,
AssistantReasoningData,
AssistantReasoningDeltaData,
SessionIdleData,
)
from copilot.session import PermissionHandler

async def main():
Expand All @@ -347,24 +360,24 @@ async def main():
done = asyncio.Event()

def on_event(event):
match event.type.value:
case "assistant.message_delta":
match event.data:
case AssistantMessageDeltaData() as data:
# Streaming message chunk - print incrementally
delta = event.data.delta_content or ""
delta = data.delta_content or ""
print(delta, end="", flush=True)
case "assistant.reasoning_delta":
case AssistantReasoningDeltaData() as data:
# Streaming reasoning chunk (if model supports reasoning)
delta = event.data.delta_content or ""
delta = data.delta_content or ""
print(delta, end="", flush=True)
case "assistant.message":
case AssistantMessageData() as data:
# Final message - complete content
print("\n--- Final message ---")
print(event.data.content)
case "assistant.reasoning":
print(data.content)
case AssistantReasoningData() as data:
# Final reasoning content (if model supports reasoning)
print("--- Reasoning ---")
print(event.data.content)
case "session.idle":
print(data.content)
case SessionIdleData():
# Session finished processing
done.set()

Expand Down Expand Up @@ -545,9 +558,11 @@ Provide your own function to inspect each request and apply custom logic (sync o

```python
from copilot.session import PermissionRequestResult
from copilot.generated.session_events import PermissionRequest
from copilot.generated.session_events import PermissionRequestedDataPermissionRequest

def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult:
def on_permission_request(
request: PermissionRequestedDataPermissionRequest, invocation: dict
) -> PermissionRequestResult:
# request.kind — what type of operation is being requested:
# "shell" — executing a shell command
# "write" — writing or editing a file
Expand Down Expand Up @@ -577,7 +592,9 @@ session = await client.create_session(
Async handlers are also supported:

```python
async def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult:
async def on_permission_request(
request: PermissionRequestedDataPermissionRequest, invocation: dict
) -> PermissionRequestResult:
# Simulate an async approval check (e.g., prompting a user over a network)
await asyncio.sleep(0)
return PermissionRequestResult(kind="approved")
Expand Down
8 changes: 6 additions & 2 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@
ServerRpc,
register_client_session_api_handlers,
)
from .generated.session_events import PermissionRequest, SessionEvent, session_event_from_dict
from .generated.session_events import (
PermissionRequestedDataPermissionRequest,
SessionEvent,
session_event_from_dict,
)
from .session import (
CommandDefinition,
CopilotSession,
Expand Down Expand Up @@ -2635,7 +2639,7 @@ async def _handle_permission_request_v2(self, params: dict) -> dict:
raise ValueError(f"unknown session {session_id}")

try:
perm_request = PermissionRequest.from_dict(permission_request)
perm_request = PermissionRequestedDataPermissionRequest.from_dict(permission_request)
result = await session._handle_permission_request(perm_request)
if result.kind == "no-result":
raise ValueError(NO_RESULT_PERMISSION_V2_ERROR)
Expand Down
Loading
Loading