@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,180 @@
1
+ import type { z } from "zod";
2
+ import type { AgentLimits } from "../config.js";
3
+ /**
4
+ * What a recipe domain declares.
5
+ *
6
+ * These types flow **outward**: the runtime consumes them, and nothing in this
7
+ * file imports from the runtime. That direction is the whole reason a plugin can
8
+ * live in a separate package — a new domain is a new module plus one line in the
9
+ * consuming agent's plugin list, with no edit inside core.
10
+ *
11
+ * Two shapes, and the distinction between them carries real weight:
12
+ *
13
+ * - A **type** ({@link SubtaskTypeSpec}) is the semantic contract of a unit of
14
+ * work: what it means, and what it must be given to be doable at all.
15
+ * - A **Recipe** ({@link ResolvedRecipe}) is the execution *configuration* —
16
+ * models, soul, tool families, budgets. It declares no params, and one Recipe
17
+ * may serve several types.
18
+ */
19
+ /** Params carried by a Subtask: model-chosen, string-valued, shape-checked. */
20
+ export type SubtaskParams = Record<string, string>;
21
+ /**
22
+ * A type's param declaration, as a shape the agent can read back — not an opaque
23
+ * validator. The keys have to be *enumerable* because the delegate tool's schema
24
+ * is built from them: a param the model cannot see declared is a param it will
25
+ * not send. See {@link SubtaskTypeSpec.params}.
26
+ */
27
+ export type SubtaskParamsShape = Record<string, z.ZodType<string>>;
28
+ export type SubtaskParamsSchema = z.ZodObject<SubtaskParamsShape>;
29
+ /**
30
+ * The control-tool names the agent injects when it renders a type's
31
+ * {@link SubtaskTypeSpec.delegationGuidance}.
32
+ *
33
+ * Guidance legitimately names those tools ("ask the user with `final_reply`
34
+ * rather than guessing"), and their names are the agent's to own. Injecting them
35
+ * is what lets a domain write that sentence without importing from the runtime —
36
+ * the direction this module depends on.
37
+ */
38
+ export interface DelegationNames {
39
+ delegateTool: string;
40
+ finalReplyTool: string;
41
+ }
42
+ /**
43
+ * The execution budget for one Recipe, enforced by the resumable runner (not the
44
+ * Workflow). Exactly two fields, because there are exactly two things worth
45
+ * bounding: what an execution **costs** and how long it can **run away for**. The
46
+ * run ends on whichever it reaches first, and either way through the graceful
47
+ * budget summary.
48
+ *
49
+ * Deliberately *not* here: how a run is sliced into durable chunks. That is a
50
+ * Workers step-timeout constraint, it is identical for every Recipe, and it lives
51
+ * in {@link file://../platform.ts}. A turn count cannot do that job — nothing
52
+ * predicts how long a turn takes — so no per-chunk turn budget belongs on a
53
+ * Recipe.
54
+ *
55
+ * Structurally identical to {@link AgentLimits} — a budget is a budget at both
56
+ * levels, and keeping one shape means `resolveLimits` merges a recipe's
57
+ * declaration straight over the host's baseline with no translation. Aliased
58
+ * rather than redeclared so the two can never drift apart.
59
+ */
60
+ export type RecipeLimits = AgentLimits;
61
+ /**
62
+ * A Recipe configuration as a domain **declares** it, one per folder in
63
+ * `recipes/<domain>/recipe.ts`; caller-local DB rows mapping into this shape are
64
+ * deferred until a Recipe admin surface exists. Model ids, tool families, and
65
+ * limits are code-validated downstream
66
+ * ({@link file://./validation.ts validateRecipe}), which is also what turns this
67
+ * into a {@link ValidatedRecipe}.
68
+ *
69
+ * One rule governs every field here, and it is worth stating once: **if
70
+ * `config.ts` declares a baseline, a Recipe overrides it; if it does not, the
71
+ * Recipe must supply it.** So `limits` is partial and merges, while `soul` and
72
+ * `historyWindow` are required and a missing one is refused rather than filled in.
73
+ */
74
+ export interface ResolvedRecipe {
75
+ key: string;
76
+ version: number;
77
+ /** Required, never defaulted — see `validateRecipe`. */
78
+ soul: string;
79
+ toolFamilies: string[];
80
+ enabled: boolean;
81
+ /**
82
+ * Only the budget fields this Recipe overrides; the rest come from the host's
83
+ * `CoreConfig.subagentLimits`, which reaches validation as
84
+ * `RecipePolicy.baselineLimits`. `{}` means "the baseline", which is what most
85
+ * Recipes want.
86
+ */
87
+ limits: Partial<RecipeLimits>;
88
+ /**
89
+ * Most-recent turns kept verbatim in the rolling model context; older turns are
90
+ * pruned. Required, and required *of the Recipe*: how much context a domain
91
+ * needs is a property of the domain, so there is no house default to fall back
92
+ * to and a missing one is a refused Recipe.
93
+ */
94
+ historyWindow: number;
95
+ /** Append a runtime metrics footer (turns, model calls, wall-clock) to the final result. */
96
+ reportMetrics: boolean;
97
+ }
98
+ /**
99
+ * A Recipe that has been through {@link file://./validation.ts validateRecipe} —
100
+ * limits merged over the baseline, models and tool families checked. The only
101
+ * shape the runner ever consumes: a `Partial<RecipeLimits>` can never reach it.
102
+ */
103
+ export interface ValidatedRecipe extends ResolvedRecipe {
104
+ limits: RecipeLimits;
105
+ /**
106
+ * The pair this recipe will actually run on — **the host's, always**.
107
+ *
108
+ * These exist only here, never on {@link ResolvedRecipe}, and that asymmetry
109
+ * is the design: a recipe cannot state a model, so the only way to hold one is
110
+ * to have been through `validateRecipe`, which copies the host's. Recipe data
111
+ * has no path to influence them.
112
+ *
113
+ * Guaranteed non-empty and guaranteed distinct, because `resolveConfig`
114
+ * refuses a config that is either — so the fallback is always a genuinely
115
+ * different model from the primary, which is the entire point of having one.
116
+ */
117
+ primaryModelId: string;
118
+ fallbackModelId: string;
119
+ }
120
+ /**
121
+ * One entry in the closed set of Subtask types the main agent may delegate,
122
+ * declared by the domain that owns it and collected in
123
+ * {@link file://./index.ts}.
124
+ *
125
+ * Two things follow from the set being closed rather than free prose:
126
+ *
127
+ * - The delegating model picks from an enum, so an invented type is rejected by
128
+ * the tool schema itself instead of silently falling back to a general recipe.
129
+ * - A type can *require params*. `arc-game` cannot be attempted without a
130
+ * scorecard and a game, so a subtask that names neither is refused up front
131
+ * rather than discovering it has nothing to play several turns later.
132
+ *
133
+ * Params are the model's declared inputs — ids it chose, validated for shape and
134
+ * resolved against durable rows at execution start. They are never the place for
135
+ * anything the model cannot know: an API session pinned to one of those ids is
136
+ * resolved by the parent from the id, never carried in the params.
137
+ */
138
+ export interface SubtaskTypeSpec {
139
+ key: string;
140
+ /** One line shown to the delegating model so it picks the right type. */
141
+ description: string;
142
+ /**
143
+ * Required params for this type, or null when it takes none. Kept to flat
144
+ * strings: these are ids the model quotes from a tool result, not structures.
145
+ *
146
+ * A `z.object`, not an opaque `z.ZodType`, and that is load-bearing: the agent
147
+ * reads `.shape` back to build the `params` field of the delegate tool's schema
148
+ * (see `SubtaskTypeRegistry.paramProperties`). Declaring a param the model is
149
+ * never shown is the failure this shape exists to prevent — describe each key
150
+ * with `.describe()`, because that text is what the model reads.
151
+ */
152
+ params: SubtaskParamsSchema | null;
153
+ /** How the model is told to obtain each param, appended to the description. */
154
+ paramsHelp?: string;
155
+ /**
156
+ * What the main agent is told it can *do* with this domain, rendered into its
157
+ * soul alongside the other capability blocks. Omit when the type needs no
158
+ * introduction beyond {@link description}.
159
+ *
160
+ * These two prompt fields exist to hold one rule: **everything the main agent
161
+ * is told about a domain is declared here, never written inside the runtime.**
162
+ * Advice written in the runtime instead has to be repeated per call site, and
163
+ * the copies drift — into telling the main agent two contradictory things
164
+ * about the same domain, twice per round.
165
+ */
166
+ capability?: string;
167
+ /**
168
+ * How to construct a `delegate` payload for this type, rendered into the round
169
+ * contract the main agent reads every round.
170
+ *
171
+ * A function because the text names the control tools, whose names belong to
172
+ * the agent (see {@link DelegationNames}). Three rules, none of them enforced
173
+ * beyond a test: it must open with its own `## ` heading, it must not restate
174
+ * the params schema — the `delegate` tool description already renders that from
175
+ * {@link params} — and it must stay short, because every round pays for it.
176
+ */
177
+ delegationGuidance?: (names: DelegationNames) => string;
178
+ /** The execution configuration this type runs under. */
179
+ recipe: ResolvedRecipe;
180
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ import type { AgentLimits } from "../config.js";
2
+ import type { ResolvedRecipe, ValidatedRecipe } from "./recipe.js";
3
+ /**
4
+ * The capability boundary every recipe passes through, whatever declared it.
5
+ *
6
+ * This module imports no domain. It owns only what code must be able to say
7
+ * about *any* recipe: which tool families it may select, which models it runs
8
+ * on, and how a malformed one is made safe or refused. A domain cannot widen
9
+ * its *capabilities* by declaring them — the legal tool families come from
10
+ * {@link RecipePolicy}, which the host builds from its installed plugins, and
11
+ * the model pair comes from the host's config with no declaration involved at
12
+ * all. Its budget is the deliberate exception; see {@link resolveLimits}.
13
+ *
14
+ * In the predecessor repo the two allowlists were module constants, and one of
15
+ * them hardcoded a domain key (`"arc-game"`) inside otherwise generic code. That
16
+ * is the coupling this indirection removes: the set of legal families is now
17
+ * exactly the set of families the installed plugins registered.
18
+ */
19
+ export interface RecipePolicy {
20
+ /**
21
+ * The pair every recipe runs on: the host agent's own, copied verbatim onto
22
+ * each {@link ValidatedRecipe}.
23
+ *
24
+ * There is no allowlist any more, because there is nothing to check against
25
+ * it — a recipe cannot state a model. `modelAllowlist` existed to police
26
+ * recipe-stated preferences, and it could only ever contain these same two
27
+ * ids, so the "preference" it policed could never reach a third model. It
28
+ * could only swap the primary for the fallback, which is not a capability
29
+ * anyone wants and which broke the pair's distinctness.
30
+ */
31
+ primaryModelId: string;
32
+ /** See {@link primaryModelId}. Guaranteed distinct from it by `resolveConfig`. */
33
+ fallbackModelId: string;
34
+ /**
35
+ * Tool-family keys the runtime recognizes — derived from the installed
36
+ * plugins, never hardcoded.
37
+ *
38
+ * A main-agent-only tool is never a valid family — the Session's
39
+ * `set_context`, or anything a plugin offers only through `mainAgentTools`
40
+ * (an episodic-memory search, say). A subagent has no Session or durable
41
+ * memory to reach, and such a tool's absence from every plugin's
42
+ * `toolFamilies` makes it structurally impossible to enable through recipe
43
+ * data.
44
+ */
45
+ knownToolFamilies: ReadonlySet<string>;
46
+ /** The baseline a recipe's declared `limits` merge over. */
47
+ baselineLimits: AgentLimits;
48
+ }
49
+ /**
50
+ * Merge a recipe's declared budget over the baseline, per field. A positive
51
+ * integer wins; anything else — missing, null, zero, fractional — falls back to
52
+ * the baseline rather than reaching the runner.
53
+ *
54
+ * The baseline is a default, not a ceiling: a recipe may declare a budget larger
55
+ * than the baseline and it is honored — sizing its own branch is what declaring
56
+ * `limits` is for, so nothing here clamps. What the merge buys, as
57
+ * defense-in-depth for a recipe that ever comes from data rather than code, is
58
+ * that a corrupt or absent value cannot reach the runner as a zero, fractional,
59
+ * or missing budget.
60
+ *
61
+ * Exported separately from {@link validateRecipe} because it must never throw.
62
+ */
63
+ export declare function resolveLimits(limits: Partial<AgentLimits>, baseline: AgentLimits): AgentLimits;
64
+ /**
65
+ * Thrown by {@link validateRecipe} for a recipe that is unusable as given — it
66
+ * is disabled, carries no soul, or states no history window. All are
67
+ * deterministic caller bugs (the parent must only hand enabled, complete recipes
68
+ * to a subagent), so the child maps this to a terminal failed result rather than
69
+ * retrying.
70
+ */
71
+ export declare class RecipeValidationError extends Error {
72
+ constructor(message: string);
73
+ }
74
+ /**
75
+ * Code-owned defensive validation of an already-resolved recipe. Returns a
76
+ * normalized copy (never mutates the input): the host's model pair is stamped
77
+ * on, and unknown tool families are dropped (deduped, order-preserving).
78
+ * Applied by the parent when it resolves a recipe and re-applied by the subagent
79
+ * on its inbound request, so recipe data can never select arbitrary models or
80
+ * tools — models because there is no field to select one with, tools because
81
+ * the legal set comes from the installed plugins rather than the declaration.
82
+ *
83
+ * The split between what is normalized and what is refused follows one rule:
84
+ * **the host declares a baseline ⇒ merge; it does not ⇒ require.** `limits`
85
+ * merge. A soul and a `historyWindow` do not: substituting a generic soul would
86
+ * run the work under an identity nobody declared — the model would answer,
87
+ * plausibly, as something other than what the recipe is for — and how much
88
+ * context a domain needs is likewise a property of the domain, not something a
89
+ * house default can guess. Both fail the recipe outright.
90
+ */
91
+ export declare function validateRecipe(recipe: ResolvedRecipe, policy: RecipePolicy): ValidatedRecipe;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Merge a recipe's declared budget over the baseline, per field. A positive
3
+ * integer wins; anything else — missing, null, zero, fractional — falls back to
4
+ * the baseline rather than reaching the runner.
5
+ *
6
+ * The baseline is a default, not a ceiling: a recipe may declare a budget larger
7
+ * than the baseline and it is honored — sizing its own branch is what declaring
8
+ * `limits` is for, so nothing here clamps. What the merge buys, as
9
+ * defense-in-depth for a recipe that ever comes from data rather than code, is
10
+ * that a corrupt or absent value cannot reach the runner as a zero, fractional,
11
+ * or missing budget.
12
+ *
13
+ * Exported separately from {@link validateRecipe} because it must never throw.
14
+ */
15
+ export function resolveLimits(limits, baseline) {
16
+ const positiveInt = (n, fallback) => typeof n === "number" && Number.isInteger(n) && n > 0 ? n : fallback;
17
+ return {
18
+ maxTurns: positiveInt(limits?.maxTurns, baseline.maxTurns),
19
+ maxWallMs: positiveInt(limits?.maxWallMs, baseline.maxWallMs)
20
+ };
21
+ }
22
+ /**
23
+ * Thrown by {@link validateRecipe} for a recipe that is unusable as given — it
24
+ * is disabled, carries no soul, or states no history window. All are
25
+ * deterministic caller bugs (the parent must only hand enabled, complete recipes
26
+ * to a subagent), so the child maps this to a terminal failed result rather than
27
+ * retrying.
28
+ */
29
+ export class RecipeValidationError extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = "RecipeValidationError";
33
+ }
34
+ }
35
+ /**
36
+ * Code-owned defensive validation of an already-resolved recipe. Returns a
37
+ * normalized copy (never mutates the input): the host's model pair is stamped
38
+ * on, and unknown tool families are dropped (deduped, order-preserving).
39
+ * Applied by the parent when it resolves a recipe and re-applied by the subagent
40
+ * on its inbound request, so recipe data can never select arbitrary models or
41
+ * tools — models because there is no field to select one with, tools because
42
+ * the legal set comes from the installed plugins rather than the declaration.
43
+ *
44
+ * The split between what is normalized and what is refused follows one rule:
45
+ * **the host declares a baseline ⇒ merge; it does not ⇒ require.** `limits`
46
+ * merge. A soul and a `historyWindow` do not: substituting a generic soul would
47
+ * run the work under an identity nobody declared — the model would answer,
48
+ * plausibly, as something other than what the recipe is for — and how much
49
+ * context a domain needs is likewise a property of the domain, not something a
50
+ * house default can guess. Both fail the recipe outright.
51
+ */
52
+ export function validateRecipe(recipe, policy) {
53
+ if (!recipe.enabled) {
54
+ throw new RecipeValidationError(`recipe "${recipe.key}" (v${recipe.version}) is disabled`);
55
+ }
56
+ if (recipe.soul.trim() === "") {
57
+ throw new RecipeValidationError(`recipe "${recipe.key}" (v${recipe.version}) has no soul`);
58
+ }
59
+ if (!Number.isInteger(recipe.historyWindow) || recipe.historyWindow <= 0) {
60
+ throw new RecipeValidationError(`recipe "${recipe.key}" (v${recipe.version}) has no usable historyWindow ` +
61
+ `(got ${recipe.historyWindow}); every recipe states its own`);
62
+ }
63
+ const toolFamilies = [...new Set(recipe.toolFamilies)].filter((family) => policy.knownToolFamilies.has(family));
64
+ return {
65
+ ...recipe,
66
+ // The host's pair, copied. Not selected, not substituted, not merged —
67
+ // there is nothing on a `ResolvedRecipe` to select *from*.
68
+ //
69
+ // This used to substitute a recipe's stated preference against
70
+ // an allowlist, per slot and independently. That produced a real defect:
71
+ // a recipe preferring the host's *fallback* as its primary, and stating no
72
+ // fallback of its own, resolved to the same id in both slots — so the
73
+ // fallback retried the model that had just failed, defeating the distinct-
74
+ // pair invariant `resolveConfig` enforces one layer up.
75
+ //
76
+ // Copying wholesale makes that unrepresentable: the pair here is exactly the
77
+ // pair the agent configured, which `resolveConfig` has already guaranteed is
78
+ // non-empty and distinct.
79
+ primaryModelId: policy.primaryModelId,
80
+ fallbackModelId: policy.fallbackModelId,
81
+ toolFamilies,
82
+ limits: resolveLimits(recipe.limits, policy.baselineLimits)
83
+ };
84
+ }
@@ -0,0 +1,147 @@
1
+ import { type DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
2
+ import * as schema from "./schema.js";
3
+ export type DB = DrizzleSqliteDODatabase<typeof schema>;
4
+ /**
5
+ * A table owner outside core's migration journal.
6
+ *
7
+ * What a plugin cannot share is the **migrator**. `drizzle-orm/durable-sqlite/
8
+ * migrator` keeps one flat integer journal and one global
9
+ * `__drizzle_migrations` table, and two independently-versioned npm packages
10
+ * cannot share that index space — not hypothetically: the two predecessor
11
+ * agents, both consuming the same `notify_tasks` module, had already forked the
12
+ * journal at index 1 (`0001_unusual_nova` vs `0001_great_goliath`). Worse,
13
+ * `drizzle-kit generate` diffs against a snapshot in one output directory, so a
14
+ * plugin shipping from its own repo cannot produce a correct diff at all.
15
+ *
16
+ * **The query builder is a different thing entirely, and a plugin should use
17
+ * it.** `drizzle(storage, { schema })` is a typed wrapper over the same
18
+ * `DurableObjectStorage`; it holds no journal, no connection, and no state that
19
+ * a second handle could disturb. So the rule is narrow — *never import the
20
+ * migrator* — rather than "no drizzle".
21
+ *
22
+ * A plugin therefore does three things: declares its tables with `sqliteTable`
23
+ * as usual, emits idempotent DDL here, and queries through its own drizzle
24
+ * handle. Version bookkeeping lives in the {@link PLUGIN_MIGRATIONS_TABLE} row
25
+ * this class manages for it. Only the DDL is hand-written, and that pattern is
26
+ * not novel here — it is what the subagent facet already does for its own two
27
+ * tables.
28
+ *
29
+ * ```ts
30
+ * // schema.ts — an ordinary drizzle table, under the plugin's own prefix.
31
+ * export const arcScorecards = sqliteTable("arc_scorecards", {
32
+ * cardId: text("card_id").primaryKey(),
33
+ * lastUsedAt: integer("last_used_at").notNull()
34
+ * });
35
+ *
36
+ * // The DDL half: idempotent, re-run on every hibernation wake-up.
37
+ * const store: PluginStore = {
38
+ * plugin: "arc-agi",
39
+ * version: 2,
40
+ * ensureTables(sql, from) {
41
+ * sql.exec(`CREATE TABLE IF NOT EXISTS arc_scorecards (…)`);
42
+ * if (from < 2) sql.exec(`ALTER TABLE arc_scorecards ADD COLUMN …`);
43
+ * }
44
+ * };
45
+ *
46
+ * // The query half: drizzle, over the plugin's own handle.
47
+ * const db = drizzle(storage, { schema: { arcScorecards } });
48
+ * db.select().from(arcScorecards).where(gte(arcScorecards.lastUsedAt, since));
49
+ * ```
50
+ */
51
+ export interface PluginStore {
52
+ /** Stable identifier. Also the bookkeeping row's key — never rename it. */
53
+ plugin: string;
54
+ /**
55
+ * Monotonically increasing schema version, starting at 1. Bump it whenever
56
+ * {@link ensureTables} needs to do something it did not do before.
57
+ */
58
+ version: number;
59
+ /**
60
+ * Bring this plugin's tables up to {@link version}.
61
+ *
62
+ * Called once per DO instance, and re-called on every hibernation wake-up, so
63
+ * it **must be idempotent**. `from` is the version last recorded for this
64
+ * plugin (0 on first ever run), which is what lets an upgrade path branch
65
+ * without re-running earlier DDL.
66
+ */
67
+ ensureTables(sql: SqlStorage, from: number): void;
68
+ }
69
+ /** Bookkeeping for {@link PluginStore} versions. Core owns the table; plugins own their rows. */
70
+ export declare const PLUGIN_MIGRATIONS_TABLE = "plugin_migrations";
71
+ export interface AgentDBOptions {
72
+ /**
73
+ * Plugin-owned stores to bring up alongside core's own tables. Order is
74
+ * preserved but must not matter: a plugin that depends on another plugin's
75
+ * table is a plugin that should have been one plugin.
76
+ */
77
+ stores?: readonly PluginStore[];
78
+ /**
79
+ * `CoreConfig.maxSubtasks` — the durable guard `createDecomposition` enforces.
80
+ * Passed rather than imported so one resolved config governs both the schema
81
+ * offered to the model and the write that has to hold.
82
+ */
83
+ maxSubtasks: number;
84
+ }
85
+ /**
86
+ * The agent's database: one drizzle handle over the DO's SQLite, with a memoized
87
+ * namespace per table domain (`db.tasks`, `db.subtasks`).
88
+ *
89
+ * Constructed once per DO instance. Core's migrations run in the constructor —
90
+ * the durable-sqlite migrator is idempotent, so a fresh `AgentDB` on every
91
+ * hibernation wake-up re-validates the schema safely. Call `ensureReady()` (and
92
+ * await it) before issuing any queries.
93
+ *
94
+ * Plugin stores are brought up in the same place but through a different
95
+ * mechanism, deliberately — see {@link PluginStore}.
96
+ */
97
+ export declare class AgentDB {
98
+ private readonly storage;
99
+ private readonly options;
100
+ private readonly db;
101
+ private readonly _ready;
102
+ private _tasks?;
103
+ private _subtasks?;
104
+ constructor(storage: DurableObjectStorage, options: AgentDBOptions);
105
+ ensureReady(): Promise<void>;
106
+ get tasks(): {
107
+ begin(input: {
108
+ messageId: string;
109
+ taskId: string;
110
+ contextId: string;
111
+ }): import("../a2a/task.js").PlainTask;
112
+ get(taskId: string): import("../a2a/task.js").PlainTask | null;
113
+ list(query: import("./index.js").TaskListQuery): {
114
+ tasks: import("../a2a/task.js").PlainTask[];
115
+ totalSize: number;
116
+ };
117
+ save(task: import("@a2a-js/sdk").Task): boolean;
118
+ markWorking(taskId: string): "ok" | "canceled";
119
+ cancel(taskId: string): import("../a2a/task.js").PlainTask | null;
120
+ cleanup(): void;
121
+ };
122
+ get subtasks(): {
123
+ createDecomposition(taskId: string, round: number, drafts: import("../subtasks/types.js").SubtaskDraft[]): import("../subtasks/types.js").Subtask[];
124
+ get(id: import("../subtasks/types.js").SubtaskId): import("../subtasks/types.js").Subtask | null;
125
+ list(taskId: string): import("../subtasks/types.js").Subtask[];
126
+ listRound(taskId: string, round: number): import("../subtasks/types.js").Subtask[];
127
+ start(id: import("../subtasks/types.js").SubtaskId, recipe: {
128
+ recipeId: string;
129
+ recipeVersion: number;
130
+ }): boolean;
131
+ complete(id: import("../subtasks/types.js").SubtaskId, resultParts: import("../subtasks/types.js").SubtaskResultPart[]): boolean;
132
+ fail(id: import("../subtasks/types.js").SubtaskId, error: string): boolean;
133
+ cancelRunning(id: import("../subtasks/types.js").SubtaskId): boolean;
134
+ cancelPending(taskId: string): number;
135
+ cleanup(): void;
136
+ };
137
+ /**
138
+ * Run each plugin store's DDL and record its version, after core's own
139
+ * migrations so a plugin may safely reference core tables (nothing should, but
140
+ * the ordering is free and the alternative is a surprising failure).
141
+ *
142
+ * A store that throws fails DO start rather than being skipped: a plugin whose
143
+ * tables are missing would otherwise fail later, at its first tool call, in a
144
+ * request the user is waiting on.
145
+ */
146
+ private applyStores;
147
+ }
package/dist/db/db.js ADDED
@@ -0,0 +1,90 @@
1
+ import { drizzle } from "drizzle-orm/durable-sqlite";
2
+ import { migrate } from "drizzle-orm/durable-sqlite/migrator";
3
+ import * as schema from "./schema.js";
4
+ import dbMigrations from "./migrations/index.js";
5
+ import { makeTasks } from "./models/tasks.js";
6
+ import { makeSubtasks } from "./models/subtasks.js";
7
+ /** Bookkeeping for {@link PluginStore} versions. Core owns the table; plugins own their rows. */
8
+ export const PLUGIN_MIGRATIONS_TABLE = "plugin_migrations";
9
+ /**
10
+ * The agent's database: one drizzle handle over the DO's SQLite, with a memoized
11
+ * namespace per table domain (`db.tasks`, `db.subtasks`).
12
+ *
13
+ * Constructed once per DO instance. Core's migrations run in the constructor —
14
+ * the durable-sqlite migrator is idempotent, so a fresh `AgentDB` on every
15
+ * hibernation wake-up re-validates the schema safely. Call `ensureReady()` (and
16
+ * await it) before issuing any queries.
17
+ *
18
+ * Plugin stores are brought up in the same place but through a different
19
+ * mechanism, deliberately — see {@link PluginStore}.
20
+ */
21
+ export class AgentDB {
22
+ storage;
23
+ options;
24
+ db;
25
+ _ready;
26
+ _tasks;
27
+ _subtasks;
28
+ constructor(storage, options) {
29
+ this.storage = storage;
30
+ this.options = options;
31
+ this.db = drizzle(storage, { schema });
32
+ this._ready = migrate(this.db, dbMigrations).then(() => {
33
+ this.applyStores(options.stores ?? []);
34
+ });
35
+ }
36
+ ensureReady() {
37
+ return this._ready;
38
+ }
39
+ get tasks() {
40
+ return (this._tasks ??= makeTasks(this.db));
41
+ }
42
+ get subtasks() {
43
+ return (this._subtasks ??= makeSubtasks(this.db, {
44
+ maxSubtasks: this.options.maxSubtasks
45
+ }));
46
+ }
47
+ /**
48
+ * Run each plugin store's DDL and record its version, after core's own
49
+ * migrations so a plugin may safely reference core tables (nothing should, but
50
+ * the ordering is free and the alternative is a surprising failure).
51
+ *
52
+ * A store that throws fails DO start rather than being skipped: a plugin whose
53
+ * tables are missing would otherwise fail later, at its first tool call, in a
54
+ * request the user is waiting on.
55
+ */
56
+ applyStores(stores) {
57
+ if (stores.length === 0)
58
+ return;
59
+ const sql = this.storage.sql;
60
+ sql.exec(`CREATE TABLE IF NOT EXISTS ${PLUGIN_MIGRATIONS_TABLE} (
61
+ plugin TEXT PRIMARY KEY,
62
+ version INTEGER NOT NULL,
63
+ updated_at INTEGER NOT NULL
64
+ )`);
65
+ const seen = new Set();
66
+ for (const store of stores) {
67
+ if (seen.has(store.plugin)) {
68
+ throw new Error(`duplicate PluginStore '${store.plugin}' — two plugins claim the same storage namespace`);
69
+ }
70
+ seen.add(store.plugin);
71
+ if (!Number.isInteger(store.version) || store.version < 1) {
72
+ throw new Error(`PluginStore '${store.plugin}' version must be an integer >= 1, got ${store.version}`);
73
+ }
74
+ const row = sql
75
+ .exec(`SELECT version FROM ${PLUGIN_MIGRATIONS_TABLE} WHERE plugin = ?`, store.plugin)
76
+ .toArray()[0];
77
+ const from = row?.version ?? 0;
78
+ if (from > store.version) {
79
+ throw new Error(`PluginStore '${store.plugin}' is at version ${from} on disk but the ` +
80
+ `installed plugin declares ${store.version} — downgrade is not supported`);
81
+ }
82
+ store.ensureTables(sql, from);
83
+ if (from !== store.version) {
84
+ sql.exec(`INSERT INTO ${PLUGIN_MIGRATIONS_TABLE} (plugin, version, updated_at)
85
+ VALUES (?, ?, ?)
86
+ ON CONFLICT(plugin) DO UPDATE SET version = excluded.version, updated_at = excluded.updated_at`, store.plugin, store.version, Date.now());
87
+ }
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `@dynamicagents/core/db` — the two tables core owns, and the seam a plugin uses to
3
+ * own its own without touching core's migration journal.
4
+ */
5
+ export { AgentDB, PLUGIN_MIGRATIONS_TABLE, type AgentDBOptions, type DB, type PluginStore } from "./db.js";
6
+ export { notifyTasks, subtasks } from "./schema.js";
7
+ export { makeTasks, stateOf, type TaskListQuery } from "./models/tasks.js";
8
+ export { makeSubtasks, type SubtaskModelOptions } from "./models/subtasks.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `@dynamicagents/core/db` — the two tables core owns, and the seam a plugin uses to
3
+ * own its own without touching core's migration journal.
4
+ */
5
+ export { AgentDB, PLUGIN_MIGRATIONS_TABLE } from "./db.js";
6
+ export { notifyTasks, subtasks } from "./schema.js";
7
+ export { makeTasks, stateOf } from "./models/tasks.js";
8
+ export { makeSubtasks } from "./models/subtasks.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Drizzle migrations, inlined for Cloudflare Workers.
3
+ *
4
+ * GENERATED FILE — do not edit. Run `npm run db:generate` after changing
5
+ * `src/db/schema.ts`; that runs `drizzle-kit generate` and then
6
+ * `scripts/build-migrations.mjs`, which rebuilds this from
7
+ * `meta/_journal.json` and the `.sql` files beside it.
8
+ *
9
+ * Key format: "m{zero-padded idx}" — what drizzle's durable-sqlite migrator
10
+ * reads from `config.migrations[key]`.
11
+ *
12
+ * This journal is CORE's alone. A plugin must never add an entry: the journal is
13
+ * a flat integer sequence over one shared `__drizzle_migrations` table, and two
14
+ * independently-versioned packages writing to it will collide. Plugins own their
15
+ * tables through `PluginStore` instead — see `src/db/db.ts`.
16
+ */
17
+ import type { migrate } from "drizzle-orm/durable-sqlite/migrator";
18
+ type MigrationConfig = Parameters<typeof migrate>[1];
19
+ declare const dbMigrations: MigrationConfig;
20
+ export default dbMigrations;