@zvada/agent-server 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +317 -0
- package/README.md +34 -4
- package/docs/consuming.md +269 -0
- package/docs/deploy.md +80 -0
- package/docs/harnesses.md +64 -0
- package/docs/rfds/0001-deterministic-echo-ids.md +44 -0
- package/package.json +23 -3
- package/src/client/client.ts +153 -49
- package/src/core/agents/acp/acp-agent.ts +9 -0
- package/src/core/agents/acp/mappings.ts +3 -3
- package/src/core/agents/base.ts +35 -5
- package/src/core/agents/claude-code/adapter.ts +116 -26
- package/src/core/agents/claude-code/claude-agent.ts +44 -7
- package/src/core/agents/claude-code/generator-session.ts +53 -14
- package/src/core/agents/claude-code/options.ts +13 -3
- package/src/core/agents/claude-code/session-manager.ts +9 -4
- package/src/core/agents/codex-app-server/codex-app-server-agent.ts +25 -6
- package/src/core/agents/codex-sdk/codex-sdk-agent.ts +3 -3
- package/src/core/agents/types.ts +1 -1
- package/src/core/diagnostics.ts +59 -0
- package/src/core/index.ts +6 -2
- package/src/core/presets.ts +15 -2
- package/src/core/provision/pins.ts +5 -1
- package/src/core/proxy/anthropic-proxy.ts +76 -3
- package/src/core/runtime/agent-runtime.ts +215 -28
- package/src/core/runtime/event-processor.ts +51 -26
- package/src/core/utils/errors.ts +37 -3
- package/src/protocol/config.ts +8 -6
- package/src/{core/agents/error-classifier.ts → protocol/errors.ts} +33 -7
- package/src/protocol/factories.ts +106 -10
- package/src/protocol/guards.ts +53 -0
- package/src/protocol/index.ts +10 -0
- package/src/protocol/lifecycle.ts +289 -112
- package/src/protocol/meta.ts +14 -0
- package/src/protocol/part-input.ts +56 -7
- package/src/protocol/parts.ts +125 -10
- package/src/protocol/reduce.ts +749 -0
- package/src/protocol/selectors.ts +162 -0
- package/src/protocol/seq-cursor.ts +87 -0
- package/src/protocol/stop-reasons.ts +45 -0
- package/src/protocol/time.ts +23 -0
- package/src/protocol/tokens.ts +23 -0
- package/src/protocol/tool-state.ts +85 -25
- package/src/protocol/verify.ts +440 -0
- package/src/protocol/vocabulary.ts +18 -0
- package/src/protocol/wire.ts +103 -7
- package/src/server/acp/binding.ts +23 -2
- package/src/server/acp/translate.ts +51 -14
- package/src/server/agent-server.ts +109 -6
|
@@ -1,28 +1,56 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ErrorCategorySchema, ErrorInfoSchema } from "./errors.ts";
|
|
3
|
+
import { isKnownLifecycleEventType } from "./guards.ts";
|
|
2
4
|
import { AgentHarnessSchema } from "./harness.ts";
|
|
3
|
-
import {
|
|
5
|
+
import { MetaSchema } from "./meta.ts";
|
|
6
|
+
import { type Part, PartSchema, type UnknownPart, decodePart } from "./parts.ts";
|
|
7
|
+
import { EpochMsSchema } from "./time.ts";
|
|
4
8
|
import { TokenUsageSchema } from "./tokens.ts";
|
|
5
9
|
import { ToolKindSchema, ToolLocationSchema } from "./tool-state.ts";
|
|
10
|
+
import { openVocabulary } from "./vocabulary.ts";
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* The normalized event stream every harness is funneled into. Consumers (a WS
|
|
9
14
|
* bridge, an SSE endpoint, a CLI, a test sink) subscribe to these and never see
|
|
10
15
|
* raw SDK payloads. All keys are camelCase; serialize at your transport edge.
|
|
16
|
+
* Every event carries `sessionId`, `timestamp` (epoch ms), and `_meta`.
|
|
11
17
|
*
|
|
12
18
|
* Ordering within a turn:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
19
|
+
* turn.started
|
|
20
|
+
* -> [ USER ECHO: message.started{role:"user", outputIndex:0}
|
|
21
|
+
* -> message.part* -> message.ended ]
|
|
22
|
+
* -> ( message.started{role:"assistant"} -> message.part*
|
|
23
|
+
* / message.part.delta* -> message.ended )*
|
|
24
|
+
* -> turn.ended
|
|
25
|
+
* session.created lands WHEN THE HARNESS FIRST REPORTS its native session id
|
|
26
|
+
* — its first yield — so arriving MID-TURN, after the echo and before the
|
|
27
|
+
* first assistant message, is the normal case rather than an anomaly. It
|
|
28
|
+
* cannot be hoisted: the id does not exist until the harness produces it,
|
|
29
|
+
* and holding the echo back for it would delay the transcript of every turn
|
|
30
|
+
* on a subprocess spawn. Persist it whenever it arrives; never treat it as a
|
|
31
|
+
* turn preamble.
|
|
32
|
+
* session.compaction may appear anywhere within a turn (position = first
|
|
33
|
+
* appearance). permission.* and raw interleave anywhere within a turn.
|
|
34
|
+
* session.ended may follow at any later point.
|
|
15
35
|
*
|
|
16
|
-
*
|
|
17
|
-
* - a `message.part` may arrive after its message ended
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
36
|
+
* Exceptions to strict message bracketing:
|
|
37
|
+
* - a `message.part` may arrive after its message (or turn) ended — a tool
|
|
38
|
+
* completing late. The event's `messageId` names the ORIGINAL message and
|
|
39
|
+
* the upsert lands there.
|
|
40
|
+
* - snapshots are authoritative over deltas: a content-bearing `message.part`
|
|
41
|
+
* wipes the delta accumulator for that part; later deltas append to the
|
|
42
|
+
* replacement. A non-terminal snapshot must not carry partially-accumulated
|
|
43
|
+
* streamed content (field-ownership rule).
|
|
21
44
|
*/
|
|
22
45
|
|
|
23
46
|
/**
|
|
24
|
-
* Why a turn stopped (ACP
|
|
47
|
+
* Why a turn stopped (ACP StopReason verbatim, plus `error`). Normalized
|
|
25
48
|
* terminal status; the raw provider string travels in `finishReason`.
|
|
49
|
+
*
|
|
50
|
+
* OPEN (Law 3): `_`-prefixed values are adapter extensions (e.g. Claude's
|
|
51
|
+
* error_max_budget_usd -> `_max_budget`); unknown non-`_` values are reserved
|
|
52
|
+
* for this protocol and MUST be preserved. `refusal` covers provider
|
|
53
|
+
* content-filter/safety stops.
|
|
26
54
|
*/
|
|
27
55
|
export const STOP_REASONS = [
|
|
28
56
|
"end_turn",
|
|
@@ -32,8 +60,25 @@ export const STOP_REASONS = [
|
|
|
32
60
|
"cancelled",
|
|
33
61
|
"error",
|
|
34
62
|
] as const;
|
|
35
|
-
export type StopReason = (typeof STOP_REASONS)[number];
|
|
36
|
-
export const StopReasonSchema =
|
|
63
|
+
export type StopReason = (typeof STOP_REASONS)[number] | (string & {});
|
|
64
|
+
export const StopReasonSchema = openVocabulary<StopReason>();
|
|
65
|
+
|
|
66
|
+
/** Why a harness session ended. OPEN. */
|
|
67
|
+
export const SESSION_END_REASONS = ["idle", "replaced", "released", "shutdown"] as const;
|
|
68
|
+
export type SessionEndReason = (typeof SESSION_END_REASONS)[number] | (string & {});
|
|
69
|
+
export const SessionEndReasonSchema = openVocabulary<SessionEndReason>();
|
|
70
|
+
|
|
71
|
+
/** Compaction entity status. OPEN. */
|
|
72
|
+
export const COMPACTION_STATUSES = ["in_progress", "completed", "failed", "cancelled"] as const;
|
|
73
|
+
export type CompactionStatus = (typeof COMPACTION_STATUSES)[number] | (string & {});
|
|
74
|
+
export const CompactionStatusSchema = openVocabulary<CompactionStatus>();
|
|
75
|
+
|
|
76
|
+
/** What caused a compaction (Claude: `auto` | `manual`). OPEN. */
|
|
77
|
+
export const COMPACTION_TRIGGERS = ["auto", "manual"] as const;
|
|
78
|
+
export type CompactionTrigger = (typeof COMPACTION_TRIGGERS)[number] | (string & {});
|
|
79
|
+
export const CompactionTriggerSchema = openVocabulary<CompactionTrigger>();
|
|
80
|
+
|
|
81
|
+
// ---- session events ---------------------------------------------------------
|
|
37
82
|
|
|
38
83
|
/** Emitted once the harness-native session id is known — persist it to resume later. */
|
|
39
84
|
export const SessionCreatedEventSchema = z.object({
|
|
@@ -51,22 +96,110 @@ export const SessionCreatedEventSchema = z.object({
|
|
|
51
96
|
* successful resume (Claude).
|
|
52
97
|
*/
|
|
53
98
|
resumed: z.boolean().optional(),
|
|
54
|
-
timestamp:
|
|
99
|
+
timestamp: EpochMsSchema,
|
|
100
|
+
_meta: MetaSchema,
|
|
55
101
|
});
|
|
56
102
|
export type SessionCreatedEvent = z.infer<typeof SessionCreatedEventSchema>;
|
|
57
103
|
|
|
104
|
+
/** The harness session is over (idle timeout, replacement, release, shutdown). */
|
|
105
|
+
export const SessionEndedEventSchema = z.object({
|
|
106
|
+
type: z.literal("session.ended"),
|
|
107
|
+
sessionId: z.string(),
|
|
108
|
+
reason: SessionEndReasonSchema,
|
|
109
|
+
timestamp: EpochMsSchema,
|
|
110
|
+
_meta: MetaSchema,
|
|
111
|
+
});
|
|
112
|
+
export type SessionEndedEvent = z.infer<typeof SessionEndedEventSchema>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Context-window gauge, emitted whenever the harness reports fresh numbers
|
|
116
|
+
* (per model message on Claude, per token-count update on Codex, per ACP
|
|
117
|
+
* `usage_update`). Vocabulary tracks ACP's stable `usage_update` shape:
|
|
118
|
+
* `used`/`size`/`cost`. Distinct from `turn.ended.tokens`, which is the
|
|
119
|
+
* turn's billing total — this is "how full is the session right now".
|
|
120
|
+
*/
|
|
121
|
+
export const SessionUsageEventSchema = z.object({
|
|
122
|
+
type: z.literal("session.usage"),
|
|
123
|
+
sessionId: z.string(),
|
|
124
|
+
turnId: z.string(),
|
|
125
|
+
/** Tokens currently occupying the context window (prompt + output). */
|
|
126
|
+
used: z.number(),
|
|
127
|
+
/** The model's context-window size, when the harness reports it. */
|
|
128
|
+
size: z.number().optional(),
|
|
129
|
+
/** Cumulative session cost in USD, when the harness reports it. */
|
|
130
|
+
cost: z.number().optional(),
|
|
131
|
+
timestamp: EpochMsSchema,
|
|
132
|
+
_meta: MetaSchema,
|
|
133
|
+
});
|
|
134
|
+
export type SessionUsageEvent = z.infer<typeof SessionUsageEventSchema>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The compaction entity: an ID-addressed, POSITIONAL timeline entity — UPSERT
|
|
138
|
+
* by `compactionId`. The first event for an id anchors the entity's position
|
|
139
|
+
* in the ordered stream (the reducer places it there in the timeline); later
|
|
140
|
+
* upserts advance `status` and stream `summary` without moving it. Neither a
|
|
141
|
+
* part nor a bare notification: a replayed transcript needs the divider IN the
|
|
142
|
+
* order (Codex deprecated their session-level notification for exactly this;
|
|
143
|
+
* matches ACP's session-compaction draft so a future binding is a passthrough).
|
|
144
|
+
*/
|
|
145
|
+
export const SessionCompactionEventSchema = z.object({
|
|
146
|
+
type: z.literal("session.compaction"),
|
|
147
|
+
sessionId: z.string(),
|
|
148
|
+
turnId: z.string(),
|
|
149
|
+
compactionId: z.string(),
|
|
150
|
+
status: CompactionStatusSchema,
|
|
151
|
+
/** What triggered it, when known (Claude: `manual` | `auto`). */
|
|
152
|
+
trigger: CompactionTriggerSchema.optional(),
|
|
153
|
+
/** Context tokens before/after the compaction, when reported. */
|
|
154
|
+
preTokens: z.number().optional(),
|
|
155
|
+
postTokens: z.number().optional(),
|
|
156
|
+
/** User-displayable summary; may stream via successive upserts. */
|
|
157
|
+
summary: z.string().optional(),
|
|
158
|
+
/** Part ids the summary subsumed — auditable compaction lineage. */
|
|
159
|
+
abstractsIds: z.array(z.string()).optional(),
|
|
160
|
+
timestamp: EpochMsSchema,
|
|
161
|
+
_meta: MetaSchema,
|
|
162
|
+
});
|
|
163
|
+
export type SessionCompactionEvent = z.infer<typeof SessionCompactionEventSchema>;
|
|
164
|
+
|
|
165
|
+
// ---- turn events ------------------------------------------------------------
|
|
166
|
+
|
|
58
167
|
export const TurnStartedEventSchema = z.object({
|
|
59
168
|
type: z.literal("turn.started"),
|
|
60
|
-
turnId: z.string(),
|
|
61
169
|
sessionId: z.string(),
|
|
62
|
-
|
|
170
|
+
turnId: z.string(),
|
|
171
|
+
timestamp: EpochMsSchema,
|
|
172
|
+
_meta: MetaSchema,
|
|
63
173
|
});
|
|
64
174
|
export type TurnStartedEvent = z.infer<typeof TurnStartedEventSchema>;
|
|
65
175
|
|
|
176
|
+
export const TurnEndedEventSchema = z.object({
|
|
177
|
+
type: z.literal("turn.ended"),
|
|
178
|
+
sessionId: z.string(),
|
|
179
|
+
turnId: z.string(),
|
|
180
|
+
/** Normalized terminal status — `cancelled` and `error` are first-class. */
|
|
181
|
+
stopReason: StopReasonSchema,
|
|
182
|
+
/** Raw provider finish string (`end_turn`, `completed`, …), always preserved when reported. */
|
|
183
|
+
finishReason: z.string().optional(),
|
|
184
|
+
/** The turn's BILLING total (never the context gauge — that is session.usage). */
|
|
185
|
+
tokens: TokenUsageSchema.optional(),
|
|
186
|
+
/** USD for this turn, when reported. */
|
|
187
|
+
cost: z.number().optional(),
|
|
188
|
+
/** Present when stopReason === "error". */
|
|
189
|
+
error: ErrorInfoSchema.optional(),
|
|
190
|
+
timestamp: EpochMsSchema,
|
|
191
|
+
_meta: MetaSchema,
|
|
192
|
+
});
|
|
193
|
+
export type TurnEndedEvent = z.infer<typeof TurnEndedEventSchema>;
|
|
194
|
+
|
|
195
|
+
// ---- message + part events --------------------------------------------------
|
|
196
|
+
|
|
66
197
|
export const MessageStartedEventSchema = z.object({
|
|
67
198
|
type: z.literal("message.started"),
|
|
199
|
+
sessionId: z.string(),
|
|
68
200
|
turnId: z.string(),
|
|
69
201
|
messageId: z.string(),
|
|
202
|
+
/** Position of this message within the turn. The user echo is 0; assistant messages are 1..n. */
|
|
70
203
|
outputIndex: z.number(),
|
|
71
204
|
role: z.enum(["user", "assistant"]),
|
|
72
205
|
/**
|
|
@@ -75,36 +208,37 @@ export const MessageStartedEventSchema = z.object({
|
|
|
75
208
|
* message — consumers nest it under that tool call. Absent for top-level
|
|
76
209
|
* messages.
|
|
77
210
|
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
sessionId: z.string().optional(),
|
|
83
|
-
harness: z.string().optional(),
|
|
84
|
-
model: z.string().optional(),
|
|
85
|
-
})
|
|
86
|
-
.optional(),
|
|
211
|
+
parentToolCallId: z.string().optional(),
|
|
212
|
+
model: z.string().optional(),
|
|
213
|
+
timestamp: EpochMsSchema,
|
|
214
|
+
_meta: MetaSchema,
|
|
87
215
|
});
|
|
88
216
|
export type MessageStartedEvent = z.infer<typeof MessageStartedEventSchema>;
|
|
89
217
|
|
|
90
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* A full part snapshot. UPSERT by `part.id` — authoritative, idempotent,
|
|
220
|
+
* re-delivery-safe. Full-snapshot semantics, never field patches: "clear a
|
|
221
|
+
* field" = omit it from the next snapshot.
|
|
222
|
+
*/
|
|
91
223
|
export const MessagePartEventSchema = z.object({
|
|
92
224
|
type: z.literal("message.part"),
|
|
225
|
+
sessionId: z.string(),
|
|
93
226
|
turnId: z.string(),
|
|
94
227
|
messageId: z.string(),
|
|
95
228
|
outputIndex: z.number(),
|
|
229
|
+
/** Position of this part within its message — persist as the ordering key. */
|
|
96
230
|
partIndex: z.number(),
|
|
97
231
|
part: PartSchema,
|
|
98
|
-
|
|
99
|
-
|
|
232
|
+
timestamp: EpochMsSchema,
|
|
233
|
+
_meta: MetaSchema,
|
|
100
234
|
});
|
|
101
235
|
export type MessagePartEvent = z.infer<typeof MessagePartEventSchema>;
|
|
102
236
|
|
|
103
237
|
export const DeltaSchema = z.discriminatedUnion("type", [
|
|
104
|
-
z.object({ type: z.literal("text
|
|
105
|
-
z.object({ type: z.literal("reasoning
|
|
238
|
+
z.object({ type: z.literal("text"), text: z.string() }),
|
|
239
|
+
z.object({ type: z.literal("reasoning"), text: z.string() }),
|
|
106
240
|
z.object({
|
|
107
|
-
type: z.literal("
|
|
241
|
+
type: z.literal("tool_input"),
|
|
108
242
|
toolCallId: z.string(),
|
|
109
243
|
toolName: z.string(),
|
|
110
244
|
input: z.string(),
|
|
@@ -112,103 +246,47 @@ export const DeltaSchema = z.discriminatedUnion("type", [
|
|
|
112
246
|
]);
|
|
113
247
|
export type Delta = z.infer<typeof DeltaSchema>;
|
|
114
248
|
|
|
115
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* An additive streaming aid for a streaming part field. Advisory: snapshots
|
|
251
|
+
* are authoritative and wipe accumulated deltas. Deltas are NON-DURABLE —
|
|
252
|
+
* `events/replay` may omit them; snapshots fully reconstruct state.
|
|
253
|
+
*/
|
|
116
254
|
export const MessagePartDeltaEventSchema = z.object({
|
|
117
255
|
type: z.literal("message.part.delta"),
|
|
256
|
+
sessionId: z.string(),
|
|
118
257
|
turnId: z.string(),
|
|
119
258
|
messageId: z.string(),
|
|
120
259
|
outputIndex: z.number(),
|
|
121
260
|
partIndex: z.number(),
|
|
122
261
|
partId: z.string(),
|
|
123
262
|
delta: DeltaSchema,
|
|
124
|
-
|
|
125
|
-
|
|
263
|
+
timestamp: EpochMsSchema,
|
|
264
|
+
_meta: MetaSchema,
|
|
126
265
|
});
|
|
127
266
|
export type MessagePartDeltaEvent = z.infer<typeof MessagePartDeltaEventSchema>;
|
|
128
267
|
|
|
268
|
+
/** Bracket marker ONLY — billing lives on turn.ended, never here. */
|
|
129
269
|
export const MessageEndedEventSchema = z.object({
|
|
130
270
|
type: z.literal("message.ended"),
|
|
271
|
+
sessionId: z.string(),
|
|
131
272
|
turnId: z.string(),
|
|
132
273
|
messageId: z.string(),
|
|
133
|
-
timestamp:
|
|
274
|
+
timestamp: EpochMsSchema,
|
|
275
|
+
_meta: MetaSchema,
|
|
134
276
|
});
|
|
135
277
|
export type MessageEndedEvent = z.infer<typeof MessageEndedEventSchema>;
|
|
136
278
|
|
|
137
|
-
export const TurnEndedEventSchema = z.object({
|
|
138
|
-
type: z.literal("turn.ended"),
|
|
139
|
-
turnId: z.string(),
|
|
140
|
-
sessionId: z.string(),
|
|
141
|
-
/** Normalized terminal status — `cancelled` and `error` are first-class. */
|
|
142
|
-
stopReason: StopReasonSchema,
|
|
143
|
-
/** Raw provider finish string (`end_turn`, `completed`, …), when reported. */
|
|
144
|
-
finishReason: z.string().optional(),
|
|
145
|
-
tokens: TokenUsageSchema.optional(),
|
|
146
|
-
cost: z.number().optional(),
|
|
147
|
-
error: z.object({ name: z.string(), message: z.string() }).optional(),
|
|
148
|
-
timestamp: z.number(),
|
|
149
|
-
});
|
|
150
|
-
export type TurnEndedEvent = z.infer<typeof TurnEndedEventSchema>;
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Context-window gauge, emitted whenever the harness reports fresh numbers
|
|
154
|
-
* (per model message on Claude, per token-count update on Codex, per ACP
|
|
155
|
-
* `usage_update`). Vocabulary tracks ACP's stable `usage_update` shape:
|
|
156
|
-
* `used`/`size`/`cost`. Distinct from `turn.ended.tokens`, which is the
|
|
157
|
-
* turn's billing total — this is "how full is the session right now".
|
|
158
|
-
*/
|
|
159
|
-
export const SessionUsageEventSchema = z.object({
|
|
160
|
-
type: z.literal("session.usage"),
|
|
161
|
-
sessionId: z.string(),
|
|
162
|
-
turnId: z.string(),
|
|
163
|
-
/** Tokens currently occupying the context window (prompt + output). */
|
|
164
|
-
used: z.number(),
|
|
165
|
-
/** The model's context-window size, when the harness reports it. */
|
|
166
|
-
size: z.number().optional(),
|
|
167
|
-
/** Cumulative session cost in USD, when the harness reports it. */
|
|
168
|
-
cost: z.number().optional(),
|
|
169
|
-
timestamp: z.number(),
|
|
170
|
-
});
|
|
171
|
-
export type SessionUsageEvent = z.infer<typeof SessionUsageEventSchema>;
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* The harness compacted the session's history (context summarization). A
|
|
175
|
-
* boundary marker, not a rewrite: consumers that render history decide what
|
|
176
|
-
* to do at the boundary; the engine stays append-only.
|
|
177
|
-
*/
|
|
178
|
-
export const SessionCompactedEventSchema = z.object({
|
|
179
|
-
type: z.literal("session.compacted"),
|
|
180
|
-
sessionId: z.string(),
|
|
181
|
-
turnId: z.string(),
|
|
182
|
-
/** What triggered it, when known (Claude: `manual` | `auto`). */
|
|
183
|
-
trigger: z.string().optional(),
|
|
184
|
-
/** Context tokens before/after the compaction, when reported. */
|
|
185
|
-
preTokens: z.number().optional(),
|
|
186
|
-
postTokens: z.number().optional(),
|
|
187
|
-
timestamp: z.number(),
|
|
188
|
-
});
|
|
189
|
-
export type SessionCompactedEvent = z.infer<typeof SessionCompactedEventSchema>;
|
|
190
|
-
|
|
191
|
-
export const ErrorEventSchema = z.object({
|
|
192
|
-
type: z.literal("error"),
|
|
193
|
-
turnId: z.string().optional(),
|
|
194
|
-
sessionId: z.string().optional(),
|
|
195
|
-
error: z.string(),
|
|
196
|
-
recoverable: z.boolean(),
|
|
197
|
-
code: z.string().optional(),
|
|
198
|
-
stack: z.string().optional(),
|
|
199
|
-
timestamp: z.number(),
|
|
200
|
-
});
|
|
201
|
-
export type ErrorEvent = z.infer<typeof ErrorEventSchema>;
|
|
202
|
-
|
|
203
279
|
// ---- permissions (ACP request_permission-shaped) ----------------------------
|
|
204
280
|
|
|
205
|
-
|
|
281
|
+
/** OPEN (Law 3) — but unknown outcomes/kinds MUST be handled conservatively: never treated as approval. */
|
|
282
|
+
export const PERMISSION_OPTION_KINDS = [
|
|
206
283
|
"allow_once",
|
|
207
284
|
"allow_always",
|
|
208
285
|
"reject_once",
|
|
209
286
|
"reject_always",
|
|
210
|
-
]
|
|
211
|
-
export type PermissionOptionKind =
|
|
287
|
+
] as const;
|
|
288
|
+
export type PermissionOptionKind = (typeof PERMISSION_OPTION_KINDS)[number] | (string & {});
|
|
289
|
+
export const PermissionOptionKindSchema = openVocabulary<PermissionOptionKind>();
|
|
212
290
|
|
|
213
291
|
export const PermissionOptionSchema = z.object({
|
|
214
292
|
optionId: z.string(),
|
|
@@ -234,7 +312,16 @@ export const PermissionToolCallSchema = z.object({
|
|
|
234
312
|
export type PermissionToolCall = z.infer<typeof PermissionToolCallSchema>;
|
|
235
313
|
|
|
236
314
|
export const PermissionOutcomeSchema = z.discriminatedUnion("outcome", [
|
|
237
|
-
z.object({
|
|
315
|
+
z.object({
|
|
316
|
+
outcome: z.literal("selected"),
|
|
317
|
+
optionId: z.string(),
|
|
318
|
+
/**
|
|
319
|
+
* Allow-with-modified-input. Without this, products bypass the broker
|
|
320
|
+
* entirely (Claude's canUseTool supports it; ACP v1's outcome could not
|
|
321
|
+
* carry it).
|
|
322
|
+
*/
|
|
323
|
+
updatedInput: z.record(z.string(), z.unknown()).optional(),
|
|
324
|
+
}),
|
|
238
325
|
z.object({ outcome: z.literal("cancelled") }),
|
|
239
326
|
]);
|
|
240
327
|
export type PermissionOutcome = z.infer<typeof PermissionOutcomeSchema>;
|
|
@@ -243,6 +330,8 @@ export type PermissionOutcome = z.infer<typeof PermissionOutcomeSchema>;
|
|
|
243
330
|
* The harness is blocked waiting for a decision. Answer via
|
|
244
331
|
* `AgentRuntime.respondPermission(sessionId, requestId, outcome)`. Pending
|
|
245
332
|
* requests resolve as `cancelled` when the turn is cancelled or ends.
|
|
333
|
+
* `options` is THE source of buttons — clients render the server's list,
|
|
334
|
+
* never derive affordances from local policy.
|
|
246
335
|
*/
|
|
247
336
|
export const PermissionRequestedEventSchema = z.object({
|
|
248
337
|
type: z.literal("permission.requested"),
|
|
@@ -258,28 +347,48 @@ export const PermissionRequestedEventSchema = z.object({
|
|
|
258
347
|
*/
|
|
259
348
|
toolCall: PermissionToolCallSchema.optional(),
|
|
260
349
|
options: z.array(PermissionOptionSchema),
|
|
261
|
-
timestamp:
|
|
350
|
+
timestamp: EpochMsSchema,
|
|
351
|
+
_meta: MetaSchema,
|
|
262
352
|
});
|
|
263
353
|
export type PermissionRequestedEvent = z.infer<typeof PermissionRequestedEventSchema>;
|
|
264
354
|
|
|
265
|
-
/** A pending permission request was answered (or cancelled). */
|
|
355
|
+
/** A pending permission request was answered (or cancelled). Resolves exactly once. */
|
|
266
356
|
export const PermissionResolvedEventSchema = z.object({
|
|
267
357
|
type: z.literal("permission.resolved"),
|
|
268
358
|
sessionId: z.string(),
|
|
269
359
|
turnId: z.string(),
|
|
270
360
|
requestId: z.string(),
|
|
271
361
|
outcome: PermissionOutcomeSchema,
|
|
272
|
-
timestamp:
|
|
362
|
+
timestamp: EpochMsSchema,
|
|
363
|
+
_meta: MetaSchema,
|
|
273
364
|
});
|
|
274
365
|
export type PermissionResolvedEvent = z.infer<typeof PermissionResolvedEventSchema>;
|
|
275
366
|
|
|
276
|
-
// ---- raw
|
|
367
|
+
// ---- error + raw ------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
export const ErrorEventSchema = z.object({
|
|
370
|
+
type: z.literal("error"),
|
|
371
|
+
/** Absent only for pre-session failures. */
|
|
372
|
+
sessionId: z.string().optional(),
|
|
373
|
+
turnId: z.string().optional(),
|
|
374
|
+
category: ErrorCategorySchema,
|
|
375
|
+
message: z.string(),
|
|
376
|
+
/**
|
|
377
|
+
* true = the turn continues (retry/backoff in flight). Consumers MUST NOT
|
|
378
|
+
* flip UI to an error state on recoverable errors — surface as diagnostics.
|
|
379
|
+
*/
|
|
380
|
+
recoverable: z.boolean(),
|
|
381
|
+
stack: z.string().optional(),
|
|
382
|
+
timestamp: EpochMsSchema,
|
|
383
|
+
_meta: MetaSchema,
|
|
384
|
+
});
|
|
385
|
+
export type ErrorEvent = z.infer<typeof ErrorEventSchema>;
|
|
277
386
|
|
|
278
387
|
/**
|
|
279
388
|
* Opt-in (`RunConfig.includeRaw`) passthrough of the harness's raw event,
|
|
280
|
-
* interleaved with the normalized stream. Escape hatch for
|
|
281
|
-
*
|
|
282
|
-
*
|
|
389
|
+
* interleaved with the normalized stream. Escape hatch for debugging and
|
|
390
|
+
* fixture recording — `data` carries NO stability guarantees and is not part
|
|
391
|
+
* of the contract. Building a product pipeline on `raw` is a bug.
|
|
283
392
|
*/
|
|
284
393
|
export const RawEventSchema = z.object({
|
|
285
394
|
type: z.literal("raw"),
|
|
@@ -287,12 +396,16 @@ export const RawEventSchema = z.object({
|
|
|
287
396
|
turnId: z.string(),
|
|
288
397
|
harness: AgentHarnessSchema,
|
|
289
398
|
data: z.unknown(),
|
|
290
|
-
timestamp:
|
|
399
|
+
timestamp: EpochMsSchema,
|
|
400
|
+
_meta: MetaSchema,
|
|
291
401
|
});
|
|
292
402
|
export type RawEvent = z.infer<typeof RawEventSchema>;
|
|
293
403
|
|
|
404
|
+
// ---- the union --------------------------------------------------------------
|
|
405
|
+
|
|
294
406
|
export const LifecycleEventSchema = z.discriminatedUnion("type", [
|
|
295
407
|
SessionCreatedEventSchema,
|
|
408
|
+
SessionEndedEventSchema,
|
|
296
409
|
TurnStartedEventSchema,
|
|
297
410
|
MessageStartedEventSchema,
|
|
298
411
|
MessagePartEventSchema,
|
|
@@ -300,10 +413,74 @@ export const LifecycleEventSchema = z.discriminatedUnion("type", [
|
|
|
300
413
|
MessageEndedEventSchema,
|
|
301
414
|
TurnEndedEventSchema,
|
|
302
415
|
SessionUsageEventSchema,
|
|
303
|
-
|
|
416
|
+
SessionCompactionEventSchema,
|
|
304
417
|
ErrorEventSchema,
|
|
305
418
|
PermissionRequestedEventSchema,
|
|
306
419
|
PermissionResolvedEventSchema,
|
|
307
420
|
RawEventSchema,
|
|
308
421
|
]);
|
|
309
422
|
export type LifecycleEvent = z.infer<typeof LifecycleEventSchema>;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Law 6 — tolerant of the unknown, strict about the known: an event with an
|
|
426
|
+
* unknown `type` is preserved (in order, never dropped) as an UnknownEvent;
|
|
427
|
+
* a KNOWN type with a malformed body is a hard error.
|
|
428
|
+
*/
|
|
429
|
+
export interface UnknownEvent {
|
|
430
|
+
type: string & {};
|
|
431
|
+
sessionId?: string;
|
|
432
|
+
raw: Record<string, unknown>;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* A `message.part` whose part may be an `UnknownPart`: the event envelope is
|
|
437
|
+
* protocol-owned (strict), the part inside it is not (Law 6).
|
|
438
|
+
*/
|
|
439
|
+
export interface DecodedMessagePartEvent extends Omit<MessagePartEvent, "part"> {
|
|
440
|
+
part: Part | UnknownPart;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* What a Law-6 decoder yields for a KNOWN event type. Identical to
|
|
445
|
+
* `LifecycleEvent` except that `message.part` may carry an unknown part — so
|
|
446
|
+
* every `LifecycleEvent` is a `DecodedLifecycleEvent`, and consumers that
|
|
447
|
+
* accept this type accept both.
|
|
448
|
+
*/
|
|
449
|
+
export type DecodedLifecycleEvent =
|
|
450
|
+
| Exclude<LifecycleEvent, MessagePartEvent>
|
|
451
|
+
| DecodedMessagePartEvent;
|
|
452
|
+
|
|
453
|
+
/** The `message.part` envelope without its part — decoded separately, leniently. */
|
|
454
|
+
const MessagePartEnvelopeSchema = MessagePartEventSchema.omit({ part: true });
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Decode a wire value into a LifecycleEvent, preserving unknown event types
|
|
458
|
+
* (and unknown PART types inside `message.part`) instead of dropping them.
|
|
459
|
+
* Throws on a known type with a malformed body.
|
|
460
|
+
*
|
|
461
|
+
* This is the decoder every wire consumer should use — dropping an event a
|
|
462
|
+
* newer server sent is not a graceful degradation, it is a hole in the
|
|
463
|
+
* transcript and, for a seq-tracking client, a fabricated gap.
|
|
464
|
+
*/
|
|
465
|
+
export function decodeLifecycleEvent(value: unknown): DecodedLifecycleEvent | UnknownEvent {
|
|
466
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
467
|
+
throw new Error("lifecycle event must be an object");
|
|
468
|
+
}
|
|
469
|
+
const record = value as Record<string, unknown>;
|
|
470
|
+
const type = record.type;
|
|
471
|
+
if (typeof type !== "string" || type.length === 0) {
|
|
472
|
+
throw new Error("lifecycle event is missing its type");
|
|
473
|
+
}
|
|
474
|
+
if (!isKnownLifecycleEventType(type)) {
|
|
475
|
+
const sessionId = typeof record.sessionId === "string" ? record.sessionId : undefined;
|
|
476
|
+
return { type, ...(sessionId !== undefined ? { sessionId } : {}), raw: record };
|
|
477
|
+
}
|
|
478
|
+
if (type === "message.part") {
|
|
479
|
+
return { ...MessagePartEnvelopeSchema.parse(record), part: decodePart(record.part) };
|
|
480
|
+
}
|
|
481
|
+
return LifecycleEventSchema.parse(value);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// `LIFECYCLE_EVENT_TYPES`, `isKnownLifecycleEventType` and `isUnknownEvent`
|
|
485
|
+
// live in `./guards.ts` — same exports from the barrel, but reachable without
|
|
486
|
+
// loading zod.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The extension slot (Law 4 — ACP pattern): present on every event, part,
|
|
5
|
+
* tool state, and result-content item. Adapters and products MUST NOT add
|
|
6
|
+
* root-level fields to canonical types — all root names are reserved for
|
|
7
|
+
* future protocol versions; extensions live here under namespaced keys
|
|
8
|
+
* (`deus/...`, `agnt/...`).
|
|
9
|
+
*
|
|
10
|
+
* Reserved keys (W3C trace context, MCP-compatible): `traceparent`,
|
|
11
|
+
* `tracestate`, `baggage`.
|
|
12
|
+
*/
|
|
13
|
+
export const MetaSchema = z.record(z.string(), z.unknown()).optional();
|
|
14
|
+
export type Meta = Record<string, unknown>;
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A UI-authored span over the text (mentions, pills, inline references) that
|
|
5
|
+
* survives the round trip through the model, the user echo, and persistence.
|
|
6
|
+
* The model sees plain text; `byteRange` is [start, end) over the UTF-8 bytes
|
|
7
|
+
* of the parent `text`.
|
|
8
|
+
*/
|
|
9
|
+
export const TextElementSchema = z.object({
|
|
10
|
+
byteRange: z
|
|
11
|
+
.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()])
|
|
12
|
+
.refine(([start, end]) => start <= end, {
|
|
13
|
+
// A reversed range would survive the echo and persistence, and every
|
|
14
|
+
// consumer slicing with it would render an empty or wrong span with
|
|
15
|
+
// the producer bug invisible — reject it at the boundary.
|
|
16
|
+
message: "byteRange must be ordered: start <= end",
|
|
17
|
+
}),
|
|
18
|
+
/** Human-readable placeholder for the element, displayed in the UI. */
|
|
19
|
+
placeholder: z.string().optional(),
|
|
20
|
+
});
|
|
21
|
+
export type TextElement = z.infer<typeof TextElementSchema>;
|
|
22
|
+
|
|
3
23
|
/** Inbound user content for a turn: plain text, or multimodal blocks. */
|
|
4
24
|
export const TextPartInputSchema = z.object({
|
|
5
25
|
type: z.literal("text"),
|
|
6
26
|
id: z.string().optional(),
|
|
7
27
|
text: z.string().min(1),
|
|
28
|
+
elements: z.array(TextElementSchema).optional(),
|
|
8
29
|
});
|
|
9
30
|
export type TextPartInput = z.infer<typeof TextPartInputSchema>;
|
|
10
31
|
|
|
@@ -15,11 +36,15 @@ export const ImagePartInputSchema = z
|
|
|
15
36
|
/** Either a URL or base64 `data` (one is required). */
|
|
16
37
|
url: z.string().min(1).optional(),
|
|
17
38
|
data: z.string().min(1).optional(),
|
|
18
|
-
|
|
19
|
-
filename:
|
|
39
|
+
mimeType: z.string(),
|
|
40
|
+
// No `filename` here: ImagePartSchema has no such field, so accepting it
|
|
41
|
+
// would validate a value the echo then silently drops (Law 7).
|
|
20
42
|
})
|
|
21
|
-
.refine((part) => part.
|
|
22
|
-
|
|
43
|
+
.refine((part) => (part.data !== undefined) !== (part.url !== undefined), {
|
|
44
|
+
// Exactly one, matching the OUTPUT part schemas' `exactlyOneSource`: an
|
|
45
|
+
// input carrying both would echo as a part every consumer's decode
|
|
46
|
+
// rejects — the ambiguity dies at the boundary, not on the stream.
|
|
47
|
+
message: "image input requires exactly one of data | url",
|
|
23
48
|
});
|
|
24
49
|
export type ImagePartInput = z.infer<typeof ImagePartInputSchema>;
|
|
25
50
|
|
|
@@ -29,11 +54,11 @@ export const FilePartInputSchema = z
|
|
|
29
54
|
id: z.string().optional(),
|
|
30
55
|
url: z.string().min(1).optional(),
|
|
31
56
|
data: z.string().min(1).optional(),
|
|
32
|
-
|
|
57
|
+
mimeType: z.string(),
|
|
33
58
|
filename: z.string().optional(),
|
|
34
59
|
})
|
|
35
|
-
.refine((part) => part.
|
|
36
|
-
message: "file input requires
|
|
60
|
+
.refine((part) => (part.data !== undefined) !== (part.url !== undefined), {
|
|
61
|
+
message: "file input requires exactly one of data | url",
|
|
37
62
|
});
|
|
38
63
|
export type FilePartInput = z.infer<typeof FilePartInputSchema>;
|
|
39
64
|
|
|
@@ -48,6 +73,30 @@ export type PartInput = z.infer<typeof PartInputSchema>;
|
|
|
48
73
|
export const AgentInputSchema = z.union([z.string(), z.array(PartInputSchema)]);
|
|
49
74
|
export type AgentInput = z.infer<typeof AgentInputSchema>;
|
|
50
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Decode an untrusted value (an HTTP body, a queue message, a DB column) into
|
|
78
|
+
* an `AgentInput`, or throw with a message that names the offending path.
|
|
79
|
+
*
|
|
80
|
+
* The schema is the contract; a hand-rolled decoder in front of it is where
|
|
81
|
+
* tolerance for shapes the engine never accepted creeps in (`content` arrays,
|
|
82
|
+
* bare `{text}` objects) and where the error a product shows its user
|
|
83
|
+
* degrades to "invalid input". (An empty STRING is valid input — deliberately:
|
|
84
|
+
* "continue" semantics belong to the product, not the schema.)
|
|
85
|
+
*/
|
|
86
|
+
export function parseAgentInput(value: unknown): AgentInput {
|
|
87
|
+
const parsed = AgentInputSchema.safeParse(value);
|
|
88
|
+
if (parsed.success) return parsed.data;
|
|
89
|
+
// A union failure's own message is just "Invalid input" — the useful paths
|
|
90
|
+
// and messages live one level down, in the per-branch issue lists.
|
|
91
|
+
const flattened = parsed.error.issues.flatMap((issue) =>
|
|
92
|
+
issue.code === "invalid_union" ? issue.errors.flat() : [issue],
|
|
93
|
+
);
|
|
94
|
+
const detail = flattened
|
|
95
|
+
.map((issue) => `${issue.path.length ? `${issue.path.join(".")}: ` : ""}${issue.message}`)
|
|
96
|
+
.join("; ");
|
|
97
|
+
throw new Error(`invalid agent input: ${detail}`, { cause: parsed.error });
|
|
98
|
+
}
|
|
99
|
+
|
|
51
100
|
/** Collapse an AgentInput down to the text the model sees first. */
|
|
52
101
|
export function inputToText(input: AgentInput): string {
|
|
53
102
|
if (typeof input === "string") return input;
|