@zvada/agent-server 0.2.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/package.json +87 -0
  4. package/src/client/client.ts +589 -0
  5. package/src/client/index.ts +18 -0
  6. package/src/client/transports.ts +84 -0
  7. package/src/core/agents/acp/acp-agent.ts +322 -0
  8. package/src/core/agents/acp/adapter.ts +260 -0
  9. package/src/core/agents/acp/client.ts +212 -0
  10. package/src/core/agents/acp/known-agents.ts +29 -0
  11. package/src/core/agents/acp/mappings.ts +136 -0
  12. package/src/core/agents/base.ts +145 -0
  13. package/src/core/agents/claude-code/adapter.ts +451 -0
  14. package/src/core/agents/claude-code/claude-agent.ts +235 -0
  15. package/src/core/agents/claude-code/generator-session.ts +344 -0
  16. package/src/core/agents/claude-code/options.ts +161 -0
  17. package/src/core/agents/claude-code/session-manager.ts +159 -0
  18. package/src/core/agents/codex-app-server/adapter.ts +214 -0
  19. package/src/core/agents/codex-app-server/client.ts +221 -0
  20. package/src/core/agents/codex-app-server/codex-app-server-agent.ts +385 -0
  21. package/src/core/agents/codex-items.ts +122 -0
  22. package/src/core/agents/codex-sdk/adapter.ts +204 -0
  23. package/src/core/agents/codex-sdk/codex-sdk-agent.ts +236 -0
  24. package/src/core/agents/config-fingerprint.ts +19 -0
  25. package/src/core/agents/error-classifier.ts +68 -0
  26. package/src/core/agents/registry.ts +40 -0
  27. package/src/core/agents/session-store.ts +72 -0
  28. package/src/core/agents/tool-meta.ts +68 -0
  29. package/src/core/agents/types.ts +54 -0
  30. package/src/core/index.ts +114 -0
  31. package/src/core/presets.ts +78 -0
  32. package/src/core/provision/extract.ts +31 -0
  33. package/src/core/provision/index.ts +10 -0
  34. package/src/core/provision/npm.ts +114 -0
  35. package/src/core/provision/pins.ts +51 -0
  36. package/src/core/provision/platform.ts +73 -0
  37. package/src/core/provision/provisioner.ts +478 -0
  38. package/src/core/proxy/anthropic-proxy.ts +69 -0
  39. package/src/core/proxy/api-key-store.ts +34 -0
  40. package/src/core/proxy/index.ts +7 -0
  41. package/src/core/runtime/agent-runtime.ts +363 -0
  42. package/src/core/runtime/event-processor.ts +218 -0
  43. package/src/core/runtime/event-sink.ts +37 -0
  44. package/src/core/utils/errors.ts +41 -0
  45. package/src/index.ts +4 -0
  46. package/src/protocol/async-queue.ts +68 -0
  47. package/src/protocol/config.ts +100 -0
  48. package/src/protocol/factories.ts +125 -0
  49. package/src/protocol/harness.ts +50 -0
  50. package/src/protocol/ids.ts +53 -0
  51. package/src/protocol/index.ts +16 -0
  52. package/src/protocol/lifecycle.ts +309 -0
  53. package/src/protocol/models.ts +45 -0
  54. package/src/protocol/part-input.ts +58 -0
  55. package/src/protocol/parts.ts +60 -0
  56. package/src/protocol/thinking.ts +32 -0
  57. package/src/protocol/tokens.ts +39 -0
  58. package/src/protocol/tool-state.ts +89 -0
  59. package/src/protocol/wire.ts +313 -0
  60. package/src/server/acp/binding.ts +163 -0
  61. package/src/server/acp/translate.ts +160 -0
  62. package/src/server/agent-server.ts +357 -0
  63. package/src/server/bin.ts +174 -0
  64. package/src/server/index.ts +24 -0
  65. package/src/server/install.ts +51 -0
  66. package/src/server/session-log.ts +66 -0
  67. package/src/server/transports.ts +149 -0
@@ -0,0 +1,309 @@
1
+ import { z } from "zod";
2
+ import { AgentHarnessSchema } from "./harness.ts";
3
+ import { PartSchema } from "./parts.ts";
4
+ import { TokenUsageSchema } from "./tokens.ts";
5
+ import { ToolKindSchema, ToolLocationSchema } from "./tool-state.ts";
6
+
7
+ /**
8
+ * The normalized event stream every harness is funneled into. Consumers (a WS
9
+ * bridge, an SSE endpoint, a CLI, a test sink) subscribe to these and never see
10
+ * raw SDK payloads. All keys are camelCase; serialize at your transport edge.
11
+ *
12
+ * Ordering within a turn:
13
+ * session.created? -> turn.started -> (message.started -> message.part*
14
+ * / message.part.delta* -> message.ended)* -> turn.ended
15
+ *
16
+ * Two exceptions to strict message bracketing:
17
+ * - a `message.part` may arrive after its message ended (a tool completing
18
+ * after the model message that issued it) — upsert by `part.id`.
19
+ * - `permission.requested`/`permission.resolved` and `raw` events interleave
20
+ * anywhere between turn.started and turn.ended.
21
+ */
22
+
23
+ /**
24
+ * Why a turn stopped (ACP-aligned, plus `error`). This is the normalized
25
+ * terminal status; the raw provider string travels in `finishReason`.
26
+ */
27
+ export const STOP_REASONS = [
28
+ "end_turn",
29
+ "max_tokens",
30
+ "max_turn_requests",
31
+ "refusal",
32
+ "cancelled",
33
+ "error",
34
+ ] as const;
35
+ export type StopReason = (typeof STOP_REASONS)[number];
36
+ export const StopReasonSchema = z.enum(STOP_REASONS);
37
+
38
+ /** Emitted once the harness-native session id is known — persist it to resume later. */
39
+ export const SessionCreatedEventSchema = z.object({
40
+ type: z.literal("session.created"),
41
+ sessionId: z.string(),
42
+ /** Native id to pass back as `resumeSessionId` (Claude session_id / Codex threadId). */
43
+ nativeSessionId: z.string(),
44
+ harness: AgentHarnessSchema,
45
+ model: z.string().optional(),
46
+ /**
47
+ * Present only when this turn requested a resume: whether the harness
48
+ * actually continued that conversation. `false` = it fell back to a fresh
49
+ * session (context lost) — surface this, never swallow it. Compare with
50
+ * this flag, not ids: some harnesses mint a NEW nativeSessionId on a
51
+ * successful resume (Claude).
52
+ */
53
+ resumed: z.boolean().optional(),
54
+ timestamp: z.number(),
55
+ });
56
+ export type SessionCreatedEvent = z.infer<typeof SessionCreatedEventSchema>;
57
+
58
+ export const TurnStartedEventSchema = z.object({
59
+ type: z.literal("turn.started"),
60
+ turnId: z.string(),
61
+ sessionId: z.string(),
62
+ timestamp: z.number(),
63
+ });
64
+ export type TurnStartedEvent = z.infer<typeof TurnStartedEventSchema>;
65
+
66
+ export const MessageStartedEventSchema = z.object({
67
+ type: z.literal("message.started"),
68
+ turnId: z.string(),
69
+ messageId: z.string(),
70
+ outputIndex: z.number(),
71
+ role: z.enum(["user", "assistant"]),
72
+ /**
73
+ * Set when this message is a sub-agent's output: the `toolCallId` of the
74
+ * tool (e.g. Task) that spawned it. Such a message is NOT a top-level model
75
+ * message — consumers nest it under that tool call. Absent for top-level
76
+ * messages.
77
+ */
78
+ parentToolUseId: z.string().optional(),
79
+ timestamp: z.number(),
80
+ metadata: z
81
+ .object({
82
+ sessionId: z.string().optional(),
83
+ harness: z.string().optional(),
84
+ model: z.string().optional(),
85
+ })
86
+ .optional(),
87
+ });
88
+ export type MessageStartedEvent = z.infer<typeof MessageStartedEventSchema>;
89
+
90
+ /** A fully-formed (or newly-finalized) part snapshot. Upsert by `part.id`. */
91
+ export const MessagePartEventSchema = z.object({
92
+ type: z.literal("message.part"),
93
+ turnId: z.string(),
94
+ messageId: z.string(),
95
+ outputIndex: z.number(),
96
+ partIndex: z.number(),
97
+ part: PartSchema,
98
+ parentToolUseId: z.string().optional(),
99
+ timestamp: z.number(),
100
+ });
101
+ export type MessagePartEvent = z.infer<typeof MessagePartEventSchema>;
102
+
103
+ export const DeltaSchema = z.discriminatedUnion("type", [
104
+ z.object({ type: z.literal("text-delta"), text: z.string() }),
105
+ z.object({ type: z.literal("reasoning-delta"), text: z.string() }),
106
+ z.object({
107
+ type: z.literal("tool-input-delta"),
108
+ toolCallId: z.string(),
109
+ toolName: z.string(),
110
+ input: z.string(),
111
+ }),
112
+ ]);
113
+ export type Delta = z.infer<typeof DeltaSchema>;
114
+
115
+ /** An incremental update to a streaming part (text/reasoning/tool-input). */
116
+ export const MessagePartDeltaEventSchema = z.object({
117
+ type: z.literal("message.part.delta"),
118
+ turnId: z.string(),
119
+ messageId: z.string(),
120
+ outputIndex: z.number(),
121
+ partIndex: z.number(),
122
+ partId: z.string(),
123
+ delta: DeltaSchema,
124
+ parentToolUseId: z.string().optional(),
125
+ timestamp: z.number(),
126
+ });
127
+ export type MessagePartDeltaEvent = z.infer<typeof MessagePartDeltaEventSchema>;
128
+
129
+ export const MessageEndedEventSchema = z.object({
130
+ type: z.literal("message.ended"),
131
+ turnId: z.string(),
132
+ messageId: z.string(),
133
+ timestamp: z.number(),
134
+ });
135
+ export type MessageEndedEvent = z.infer<typeof MessageEndedEventSchema>;
136
+
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
+ // ---- permissions (ACP request_permission-shaped) ----------------------------
204
+
205
+ export const PermissionOptionKindSchema = z.enum([
206
+ "allow_once",
207
+ "allow_always",
208
+ "reject_once",
209
+ "reject_always",
210
+ ]);
211
+ export type PermissionOptionKind = z.infer<typeof PermissionOptionKindSchema>;
212
+
213
+ export const PermissionOptionSchema = z.object({
214
+ optionId: z.string(),
215
+ name: z.string(),
216
+ kind: PermissionOptionKindSchema,
217
+ });
218
+ export type PermissionOption = z.infer<typeof PermissionOptionSchema>;
219
+
220
+ /** The tool call a permission request is about (snapshot, not a live part). */
221
+ export const PermissionToolCallSchema = z.object({
222
+ /**
223
+ * The `toolCallId` of the in-flight tool part this request gates. Real for
224
+ * codex approvals AND Claude (`canUseTool` exposes `toolUseID` since SDK
225
+ * 0.2.138). Optional only for harnesses that genuinely cannot correlate.
226
+ */
227
+ toolCallId: z.string().optional(),
228
+ toolName: z.string(),
229
+ kind: ToolKindSchema.optional(),
230
+ title: z.string().optional(),
231
+ locations: z.array(ToolLocationSchema).optional(),
232
+ rawInput: z.record(z.string(), z.unknown()).optional(),
233
+ });
234
+ export type PermissionToolCall = z.infer<typeof PermissionToolCallSchema>;
235
+
236
+ export const PermissionOutcomeSchema = z.discriminatedUnion("outcome", [
237
+ z.object({ outcome: z.literal("selected"), optionId: z.string() }),
238
+ z.object({ outcome: z.literal("cancelled") }),
239
+ ]);
240
+ export type PermissionOutcome = z.infer<typeof PermissionOutcomeSchema>;
241
+
242
+ /**
243
+ * The harness is blocked waiting for a decision. Answer via
244
+ * `AgentRuntime.respondPermission(sessionId, requestId, outcome)`. Pending
245
+ * requests resolve as `cancelled` when the turn is cancelled or ends.
246
+ */
247
+ export const PermissionRequestedEventSchema = z.object({
248
+ type: z.literal("permission.requested"),
249
+ sessionId: z.string(),
250
+ turnId: z.string(),
251
+ requestId: z.string(),
252
+ /** Human-readable one-line prompt, always present (render this). */
253
+ title: z.string(),
254
+ /**
255
+ * Structured context when the request gates a tool call — today every
256
+ * harness permission is tool-scoped, but the field is optional because a
257
+ * future `acp` harness can raise subject-less requests (ACP v2 shape).
258
+ */
259
+ toolCall: PermissionToolCallSchema.optional(),
260
+ options: z.array(PermissionOptionSchema),
261
+ timestamp: z.number(),
262
+ });
263
+ export type PermissionRequestedEvent = z.infer<typeof PermissionRequestedEventSchema>;
264
+
265
+ /** A pending permission request was answered (or cancelled). */
266
+ export const PermissionResolvedEventSchema = z.object({
267
+ type: z.literal("permission.resolved"),
268
+ sessionId: z.string(),
269
+ turnId: z.string(),
270
+ requestId: z.string(),
271
+ outcome: PermissionOutcomeSchema,
272
+ timestamp: z.number(),
273
+ });
274
+ export type PermissionResolvedEvent = z.infer<typeof PermissionResolvedEventSchema>;
275
+
276
+ // ---- raw passthrough ---------------------------------------------------------
277
+
278
+ /**
279
+ * Opt-in (`RunConfig.includeRaw`) passthrough of the harness's raw event,
280
+ * interleaved with the normalized stream. Escape hatch for migration,
281
+ * debugging, and fixture recording — `data` carries NO stability guarantees
282
+ * and is not part of the contract.
283
+ */
284
+ export const RawEventSchema = z.object({
285
+ type: z.literal("raw"),
286
+ sessionId: z.string(),
287
+ turnId: z.string(),
288
+ harness: AgentHarnessSchema,
289
+ data: z.unknown(),
290
+ timestamp: z.number(),
291
+ });
292
+ export type RawEvent = z.infer<typeof RawEventSchema>;
293
+
294
+ export const LifecycleEventSchema = z.discriminatedUnion("type", [
295
+ SessionCreatedEventSchema,
296
+ TurnStartedEventSchema,
297
+ MessageStartedEventSchema,
298
+ MessagePartEventSchema,
299
+ MessagePartDeltaEventSchema,
300
+ MessageEndedEventSchema,
301
+ TurnEndedEventSchema,
302
+ SessionUsageEventSchema,
303
+ SessionCompactedEventSchema,
304
+ ErrorEventSchema,
305
+ PermissionRequestedEventSchema,
306
+ PermissionResolvedEventSchema,
307
+ RawEventSchema,
308
+ ]);
309
+ export type LifecycleEvent = z.infer<typeof LifecycleEventSchema>;
@@ -0,0 +1,45 @@
1
+ import type { AgentHarness } from "./harness.ts";
2
+ import type { ThinkingLevel } from "./thinking.ts";
3
+
4
+ /**
5
+ * Advisory catalog of known-good models per harness. `model` on a RunConfig is
6
+ * a free-form string (harnesses pass it through), so unlisted models still work;
7
+ * this catalog drives UIs/pickers and supplies sensible per-harness defaults.
8
+ */
9
+ export interface ModelInfo {
10
+ readonly id: string;
11
+ readonly harness: AgentHarness;
12
+ readonly label: string;
13
+ readonly thinking: readonly ThinkingLevel[];
14
+ }
15
+
16
+ const ALL: readonly ThinkingLevel[] = ["off", "low", "medium", "high", "xhigh"];
17
+
18
+ export const MODEL_CATALOG: readonly ModelInfo[] = [
19
+ // Claude (the claude CLI also accepts the bare aliases "sonnet"/"opus"/"haiku").
20
+ { id: "sonnet", harness: "claude-code", label: "Claude Sonnet (latest)", thinking: ALL },
21
+ { id: "opus", harness: "claude-code", label: "Claude Opus (latest)", thinking: ALL },
22
+ { id: "haiku", harness: "claude-code", label: "Claude Haiku (latest)", thinking: ALL },
23
+ // Codex (shared between codex-sdk and codex-app-server).
24
+ { id: "gpt-5.5", harness: "codex-sdk", label: "GPT-5.5", thinking: ALL },
25
+ { id: "gpt-5.5-codex", harness: "codex-sdk", label: "GPT-5.5 Codex", thinking: ALL },
26
+ { id: "gpt-5.5", harness: "codex-app-server", label: "GPT-5.5", thinking: ALL },
27
+ { id: "gpt-5.5-codex", harness: "codex-app-server", label: "GPT-5.5 Codex", thinking: ALL },
28
+ ];
29
+
30
+ /**
31
+ * Default model per harness. `undefined` means "let the harness/CLI use its own
32
+ * configured default" (Codex reads `~/.codex/config.toml`).
33
+ */
34
+ export const DEFAULT_MODELS: Record<AgentHarness, string | undefined> = {
35
+ "claude-code": "sonnet",
36
+ "codex-sdk": undefined,
37
+ "codex-app-server": undefined,
38
+ // ACP v1 has no portable model parameter (selection is agent-defined via
39
+ // config options), so the target agent always uses its own default.
40
+ acp: undefined,
41
+ };
42
+
43
+ export function modelsForHarness(harness: AgentHarness): readonly ModelInfo[] {
44
+ return MODEL_CATALOG.filter((m) => m.harness === harness);
45
+ }
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+
3
+ /** Inbound user content for a turn: plain text, or multimodal blocks. */
4
+ export const TextPartInputSchema = z.object({
5
+ type: z.literal("text"),
6
+ id: z.string().optional(),
7
+ text: z.string().min(1),
8
+ });
9
+ export type TextPartInput = z.infer<typeof TextPartInputSchema>;
10
+
11
+ export const ImagePartInputSchema = z
12
+ .object({
13
+ type: z.literal("image"),
14
+ id: z.string().optional(),
15
+ /** Either a URL or base64 `data` (one is required). */
16
+ url: z.string().min(1).optional(),
17
+ data: z.string().min(1).optional(),
18
+ mediaType: z.string(),
19
+ filename: z.string().optional(),
20
+ })
21
+ .refine((part) => part.url !== undefined || part.data !== undefined, {
22
+ message: "image input requires a non-empty url or data payload",
23
+ });
24
+ export type ImagePartInput = z.infer<typeof ImagePartInputSchema>;
25
+
26
+ export const FilePartInputSchema = z
27
+ .object({
28
+ type: z.literal("file"),
29
+ id: z.string().optional(),
30
+ url: z.string().min(1).optional(),
31
+ data: z.string().min(1).optional(),
32
+ mediaType: z.string(),
33
+ filename: z.string().optional(),
34
+ })
35
+ .refine((part) => part.url !== undefined || part.data !== undefined, {
36
+ message: "file input requires a non-empty url or data payload",
37
+ });
38
+ export type FilePartInput = z.infer<typeof FilePartInputSchema>;
39
+
40
+ export const PartInputSchema = z.discriminatedUnion("type", [
41
+ TextPartInputSchema,
42
+ ImagePartInputSchema,
43
+ FilePartInputSchema,
44
+ ]);
45
+ export type PartInput = z.infer<typeof PartInputSchema>;
46
+
47
+ /** A turn's input is either a bare string or a list of typed parts. */
48
+ export const AgentInputSchema = z.union([z.string(), z.array(PartInputSchema)]);
49
+ export type AgentInput = z.infer<typeof AgentInputSchema>;
50
+
51
+ /** Collapse an AgentInput down to the text the model sees first. */
52
+ export function inputToText(input: AgentInput): string {
53
+ if (typeof input === "string") return input;
54
+ return input
55
+ .filter((p): p is TextPartInput => p.type === "text")
56
+ .map((p) => p.text)
57
+ .join("\n\n");
58
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { ToolKindSchema, ToolLocationSchema, ToolStateSchema } from "./tool-state.ts";
3
+
4
+ /**
5
+ * A Part is the atomic unit of assistant output, normalized across harnesses.
6
+ * Every part belongs to a message within a session. Parts stream: a TextPart
7
+ * arrives `streaming` and is finalized `done`; a ToolPart's `state` advances
8
+ * through the tool lifecycle. `parentToolUseId` links a part produced inside a
9
+ * sub-agent / nested tool execution back to the tool call that spawned it.
10
+ *
11
+ * Rule: this union contains only part types adapters actually emit — the
12
+ * protocol advertises nothing it doesn't deliver.
13
+ */
14
+ const PartBase = z.object({
15
+ id: z.string(),
16
+ sessionId: z.string(),
17
+ messageId: z.string(),
18
+ parentToolUseId: z.string().optional(),
19
+ });
20
+
21
+ export const TextPartSchema = PartBase.extend({
22
+ type: z.literal("text"),
23
+ text: z.string(),
24
+ state: z.enum(["streaming", "done"]).optional(),
25
+ });
26
+ export type TextPart = z.infer<typeof TextPartSchema>;
27
+
28
+ export const ReasoningPartSchema = PartBase.extend({
29
+ type: z.literal("reasoning"),
30
+ text: z.string(),
31
+ state: z.enum(["streaming", "done"]).optional(),
32
+ providerMetadata: z.record(z.string(), z.unknown()).optional(),
33
+ });
34
+ export type ReasoningPart = z.infer<typeof ReasoningPartSchema>;
35
+
36
+ export const ToolPartSchema = PartBase.extend({
37
+ type: z.literal("tool"),
38
+ toolCallId: z.string(),
39
+ /** Provider-native tool name (`Bash`, `shell`, `mcp.search`, …). */
40
+ toolName: z.string(),
41
+ /** Normalized classification of what the call does (ACP ToolKind). */
42
+ kind: ToolKindSchema.optional(),
43
+ /** Human-readable label when the harness provides one. */
44
+ title: z.string().optional(),
45
+ /** Files this call touches, for follow-along UIs. */
46
+ locations: z.array(ToolLocationSchema).optional(),
47
+ state: ToolStateSchema,
48
+ });
49
+ export type ToolPart = z.infer<typeof ToolPartSchema>;
50
+
51
+ export const PartSchema = z.discriminatedUnion("type", [
52
+ TextPartSchema,
53
+ ReasoningPartSchema,
54
+ ToolPartSchema,
55
+ ]);
56
+ export type Part = z.infer<typeof PartSchema>;
57
+
58
+ export const isTextPart = (p: Part): p is TextPart => p.type === "text";
59
+ export const isReasoningPart = (p: Part): p is ReasoningPart => p.type === "reasoning";
60
+ export const isToolPart = (p: Part): p is ToolPart => p.type === "tool";
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * A single, harness-agnostic reasoning dial. Each harness maps it to its own
5
+ * native control: Claude -> adaptive `thinking` + `effort`; Codex ->
6
+ * `ReasoningEffort`.
7
+ */
8
+ export const THINKING_LEVELS = ["off", "low", "medium", "high", "xhigh"] as const;
9
+ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
10
+ export const ThinkingLevelSchema = z.enum(THINKING_LEVELS);
11
+ export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "medium";
12
+
13
+ export function isThinkingLevel(v: string): v is ThinkingLevel {
14
+ return (THINKING_LEVELS as readonly string[]).includes(v);
15
+ }
16
+
17
+ /** Codex reasoning-effort string for a level (matches the `ReasoningEffort` enum). */
18
+ export type CodexReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
19
+ export function codexReasoningEffort(level: ThinkingLevel): CodexReasoningEffort {
20
+ switch (level) {
21
+ case "off":
22
+ return "minimal";
23
+ case "low":
24
+ return "low";
25
+ case "medium":
26
+ return "medium";
27
+ case "high":
28
+ return "high";
29
+ case "xhigh":
30
+ return "xhigh";
31
+ }
32
+ }
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Normalized token accounting. Every harness reports usage differently
5
+ * (Claude: input/output/cache_creation/cache_read; Codex: input/output/
6
+ * reasoning/cached). Adapters fold those into this single shape.
7
+ */
8
+ export const TokenUsageSchema = z.object({
9
+ input: z.number().default(0),
10
+ output: z.number().default(0),
11
+ reasoning: z.number().optional(),
12
+ cache: z
13
+ .object({
14
+ read: z.number().default(0),
15
+ write: z.number().default(0),
16
+ })
17
+ .optional(),
18
+ });
19
+
20
+ export type TokenUsage = z.infer<typeof TokenUsageSchema>;
21
+
22
+ export const DEFAULT_TOKEN_USAGE: TokenUsage = {
23
+ input: 0,
24
+ output: 0,
25
+ reasoning: 0,
26
+ cache: { read: 0, write: 0 },
27
+ };
28
+
29
+ export function addTokenUsage(a: TokenUsage, b: TokenUsage): TokenUsage {
30
+ return {
31
+ input: a.input + b.input,
32
+ output: a.output + b.output,
33
+ reasoning: (a.reasoning ?? 0) + (b.reasoning ?? 0),
34
+ cache: {
35
+ read: (a.cache?.read ?? 0) + (b.cache?.read ?? 0),
36
+ write: (a.cache?.write ?? 0) + (b.cache?.write ?? 0),
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Runtime tool lifecycle: a tool call moves pending -> in_progress ->
5
+ * completed|failed (ACP-aligned status names). `pending` carries the partial
6
+ * (streaming) JSON input before it parses; the other states carry the
7
+ * fully-parsed input object.
8
+ */
9
+ export const RUNTIME_TOOL_STATUSES = ["pending", "in_progress", "completed", "failed"] as const;
10
+ export type RuntimeToolStatus = (typeof RUNTIME_TOOL_STATUSES)[number];
11
+ export const RuntimeToolStatusSchema = z.enum(RUNTIME_TOOL_STATUSES);
12
+
13
+ /**
14
+ * What a tool call does, normalized across harnesses (ACP's ToolKind
15
+ * taxonomy). Lets a consumer pick an icon / verb without knowing
16
+ * provider-native tool names.
17
+ */
18
+ export const TOOL_KINDS = [
19
+ "read",
20
+ "edit",
21
+ "delete",
22
+ "move",
23
+ "search",
24
+ "execute",
25
+ "think",
26
+ "fetch",
27
+ "switch_mode",
28
+ "other",
29
+ ] as const;
30
+ export type ToolKind = (typeof TOOL_KINDS)[number];
31
+ export const ToolKindSchema = z.enum(TOOL_KINDS);
32
+
33
+ /** A file location a tool call touches — lets UIs "follow along". */
34
+ export const ToolLocationSchema = z.object({
35
+ path: z.string(),
36
+ line: z.number().optional(),
37
+ });
38
+ export type ToolLocation = z.infer<typeof ToolLocationSchema>;
39
+
40
+ export const ToolAttachmentSchema = z.object({
41
+ type: z.string(),
42
+ command: z.string().optional(),
43
+ output: z.string().optional(),
44
+ exitCode: z.number().optional(),
45
+ path: z.string().optional(),
46
+ diff: z.string().optional(),
47
+ });
48
+ export type ToolAttachment = z.infer<typeof ToolAttachmentSchema>;
49
+
50
+ export const ToolStatePendingSchema = z.object({
51
+ status: z.literal("pending"),
52
+ partialInput: z.string(),
53
+ });
54
+ export type ToolStatePending = z.infer<typeof ToolStatePendingSchema>;
55
+
56
+ export const ToolStateInProgressSchema = z.object({
57
+ status: z.literal("in_progress"),
58
+ input: z.record(z.string(), z.unknown()),
59
+ title: z.string().optional(),
60
+ time: z.object({ start: z.number() }),
61
+ });
62
+ export type ToolStateInProgress = z.infer<typeof ToolStateInProgressSchema>;
63
+
64
+ export const ToolStateCompletedSchema = z.object({
65
+ status: z.literal("completed"),
66
+ input: z.record(z.string(), z.unknown()),
67
+ output: z.string(),
68
+ title: z.string(),
69
+ metadata: z.record(z.string(), z.unknown()).optional(),
70
+ time: z.object({ start: z.number(), end: z.number() }),
71
+ attachments: z.array(ToolAttachmentSchema).optional(),
72
+ });
73
+ export type ToolStateCompleted = z.infer<typeof ToolStateCompletedSchema>;
74
+
75
+ export const ToolStateFailedSchema = z.object({
76
+ status: z.literal("failed"),
77
+ input: z.record(z.string(), z.unknown()),
78
+ error: z.string(),
79
+ time: z.object({ start: z.number(), end: z.number() }),
80
+ });
81
+ export type ToolStateFailed = z.infer<typeof ToolStateFailedSchema>;
82
+
83
+ export const ToolStateSchema = z.discriminatedUnion("status", [
84
+ ToolStatePendingSchema,
85
+ ToolStateInProgressSchema,
86
+ ToolStateCompletedSchema,
87
+ ToolStateFailedSchema,
88
+ ]);
89
+ export type ToolState = z.infer<typeof ToolStateSchema>;