Skip to content

Commit 617f57d

Browse files
szabta89Copilot
andcommitted
docs: add session limits guide and event coverage
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 64dcd25 commit 617f57d

6 files changed

Lines changed: 374 additions & 0 deletions

File tree

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Guides for building with the SDK's capabilities.
4646
* [MCP Servers](./features/mcp.md): integrate Model Context Protocol servers
4747
* [Skills](./features/skills.md): load reusable prompt modules
4848
* [Plugin Directories](./features/plugin-directories.md): bundle skills, hooks, MCP servers, and agents as a single loadable plugin
49+
* [Session limits](./features/session-limits.md): set an AI Credits budget for a session
4950
* [Image Input](./features/image-input.md): send images as attachments
5051
* [Streaming Events](./features/streaming-events.md): real-time event reference
5152
* [Steering & Queueing](./features/steering-and-queueing.md): message delivery modes

docs/features/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ These guides cover the capabilities you can add to your Copilot SDK application.
1515
| [MCP Servers](./mcp.md) | Integrate Model Context Protocol servers for external tool access |
1616
| [Skills](./skills.md) | Load reusable prompt modules from directories |
1717
| [Plugin Directories](./plugin-directories.md) | Bundle skills, hooks, MCP servers, and agents as a single loadable plugin |
18+
| [Session limits](./session-limits.md) | Set an AI Credits budget for a session and observe budget events |
1819
| [Image Input](./image-input.md) | Send images to sessions as attachments |
1920
| [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) |
2021
| [Steering & Queueing](./steering-and-queueing.md) | Control message delivery—immediate steering vs. sequential queueing |

docs/features/session-limits.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Session limits
2+
3+
Session limits let an application set an AI Credits budget for a Copilot session. Use `sessionLimits` when creating or resuming a session to cap the current accounting window.
4+
5+
## Configure a session limit
6+
7+
Set `maxAiCredits` to the maximum AI Credits the session can consume in its current accounting window. The SDK forwards this value to the Copilot CLI when it creates or resumes the session.
8+
9+
<details open>
10+
<summary><strong>TypeScript</strong></summary>
11+
12+
<!-- docs-validate: skip -->
13+
14+
```typescript
15+
const session = await client.createSession({
16+
onPermissionRequest: approveAll,
17+
sessionLimits: {
18+
maxAiCredits: 30,
19+
},
20+
});
21+
22+
const resumed = await client.resumeSession(session.sessionId, {
23+
onPermissionRequest: approveAll,
24+
sessionLimits: {
25+
maxAiCredits: 30,
26+
},
27+
});
28+
```
29+
30+
</details>
31+
<details>
32+
<summary><strong>Python</strong></summary>
33+
34+
<!-- docs-validate: skip -->
35+
36+
```python
37+
session = await client.create_session(
38+
on_permission_request=PermissionHandler.approve_all,
39+
session_limits={
40+
"max_ai_credits": 30,
41+
},
42+
)
43+
44+
resumed = await client.resume_session(
45+
session.session_id,
46+
on_permission_request=PermissionHandler.approve_all,
47+
session_limits={
48+
"max_ai_credits": 30,
49+
},
50+
)
51+
```
52+
53+
</details>
54+
<details>
55+
<summary><strong>Go</strong></summary>
56+
57+
<!-- docs-validate: skip -->
58+
59+
```go
60+
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
61+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
62+
SessionLimits: &rpc.SessionLimitsConfig{
63+
MaxAiCredits: copilot.Float64(30),
64+
},
65+
})
66+
67+
resumed, err := client.ResumeSessionWithOptions(ctx, session.SessionID, &copilot.ResumeSessionConfig{
68+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
69+
SessionLimits: &rpc.SessionLimitsConfig{
70+
MaxAiCredits: copilot.Float64(30),
71+
},
72+
})
73+
```
74+
75+
</details>
76+
<details>
77+
<summary><strong>.NET</strong></summary>
78+
79+
<!-- docs-validate: skip -->
80+
81+
```csharp
82+
var session = await client.CreateSessionAsync(new SessionConfig
83+
{
84+
OnPermissionRequest = PermissionHandler.ApproveAll,
85+
SessionLimits = new SessionLimitsConfig
86+
{
87+
MaxAiCredits = 30,
88+
},
89+
});
90+
91+
var resumed = await client.ResumeSessionAsync(session.SessionId, new ResumeSessionConfig
92+
{
93+
OnPermissionRequest = PermissionHandler.ApproveAll,
94+
SessionLimits = new SessionLimitsConfig
95+
{
96+
MaxAiCredits = 30,
97+
},
98+
});
99+
```
100+
101+
</details>
102+
<details>
103+
<summary><strong>Java</strong></summary>
104+
105+
<!-- docs-validate: skip -->
106+
107+
```java
108+
CopilotSession session = client
109+
.createSession(new SessionConfig()
110+
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
111+
.setSessionLimits(new SessionLimitsConfig(30.0)))
112+
.get();
113+
114+
CopilotSession resumed = client
115+
.resumeSession(session.getSessionId(), new ResumeSessionConfig()
116+
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
117+
.setSessionLimits(new SessionLimitsConfig(30.0)))
118+
.get();
119+
```
120+
121+
</details>
122+
<details>
123+
<summary><strong>Rust</strong></summary>
124+
125+
<!-- docs-validate: skip -->
126+
127+
```rust
128+
let limits = SessionLimitsConfig {
129+
max_ai_credits: Some(30.0),
130+
};
131+
132+
let session = client
133+
.create_session(
134+
SessionConfig::new()
135+
.approve_all_permissions()
136+
.with_session_limits(limits.clone()),
137+
)
138+
.await?;
139+
140+
let resumed = client
141+
.resume_session(
142+
ResumeSessionConfig::new(session.id().clone())
143+
.approve_all_permissions()
144+
.with_session_limits(limits),
145+
)
146+
.await?;
147+
```
148+
149+
</details>
150+
151+
## Observe budget events
152+
153+
Applications can subscribe to session events to update UI when the limit changes or the session exhausts its budget.
154+
155+
| Event type | When it is emitted | Important fields |
156+
|---|---|---|
157+
| `session.session_limits_changed` | Active session limits changed. A `null` `sessionLimits` value means no limits are active. | `sessionLimits.maxAiCredits?` |
158+
| `session.usage_checkpoint` | The runtime records durable aggregate usage for resume and accounting. | `totalNanoAiu`, `totalPremiumRequests?` |
159+
| `session_limits_exhausted.requested` | The session exhausted its configured budget and needs a user decision before continuing. | `requestId`, `maxAiCredits`, `usedAiCredits` |
160+
| `session_limits_exhausted.completed` | The exhausted-limit prompt was resolved. | `requestId`, `response.action`, `response.additionalAiCredits?`, `response.maxAiCredits?` |
161+
162+
Use the generated event types for the SDK language you are using. For example, TypeScript narrows by `event.type`:
163+
164+
```typescript
165+
session.on((event) => {
166+
if (event.type === "session_limits_exhausted.requested") {
167+
showBudgetDialog({
168+
requestId: event.data.requestId,
169+
maxAiCredits: event.data.maxAiCredits,
170+
usedAiCredits: event.data.usedAiCredits,
171+
});
172+
}
173+
});
174+
```
175+
176+
## Current limitations
177+
178+
The SDKs expose session limits when creating or resuming a session. They do not currently expose a high-level convenience API for changing or clearing limits on an already-running session. To apply a different limit through the public SDK surface, create or resume a session with a new `sessionLimits` value.

docs/features/streaming-events.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,24 @@ Ephemeral. Context window utilization snapshot.
472472
| `currentTokens` | `number` || Current tokens in the context window |
473473
| `messagesLength` | `number` || Current message count in the conversation |
474474

475+
### `session.session_limits_changed`
476+
477+
Session limits changed for the current accounting window. A `null` `sessionLimits` value means no limits are active.
478+
479+
| Data Field | Type | Required | Description |
480+
|------------|------|----------|-------------|
481+
| `sessionLimits` | `SessionLimitsConfig \| null` || Current session limits, or `null` when no limits are active |
482+
| `sessionLimits.maxAiCredits` | `number` | | Maximum AI Credits allowed across the session's current accounting window |
483+
484+
### `session.usage_checkpoint`
485+
486+
Durable aggregate usage checkpoint used to reconstruct accounting when a session is resumed.
487+
488+
| Data Field | Type | Required | Description |
489+
|------------|------|----------|-------------|
490+
| `totalNanoAiu` | `number` || Session-wide accumulated nano-AI units cost at checkpoint time |
491+
| `totalPremiumRequests` | `number` | | Total number of premium API requests used at checkpoint time |
492+
475493
### `session.task_complete`
476494

477495
The agent has completed its assigned task.
@@ -721,6 +739,27 @@ Ephemeral. A queued command was resolved.
721739
|------------|------|----------|-------------|
722740
| `requestId` | `string` || Matches the corresponding `command.queued` |
723741

742+
### `session_limits_exhausted.requested`
743+
744+
Ephemeral. The current session budget was exhausted and the runtime needs a user decision before continuing.
745+
746+
| Data Field | Type | Required | Description |
747+
|------------|------|----------|-------------|
748+
| `requestId` | `string` || Use this ID when responding to the pending exhausted-limit request |
749+
| `maxAiCredits` | `number` || Configured max AI Credits for the current accounting window |
750+
| `usedAiCredits` | `number` || AI Credits already consumed in the current accounting window |
751+
752+
### `session_limits_exhausted.completed`
753+
754+
Ephemeral. A pending exhausted-limit request was resolved.
755+
756+
| Data Field | Type | Required | Description |
757+
|------------|------|----------|-------------|
758+
| `requestId` | `string` || Matches the corresponding `session_limits_exhausted.requested` event |
759+
| `response.action` | `"add" \| "set" \| "unset" \| "cancel"` || Action selected for the exhausted-limit request |
760+
| `response.additionalAiCredits` | `number` | | AI Credits to add to the current max when `response.action` is `"add"` |
761+
| `response.maxAiCredits` | `number` | | New absolute max AI Credits when `response.action` is `"set"` |
762+
724763
## Quick reference: agentic turn flow
725764

726765
A typical agentic turn emits events in this order:
@@ -773,6 +812,8 @@ session.idle → Ready for next message (ephemeral)
773812
| `session.title_changed` || Session | `title` |
774813
| `session.context_changed` | | Session | `cwd`, `gitRoot?`, `repository?`, `branch?` |
775814
| `session.usage_info` || Session | `tokenLimit`, `currentTokens`, `messagesLength` |
815+
| `session.session_limits_changed` | | Session | `sessionLimits` |
816+
| `session.usage_checkpoint` | | Session | `totalNanoAiu`, `totalPremiumRequests?` |
776817
| `session.task_complete` | | Session | `summary?` |
777818
| `session.shutdown` | | Session | `shutdownType`, `codeChanges`, `modelMetrics` |
778819
| `permission.requested` || Permission | `requestId`, `permissionRequest` |
@@ -794,5 +835,7 @@ session.idle → Ready for next message (ephemeral)
794835
| `external_tool.completed` || External Tool | `requestId` |
795836
| `command.queued` || Command | `requestId`, `command` |
796837
| `command.completed` || Command | `requestId` |
838+
| `session_limits_exhausted.requested` || Session | `requestId`, `maxAiCredits`, `usedAiCredits` |
839+
| `session_limits_exhausted.completed` || Session | `requestId`, `response.action` |
797840
| `exit_plan_mode.requested` || Plan Mode | `requestId`, `summary`, `planContent`, `actions` |
798841
| `exit_plan_mode.completed` || Plan Mode | `requestId` |

go/session_event_serialization_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,90 @@ func TestSessionEventTypeDerivedFromData(t *testing.T) {
7676
}
7777
}
7878

79+
func TestSessionLimitsEventsRoundTrip(t *testing.T) {
80+
var changed SessionEvent
81+
if err := json.Unmarshal([]byte(`{
82+
"id": "00000000-0000-0000-0000-000000000003",
83+
"timestamp": "2026-01-01T00:00:00Z",
84+
"parentId": null,
85+
"type": "session.session_limits_changed",
86+
"data": {
87+
"sessionLimits": {
88+
"maxAiCredits": 30
89+
}
90+
}
91+
}`), &changed); err != nil {
92+
t.Fatalf("failed to unmarshal session limits changed event: %v", err)
93+
}
94+
95+
changedData, ok := changed.Data.(*SessionSessionLimitsChangedData)
96+
if !ok {
97+
t.Fatalf("expected session limits changed data, got %T", changed.Data)
98+
}
99+
if changed.Type() != SessionEventTypeSessionSessionLimitsChanged {
100+
t.Fatalf("expected session limits changed type, got %q", changed.Type())
101+
}
102+
if changedData.SessionLimits == nil || changedData.SessionLimits.MaxAiCredits == nil || *changedData.SessionLimits.MaxAiCredits != 30 {
103+
t.Fatalf("expected maxAiCredits to round-trip, got %#v", changedData.SessionLimits)
104+
}
105+
106+
var exhausted SessionEvent
107+
if err := json.Unmarshal([]byte(`{
108+
"id": "00000000-0000-0000-0000-000000000004",
109+
"timestamp": "2026-01-01T00:00:01Z",
110+
"parentId": "00000000-0000-0000-0000-000000000003",
111+
"ephemeral": true,
112+
"type": "session_limits_exhausted.requested",
113+
"data": {
114+
"requestId": "limit-request-1",
115+
"maxAiCredits": 30,
116+
"usedAiCredits": 31
117+
}
118+
}`), &exhausted); err != nil {
119+
t.Fatalf("failed to unmarshal session limits exhausted event: %v", err)
120+
}
121+
122+
exhaustedData, ok := exhausted.Data.(*SessionLimitsExhaustedRequestedData)
123+
if !ok {
124+
t.Fatalf("expected session limits exhausted requested data, got %T", exhausted.Data)
125+
}
126+
if exhausted.Type() != SessionEventTypeSessionLimitsExhaustedRequested {
127+
t.Fatalf("expected session limits exhausted requested type, got %q", exhausted.Type())
128+
}
129+
if exhaustedData.RequestID != "limit-request-1" || exhaustedData.UsedAiCredits <= exhaustedData.MaxAiCredits {
130+
t.Fatalf("unexpected exhausted limit payload: %#v", exhaustedData)
131+
}
132+
133+
var completed SessionEvent
134+
if err := json.Unmarshal([]byte(`{
135+
"id": "00000000-0000-0000-0000-000000000005",
136+
"timestamp": "2026-01-01T00:00:02Z",
137+
"parentId": "00000000-0000-0000-0000-000000000004",
138+
"ephemeral": true,
139+
"type": "session_limits_exhausted.completed",
140+
"data": {
141+
"requestId": "limit-request-1",
142+
"response": {
143+
"action": "add",
144+
"additionalAiCredits": 10
145+
}
146+
}
147+
}`), &completed); err != nil {
148+
t.Fatalf("failed to unmarshal session limits exhausted completion event: %v", err)
149+
}
150+
151+
completedData, ok := completed.Data.(*SessionLimitsExhaustedCompletedData)
152+
if !ok {
153+
t.Fatalf("expected session limits exhausted completed data, got %T", completed.Data)
154+
}
155+
if completed.Type() != SessionEventTypeSessionLimitsExhaustedCompleted {
156+
t.Fatalf("expected session limits exhausted completed type, got %q", completed.Type())
157+
}
158+
if completedData.Response.Action != SessionLimitsExhaustedResponseActionAdd || completedData.Response.AdditionalAiCredits == nil || *completedData.Response.AdditionalAiCredits != 10 {
159+
t.Fatalf("unexpected session limit completion payload: %#v", completedData.Response)
160+
}
161+
}
162+
79163
func TestSessionEventAgentIDRoundTripsUnknownEvent(t *testing.T) {
80164
var event SessionEvent
81165
if err := json.Unmarshal([]byte(`{

0 commit comments

Comments
 (0)