@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.
Files changed (168) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +522 -0
  3. package/dist/a2a/agent-stub.d.ts +60 -0
  4. package/dist/a2a/agent-stub.js +1 -0
  5. package/dist/a2a/caller-token.d.ts +44 -0
  6. package/dist/a2a/caller-token.js +61 -0
  7. package/dist/a2a/caller.d.ts +23 -0
  8. package/dist/a2a/caller.js +33 -0
  9. package/dist/a2a/card.d.ts +158 -0
  10. package/dist/a2a/card.js +163 -0
  11. package/dist/a2a/context.d.ts +19 -0
  12. package/dist/a2a/context.js +61 -0
  13. package/dist/a2a/deliver.d.ts +159 -0
  14. package/dist/a2a/deliver.js +185 -0
  15. package/dist/a2a/executor.d.ts +84 -0
  16. package/dist/a2a/executor.js +106 -0
  17. package/dist/a2a/index.d.ts +35 -0
  18. package/dist/a2a/index.js +34 -0
  19. package/dist/a2a/notify.d.ts +106 -0
  20. package/dist/a2a/notify.js +180 -0
  21. package/dist/a2a/parts.d.ts +45 -0
  22. package/dist/a2a/parts.js +94 -0
  23. package/dist/a2a/push.d.ts +70 -0
  24. package/dist/a2a/push.js +53 -0
  25. package/dist/a2a/self-origin.d.ts +91 -0
  26. package/dist/a2a/self-origin.js +114 -0
  27. package/dist/a2a/task-store.d.ts +44 -0
  28. package/dist/a2a/task-store.js +99 -0
  29. package/dist/a2a/task.d.ts +85 -0
  30. package/dist/a2a/task.js +15 -0
  31. package/dist/a2a/verify.d.ts +80 -0
  32. package/dist/a2a/verify.js +143 -0
  33. package/dist/agent/budget.d.ts +46 -0
  34. package/dist/agent/budget.js +42 -0
  35. package/dist/agent/control.d.ts +109 -0
  36. package/dist/agent/control.js +115 -0
  37. package/dist/agent/errors.d.ts +85 -0
  38. package/dist/agent/errors.js +64 -0
  39. package/dist/agent/final-reply.d.ts +49 -0
  40. package/dist/agent/final-reply.js +68 -0
  41. package/dist/agent/history.d.ts +97 -0
  42. package/dist/agent/history.js +133 -0
  43. package/dist/agent/index.d.ts +29 -0
  44. package/dist/agent/index.js +29 -0
  45. package/dist/agent/inference.d.ts +110 -0
  46. package/dist/agent/inference.js +120 -0
  47. package/dist/agent/model.d.ts +90 -0
  48. package/dist/agent/model.js +1 -0
  49. package/dist/agent/session.d.ts +100 -0
  50. package/dist/agent/session.js +82 -0
  51. package/dist/agent/workers-ai/index.d.ts +23 -0
  52. package/dist/agent/workers-ai/index.js +23 -0
  53. package/dist/agent/workers-ai/runtime.d.ts +42 -0
  54. package/dist/agent/workers-ai/runtime.js +63 -0
  55. package/dist/alarm/index.d.ts +77 -0
  56. package/dist/alarm/index.js +116 -0
  57. package/dist/config.d.ts +202 -0
  58. package/dist/config.js +135 -0
  59. package/dist/contract/index.d.ts +9 -0
  60. package/dist/contract/index.js +8 -0
  61. package/dist/contract/plugin.d.ts +324 -0
  62. package/dist/contract/plugin.js +114 -0
  63. package/dist/contract/recipe.d.ts +180 -0
  64. package/dist/contract/recipe.js +1 -0
  65. package/dist/contract/validation.d.ts +91 -0
  66. package/dist/contract/validation.js +84 -0
  67. package/dist/db/db.d.ts +147 -0
  68. package/dist/db/db.js +90 -0
  69. package/dist/db/index.d.ts +8 -0
  70. package/dist/db/index.js +8 -0
  71. package/dist/db/migrations/index.d.ts +20 -0
  72. package/dist/db/migrations/index.js +60 -0
  73. package/dist/db/models/subtasks.d.ts +100 -0
  74. package/dist/db/models/subtasks.js +241 -0
  75. package/dist/db/models/tasks.d.ts +118 -0
  76. package/dist/db/models/tasks.js +274 -0
  77. package/dist/db/schema.d.ts +468 -0
  78. package/dist/db/schema.js +88 -0
  79. package/dist/env.d.ts +53 -0
  80. package/dist/env.js +47 -0
  81. package/dist/host/agent.d.ts +305 -0
  82. package/dist/host/agent.js +400 -0
  83. package/dist/host/index.d.ts +20 -0
  84. package/dist/host/index.js +19 -0
  85. package/dist/host/plugin-host.d.ts +42 -0
  86. package/dist/host/plugin-host.js +1 -0
  87. package/dist/index.d.ts +19 -0
  88. package/dist/index.js +17 -0
  89. package/dist/job/index.d.ts +21 -0
  90. package/dist/job/index.js +21 -0
  91. package/dist/job/lifecycle.d.ts +176 -0
  92. package/dist/job/lifecycle.js +230 -0
  93. package/dist/job/state.d.ts +92 -0
  94. package/dist/job/state.js +40 -0
  95. package/dist/platform.d.ts +138 -0
  96. package/dist/platform.js +140 -0
  97. package/dist/round/agent.d.ts +271 -0
  98. package/dist/round/agent.js +678 -0
  99. package/dist/round/index.d.ts +25 -0
  100. package/dist/round/index.js +23 -0
  101. package/dist/round/policy.d.ts +98 -0
  102. package/dist/round/policy.js +1 -0
  103. package/dist/round/subagent.d.ts +87 -0
  104. package/dist/round/subagent.js +108 -0
  105. package/dist/round/turn.d.ts +249 -0
  106. package/dist/round/turn.js +564 -0
  107. package/dist/round/workflow.d.ts +147 -0
  108. package/dist/round/workflow.js +387 -0
  109. package/dist/runtime/index.d.ts +127 -0
  110. package/dist/runtime/index.js +186 -0
  111. package/dist/runtime/tool-families.d.ts +32 -0
  112. package/dist/runtime/tool-families.js +61 -0
  113. package/dist/subagent/fingerprint.d.ts +37 -0
  114. package/dist/subagent/fingerprint.js +92 -0
  115. package/dist/subagent/index.d.ts +169 -0
  116. package/dist/subagent/index.js +330 -0
  117. package/dist/subagent/prompt.d.ts +32 -0
  118. package/dist/subagent/prompt.js +37 -0
  119. package/dist/subagent/run.d.ts +157 -0
  120. package/dist/subagent/run.js +540 -0
  121. package/dist/subagent/workspace.d.ts +85 -0
  122. package/dist/subagent/workspace.js +127 -0
  123. package/dist/subtasks/catalog.d.ts +40 -0
  124. package/dist/subtasks/catalog.js +36 -0
  125. package/dist/subtasks/decomposition.d.ts +85 -0
  126. package/dist/subtasks/decomposition.js +156 -0
  127. package/dist/subtasks/delegate.d.ts +120 -0
  128. package/dist/subtasks/delegate.js +131 -0
  129. package/dist/subtasks/index.d.ts +9 -0
  130. package/dist/subtasks/index.js +9 -0
  131. package/dist/subtasks/subtask-types.d.ts +91 -0
  132. package/dist/subtasks/subtask-types.js +103 -0
  133. package/dist/subtasks/types.d.ts +295 -0
  134. package/dist/subtasks/types.js +15 -0
  135. package/dist/testing/auth.d.ts +34 -0
  136. package/dist/testing/auth.js +35 -0
  137. package/dist/testing/do.d.ts +29 -0
  138. package/dist/testing/do.js +25 -0
  139. package/dist/testing/fake-session.d.ts +26 -0
  140. package/dist/testing/fake-session.js +37 -0
  141. package/dist/testing/fixtures.d.ts +64 -0
  142. package/dist/testing/fixtures.js +104 -0
  143. package/dist/testing/harness.d.ts +97 -0
  144. package/dist/testing/harness.js +138 -0
  145. package/dist/testing/index.d.ts +31 -0
  146. package/dist/testing/index.js +35 -0
  147. package/dist/testing/mock-model.d.ts +77 -0
  148. package/dist/testing/mock-model.js +136 -0
  149. package/dist/testing/node.d.ts +56 -0
  150. package/dist/testing/node.js +56 -0
  151. package/dist/testing/vcr-global-setup.d.ts +12 -0
  152. package/dist/testing/vcr-global-setup.js +15 -0
  153. package/dist/testing/vcr-shared.d.ts +38 -0
  154. package/dist/testing/vcr-shared.js +33 -0
  155. package/dist/testing/vcr-spec.d.ts +25 -0
  156. package/dist/testing/vcr-spec.js +124 -0
  157. package/dist/testing/vcr-store.d.ts +86 -0
  158. package/dist/testing/vcr-store.js +191 -0
  159. package/dist/testing/vcr.d.ts +117 -0
  160. package/dist/testing/vcr.js +275 -0
  161. package/dist/worker/define-agent.d.ts +123 -0
  162. package/dist/worker/define-agent.js +20 -0
  163. package/dist/worker/index.d.ts +218 -0
  164. package/dist/worker/index.js +369 -0
  165. package/eslint-rules/index.js +31 -0
  166. package/eslint-rules/no-deprecated-object-properties.js +81 -0
  167. package/package.json +178 -0
  168. 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[];