@zvada/agent-server 0.2.2 → 0.3.1

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 (51) hide show
  1. package/CHANGELOG.md +317 -0
  2. package/README.md +20 -4
  3. package/docs/consuming.md +269 -0
  4. package/docs/deploy.md +80 -0
  5. package/docs/harnesses.md +64 -0
  6. package/docs/rfds/0001-deterministic-echo-ids.md +44 -0
  7. package/package.json +23 -3
  8. package/src/client/client.ts +143 -50
  9. package/src/core/agents/acp/acp-agent.ts +9 -0
  10. package/src/core/agents/acp/mappings.ts +3 -3
  11. package/src/core/agents/base.ts +9 -1
  12. package/src/core/agents/claude-code/adapter.ts +116 -26
  13. package/src/core/agents/claude-code/claude-agent.ts +31 -3
  14. package/src/core/agents/claude-code/generator-session.ts +16 -5
  15. package/src/core/agents/claude-code/options.ts +13 -3
  16. package/src/core/agents/claude-code/session-manager.ts +9 -4
  17. package/src/core/agents/codex-app-server/codex-app-server-agent.ts +17 -3
  18. package/src/core/agents/codex-sdk/codex-sdk-agent.ts +3 -3
  19. package/src/core/agents/types.ts +1 -1
  20. package/src/core/diagnostics.ts +59 -0
  21. package/src/core/index.ts +3 -1
  22. package/src/core/presets.ts +15 -2
  23. package/src/core/provision/pins.ts +5 -1
  24. package/src/core/proxy/anthropic-proxy.ts +43 -2
  25. package/src/core/proxy/api-key-store.ts +37 -5
  26. package/src/core/proxy/index.ts +8 -1
  27. package/src/core/runtime/agent-runtime.ts +66 -18
  28. package/src/core/runtime/event-processor.ts +51 -26
  29. package/src/protocol/config.ts +8 -6
  30. package/src/{core/agents/error-classifier.ts → protocol/errors.ts} +33 -7
  31. package/src/protocol/factories.ts +106 -10
  32. package/src/protocol/guards.ts +53 -0
  33. package/src/protocol/index.ts +10 -0
  34. package/src/protocol/lifecycle.ts +289 -112
  35. package/src/protocol/meta.ts +14 -0
  36. package/src/protocol/part-input.ts +56 -7
  37. package/src/protocol/parts.ts +125 -10
  38. package/src/protocol/reduce.ts +749 -0
  39. package/src/protocol/selectors.ts +162 -0
  40. package/src/protocol/seq-cursor.ts +87 -0
  41. package/src/protocol/stop-reasons.ts +45 -0
  42. package/src/protocol/time.ts +23 -0
  43. package/src/protocol/tokens.ts +23 -0
  44. package/src/protocol/tool-state.ts +85 -25
  45. package/src/protocol/verify.ts +440 -0
  46. package/src/protocol/vocabulary.ts +18 -0
  47. package/src/protocol/wire.ts +81 -13
  48. package/src/server/acp/binding.ts +23 -2
  49. package/src/server/acp/translate.ts +51 -14
  50. package/src/server/agent-server.ts +85 -6
  51. package/AGENTS.md +0 -21
@@ -1,12 +1,13 @@
1
1
  import { generateUUIDv7 } from "./ids.ts";
2
- import type { ReasoningPart, TextPart, ToolPart } from "./parts.ts";
3
- import type { ToolKind, ToolLocation } from "./tool-state.ts";
2
+ import type { AgentInput } from "./part-input.ts";
3
+ import type { Part, ReasoningPart, SubagentMetadata, TextPart, ToolPart } from "./parts.ts";
4
+ import type { ToolKind, ToolLocation, ToolResultContent } from "./tool-state.ts";
4
5
 
5
6
  /** Context an adapter threads through while building parts for one message. */
6
7
  export interface StreamContext {
7
8
  sessionId: string;
8
9
  messageId: string;
9
- parentToolUseId?: string;
10
+ parentToolCallId?: string;
10
11
  }
11
12
 
12
13
  /** Optional normalized metadata for a tool part (see ToolPart). */
@@ -14,6 +15,8 @@ export interface ToolMeta {
14
15
  kind?: ToolKind;
15
16
  title?: string;
16
17
  locations?: ToolLocation[];
18
+ /** Present when this tool spawns a subagent (implies kind "task"). */
19
+ subagent?: SubagentMetadata;
17
20
  }
18
21
 
19
22
  export function createTextPart(ctx: StreamContext, text: string, streaming = true): TextPart {
@@ -21,7 +24,7 @@ export function createTextPart(ctx: StreamContext, text: string, streaming = tru
21
24
  id: generateUUIDv7(),
22
25
  sessionId: ctx.sessionId,
23
26
  messageId: ctx.messageId,
24
- ...(ctx.parentToolUseId && { parentToolUseId: ctx.parentToolUseId }),
27
+ ...(ctx.parentToolCallId && { parentToolCallId: ctx.parentToolCallId }),
25
28
  type: "text",
26
29
  text,
27
30
  state: streaming ? "streaming" : "done",
@@ -37,7 +40,7 @@ export function createReasoningPart(
37
40
  id: generateUUIDv7(),
38
41
  sessionId: ctx.sessionId,
39
42
  messageId: ctx.messageId,
40
- ...(ctx.parentToolUseId && { parentToolUseId: ctx.parentToolUseId }),
43
+ ...(ctx.parentToolCallId && { parentToolCallId: ctx.parentToolCallId }),
41
44
  type: "reasoning",
42
45
  text,
43
46
  state: streaming ? "streaming" : "done",
@@ -49,13 +52,14 @@ function toolBase(ctx: StreamContext, toolCallId: string, toolName: string, meta
49
52
  id: generateUUIDv7(),
50
53
  sessionId: ctx.sessionId,
51
54
  messageId: ctx.messageId,
52
- ...(ctx.parentToolUseId && { parentToolUseId: ctx.parentToolUseId }),
55
+ ...(ctx.parentToolCallId && { parentToolCallId: ctx.parentToolCallId }),
53
56
  type: "tool" as const,
54
57
  toolCallId,
55
58
  toolName,
56
- ...(meta?.kind && { kind: meta.kind }),
59
+ ...(meta?.subagent ? { kind: "task" as const } : meta?.kind ? { kind: meta.kind } : {}),
57
60
  ...(meta?.title && { title: meta.title }),
58
61
  ...(meta?.locations?.length && { locations: meta.locations }),
62
+ ...(meta?.subagent && { subagent: meta.subagent }),
59
63
  };
60
64
  }
61
65
 
@@ -101,7 +105,12 @@ export function appendToolInput(part: ToolPart, partialJson: string): void {
101
105
 
102
106
  /** Attach/refresh normalized metadata on an existing tool part. */
103
107
  export function setToolMeta(part: ToolPart, meta: ToolMeta): void {
104
- if (meta.kind) part.kind = meta.kind;
108
+ if (meta.subagent) {
109
+ part.subagent = meta.subagent;
110
+ part.kind = "task";
111
+ } else if (meta.kind) {
112
+ part.kind = meta.kind;
113
+ }
105
114
  if (meta.title) part.title = meta.title;
106
115
  if (meta.locations?.length) part.locations = meta.locations;
107
116
  }
@@ -109,7 +118,15 @@ export function setToolMeta(part: ToolPart, meta: ToolMeta): void {
109
118
  /** Mutates a tool part into its terminal `completed`/`failed` state. */
110
119
  export function completeToolPart(
111
120
  part: ToolPart,
112
- result: { output: string; isError?: boolean; title?: string },
121
+ result: {
122
+ output: string;
123
+ isError?: boolean;
124
+ title?: string;
125
+ /** Display-grade structured output (diffs, images, terminals). */
126
+ content?: ToolResultContent[];
127
+ /** Machine channel: harness-native extras (e.g. exitCode). */
128
+ metadata?: Record<string, unknown>;
129
+ },
113
130
  ): void {
114
131
  const start = part.state.status === "in_progress" ? part.state.time.start : Date.now();
115
132
  const input = "input" in part.state ? part.state.input : {};
@@ -119,7 +136,86 @@ export function completeToolPart(
119
136
  status: "completed",
120
137
  input,
121
138
  output: result.output,
122
- title: result.title ?? part.toolName,
139
+ ...(result.title !== undefined && { title: result.title }),
140
+ ...(result.content?.length && { content: result.content }),
141
+ ...(result.metadata && { metadata: result.metadata }),
123
142
  time: { start, end: Date.now() },
124
143
  };
125
144
  }
145
+
146
+ /**
147
+ * The user echo's message id, derived from the turn id.
148
+ *
149
+ * A DELIBERATE, documented exception to Law 8 (engine-minted UUIDv7): the echo
150
+ * is not new information, it is the caller's own input played back, and one
151
+ * turn has exactly one echo message. Deriving the id means a consumer can
152
+ * predict the echo byte-for-byte from the `turnId` it minted — so an
153
+ * optimistically-rendered prompt bubble IS the echo (same id, same parts)
154
+ * instead of a look-alike that has to be reconciled and swapped. The
155
+ * reconcile-by-turnId dance every product wrote is what this replaces.
156
+ *
157
+ * Ids stay opaque to peers: nothing may parse structure out of them. This is a
158
+ * PRODUCER rule (how the engine mints), matched by a consumer helper — not a
159
+ * licence to infer turn ids from message ids elsewhere.
160
+ *
161
+ * Two consequences of NOT being UUIDv7, for consumers that leaned on Law 8's
162
+ * side effects: echo ids do not TIME-SORT against UUIDv7 ids (`echo-` sorts
163
+ * after every hex digit — never order by id), and they do not fit uuid-typed
164
+ * columns or id-derived timestamps (`createdAtFromUUID7(echoId)` is 0 — derive
165
+ * the echo's time from the `turnId` FIELD the event/row carries, never by
166
+ * parsing the echo id string; ids stay opaque).
167
+ */
168
+ export function echoMessageId(turnId: string): string {
169
+ return `echo-${turnId}`;
170
+ }
171
+
172
+ /** The id of the echo part at `index` — same derivation, same predictability. */
173
+ export function echoPartId(turnId: string, index: number): string {
174
+ return `echo-${turnId}-${index}`;
175
+ }
176
+
177
+ /**
178
+ * Build the parts for the user echo (spec §7.2) from a turn's input. Part ids
179
+ * are derived from the turn (see `echoPartId`), so the whole echo message is
180
+ * predictable from `{turnId, input}` alone. The EventProcessor stamps
181
+ * `sessionId`/`messageId` at emit time. Text `elements` spans ride `_meta`
182
+ * (the part vocabulary stays output-shaped; the spans are UI-authored
183
+ * annotations).
184
+ */
185
+ export function createUserEchoParts(input: AgentInput, turnId: string): Part[] {
186
+ const items = typeof input === "string" ? [{ type: "text" as const, text: input }] : input;
187
+ const parts: Part[] = [];
188
+ for (let index = 0; index < items.length; index++) {
189
+ const item = items[index] as (typeof items)[number];
190
+ const base = { id: echoPartId(turnId, index), sessionId: "", messageId: "" };
191
+ if (item.type === "text") {
192
+ parts.push({
193
+ ...base,
194
+ type: "text",
195
+ text: item.text,
196
+ state: "done",
197
+ ...("elements" in item && item.elements?.length
198
+ ? { _meta: { elements: item.elements } }
199
+ : {}),
200
+ });
201
+ } else if (item.type === "image") {
202
+ parts.push({
203
+ ...base,
204
+ type: "image",
205
+ ...(item.data !== undefined && { data: item.data }),
206
+ ...(item.url !== undefined && { url: item.url }),
207
+ mimeType: item.mimeType,
208
+ });
209
+ } else {
210
+ parts.push({
211
+ ...base,
212
+ type: "file",
213
+ ...(item.data !== undefined && { data: item.data }),
214
+ ...(item.url !== undefined && { url: item.url }),
215
+ mimeType: item.mimeType,
216
+ ...(item.filename !== undefined && { filename: item.filename }),
217
+ });
218
+ }
219
+ }
220
+ return parts;
221
+ }
@@ -0,0 +1,53 @@
1
+ // The Law-6 membership tests, kept in one zod-free module.
2
+ //
3
+ // A guard is a string comparison against a frozen list — but when it lives
4
+ // next to the schema it guards, importing it pulls zod (and the whole part /
5
+ // event union) into the importer's bundle. Browser consumers pay ~100kB to ask
6
+ // "is this part type one I know?". These are re-exported from `./index.ts`
7
+ // (nothing moved for `@zvada/agent-server/protocol` importers) and are also
8
+ // reachable directly as `@zvada/agent-server/protocol/guards`, which is
9
+ // guaranteed zod-free at runtime (`test/protocol/zod-free.test.ts` walks the
10
+ // transitive import graph).
11
+ //
12
+ // Type-only imports below are erased at compile time and cost nothing at
13
+ // runtime — the modules they name are never loaded by this one.
14
+ import type { DecodedLifecycleEvent, UnknownEvent } from "./lifecycle.ts";
15
+ import type { Part, UnknownPart } from "./parts.ts";
16
+
17
+ export const PART_TYPES = ["text", "reasoning", "tool", "image", "file"] as const;
18
+
19
+ export function isKnownPartType(type: unknown): type is (typeof PART_TYPES)[number] {
20
+ return typeof type === "string" && (PART_TYPES as readonly string[]).includes(type);
21
+ }
22
+
23
+ export function isUnknownPart(part: Part | UnknownPart): part is UnknownPart {
24
+ return !isKnownPartType(part.type);
25
+ }
26
+ // No per-type guards for known parts: `p.type === "text"` narrows natively.
27
+
28
+ export const LIFECYCLE_EVENT_TYPES = [
29
+ "session.created",
30
+ "session.ended",
31
+ "turn.started",
32
+ "message.started",
33
+ "message.part",
34
+ "message.part.delta",
35
+ "message.ended",
36
+ "turn.ended",
37
+ "session.usage",
38
+ "session.compaction",
39
+ "error",
40
+ "permission.requested",
41
+ "permission.resolved",
42
+ "raw",
43
+ ] as const;
44
+
45
+ export function isKnownLifecycleEventType(
46
+ type: unknown,
47
+ ): type is (typeof LIFECYCLE_EVENT_TYPES)[number] {
48
+ return typeof type === "string" && (LIFECYCLE_EVENT_TYPES as readonly string[]).includes(type);
49
+ }
50
+
51
+ export function isUnknownEvent(event: DecodedLifecycleEvent | UnknownEvent): event is UnknownEvent {
52
+ return !isKnownLifecycleEventType((event as { type: string }).type);
53
+ }
@@ -3,8 +3,13 @@
3
3
 
4
4
  export * from "./ids.ts";
5
5
  export * from "./async-queue.ts";
6
+ export * from "./meta.ts";
7
+ export * from "./time.ts";
8
+ export * from "./vocabulary.ts";
9
+ export * from "./errors.ts";
6
10
  export * from "./tokens.ts";
7
11
  export * from "./tool-state.ts";
12
+ export * from "./guards.ts";
8
13
  export * from "./parts.ts";
9
14
  export * from "./part-input.ts";
10
15
  export * from "./factories.ts";
@@ -13,4 +18,9 @@ export * from "./thinking.ts";
13
18
  export * from "./models.ts";
14
19
  export * from "./config.ts";
15
20
  export * from "./lifecycle.ts";
21
+ export * from "./stop-reasons.ts";
22
+ export * from "./seq-cursor.ts";
23
+ export * from "./reduce.ts";
24
+ export * from "./selectors.ts";
25
+ export * from "./verify.ts";
16
26
  export * from "./wire.ts";