@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,20 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { openVocabulary } from "./vocabulary.ts";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
+
* The one error vocabulary (Law: one shape per concept). Used by
|
|
6
|
+
* `turn.ended.error`, the standalone `error` event, and `RunSummary.error`.
|
|
7
|
+
*
|
|
8
|
+
* OPEN (Law 3): products may extend with their own categories (deus adds
|
|
9
|
+
* "db_write"); unknown non-`_` values are reserved for this protocol.
|
|
10
|
+
* `usage_limit` (plan/quota exhausted) and `rate_limit` (429/backoff) are
|
|
11
|
+
* deliberately distinct — they need different user guidance; collapsing them
|
|
12
|
+
* is a display choice, never a wire merge.
|
|
5
13
|
*/
|
|
6
14
|
export const ERROR_CATEGORIES = [
|
|
7
15
|
"auth",
|
|
8
16
|
"rate_limit",
|
|
17
|
+
"usage_limit",
|
|
9
18
|
"context_limit",
|
|
10
19
|
"network",
|
|
11
20
|
"abort",
|
|
21
|
+
"invalid_request",
|
|
12
22
|
"process_exit",
|
|
13
|
-
"
|
|
14
|
-
"unknown",
|
|
23
|
+
"internal",
|
|
15
24
|
] as const;
|
|
16
|
-
export type ErrorCategory = (typeof ERROR_CATEGORIES)[number];
|
|
25
|
+
export type ErrorCategory = (typeof ERROR_CATEGORIES)[number] | (string & {});
|
|
26
|
+
export const ErrorCategorySchema = openVocabulary<ErrorCategory>();
|
|
27
|
+
|
|
28
|
+
export const ErrorInfoSchema = z.object({
|
|
29
|
+
category: ErrorCategorySchema,
|
|
30
|
+
message: z.string(),
|
|
31
|
+
});
|
|
32
|
+
export type ErrorInfo = z.infer<typeof ErrorInfoSchema>;
|
|
17
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Maps a raw harness error into a stable category, so callers can react
|
|
36
|
+
* (retry on `network`/`rate_limit`, surface `auth` prominently, treat `abort`
|
|
37
|
+
* as success). Rule order matters. Distilled from deus-machine's lifecycle
|
|
38
|
+
* classifier; lives in /protocol because categories are vocabulary.
|
|
39
|
+
*/
|
|
18
40
|
const RULES: ReadonlyArray<[ErrorCategory, readonly string[]]> = [
|
|
19
41
|
["abort", ["aborted", "aborterror", "cancelled", "canceled", "interrupted", "sigint"]],
|
|
20
42
|
[
|
|
@@ -46,6 +68,10 @@ const RULES: ReadonlyArray<[ErrorCategory, readonly string[]]> = [
|
|
|
46
68
|
"socket hang up",
|
|
47
69
|
],
|
|
48
70
|
],
|
|
71
|
+
[
|
|
72
|
+
"invalid_request",
|
|
73
|
+
["invalid request", "invalid_request_error", "status 400", "http 400", "bad request"],
|
|
74
|
+
],
|
|
49
75
|
["process_exit", ["exited with code", "process exited", "spawn", "enoent", "killed"]],
|
|
50
76
|
];
|
|
51
77
|
|
|
@@ -54,7 +80,7 @@ export function classifyError(error: unknown): ErrorCategory {
|
|
|
54
80
|
for (const [category, needles] of RULES) {
|
|
55
81
|
if (needles.some((n) => message.includes(n))) return category;
|
|
56
82
|
}
|
|
57
|
-
return "
|
|
83
|
+
return "internal";
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
/** Whether a turn can plausibly be retried after this error. */
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { generateUUIDv7 } from "./ids.ts";
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
55
|
+
...(ctx.parentToolCallId && { parentToolCallId: ctx.parentToolCallId }),
|
|
53
56
|
type: "tool" as const,
|
|
54
57
|
toolCallId,
|
|
55
58
|
toolName,
|
|
56
|
-
...(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.
|
|
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: {
|
|
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
|
|
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
|
+
}
|
package/src/protocol/index.ts
CHANGED
|
@@ -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";
|