@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,131 @@
1
+ import { tool } from "ai";
2
+ import { makeDecompositionProposalSchema } from "./decomposition.js";
3
+ /**
4
+ * The `delegate` tool — the single act by which the main agent hands work to
5
+ * subagents, and the shape both ends of that act agree on.
6
+ *
7
+ * Delegation is a **durable tool call**: a round emits it for real (the model
8
+ * picks it and fills its input), the Workflow performs it over minutes or hours,
9
+ * and a later round reassembles the call with its result to keep working. The two
10
+ * halves are separated by a Workflow boundary, not by a fiction — nothing here is
11
+ * fabricated on the model's behalf.
12
+ *
13
+ * This module owns the tool's identity because every round depends on it
14
+ * agreeing: a later round pairs a synthesized `tool-result` to an earlier round's
15
+ * `tool-call` by name and id, and a mismatch would not throw — it would silently
16
+ * produce a malformed history that the model quietly misreads.
17
+ */
18
+ export const DELEGATE_TOOL_NAME = "delegate";
19
+ /**
20
+ * The tool as the model sees it. Deliberately **without `execute`**: the Workflow
21
+ * performs this call, durably, outside the inference — so there is nothing for the
22
+ * SDK to run, and the tool loop halts on the call rather than trying to continue
23
+ * past it. That is what makes it a *control* tool: unlike the agent's work tools
24
+ * (`set_context`, and whatever the installed plugins offer), calling it ends the
25
+ * round.
26
+ *
27
+ * Its `inputSchema` is the delegation contract itself, and it is the **only**
28
+ * declaration of this tool. One schema has to serve both directions — the calls
29
+ * the model emits now, and the calls reconstructed from durable rows in later
30
+ * rounds — because a provider cannot be shown two shapes for one tool name in a
31
+ * single request. See {@link delegateCallInput} for how a durable row is rendered
32
+ * back into it.
33
+ *
34
+ * Built per registry rather than declared as a module constant: both the type
35
+ * enum in its schema and the catalogue in its description are facts about which
36
+ * plugins are installed, and in the predecessor repo both were frozen at import
37
+ * time — which is exactly what made the type set unoverridable.
38
+ */
39
+ export function makeDelegateTool(types, maxSubtasks) {
40
+ return tool({
41
+ description: "Delegate part of the user's request to isolated subagents and acknowledge it. Their results return to you, and you then decide what to do next — answer the user, or delegate again.\n\n" +
42
+ "Every subtask must name one of these types, and supply the params that type requires:\n" +
43
+ types.renderTypes(),
44
+ inputSchema: makeDecompositionProposalSchema(types, maxSubtasks)
45
+ });
46
+ }
47
+ /**
48
+ * The call's id, derived from the parent Task and the round that emitted it —
49
+ * deterministic and replay-safe, the same discipline as the Session message ids
50
+ * it sits alongside (see {@link file://../agent/history.ts}). Later rounds rebuild it
51
+ * rather than storing it.
52
+ *
53
+ * **Underscores, not colons, and this is load-bearing.** Unlike a Session message
54
+ * id, this one is sent to a provider as a `tool_use.id`, and Anthropic validates
55
+ * that field against `^[a-zA-Z0-9_-]+$`. The colon-separated form this used to
56
+ * return failed every round from the first delegation onwards — round 0 was fine
57
+ * because the model authors its own ids, and round 1 reconstructs this one, so
58
+ * the request 400d deterministically on both the primary and the fallback until
59
+ * the deterministic join fired. Workers AI never validated the field, which is
60
+ * why it took a Claude-backed agent to surface it.
61
+ *
62
+ * Nothing persists this: both halves of the pair are rebuilt together on every
63
+ * request, so changing the shape needs no migration. There is no longer a
64
+ * provider-side backstop in core — the adapter that carried one went with
65
+ * `./anthropic` in 0.8.0 — so a provider added here that validates tool-call ids
66
+ * needs to sanitize them on its own way out.
67
+ */
68
+ export function delegateToolCallId(taskId, round) {
69
+ return `task_${taskId}_round_${round}_delegate`;
70
+ }
71
+ /**
72
+ * Ceiling on one branch's `output`, applied to both halves of it.
73
+ *
74
+ * A round's history holds every branch of every earlier round, so this is
75
+ * multiplied by the whole delegation history rather than paid once. Generous
76
+ * enough for a report a subagent meant to be read, far short of a build log a
77
+ * failing one dumped into `error`.
78
+ */
79
+ const MAX_OUTPUT_CHARS = 8_000;
80
+ function bounded(text) {
81
+ if (text.length <= MAX_OUTPUT_CHARS)
82
+ return text;
83
+ const suffix = "\n…[truncated]";
84
+ return `${text.slice(0, MAX_OUTPUT_CHARS - suffix.length)}${suffix}`;
85
+ }
86
+ /**
87
+ * Rebuild one round's call input from its durable rows, in stable ordinal order.
88
+ * Typed as {@link DecompositionProposal} — the same type the model's own calls
89
+ * are validated into — so the reconstructed call and an emitted one cannot drift
90
+ * apart in shape.
91
+ *
92
+ * `referenceIndexes` is omitted rather than faked: this round's references were
93
+ * snapshotted verbatim onto the rows when it ran, and the catalog they were
94
+ * chosen from is long gone.
95
+ *
96
+ * A subtask proposal carries no identifier of its own, so the model pairs each
97
+ * entry here with its outcome **by position**: both arrays are built from the
98
+ * same ordinal-ordered `branches`, and the outcome additionally repeats `type`
99
+ * and carries the durable `subtaskId`.
100
+ */
101
+ export function delegateCallInput(reply, branches) {
102
+ return {
103
+ reply,
104
+ subtasks: branches.map((branch) => ({
105
+ type: branch.type,
106
+ prompt: branch.prompt,
107
+ // Reconstructed verbatim from the row: a later round must see the same
108
+ // params the call really carried, or it cannot reason about what ran.
109
+ params: branch.params
110
+ }))
111
+ };
112
+ }
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 function delegateCallOutput(branches) {
121
+ return branches.map((branch) => ({
122
+ subtaskId: branch.subtaskId,
123
+ type: branch.type,
124
+ status: branch.status,
125
+ output: branch.status === "completed"
126
+ ? bounded((branch.resultParts ?? []).map((part) => part.text).join("\n"))
127
+ : branch.error
128
+ ? bounded(branch.error)
129
+ : null
130
+ }));
131
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@dynamicagents/core/subtasks` — the delegation layer: what a round may hand out,
3
+ * and how a proposal becomes durable drafts.
4
+ */
5
+ export { makeSubtaskTypes, SubtaskParamsError, type SubtaskTypeRegistry } from "./subtask-types.js";
6
+ export { DecompositionValidationError, makeDecompositionProposalSchema, nonBlank, resolveDecomposition } from "./decomposition.js";
7
+ export { DELEGATE_TOOL_NAME, makeDelegateTool, delegateToolCallId, delegateCallInput, delegateCallOutput, type DelegateSubtaskOutcome } from "./delegate.js";
8
+ export { isCatalogEligible, type ReferenceCatalogEntry } from "./catalog.js";
9
+ export * from "./types.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@dynamicagents/core/subtasks` — the delegation layer: what a round may hand out,
3
+ * and how a proposal becomes durable drafts.
4
+ */
5
+ export { makeSubtaskTypes, SubtaskParamsError } from "./subtask-types.js";
6
+ export { DecompositionValidationError, makeDecompositionProposalSchema, nonBlank, resolveDecomposition } from "./decomposition.js";
7
+ export { DELEGATE_TOOL_NAME, makeDelegateTool, delegateToolCallId, delegateCallInput, delegateCallOutput } from "./delegate.js";
8
+ export { isCatalogEligible } from "./catalog.js";
9
+ export * from "./types.js";
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import type { DelegationNames, ResolvedRecipe, SubtaskParams, SubtaskTypeSpec } from "../contract/recipe.js";
3
+ /**
4
+ * The **closed set of subtask types** the main agent may delegate, as the
5
+ * runtime uses it: lookup, the delegate enum, param validation, and the
6
+ * catalogue the delegating model is shown.
7
+ *
8
+ * The set is not declared here, and — unlike the predecessor repo — it is not a
9
+ * module constant. There, `SUBTASK_TYPE_SPECS` was imported at module scope and
10
+ * every derived value (`SUBTASK_TYPES`, `SUBTASK_TYPE_KEYS`, the delegate tool's
11
+ * description, the round contract) was computed at *import time*. That made the
12
+ * registry unoverridable, froze it before `env` exists, and pulled every
13
+ * domain's module into the bundle whether or not the agent used it.
14
+ *
15
+ * So it is a factory. The host builds one registry per DO instance from its
16
+ * installed plugins, and everything downstream reads that object.
17
+ */
18
+ export type { DelegationNames, SubtaskParams, SubtaskTypeSpec };
19
+ /** A subtask whose params do not satisfy its type's contract. */
20
+ export declare class SubtaskParamsError extends Error {
21
+ constructor(message: string);
22
+ }
23
+ export interface SubtaskTypeRegistry {
24
+ /** Every known spec, in declaration order. */
25
+ readonly specs: readonly SubtaskTypeSpec[];
26
+ /** Every known type, keyed by its `key`. */
27
+ readonly byKey: ReadonlyMap<string, SubtaskTypeSpec>;
28
+ /** The type keys in declaration order. Empty when the agent delegates nothing. */
29
+ readonly keys: readonly string[];
30
+ /**
31
+ * The keys as a non-empty tuple for `z.enum` in the delegate schema — the
32
+ * model cannot emit anything outside it, so an unknown type never reaches the
33
+ * data layer.
34
+ *
35
+ * Throws when no type is registered. Not a defensive check but the real
36
+ * contract: an agent with no subtask types has nothing to delegate to, and
37
+ * must not offer a delegate tool at all.
38
+ */
39
+ enumKeys(): [string, ...string[]];
40
+ /** Look up a type, or null when it is not one we know. */
41
+ spec(type: string): SubtaskTypeSpec | null;
42
+ /**
43
+ * Resolve a subtask type to the recipe it runs under. Throws
44
+ * {@link SubtaskParamsError} for an unknown or retired type — the delegate
45
+ * enum prevents the model from emitting one, so any unknown type reaching
46
+ * execution is a configuration bug and should fail terminally.
47
+ */
48
+ resolveRecipe(type: string): ResolvedRecipe;
49
+ /**
50
+ * Validate a subtask's params against its type's contract, returning the
51
+ * params to persist. A type with no schema must carry no params — a stray
52
+ * param would otherwise ride along unread, looking meaningful.
53
+ *
54
+ * Shape only: whether an id names something the outside world actually offers
55
+ * is a question for that world, answered when the work starts. Unknown keys
56
+ * are stripped rather than refused — a model that invents one has produced a
57
+ * valid subtask with noise attached, and the noise provably cannot reach the
58
+ * execution.
59
+ */
60
+ validateParams(type: string, params: SubtaskParams | undefined): SubtaskParams;
61
+ /**
62
+ * Every param key any type declares, as the optional properties of the single
63
+ * flat `params` object the delegate tool advertises.
64
+ *
65
+ * Optional is not a weakening: which keys a given type *requires* is
66
+ * {@link validateParams}'s call, and it still refuses a subtask whose type's
67
+ * params are missing. What this adds is **visibility**. One tool schema has to
68
+ * serve every type, so the only alternative to naming the union of keys here
69
+ * is naming none of them — and a key the schema does not name is a key the
70
+ * model has no legal way to send, whatever the description promises. That is
71
+ * exactly how a domain subtask once reached validation with none of its
72
+ * declared ids: the field was declared as a free-form record, which the
73
+ * provider's schema conversion flattens to an object permitting no keys at all.
74
+ *
75
+ * Descriptions are prefixed with the owning type, so a flat namespace still
76
+ * reads unambiguously to the model (and a key two types share names both).
77
+ */
78
+ paramProperties(): Record<string, z.ZodType<string | undefined>>;
79
+ /** The type catalogue as the delegating model is shown it. */
80
+ renderTypes(): string;
81
+ /** Every type's {@link SubtaskTypeSpec.delegationGuidance}, for the round contract. */
82
+ renderDelegationGuidance(names: DelegationNames): string;
83
+ }
84
+ /**
85
+ * Build the registry from the specs the installed plugins declared.
86
+ *
87
+ * Duplicate keys are refused rather than last-one-wins: two plugins claiming one
88
+ * type is a misconfiguration whose symptom would otherwise be work silently
89
+ * routed to the wrong domain.
90
+ */
91
+ export declare function makeSubtaskTypes(specs: readonly SubtaskTypeSpec[]): SubtaskTypeRegistry;
@@ -0,0 +1,103 @@
1
+ import { z } from "zod";
2
+ /** A subtask whose params do not satisfy its type's contract. */
3
+ export class SubtaskParamsError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "SubtaskParamsError";
7
+ }
8
+ }
9
+ /**
10
+ * Build the registry from the specs the installed plugins declared.
11
+ *
12
+ * Duplicate keys are refused rather than last-one-wins: two plugins claiming one
13
+ * type is a misconfiguration whose symptom would otherwise be work silently
14
+ * routed to the wrong domain.
15
+ */
16
+ export function makeSubtaskTypes(specs) {
17
+ const byKey = new Map();
18
+ for (const spec of specs) {
19
+ if (byKey.has(spec.key)) {
20
+ throw new Error(`duplicate subtask type "${spec.key}" — two plugins declare the same type`);
21
+ }
22
+ byKey.set(spec.key, spec);
23
+ }
24
+ const keys = specs.map((s) => s.key);
25
+ const lookup = (type) => byKey.get(type) ?? null;
26
+ return {
27
+ specs,
28
+ byKey,
29
+ keys,
30
+ enumKeys() {
31
+ if (keys.length === 0) {
32
+ throw new Error("no subtask types are registered — an agent with no types cannot delegate, " +
33
+ "so it must not build a delegate tool");
34
+ }
35
+ return keys;
36
+ },
37
+ spec: lookup,
38
+ resolveRecipe(type) {
39
+ const found = lookup(type);
40
+ if (!found)
41
+ throw new SubtaskParamsError(`unknown subtask type: ${type}`);
42
+ return found.recipe;
43
+ },
44
+ validateParams(type, params) {
45
+ const found = lookup(type);
46
+ if (!found)
47
+ throw new SubtaskParamsError(`unknown subtask type: ${type}`);
48
+ const supplied = params ?? {};
49
+ if (!found.params) {
50
+ const extra = Object.keys(supplied);
51
+ if (extra.length > 0) {
52
+ throw new SubtaskParamsError(`subtask type "${type}" takes no params, got: ${extra.join(", ")}`);
53
+ }
54
+ return {};
55
+ }
56
+ const parsed = found.params.safeParse(supplied);
57
+ if (!parsed.success) {
58
+ const detail = parsed.error.issues
59
+ .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
60
+ .join("; ");
61
+ throw new SubtaskParamsError(`subtask type "${type}" has invalid params — ${detail}`);
62
+ }
63
+ return parsed.data;
64
+ },
65
+ paramProperties() {
66
+ const owners = new Map();
67
+ const declared = new Map();
68
+ for (const s of specs) {
69
+ if (!s.params)
70
+ continue;
71
+ for (const [key, field] of Object.entries(s.params.shape)) {
72
+ owners.set(key, [...(owners.get(key) ?? []), s.key]);
73
+ declared.set(key, field);
74
+ }
75
+ }
76
+ return Object.fromEntries([...declared].map(([key, field]) => [
77
+ key,
78
+ // Described *after* `.optional()`, so the text sits on the schema this
79
+ // returns rather than on the type it wraps — readable by both the JSON
80
+ // Schema conversion and anything inspecting these properties directly.
81
+ field
82
+ .optional()
83
+ .describe(`${owners.get(key)?.join("/")}: ${field.description ?? key}`)
84
+ ]));
85
+ },
86
+ renderTypes() {
87
+ return specs
88
+ .map((s) => {
89
+ const help = s.paramsHelp ? ` — ${s.paramsHelp}` : "";
90
+ return `- \`${s.key}\`: ${s.description}${help}`;
91
+ })
92
+ .join("\n");
93
+ },
94
+ renderDelegationGuidance(names) {
95
+ const sections = [];
96
+ for (const s of specs) {
97
+ if (s.delegationGuidance)
98
+ sections.push(s.delegationGuidance(names));
99
+ }
100
+ return sections.join("\n\n");
101
+ }
102
+ };
103
+ }
@@ -0,0 +1,295 @@
1
+ import type { ResolvedRecipe, SubtaskParams } from "../contract/recipe.js";
2
+ import type { RoundFailureKind } from "../agent/inference.js";
3
+ export type SubtaskStatus = "pending" | "running" | "completed" | "failed" | "canceled";
4
+ /** A per-caller, SQLite-assigned, monotonically increasing Subtask identifier. */
5
+ export type SubtaskId = number;
6
+ /**
7
+ * An exact, verbatim snapshot of one selected Session history message, copied
8
+ * onto the Subtask at decomposition time. The decomposition model selects which
9
+ * messages to reference; it never rewrites their content. User-turn provenance
10
+ * (author/channel) is already inline in the message text's `<turn>` wrapper.
11
+ */
12
+ export interface SubtaskReference {
13
+ role: "user" | "assistant";
14
+ text: string;
15
+ }
16
+ /**
17
+ * One part of a Subtask's result. Text-only today; file/data kinds are additive
18
+ * later. Internal to the agent — the terminal A2A Task collapses these to a
19
+ * single text reply for the gatekeeper/human.
20
+ */
21
+ export interface SubtaskResultPart {
22
+ kind: "text";
23
+ text: string;
24
+ }
25
+ /**
26
+ * Creation input for one Subtask, before it is persisted. `ordinal` is derived
27
+ * from the draft's position in the decomposition array, which is the only thing
28
+ * that distinguishes one draft of a decomposition from another — they carry no
29
+ * identity of their own until the data layer assigns a {@link SubtaskId}.
30
+ */
31
+ export interface SubtaskDraft {
32
+ type: string;
33
+ prompt: string;
34
+ references: SubtaskReference[];
35
+ /** The type's required inputs, already validated for shape. */
36
+ params: SubtaskParams;
37
+ }
38
+ /**
39
+ * Session state the parent resolved *from* a Subtask's params at execution
40
+ * start — the part of an execution's context the model can never supply.
41
+ *
42
+ * Deliberately outside {@link RecipeExecutionRequest}: it is not part of an
43
+ * execution's identity and it can change under us, so fingerprinting it would
44
+ * make a retry look like a different request. It travels as a separate argument,
45
+ * the same discipline as the chunk number.
46
+ */
47
+ export type SubtaskRuntime = Record<string, unknown>;
48
+ /**
49
+ * Narrow a {@link SubtaskRuntime} to the shape a plugin resolved for itself.
50
+ *
51
+ * Runtime is an open bag because core cannot know what a domain needs — the
52
+ * predecessor repo declared ARC's four fields (`cardId`, `cookies`, `guid`,
53
+ * `frame`) right here in the delegation types, which meant core imported a
54
+ * domain's types and every unrelated agent carried them. A plugin now writes and
55
+ * reads its own slice, and the only thing core does with the bag is carry it.
56
+ *
57
+ * The unchecked cast is the point: the plugin that wrote the slice is the plugin
58
+ * reading it, so the assertion is local and its blast radius is one module.
59
+ */
60
+ export declare function runtimeAs<T>(runtime: SubtaskRuntime): T;
61
+ /**
62
+ * A user-facing progress note a tool emits mid-execution (e.g. a game level-up).
63
+ * The resumable runner collects these and ends the current chunk so the parent
64
+ * DO can post them promptly; `key` is a stable dedupe id the gatekeeper keys on.
65
+ */
66
+ export interface ProgressEvent {
67
+ key: string;
68
+ text: string;
69
+ }
70
+ /**
71
+ * One durable chunk's outcome as the facet reports it to the parent DO. `done`
72
+ * false means the run yielded a chunk boundary and the Workflow must run another
73
+ * chunk; `done` true carries the terminal {@link RecipeExecutionResult}. Progress
74
+ * events accumulated during the chunk ride along either way.
75
+ */
76
+ export type RecipeChunkResult = {
77
+ done: false;
78
+ progress: ProgressEvent[];
79
+ } | {
80
+ done: true;
81
+ result: RecipeExecutionResult;
82
+ progress: ProgressEvent[];
83
+ };
84
+ /**
85
+ * The parent DO's projection of a chunk outcome for the Workflow (RPC-safe, no
86
+ * result parts — those are persisted on the row). `status` is `running` until the
87
+ * run is `done`, then the terminal Subtask status.
88
+ */
89
+ export interface SubtaskChunkOutcome {
90
+ done: boolean;
91
+ status: SubtaskStatus;
92
+ progress: ProgressEvent[];
93
+ }
94
+ /**
95
+ * RPC-safe input for one isolated `RecipeSubagent` execution, assembled by the
96
+ * parent at execution start: the already-resolved (and code-validated) Recipe,
97
+ * the Subtask's non-session prompt, and its verbatim reference snapshots. The
98
+ * child re-validates the Recipe defensively but never resolves one itself.
99
+ */
100
+ export interface RecipeExecutionRequest {
101
+ taskId: string;
102
+ subtaskId: SubtaskId;
103
+ /**
104
+ * The Subtask's semantic type — what the work *is*, and what owns the params
105
+ * contract. Distinct from `recipe.key`, which names the execution
106
+ * configuration it runs under: several types may share one Recipe.
107
+ */
108
+ type: string;
109
+ recipe: ResolvedRecipe;
110
+ prompt: string;
111
+ references: SubtaskReference[];
112
+ /**
113
+ * The Subtask's validated params. Part of the execution's identity — two plays
114
+ * of different games are different work — so this IS fingerprinted, unlike
115
+ * {@link SubtaskRuntime}, which is deliberately excluded: the leased scorecard
116
+ * can legitimately differ between two chunks of one run, and must not make a
117
+ * retry look like a different execution.
118
+ */
119
+ params: SubtaskParams;
120
+ }
121
+ /**
122
+ * Terminal outcome of one `RecipeSubagent` execution (RPC-safe). `modelId` is a
123
+ * diagnostic only — which model produced the outcome (null when validation
124
+ * failed before any model call); it is never persisted on the Subtask row.
125
+ * Transient platform faults are not results: they throw so the enclosing
126
+ * Workflow step can retry.
127
+ */
128
+ export type RecipeExecutionResult = {
129
+ status: "completed";
130
+ resultParts: SubtaskResultPart[];
131
+ modelId: string;
132
+ } | {
133
+ status: "failed";
134
+ error: string;
135
+ modelId: string | null;
136
+ };
137
+ /**
138
+ * One Subtask as the delegating model emits it. The model selects references
139
+ * by **catalog index only** — it never emits reference text, and application code
140
+ * snapshots the catalog entry's exact role+text onto the Subtask (see
141
+ * `agent/subtasks/decomposition.ts`).
142
+ */
143
+ export interface SubtaskProposal {
144
+ type: string;
145
+ prompt: string;
146
+ /**
147
+ * 1-based indices into the ephemeral, per-round reference catalog. Optional:
148
+ * omitted when the subtask needs no verbatim history, and always absent from a
149
+ * *reconstructed* historical call, whose references were resolved rounds ago.
150
+ */
151
+ referenceIndexes?: number[];
152
+ /** The type's required inputs; omitted for a type that takes none. */
153
+ params?: SubtaskParams;
154
+ }
155
+ /**
156
+ * The model's complete `delegate` call: the acknowledgment the user sees while
157
+ * the work runs, plus one through eight Subtask proposals. Validated against the
158
+ * round's ephemeral catalog before anything is persisted; invalid output fails
159
+ * the attempt (and, with both models exhausted, the round) rather than being
160
+ * silently repaired.
161
+ */
162
+ export interface DecompositionProposal {
163
+ reply: string;
164
+ subtasks: SubtaskProposal[];
165
+ }
166
+ /**
167
+ * Terminal outcome of one main-agent round (RPC-safe).
168
+ *
169
+ * `replied` is the terminal answer — the round chose to answer the user rather
170
+ * than delegate, and the Workflow delivers it. `delegated` means the round's
171
+ * Subtask rows are durable and the Workflow should execute them, after which
172
+ * another round begins. `failed` means the round produced no answer, with
173
+ * {@link RoundFailureKind} carrying why; no Subtask is ever synthesized to cover
174
+ * for it. `canceled` means the caller cancelled during the round: nothing was
175
+ * persisted and nothing was published. Transient platform faults are not
176
+ * results: they throw so the enclosing Workflow step can retry (mirrors
177
+ * {@link RecipeExecutionResult}).
178
+ *
179
+ * `turns` is what this round cost, which the Workflow meters against the Task's
180
+ * budget. This is the **only** type that carries it, and it carries it because the
181
+ * count has to cross an RPC boundary to reach a Workflow in another isolate;
182
+ * everything inside the DO shares one mutable
183
+ * {@link file://../agent/budget.ts TurnBudget} instead. The field is attached in a single place — see `runTaskTurn` — so no
184
+ * branch can drop it and no branch can invent it.
185
+ *
186
+ * The idempotent recovery paths — a round replayed from durable rows rather than
187
+ * re-inferred — therefore report **0** structurally: they return before any model
188
+ * runs, so the budget they hand back is untouched. That is exact on a clean
189
+ * replay, where the Workflow's own cached step return already carries the original
190
+ * number, and under-counts by one round when a step crashed mid-flight and re-ran.
191
+ * Accepted rather than fixed: a replying round writes no durable row at all, so
192
+ * nothing exists to hang a per-round count on, and the wall clock bounds the
193
+ * crash-loop case anyway.
194
+ */
195
+ export type TurnTaskResult = {
196
+ status: "replied";
197
+ reply: string;
198
+ turns: number;
199
+ } | {
200
+ status: "delegated";
201
+ reply: string;
202
+ subtasks: Subtask[];
203
+ turns: number;
204
+ }
205
+ /**
206
+ * `kind` distinguishes a round that spent both models and got nothing usable
207
+ * (`exhausted`) from one that stopped on a fault no attempt could clear —
208
+ * where the fallback was deliberately *not* tried, and the kind is what lets
209
+ * the host say why in words an operator can act on. Same terminal Task either
210
+ * way; only the words differ.
211
+ */
212
+ | {
213
+ status: "failed";
214
+ kind: RoundFailureKind;
215
+ error: string;
216
+ turns: number;
217
+ } | {
218
+ status: "canceled";
219
+ turns: number;
220
+ };
221
+ /**
222
+ * Distributive `Omit` — applies per member, so the discriminated union survives.
223
+ * A plain `Omit<TurnTaskResult, "turns">` collapses all four into one loose shape
224
+ * whose `reply` and `subtasks` are independently optional.
225
+ */
226
+ type WithoutTurns<T> = T extends unknown ? Omit<T, "turns"> : never;
227
+ /**
228
+ * What a round decided, before its cost is attached. The DO's round logic returns
229
+ * this and lets one wrapper bill the budget, rather than every branch remembering
230
+ * to report a number it did not compute.
231
+ */
232
+ export type TurnVerdict = WithoutTurns<TurnTaskResult>;
233
+ /**
234
+ * One branch's outcome as a later round sees it — a plain, RPC-safe subset of the
235
+ * durable {@link Subtask} row, loaded across **every** round in stable ordinal
236
+ * order. Completed and failed branches are both included so the reply can use
237
+ * available successes and disclose relevant failures.
238
+ *
239
+ * Carries `round` and `prompt` — not for composing, but for reconstructing the
240
+ * per-round `delegate` call that produced these branches (see
241
+ * `agent/subtasks/delegate.ts`). `references` stays out: it is unbounded history
242
+ * text, and the call's shape does not need it.
243
+ */
244
+ export interface CompositionBranch {
245
+ subtaskId: SubtaskId;
246
+ round: number;
247
+ ordinal: number;
248
+ type: string;
249
+ prompt: string;
250
+ params: SubtaskParams;
251
+ status: SubtaskStatus;
252
+ resultParts: SubtaskResultPart[] | null;
253
+ error: string | null;
254
+ }
255
+ /**
256
+ * The `scan:<round>` projection (RPC-safe): either the caller cancelled, or the
257
+ * ids of the round's Subtasks that still owe an outcome, in ordinal order.
258
+ * Returning the verdict with the ids is what lets the Workflow drop its separate
259
+ * cancellation probe — one round trip, and no gap between asking and acting.
260
+ *
261
+ * Ids and nothing else, deliberately: a Workflow step return is capped at 1 MiB
262
+ * and a Subtask carries verbatim history snapshots bounded only by
263
+ * `MAX_INBOUND_TEXT_BYTES`, so a scan returning rows would overflow on a large
264
+ * task. The durable rows are the source of truth; the Workflow carries
265
+ * references to them and re-reads through the parent when it needs more.
266
+ */
267
+ export type SubtaskScan = {
268
+ canceled: true;
269
+ } | {
270
+ canceled: false;
271
+ ids: SubtaskId[];
272
+ };
273
+ /** Durable state owned by the main agent for one delegated unit of work. */
274
+ export interface Subtask {
275
+ id: SubtaskId;
276
+ taskId: string;
277
+ /** The main-agent round that delegated this Subtask (0-based). */
278
+ round: number;
279
+ /** Position within the parent Task, increasing across every round. */
280
+ ordinal: number;
281
+ type: string;
282
+ recipeId: string | null;
283
+ recipeVersion: number | null;
284
+ prompt: string;
285
+ references: SubtaskReference[];
286
+ /** The type's required inputs, validated at delegation time. */
287
+ params: SubtaskParams;
288
+ status: SubtaskStatus;
289
+ resultParts: SubtaskResultPart[] | null;
290
+ error: string | null;
291
+ createdAt: number;
292
+ updatedAt: number;
293
+ completedAt: number | null;
294
+ }
295
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Narrow a {@link SubtaskRuntime} to the shape a plugin resolved for itself.
3
+ *
4
+ * Runtime is an open bag because core cannot know what a domain needs — the
5
+ * predecessor repo declared ARC's four fields (`cardId`, `cookies`, `guid`,
6
+ * `frame`) right here in the delegation types, which meant core imported a
7
+ * domain's types and every unrelated agent carried them. A plugin now writes and
8
+ * reads its own slice, and the only thing core does with the bag is carry it.
9
+ *
10
+ * The unchecked cast is the point: the plugin that wrote the slice is the plugin
11
+ * reading it, so the assertion is local and its blast radius is one module.
12
+ */
13
+ export function runtimeAs(runtime) {
14
+ return runtime;
15
+ }