@dynamicagents/core 0.9.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/LICENSE +201 -0
- package/README.md +522 -0
- package/dist/a2a/agent-stub.d.ts +60 -0
- package/dist/a2a/agent-stub.js +1 -0
- package/dist/a2a/caller-token.d.ts +44 -0
- package/dist/a2a/caller-token.js +61 -0
- package/dist/a2a/caller.d.ts +23 -0
- package/dist/a2a/caller.js +33 -0
- package/dist/a2a/card.d.ts +158 -0
- package/dist/a2a/card.js +163 -0
- package/dist/a2a/context.d.ts +19 -0
- package/dist/a2a/context.js +61 -0
- package/dist/a2a/deliver.d.ts +159 -0
- package/dist/a2a/deliver.js +185 -0
- package/dist/a2a/executor.d.ts +84 -0
- package/dist/a2a/executor.js +106 -0
- package/dist/a2a/index.d.ts +35 -0
- package/dist/a2a/index.js +34 -0
- package/dist/a2a/notify.d.ts +106 -0
- package/dist/a2a/notify.js +180 -0
- package/dist/a2a/parts.d.ts +45 -0
- package/dist/a2a/parts.js +94 -0
- package/dist/a2a/push.d.ts +70 -0
- package/dist/a2a/push.js +53 -0
- package/dist/a2a/self-origin.d.ts +91 -0
- package/dist/a2a/self-origin.js +114 -0
- package/dist/a2a/task-store.d.ts +44 -0
- package/dist/a2a/task-store.js +99 -0
- package/dist/a2a/task.d.ts +85 -0
- package/dist/a2a/task.js +15 -0
- package/dist/a2a/verify.d.ts +80 -0
- package/dist/a2a/verify.js +143 -0
- package/dist/agent/budget.d.ts +46 -0
- package/dist/agent/budget.js +42 -0
- package/dist/agent/control.d.ts +109 -0
- package/dist/agent/control.js +115 -0
- package/dist/agent/errors.d.ts +85 -0
- package/dist/agent/errors.js +64 -0
- package/dist/agent/final-reply.d.ts +49 -0
- package/dist/agent/final-reply.js +68 -0
- package/dist/agent/history.d.ts +97 -0
- package/dist/agent/history.js +133 -0
- package/dist/agent/index.d.ts +29 -0
- package/dist/agent/index.js +29 -0
- package/dist/agent/inference.d.ts +110 -0
- package/dist/agent/inference.js +120 -0
- package/dist/agent/model.d.ts +90 -0
- package/dist/agent/model.js +1 -0
- package/dist/agent/session.d.ts +100 -0
- package/dist/agent/session.js +82 -0
- package/dist/agent/workers-ai/index.d.ts +23 -0
- package/dist/agent/workers-ai/index.js +23 -0
- package/dist/agent/workers-ai/runtime.d.ts +42 -0
- package/dist/agent/workers-ai/runtime.js +63 -0
- package/dist/alarm/index.d.ts +77 -0
- package/dist/alarm/index.js +116 -0
- package/dist/config.d.ts +202 -0
- package/dist/config.js +135 -0
- package/dist/contract/index.d.ts +9 -0
- package/dist/contract/index.js +8 -0
- package/dist/contract/plugin.d.ts +324 -0
- package/dist/contract/plugin.js +114 -0
- package/dist/contract/recipe.d.ts +180 -0
- package/dist/contract/recipe.js +1 -0
- package/dist/contract/validation.d.ts +91 -0
- package/dist/contract/validation.js +84 -0
- package/dist/db/db.d.ts +147 -0
- package/dist/db/db.js +90 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrations/index.d.ts +20 -0
- package/dist/db/migrations/index.js +60 -0
- package/dist/db/models/subtasks.d.ts +100 -0
- package/dist/db/models/subtasks.js +241 -0
- package/dist/db/models/tasks.d.ts +118 -0
- package/dist/db/models/tasks.js +274 -0
- package/dist/db/schema.d.ts +468 -0
- package/dist/db/schema.js +88 -0
- package/dist/env.d.ts +53 -0
- package/dist/env.js +47 -0
- package/dist/host/agent.d.ts +305 -0
- package/dist/host/agent.js +400 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/index.js +19 -0
- package/dist/host/plugin-host.d.ts +42 -0
- package/dist/host/plugin-host.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/job/index.d.ts +21 -0
- package/dist/job/index.js +21 -0
- package/dist/job/lifecycle.d.ts +176 -0
- package/dist/job/lifecycle.js +230 -0
- package/dist/job/state.d.ts +92 -0
- package/dist/job/state.js +40 -0
- package/dist/platform.d.ts +138 -0
- package/dist/platform.js +140 -0
- package/dist/round/agent.d.ts +271 -0
- package/dist/round/agent.js +678 -0
- package/dist/round/index.d.ts +25 -0
- package/dist/round/index.js +23 -0
- package/dist/round/policy.d.ts +98 -0
- package/dist/round/policy.js +1 -0
- package/dist/round/subagent.d.ts +87 -0
- package/dist/round/subagent.js +108 -0
- package/dist/round/turn.d.ts +249 -0
- package/dist/round/turn.js +564 -0
- package/dist/round/workflow.d.ts +147 -0
- package/dist/round/workflow.js +387 -0
- package/dist/runtime/index.d.ts +127 -0
- package/dist/runtime/index.js +186 -0
- package/dist/runtime/tool-families.d.ts +32 -0
- package/dist/runtime/tool-families.js +61 -0
- package/dist/subagent/fingerprint.d.ts +37 -0
- package/dist/subagent/fingerprint.js +92 -0
- package/dist/subagent/index.d.ts +169 -0
- package/dist/subagent/index.js +330 -0
- package/dist/subagent/prompt.d.ts +32 -0
- package/dist/subagent/prompt.js +37 -0
- package/dist/subagent/run.d.ts +157 -0
- package/dist/subagent/run.js +540 -0
- package/dist/subagent/workspace.d.ts +85 -0
- package/dist/subagent/workspace.js +127 -0
- package/dist/subtasks/catalog.d.ts +40 -0
- package/dist/subtasks/catalog.js +36 -0
- package/dist/subtasks/decomposition.d.ts +85 -0
- package/dist/subtasks/decomposition.js +156 -0
- package/dist/subtasks/delegate.d.ts +120 -0
- package/dist/subtasks/delegate.js +131 -0
- package/dist/subtasks/index.d.ts +9 -0
- package/dist/subtasks/index.js +9 -0
- package/dist/subtasks/subtask-types.d.ts +91 -0
- package/dist/subtasks/subtask-types.js +103 -0
- package/dist/subtasks/types.d.ts +295 -0
- package/dist/subtasks/types.js +15 -0
- package/dist/testing/auth.d.ts +34 -0
- package/dist/testing/auth.js +35 -0
- package/dist/testing/do.d.ts +29 -0
- package/dist/testing/do.js +25 -0
- package/dist/testing/fake-session.d.ts +26 -0
- package/dist/testing/fake-session.js +37 -0
- package/dist/testing/fixtures.d.ts +64 -0
- package/dist/testing/fixtures.js +104 -0
- package/dist/testing/harness.d.ts +97 -0
- package/dist/testing/harness.js +138 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.js +35 -0
- package/dist/testing/mock-model.d.ts +77 -0
- package/dist/testing/mock-model.js +136 -0
- package/dist/testing/node.d.ts +56 -0
- package/dist/testing/node.js +56 -0
- package/dist/testing/vcr-global-setup.d.ts +12 -0
- package/dist/testing/vcr-global-setup.js +15 -0
- package/dist/testing/vcr-shared.d.ts +38 -0
- package/dist/testing/vcr-shared.js +33 -0
- package/dist/testing/vcr-spec.d.ts +25 -0
- package/dist/testing/vcr-spec.js +124 -0
- package/dist/testing/vcr-store.d.ts +86 -0
- package/dist/testing/vcr-store.js +191 -0
- package/dist/testing/vcr.d.ts +117 -0
- package/dist/testing/vcr.js +275 -0
- package/dist/worker/define-agent.d.ts +123 -0
- package/dist/worker/define-agent.js +20 -0
- package/dist/worker/index.d.ts +218 -0
- package/dist/worker/index.js +369 -0
- package/eslint-rules/index.js +31 -0
- package/eslint-rules/no-deprecated-object-properties.js +81 -0
- package/package.json +178 -0
- package/scripts/generate-keys.mjs +48 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-memory {@link WorkspaceBacking} — the fallback when no installed plugin
|
|
3
|
+
* declares one.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately **not durable**, and that is the honest behaviour rather than a
|
|
6
|
+
* shortcut: a durable stand-in would have to invent a storage layout that a real
|
|
7
|
+
* backend would then have to migrate away from. An agent that delegates file
|
|
8
|
+
* work installs a backend; one that does not never writes a file, and pays
|
|
9
|
+
* nothing for the option. What this buys is that `createAgentRuntime` composes
|
|
10
|
+
* without a workspace plugin at all, so `SubagentRuntime.workspaceBacking` can
|
|
11
|
+
* stay required and a host never writes a null check.
|
|
12
|
+
*
|
|
13
|
+
* Scoped per call, so each execution gets its own map, exactly as a facet's own
|
|
14
|
+
* SQLite would give it its own tables. Contents are lost on isolate eviction —
|
|
15
|
+
* the resumable runner treats a lost workspace as a resumable state everywhere
|
|
16
|
+
* it matters.
|
|
17
|
+
*
|
|
18
|
+
* Signature matches {@link AgentPlugin.workspaceBacking}; both arguments are
|
|
19
|
+
* ignored.
|
|
20
|
+
*/
|
|
21
|
+
export function memoryWorkspaceBacking(_sql, _name) {
|
|
22
|
+
const files = new Map();
|
|
23
|
+
/** Immediate children of `dir`, as `readDir` reports them. */
|
|
24
|
+
const entriesUnder = (dir) => {
|
|
25
|
+
const prefix = dir === "" ? "" : `${dir}/`;
|
|
26
|
+
const seen = new Map();
|
|
27
|
+
for (const [path, content] of files) {
|
|
28
|
+
if (!path.startsWith(prefix))
|
|
29
|
+
continue;
|
|
30
|
+
const rest = path.slice(prefix.length);
|
|
31
|
+
if (rest === "")
|
|
32
|
+
continue;
|
|
33
|
+
const slash = rest.indexOf("/");
|
|
34
|
+
if (slash === -1) {
|
|
35
|
+
seen.set(path, {
|
|
36
|
+
path,
|
|
37
|
+
type: "file",
|
|
38
|
+
size: new TextEncoder().encode(content).length
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// A directory exists only because something under it does, so report it
|
|
43
|
+
// once and say nothing about its size.
|
|
44
|
+
const child = `${prefix}${rest.slice(0, slash)}`;
|
|
45
|
+
seen.set(child, { path: child, type: "directory", size: 0 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...seen.values()];
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
readFile: async (path) => files.get(normalize(path)) ?? null,
|
|
52
|
+
writeFile: async (path, content) => {
|
|
53
|
+
files.set(normalize(path), content);
|
|
54
|
+
},
|
|
55
|
+
// Directories are implied by their contents, so a path with anything under
|
|
56
|
+
// it exists even though nothing ever created it.
|
|
57
|
+
exists: async (path) => {
|
|
58
|
+
const key = normalize(path);
|
|
59
|
+
return files.has(key) || entriesUnder(key).length > 0;
|
|
60
|
+
},
|
|
61
|
+
deleteFile: async (path) => files.delete(normalize(path)),
|
|
62
|
+
readDir: async (dir) => entriesUnder(normalize(dir ?? "")),
|
|
63
|
+
getWorkspaceInfo: async () => ({ fileCount: files.size })
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Strip the leading/trailing slashes that would otherwise key two paths apart. */
|
|
67
|
+
function normalize(path) {
|
|
68
|
+
return path.replace(/^\/+|\/+$/g, "");
|
|
69
|
+
}
|
|
70
|
+
/** Per-file byte ceiling — safely under the 2 MB Durable Object SQLite row limit. */
|
|
71
|
+
export const WORKSPACE_MAX_FILE_BYTES = 512 * 1024;
|
|
72
|
+
/** Max number of files in one workspace — a cheap guard against runaway writes. */
|
|
73
|
+
export const WORKSPACE_MAX_FILES = 200;
|
|
74
|
+
export class WorkspaceLimitError extends Error {
|
|
75
|
+
constructor(message) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = "WorkspaceLimitError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build a {@link WorkspaceHandle} over a shell workspace (or a test fake),
|
|
82
|
+
* enforcing the per-file and file-count caps. The caps degrade a misbehaving
|
|
83
|
+
* recipe to an explicit error rather than letting it exceed the DO row limit or
|
|
84
|
+
* fill storage.
|
|
85
|
+
*/
|
|
86
|
+
export function makeWorkspaceHandle(ws) {
|
|
87
|
+
const byteLength = (s) => new TextEncoder().encode(s).length;
|
|
88
|
+
return {
|
|
89
|
+
read: (path) => ws.readFile(path),
|
|
90
|
+
async write(path, content) {
|
|
91
|
+
const bytes = byteLength(content);
|
|
92
|
+
if (bytes > WORKSPACE_MAX_FILE_BYTES) {
|
|
93
|
+
throw new WorkspaceLimitError(`file "${path}" is ${bytes} bytes, over the ${WORKSPACE_MAX_FILE_BYTES}-byte limit`);
|
|
94
|
+
}
|
|
95
|
+
// The cap bounds how many files exist, so it may only refuse a write that
|
|
96
|
+
// *creates* one. `exists` is the wrong question: it is true for a
|
|
97
|
+
// directory too (filesystem semantics, which every real backing has), so
|
|
98
|
+
// at the cap a write to `notes` — with `notes/a.txt` present — read as an
|
|
99
|
+
// overwrite and added a 201st file. `readFile` returning null is the only
|
|
100
|
+
// honest test for "there is no file here", and it runs solely when the
|
|
101
|
+
// workspace is already full, so the ordinary write still costs one call.
|
|
102
|
+
const { fileCount } = await ws.getWorkspaceInfo();
|
|
103
|
+
if (fileCount >= WORKSPACE_MAX_FILES &&
|
|
104
|
+
(await ws.readFile(path)) === null) {
|
|
105
|
+
throw new WorkspaceLimitError(`workspace already holds ${fileCount} files (max ${WORKSPACE_MAX_FILES})`);
|
|
106
|
+
}
|
|
107
|
+
await ws.writeFile(path, content);
|
|
108
|
+
},
|
|
109
|
+
exists: (path) => ws.exists(path),
|
|
110
|
+
remove: (path) => ws.deleteFile(path),
|
|
111
|
+
async list(dir) {
|
|
112
|
+
const entries = await ws.readDir(dir);
|
|
113
|
+
return entries.map((e) => ({
|
|
114
|
+
path: e.path,
|
|
115
|
+
type: e.type,
|
|
116
|
+
size: e.size
|
|
117
|
+
}));
|
|
118
|
+
},
|
|
119
|
+
async readJson(path) {
|
|
120
|
+
const raw = await ws.readFile(path);
|
|
121
|
+
return raw === null ? null : JSON.parse(raw);
|
|
122
|
+
},
|
|
123
|
+
async writeJson(path, value) {
|
|
124
|
+
await this.write(path, JSON.stringify(value, null, 2));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SessionMessage } from "agents/experimental/memory/session";
|
|
2
|
+
import type { SubtaskReference } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* One entry of the ephemeral, per-round reference catalog: a verbatim
|
|
5
|
+
* `user`/`assistant` turn plus the 1-based index the model selects it by.
|
|
6
|
+
* Structurally a {@link SubtaskReference} with an `index`, so selecting an entry
|
|
7
|
+
* and snapshotting it onto a Subtask is a plain subset copy.
|
|
8
|
+
*/
|
|
9
|
+
export interface ReferenceCatalogEntry extends SubtaskReference {
|
|
10
|
+
/** 1-based, ephemeral, per-round index the model references. */
|
|
11
|
+
index: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether a history message is a referenceable turn: a verbatim `user` or
|
|
15
|
+
* `assistant` turn with actual content.
|
|
16
|
+
*
|
|
17
|
+
* Compaction summaries are excluded via the SDK's `isCompactionMessage` (their
|
|
18
|
+
* `compaction_` id prefix) — they are generated text, never original conversation
|
|
19
|
+
* evidence. Whitespace-only turns are excluded because there is nothing to
|
|
20
|
+
* reference.
|
|
21
|
+
*
|
|
22
|
+
* This is the single eligibility rule, and it has exactly one caller:
|
|
23
|
+
* {@link file://../round/turn.ts renderTurnMessages} numbers the messages it accepts
|
|
24
|
+
* *and* marks those same messages with their `[ref N]` index, in one pass. One
|
|
25
|
+
* predicate, one walk — the marked messages and the catalog indices cannot drift.
|
|
26
|
+
*
|
|
27
|
+
* What that leaves eligible: verbatim `user` and `assistant` turns with content.
|
|
28
|
+
* System prompts, context blocks, and anything a tool injected never appear as
|
|
29
|
+
* plain history messages, so they are excluded structurally. Whitespace-only
|
|
30
|
+
* turns are skipped — there is nothing to reference, and since the model only
|
|
31
|
+
* ever selects from the catalog the renderer returns, skipping them cannot
|
|
32
|
+
* misalign indices.
|
|
33
|
+
*
|
|
34
|
+
* The inbound user turn is appended to the Session before the round infers, so
|
|
35
|
+
* one catalog covers both the current task input and past turns. The selected
|
|
36
|
+
* entries' exact text is snapshotted onto the Subtask when the round delegates,
|
|
37
|
+
* so nothing is resolved lazily and later compaction cannot affect a Subtask in
|
|
38
|
+
* flight.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isCatalogEligible(message: SessionMessage): boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isCompactionMessage } from "agents/experimental/memory/utils";
|
|
2
|
+
import { sessionText } from "../agent/history.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether a history message is a referenceable turn: a verbatim `user` or
|
|
5
|
+
* `assistant` turn with actual content.
|
|
6
|
+
*
|
|
7
|
+
* Compaction summaries are excluded via the SDK's `isCompactionMessage` (their
|
|
8
|
+
* `compaction_` id prefix) — they are generated text, never original conversation
|
|
9
|
+
* evidence. Whitespace-only turns are excluded because there is nothing to
|
|
10
|
+
* reference.
|
|
11
|
+
*
|
|
12
|
+
* This is the single eligibility rule, and it has exactly one caller:
|
|
13
|
+
* {@link file://../round/turn.ts renderTurnMessages} numbers the messages it accepts
|
|
14
|
+
* *and* marks those same messages with their `[ref N]` index, in one pass. One
|
|
15
|
+
* predicate, one walk — the marked messages and the catalog indices cannot drift.
|
|
16
|
+
*
|
|
17
|
+
* What that leaves eligible: verbatim `user` and `assistant` turns with content.
|
|
18
|
+
* System prompts, context blocks, and anything a tool injected never appear as
|
|
19
|
+
* plain history messages, so they are excluded structurally. Whitespace-only
|
|
20
|
+
* turns are skipped — there is nothing to reference, and since the model only
|
|
21
|
+
* ever selects from the catalog the renderer returns, skipping them cannot
|
|
22
|
+
* misalign indices.
|
|
23
|
+
*
|
|
24
|
+
* The inbound user turn is appended to the Session before the round infers, so
|
|
25
|
+
* one catalog covers both the current task input and past turns. The selected
|
|
26
|
+
* entries' exact text is snapshotted onto the Subtask when the round delegates,
|
|
27
|
+
* so nothing is resolved lazily and later compaction cannot affect a Subtask in
|
|
28
|
+
* flight.
|
|
29
|
+
*/
|
|
30
|
+
export function isCatalogEligible(message) {
|
|
31
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
32
|
+
return false;
|
|
33
|
+
if (isCompactionMessage(message))
|
|
34
|
+
return false;
|
|
35
|
+
return sessionText(message).trim().length > 0;
|
|
36
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ReferenceCatalogEntry } from "./catalog.js";
|
|
3
|
+
import { type SubtaskTypeRegistry } from "./subtask-types.js";
|
|
4
|
+
import type { DecompositionProposal, SubtaskDraft } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Pure validation and resolution of a `delegate` call's input. No model, no
|
|
7
|
+
* Session, no database — given a {@link DecompositionProposal} and the ephemeral
|
|
8
|
+
* reference catalog it was generated against, this either produces the drafts to
|
|
9
|
+
* persist or throws.
|
|
10
|
+
*
|
|
11
|
+
* One invariant lives here: **the model selects references by index only.** It
|
|
12
|
+
* emits catalog indices; this module copies the catalog entry's exact role+text
|
|
13
|
+
* onto the draft. Model output never becomes reference text, so a Subtask cannot
|
|
14
|
+
* carry a rewritten, summarized, or fabricated "quote" of the conversation.
|
|
15
|
+
*
|
|
16
|
+
* A proposal's subtasks are **independent of one another**: they all run at once
|
|
17
|
+
* and none can read another's output, so there is no graph to validate and
|
|
18
|
+
* nothing to order. Sequencing is expressed across rounds instead — the main
|
|
19
|
+
* agent delegates, reads the results, and delegates the next step.
|
|
20
|
+
*
|
|
21
|
+
* Invalid output is never repaired: a throw fails the attempt, which falls back to
|
|
22
|
+
* the other model, and two failed attempts fail the parent Task. Silently
|
|
23
|
+
* synthesizing a general Subtask would deliver plausible work the user never asked
|
|
24
|
+
* for.
|
|
25
|
+
*/
|
|
26
|
+
/** A model proposal that cannot be resolved into a valid decomposition. */
|
|
27
|
+
export declare class DecompositionValidationError extends Error {
|
|
28
|
+
constructor(message: string);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A required string that is not whitespace.
|
|
32
|
+
*
|
|
33
|
+
* Written as a `.regex()` rather than the equivalent `.refine()`, deliberately.
|
|
34
|
+
* Both are enforced wherever this schema is actually run, but a refinement is
|
|
35
|
+
* **invisible to the model**: custom checks do not survive JSON-Schema conversion,
|
|
36
|
+
* so the rule would be one the model is held to without ever being shown it.
|
|
37
|
+
* `/\S/` converts to `"pattern": "\\S"` next to `"minLength": 1`, so the
|
|
38
|
+
* constraint reaches the model as part of the tool schema, on every field, with
|
|
39
|
+
* no per-field `.describe()` needed to restate it.
|
|
40
|
+
*
|
|
41
|
+
* Being shown a rule is not the same as being held to it. This schema reaches
|
|
42
|
+
* the model through a **control** tool, which has no `execute`, so the SDK never
|
|
43
|
+
* validates its input — what enforces the constraint is
|
|
44
|
+
* {@link file://../agent/control.ts control.ts}, which parses every control call
|
|
45
|
+
* with the tool's own schema before the round uses it.
|
|
46
|
+
*/
|
|
47
|
+
export declare const nonBlank: (label: string) => z.ZodString;
|
|
48
|
+
/**
|
|
49
|
+
* The `delegate` tool's input schema. The per-round bound is enforced here
|
|
50
|
+
* (the model is told it, and the SDK rejects a call that breaks it) and again in
|
|
51
|
+
* the data layer, which owns the durable invariant.
|
|
52
|
+
*
|
|
53
|
+
* Blank strings are rejected at the schema edge rather than deep in execution: an
|
|
54
|
+
* empty `prompt` would otherwise burn a Subtask slot and only fail later, inside
|
|
55
|
+
* the child, with no useful diagnostic.
|
|
56
|
+
*
|
|
57
|
+
* `referenceIndexes` is optional because this one schema also has to describe the
|
|
58
|
+
* calls **reconstructed from durable rows** in later rounds, whose references
|
|
59
|
+
* were resolved to verbatim snapshots at the time and no longer have indices.
|
|
60
|
+
*/
|
|
61
|
+
export declare function makeDecompositionProposalSchema(types: SubtaskTypeRegistry, maxSubtasks: number): z.ZodObject<{
|
|
62
|
+
reply: z.ZodString;
|
|
63
|
+
subtasks: z.ZodArray<z.ZodObject<{
|
|
64
|
+
type: z.ZodEnum<{
|
|
65
|
+
[x: string]: string;
|
|
66
|
+
}>;
|
|
67
|
+
prompt: z.ZodString;
|
|
68
|
+
referenceIndexes: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
69
|
+
params: z.ZodOptional<z.ZodObject<{
|
|
70
|
+
[x: string]: z.ZodType<string | undefined, unknown, z.core.$ZodTypeInternals<string | undefined, unknown>>;
|
|
71
|
+
}, z.core.$strip>>;
|
|
72
|
+
}, z.core.$strip>>;
|
|
73
|
+
}, z.core.$strip>;
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a validated model proposal into the drafts to persist.
|
|
76
|
+
*
|
|
77
|
+
* Throws {@link DecompositionValidationError} on any structural problem: blank
|
|
78
|
+
* fields, unknown or duplicate reference indices, and params a type refuses. On
|
|
79
|
+
* success, array order is preserved — the data layer derives each Subtask's
|
|
80
|
+
* `ordinal` from it.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveDecomposition(proposal: DecompositionProposal, catalog: ReferenceCatalogEntry[], types: SubtaskTypeRegistry): {
|
|
83
|
+
reply: string;
|
|
84
|
+
drafts: SubtaskDraft[];
|
|
85
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SubtaskParamsError } from "./subtask-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pure validation and resolution of a `delegate` call's input. No model, no
|
|
5
|
+
* Session, no database — given a {@link DecompositionProposal} and the ephemeral
|
|
6
|
+
* reference catalog it was generated against, this either produces the drafts to
|
|
7
|
+
* persist or throws.
|
|
8
|
+
*
|
|
9
|
+
* One invariant lives here: **the model selects references by index only.** It
|
|
10
|
+
* emits catalog indices; this module copies the catalog entry's exact role+text
|
|
11
|
+
* onto the draft. Model output never becomes reference text, so a Subtask cannot
|
|
12
|
+
* carry a rewritten, summarized, or fabricated "quote" of the conversation.
|
|
13
|
+
*
|
|
14
|
+
* A proposal's subtasks are **independent of one another**: they all run at once
|
|
15
|
+
* and none can read another's output, so there is no graph to validate and
|
|
16
|
+
* nothing to order. Sequencing is expressed across rounds instead — the main
|
|
17
|
+
* agent delegates, reads the results, and delegates the next step.
|
|
18
|
+
*
|
|
19
|
+
* Invalid output is never repaired: a throw fails the attempt, which falls back to
|
|
20
|
+
* the other model, and two failed attempts fail the parent Task. Silently
|
|
21
|
+
* synthesizing a general Subtask would deliver plausible work the user never asked
|
|
22
|
+
* for.
|
|
23
|
+
*/
|
|
24
|
+
/** A model proposal that cannot be resolved into a valid decomposition. */
|
|
25
|
+
export class DecompositionValidationError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "DecompositionValidationError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A required string that is not whitespace.
|
|
33
|
+
*
|
|
34
|
+
* Written as a `.regex()` rather than the equivalent `.refine()`, deliberately.
|
|
35
|
+
* Both are enforced wherever this schema is actually run, but a refinement is
|
|
36
|
+
* **invisible to the model**: custom checks do not survive JSON-Schema conversion,
|
|
37
|
+
* so the rule would be one the model is held to without ever being shown it.
|
|
38
|
+
* `/\S/` converts to `"pattern": "\\S"` next to `"minLength": 1`, so the
|
|
39
|
+
* constraint reaches the model as part of the tool schema, on every field, with
|
|
40
|
+
* no per-field `.describe()` needed to restate it.
|
|
41
|
+
*
|
|
42
|
+
* Being shown a rule is not the same as being held to it. This schema reaches
|
|
43
|
+
* the model through a **control** tool, which has no `execute`, so the SDK never
|
|
44
|
+
* validates its input — what enforces the constraint is
|
|
45
|
+
* {@link file://../agent/control.ts control.ts}, which parses every control call
|
|
46
|
+
* with the tool's own schema before the round uses it.
|
|
47
|
+
*/
|
|
48
|
+
export const nonBlank = (label) => z
|
|
49
|
+
.string()
|
|
50
|
+
.min(1)
|
|
51
|
+
.regex(/\S/, { message: `${label} must not be blank` });
|
|
52
|
+
function makeSubtaskProposalSchema(types) {
|
|
53
|
+
return z.object({
|
|
54
|
+
// A closed enum, not prose: an invented type is rejected by the tool schema
|
|
55
|
+
// itself rather than silently resolving to some default recipe.
|
|
56
|
+
type: z.enum(types.enumKeys()),
|
|
57
|
+
prompt: nonBlank("prompt"),
|
|
58
|
+
referenceIndexes: z.array(z.number().int().min(1)).optional(),
|
|
59
|
+
/**
|
|
60
|
+
* The type's required inputs — ids the model quotes from a tool result.
|
|
61
|
+
* Every key any type declares is named here, gathered from those types by
|
|
62
|
+
* `SubtaskTypeRegistry.paramProperties`; which of them a given type actually
|
|
63
|
+
* *requires* is the per-type contract, checked below.
|
|
64
|
+
*
|
|
65
|
+
* Named keys rather than a free-form record, deliberately: a record's value
|
|
66
|
+
* schema is the JSON Schema `additionalProperties` slot, which the AI SDK's
|
|
67
|
+
* strict-mode pass overwrites with `false` — turning "any string key" into
|
|
68
|
+
* "no key at all" and leaving the model no legal way to send params it was
|
|
69
|
+
* told to send. Explicit properties survive that pass, and say more besides.
|
|
70
|
+
*/
|
|
71
|
+
params: z.object(types.paramProperties()).optional()
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The `delegate` tool's input schema. The per-round bound is enforced here
|
|
76
|
+
* (the model is told it, and the SDK rejects a call that breaks it) and again in
|
|
77
|
+
* the data layer, which owns the durable invariant.
|
|
78
|
+
*
|
|
79
|
+
* Blank strings are rejected at the schema edge rather than deep in execution: an
|
|
80
|
+
* empty `prompt` would otherwise burn a Subtask slot and only fail later, inside
|
|
81
|
+
* the child, with no useful diagnostic.
|
|
82
|
+
*
|
|
83
|
+
* `referenceIndexes` is optional because this one schema also has to describe the
|
|
84
|
+
* calls **reconstructed from durable rows** in later rounds, whose references
|
|
85
|
+
* were resolved to verbatim snapshots at the time and no longer have indices.
|
|
86
|
+
*/
|
|
87
|
+
export function makeDecompositionProposalSchema(types, maxSubtasks) {
|
|
88
|
+
return z.object({
|
|
89
|
+
reply: nonBlank("reply"),
|
|
90
|
+
subtasks: z.array(makeSubtaskProposalSchema(types)).min(1).max(maxSubtasks)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Snapshot the selected catalog entries onto a draft: validate every index against
|
|
95
|
+
* the catalog, reject duplicates, and copy each entry's exact role+text. Indexes
|
|
96
|
+
* are stored ascending so a Subtask's references read in conversation order
|
|
97
|
+
* regardless of the order the model listed them.
|
|
98
|
+
*
|
|
99
|
+
* `label` names the offending subtask by its 1-based position in the proposal —
|
|
100
|
+
* the only handle a subtask has, and the one the model can map back to what it
|
|
101
|
+
* just wrote.
|
|
102
|
+
*/
|
|
103
|
+
function resolveReferences(label, referenceIndexes, catalog) {
|
|
104
|
+
if (!referenceIndexes)
|
|
105
|
+
return [];
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
for (const index of referenceIndexes) {
|
|
108
|
+
if (seen.has(index)) {
|
|
109
|
+
throw new DecompositionValidationError(`${label} references index ${index} more than once`);
|
|
110
|
+
}
|
|
111
|
+
seen.add(index);
|
|
112
|
+
if (index > catalog.length) {
|
|
113
|
+
throw new DecompositionValidationError(`${label} references unknown catalog index ${index} ` +
|
|
114
|
+
`(catalog has ${catalog.length} ${catalog.length === 1 ? "entry" : "entries"})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...referenceIndexes]
|
|
118
|
+
.sort((a, b) => a - b)
|
|
119
|
+
.map((index) => {
|
|
120
|
+
// The catalog is 1-based; copy role+text verbatim (never the model's words).
|
|
121
|
+
const entry = catalog[index - 1];
|
|
122
|
+
return { role: entry.role, text: entry.text };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolve a validated model proposal into the drafts to persist.
|
|
127
|
+
*
|
|
128
|
+
* Throws {@link DecompositionValidationError} on any structural problem: blank
|
|
129
|
+
* fields, unknown or duplicate reference indices, and params a type refuses. On
|
|
130
|
+
* success, array order is preserved — the data layer derives each Subtask's
|
|
131
|
+
* `ordinal` from it.
|
|
132
|
+
*/
|
|
133
|
+
export function resolveDecomposition(proposal, catalog, types) {
|
|
134
|
+
const drafts = proposal.subtasks.map((s, index) => {
|
|
135
|
+
const label = `subtask ${index + 1}`;
|
|
136
|
+
const type = s.type.trim();
|
|
137
|
+
let params;
|
|
138
|
+
try {
|
|
139
|
+
// Shape only. Whether an id names a row that exists — and is still usable —
|
|
140
|
+
// is a question for durable state, answered when the execution starts.
|
|
141
|
+
params = types.validateParams(type, s.params);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (!(err instanceof SubtaskParamsError))
|
|
145
|
+
throw err;
|
|
146
|
+
throw new DecompositionValidationError(`${label}: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
type,
|
|
150
|
+
prompt: s.prompt.trim(),
|
|
151
|
+
references: resolveReferences(label, s.referenceIndexes, catalog),
|
|
152
|
+
params
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
return { reply: proposal.reply.trim(), drafts };
|
|
156
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type Tool } from "ai";
|
|
2
|
+
import type { SubtaskTypeRegistry } from "./subtask-types.js";
|
|
3
|
+
import type { CompositionBranch, DecompositionProposal, SubtaskId, SubtaskStatus } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The `delegate` tool — the single act by which the main agent hands work to
|
|
6
|
+
* subagents, and the shape both ends of that act agree on.
|
|
7
|
+
*
|
|
8
|
+
* Delegation is a **durable tool call**: a round emits it for real (the model
|
|
9
|
+
* picks it and fills its input), the Workflow performs it over minutes or hours,
|
|
10
|
+
* and a later round reassembles the call with its result to keep working. The two
|
|
11
|
+
* halves are separated by a Workflow boundary, not by a fiction — nothing here is
|
|
12
|
+
* fabricated on the model's behalf.
|
|
13
|
+
*
|
|
14
|
+
* This module owns the tool's identity because every round depends on it
|
|
15
|
+
* agreeing: a later round pairs a synthesized `tool-result` to an earlier round's
|
|
16
|
+
* `tool-call` by name and id, and a mismatch would not throw — it would silently
|
|
17
|
+
* produce a malformed history that the model quietly misreads.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DELEGATE_TOOL_NAME = "delegate";
|
|
20
|
+
/**
|
|
21
|
+
* The tool as the model sees it. Deliberately **without `execute`**: the Workflow
|
|
22
|
+
* performs this call, durably, outside the inference — so there is nothing for the
|
|
23
|
+
* SDK to run, and the tool loop halts on the call rather than trying to continue
|
|
24
|
+
* past it. That is what makes it a *control* tool: unlike the agent's work tools
|
|
25
|
+
* (`set_context`, and whatever the installed plugins offer), calling it ends the
|
|
26
|
+
* round.
|
|
27
|
+
*
|
|
28
|
+
* Its `inputSchema` is the delegation contract itself, and it is the **only**
|
|
29
|
+
* declaration of this tool. One schema has to serve both directions — the calls
|
|
30
|
+
* the model emits now, and the calls reconstructed from durable rows in later
|
|
31
|
+
* rounds — because a provider cannot be shown two shapes for one tool name in a
|
|
32
|
+
* single request. See {@link delegateCallInput} for how a durable row is rendered
|
|
33
|
+
* back into it.
|
|
34
|
+
*
|
|
35
|
+
* Built per registry rather than declared as a module constant: both the type
|
|
36
|
+
* enum in its schema and the catalogue in its description are facts about which
|
|
37
|
+
* plugins are installed, and in the predecessor repo both were frozen at import
|
|
38
|
+
* time — which is exactly what made the type set unoverridable.
|
|
39
|
+
*/
|
|
40
|
+
export declare function makeDelegateTool(types: SubtaskTypeRegistry, maxSubtasks: number): Tool;
|
|
41
|
+
/**
|
|
42
|
+
* The call's id, derived from the parent Task and the round that emitted it —
|
|
43
|
+
* deterministic and replay-safe, the same discipline as the Session message ids
|
|
44
|
+
* it sits alongside (see {@link file://../agent/history.ts}). Later rounds rebuild it
|
|
45
|
+
* rather than storing it.
|
|
46
|
+
*
|
|
47
|
+
* **Underscores, not colons, and this is load-bearing.** Unlike a Session message
|
|
48
|
+
* id, this one is sent to a provider as a `tool_use.id`, and Anthropic validates
|
|
49
|
+
* that field against `^[a-zA-Z0-9_-]+$`. The colon-separated form this used to
|
|
50
|
+
* return failed every round from the first delegation onwards — round 0 was fine
|
|
51
|
+
* because the model authors its own ids, and round 1 reconstructs this one, so
|
|
52
|
+
* the request 400d deterministically on both the primary and the fallback until
|
|
53
|
+
* the deterministic join fired. Workers AI never validated the field, which is
|
|
54
|
+
* why it took a Claude-backed agent to surface it.
|
|
55
|
+
*
|
|
56
|
+
* Nothing persists this: both halves of the pair are rebuilt together on every
|
|
57
|
+
* request, so changing the shape needs no migration. There is no longer a
|
|
58
|
+
* provider-side backstop in core — the adapter that carried one went with
|
|
59
|
+
* `./anthropic` in 0.8.0 — so a provider added here that validates tool-call ids
|
|
60
|
+
* needs to sanitize them on its own way out.
|
|
61
|
+
*/
|
|
62
|
+
export declare function delegateToolCallId(taskId: string, round: number): string;
|
|
63
|
+
/**
|
|
64
|
+
* One branch's outcome, as the tool result carries it. `output` carries the
|
|
65
|
+
* branch's report when it completed and its failure reason when it did not.
|
|
66
|
+
*
|
|
67
|
+
* **A failed branch says why, and that is a reversal worth explaining.** This
|
|
68
|
+
* used to be `null` for anything that did not complete, on the principle that
|
|
69
|
+
* internal diagnostics never reach the model. The principle was right about
|
|
70
|
+
* *diagnostics* and wrong about this field: what a facet writes into `error` is
|
|
71
|
+
* not a stack trace, it is a sentence addressed to the delegating model —
|
|
72
|
+
* "there is no checkout in this workspace yet… clone the repository before
|
|
73
|
+
* delegating", "every credential has reached its limit; send this request again
|
|
74
|
+
* after that". Withholding those left the parent with `status: "failed"` and
|
|
75
|
+
* nothing else, and a parent that cannot tell a transient failure from a
|
|
76
|
+
* permanent one retries. In the run that prompted this it retried twelve times
|
|
77
|
+
* over nine minutes, then apologised to the user for a wall it was never shown.
|
|
78
|
+
*
|
|
79
|
+
* What replaces the old rule is a constraint on the writer rather than a filter
|
|
80
|
+
* here: **an `error` is model-visible, so a facet must write it in words that
|
|
81
|
+
* are safe for one to read and act on.** Bounded by {@link MAX_OUTPUT_CHARS} on
|
|
82
|
+
* the way through, because a facet that ignores that is a context-window
|
|
83
|
+
* problem rather than a disclosure one.
|
|
84
|
+
*
|
|
85
|
+
* Still one field, not two. The model's question is "what came back from this
|
|
86
|
+
* branch", and `status` already says which kind of answer it is getting.
|
|
87
|
+
*
|
|
88
|
+
* A type alias, not an interface: this is serialized as the tool result's
|
|
89
|
+
* `JSONValue`, and only aliases get the implicit index signature that satisfies.
|
|
90
|
+
*/
|
|
91
|
+
export type DelegateSubtaskOutcome = {
|
|
92
|
+
subtaskId: SubtaskId;
|
|
93
|
+
type: string;
|
|
94
|
+
status: SubtaskStatus;
|
|
95
|
+
output: string | null;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Rebuild one round's call input from its durable rows, in stable ordinal order.
|
|
99
|
+
* Typed as {@link DecompositionProposal} — the same type the model's own calls
|
|
100
|
+
* are validated into — so the reconstructed call and an emitted one cannot drift
|
|
101
|
+
* apart in shape.
|
|
102
|
+
*
|
|
103
|
+
* `referenceIndexes` is omitted rather than faked: this round's references were
|
|
104
|
+
* snapshotted verbatim onto the rows when it ran, and the catalog they were
|
|
105
|
+
* chosen from is long gone.
|
|
106
|
+
*
|
|
107
|
+
* A subtask proposal carries no identifier of its own, so the model pairs each
|
|
108
|
+
* entry here with its outcome **by position**: both arrays are built from the
|
|
109
|
+
* same ordinal-ordered `branches`, and the outcome additionally repeats `type`
|
|
110
|
+
* and carries the durable `subtaskId`.
|
|
111
|
+
*/
|
|
112
|
+
export declare function delegateCallInput(reply: string, branches: CompositionBranch[]): DecompositionProposal;
|
|
113
|
+
/**
|
|
114
|
+
* Rebuild one round's call result from its durable rows, in stable ordinal order.
|
|
115
|
+
*
|
|
116
|
+
* A completed branch reports its parts; any other branch reports its `error`, or
|
|
117
|
+
* `null` when it has none to give — a cancelled branch usually does not, and
|
|
118
|
+
* inventing a sentence for it would be worse than the absence.
|
|
119
|
+
*/
|
|
120
|
+
export declare function delegateCallOutput(branches: CompositionBranch[]): DelegateSubtaskOutcome[];
|