@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,60 @@
1
+ const dbMigrations = {
2
+ journal: {
3
+ entries: [
4
+ {
5
+ idx: 0,
6
+ when: 1785537420937,
7
+ tag: "0000_init",
8
+ breakpoints: true
9
+ },
10
+ {
11
+ idx: 1,
12
+ when: 1786871925215,
13
+ tag: "0001_drop_depends_on",
14
+ breakpoints: true
15
+ }
16
+ ]
17
+ },
18
+ migrations: {
19
+ m0000: `CREATE TABLE \`notify_tasks\` (
20
+ \`task_id\` text PRIMARY KEY NOT NULL,
21
+ \`message_id\` text,
22
+ \`context_id\` text DEFAULT '' NOT NULL,
23
+ \`state\` text NOT NULL,
24
+ \`task_json\` text NOT NULL,
25
+ \`created_at\` integer NOT NULL,
26
+ \`updated_at\` integer NOT NULL
27
+ );
28
+ --> statement-breakpoint
29
+ CREATE UNIQUE INDEX \`notify_tasks_message_id_unique\` ON \`notify_tasks\` (\`message_id\`);--> statement-breakpoint
30
+ CREATE INDEX \`idx_notify_tasks_created_at\` ON \`notify_tasks\` (\`created_at\`);--> statement-breakpoint
31
+ CREATE INDEX \`idx_notify_tasks_context\` ON \`notify_tasks\` (\`context_id\`);--> statement-breakpoint
32
+ CREATE INDEX \`idx_notify_tasks_state\` ON \`notify_tasks\` (\`state\`);--> statement-breakpoint
33
+ CREATE TABLE \`subtasks\` (
34
+ \`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
35
+ \`task_id\` text NOT NULL,
36
+ \`round\` integer NOT NULL,
37
+ \`ordinal\` integer NOT NULL,
38
+ \`type\` text NOT NULL,
39
+ \`recipe_id\` text,
40
+ \`recipe_version\` integer,
41
+ \`prompt\` text NOT NULL,
42
+ \`references_json\` text NOT NULL,
43
+ \`depends_on_json\` text NOT NULL,
44
+ \`params_json\` text DEFAULT '{}' NOT NULL,
45
+ \`status\` text NOT NULL,
46
+ \`result_parts_json\` text,
47
+ \`error\` text,
48
+ \`created_at\` integer NOT NULL,
49
+ \`updated_at\` integer NOT NULL,
50
+ \`completed_at\` integer
51
+ );
52
+ --> statement-breakpoint
53
+ CREATE UNIQUE INDEX \`idx_subtasks_task_ordinal\` ON \`subtasks\` (\`task_id\`,\`ordinal\`);--> statement-breakpoint
54
+ CREATE INDEX \`idx_subtasks_task_round\` ON \`subtasks\` (\`task_id\`,\`round\`);--> statement-breakpoint
55
+ CREATE INDEX \`idx_subtasks_status\` ON \`subtasks\` (\`status\`);--> statement-breakpoint
56
+ CREATE INDEX \`idx_subtasks_created_at\` ON \`subtasks\` (\`created_at\`);`,
57
+ m0001: `ALTER TABLE \`subtasks\` DROP COLUMN \`depends_on_json\`;`
58
+ }
59
+ };
60
+ export default dbMigrations;
@@ -0,0 +1,100 @@
1
+ import type { DB } from "../db.js";
2
+ import type { Subtask, SubtaskDraft, SubtaskId, SubtaskResultPart } from "../../subtasks/types.js";
3
+ export interface SubtaskModelOptions {
4
+ /**
5
+ * Upper bound on subtasks per round, re-checked here as the **durable** guard.
6
+ * The delegation schema offers the model the same bound, but a schema is a
7
+ * suggestion to a model and this is the write that has to hold.
8
+ */
9
+ maxSubtasks: number;
10
+ }
11
+ /**
12
+ * Query methods for the `subtasks` table (durable decomposed units of work).
13
+ *
14
+ * Bound to a drizzle handle by {@link AgentDB} and reached as `db.subtasks.*`.
15
+ * durable-sqlite is synchronous; the multi-statement `createDecomposition` runs
16
+ * inside an explicit `db.transaction` (drizzle maps it to
17
+ * `storage.transactionSync`), so a mid-create failure rolls back every statement
18
+ * instead of leaving a partial decomposition. Guarded transitions filter on the expected
19
+ * current `status`, so a disallowed transition matches no row and is a no-op.
20
+ */
21
+ export declare function makeSubtasks(db: DB, opts: SubtaskModelOptions): {
22
+ /**
23
+ * Create one **round's** decomposition atomically: the whole check-and-insert
24
+ * sequence runs in one synchronous `db.transaction`, so a failure anywhere
25
+ * rolls back every statement rather than leaving a truncated subtask set.
26
+ * Idempotent on `(taskId, round)`: if this round already has Subtasks,
27
+ * returns them unchanged (a Workflow-step retry must not duplicate work); the
28
+ * unique `(task_id, ordinal)` index is the schema-level backstop. Enforces the
29
+ * 1..8 per-round bound as the durable guard.
30
+ *
31
+ * `ordinal` continues across rounds (it is the Task-wide position, and what
32
+ * the unique index is built on), so a later round's rows sort after the
33
+ * earlier ones in {@link list}.
34
+ */
35
+ createDecomposition(taskId: string, round: number, drafts: SubtaskDraft[]): Subtask[];
36
+ /** Load one Subtask by id. */
37
+ get(id: SubtaskId): Subtask | null;
38
+ /** List every round's Subtasks for a Task, in ordinal order. */
39
+ list(taskId: string): Subtask[];
40
+ /**
41
+ * List one round's Subtasks, in ordinal order — what the Workflow's `scan`
42
+ * projects. It executes a single round at a time, so it must not see a
43
+ * sibling round's rows; a later round, by contrast, reads {@link list}
44
+ * across all rounds to reunite each `delegate` call with its results.
45
+ */
46
+ listRound(taskId: string, round: number): Subtask[];
47
+ /**
48
+ * Begin execution: guarded `pending -> running`, recording the resolved
49
+ * Recipe id/version after-the-fact. Returns false if the Subtask was not
50
+ * pending (already started, terminal, or unknown).
51
+ */
52
+ start(id: SubtaskId, recipe: {
53
+ recipeId: string;
54
+ recipeVersion: number;
55
+ }): boolean;
56
+ /**
57
+ * Persist a successful terminal result: guarded `running -> completed`.
58
+ * Requires at least one non-empty text result part (a successful Recipe
59
+ * output invariant).
60
+ */
61
+ complete(id: SubtaskId, resultParts: SubtaskResultPart[]): boolean;
62
+ /**
63
+ * Persist a failure from either non-terminal status, with a diagnostic message.
64
+ *
65
+ * Both sides are reachable and both must land. A child's failed result arrives
66
+ * on a `running` row. The Workflow's last resort — `failSubtask`, once
67
+ * `execute:<id>` has exhausted every retry — can arrive on either:
68
+ * `executeSubtask` may throw before its `pending -> running` claim (an
69
+ * unresolvable recipe) or after it (a transient child fault). Leaving either
70
+ * behind strands the row in a non-terminal state that nobody is coming back
71
+ * to resolve.
72
+ *
73
+ * Returns false once the row is terminal — a late loser to the real result.
74
+ */
75
+ fail(id: SubtaskId, error: string): boolean;
76
+ /**
77
+ * Discard a late result after parent cancellation: guarded
78
+ * `running -> canceled`. The parent calls this when a child returned a
79
+ * terminal result but the Task was canceled while it ran — the result is
80
+ * dropped, and this leaves the row in a truthful terminal state instead of a
81
+ * `running` that never resolves. Returns false if the Subtask was not running.
82
+ */
83
+ cancelRunning(id: SubtaskId): boolean;
84
+ /**
85
+ * Cancel every still-pending Subtask of a Task (parent cancellation).
86
+ * Running Subtasks are left alone here — the parent transitions those with
87
+ * {@link cancelRunning} once their in-flight result comes back and is
88
+ * discarded. Returns the number canceled.
89
+ *
90
+ * Keyed on the Task rather than one id, so this is the one guarded
91
+ * transition that cannot go through {@link transition} and sets its own
92
+ * timestamps. `completedAt` is part of that and not optional: every other
93
+ * terminal write records it, and a terminal row without one reads as still
94
+ * in flight to anything measuring how long a Subtask took or when a Task
95
+ * actually stopped.
96
+ */
97
+ cancelPending(taskId: string): number;
98
+ /** Delete Subtasks older than 30 days (called by the weekly maintenance cron). */
99
+ cleanup(): void;
100
+ };
@@ -0,0 +1,241 @@
1
+ import { and, eq, inArray, lt, max } from "drizzle-orm";
2
+ import { z } from "zod";
3
+ import { subtasks } from "../schema.js";
4
+ const referenceSchema = z.object({
5
+ role: z.enum(["user", "assistant"]),
6
+ text: z.string()
7
+ });
8
+ const resultPartSchema = z.object({
9
+ kind: z.literal("text"),
10
+ text: z.string()
11
+ });
12
+ const referencesSchema = z.array(referenceSchema);
13
+ const resultPartsSchema = z.array(resultPartSchema);
14
+ const paramsSchema = z.record(z.string(), z.string());
15
+ /**
16
+ * Query methods for the `subtasks` table (durable decomposed units of work).
17
+ *
18
+ * Bound to a drizzle handle by {@link AgentDB} and reached as `db.subtasks.*`.
19
+ * durable-sqlite is synchronous; the multi-statement `createDecomposition` runs
20
+ * inside an explicit `db.transaction` (drizzle maps it to
21
+ * `storage.transactionSync`), so a mid-create failure rolls back every statement
22
+ * instead of leaving a partial decomposition. Guarded transitions filter on the expected
23
+ * current `status`, so a disallowed transition matches no row and is a no-op.
24
+ */
25
+ export function makeSubtasks(db, opts) {
26
+ const rowToSubtask = (row) => ({
27
+ id: row.id,
28
+ taskId: row.taskId,
29
+ round: row.round,
30
+ ordinal: row.ordinal,
31
+ type: row.type,
32
+ recipeId: row.recipeId,
33
+ recipeVersion: row.recipeVersion,
34
+ prompt: row.prompt,
35
+ references: referencesSchema.parse(JSON.parse(row.referencesJson)),
36
+ params: paramsSchema.parse(JSON.parse(row.paramsJson)),
37
+ status: row.status,
38
+ resultParts: row.resultPartsJson === null
39
+ ? null
40
+ : resultPartsSchema.parse(JSON.parse(row.resultPartsJson)),
41
+ error: row.error,
42
+ createdAt: row.createdAt,
43
+ updatedAt: row.updatedAt,
44
+ completedAt: row.completedAt
45
+ });
46
+ const list = (taskId) => db
47
+ .select()
48
+ .from(subtasks)
49
+ .where(eq(subtasks.taskId, taskId))
50
+ .orderBy(subtasks.ordinal)
51
+ .all()
52
+ .map(rowToSubtask);
53
+ /** Rides `idx_subtasks_task_round` — no full-Task scan to reach one round. */
54
+ const listRound = (taskId, round) => db
55
+ .select()
56
+ .from(subtasks)
57
+ .where(and(eq(subtasks.taskId, taskId), eq(subtasks.round, round)))
58
+ .orderBy(subtasks.ordinal)
59
+ .all()
60
+ .map(rowToSubtask);
61
+ /** Guarded status update: applies only from a `from` status, returns whether it did. */
62
+ const transition = (id, from, set) => {
63
+ const now = Date.now();
64
+ const statuses = Array.isArray(from) ? from : [from];
65
+ const updated = db
66
+ .update(subtasks)
67
+ .set({ ...set, updatedAt: now })
68
+ .where(and(eq(subtasks.id, id), inArray(subtasks.status, statuses)))
69
+ .returning({ id: subtasks.id })
70
+ .all();
71
+ return updated.length > 0;
72
+ };
73
+ return {
74
+ /**
75
+ * Create one **round's** decomposition atomically: the whole check-and-insert
76
+ * sequence runs in one synchronous `db.transaction`, so a failure anywhere
77
+ * rolls back every statement rather than leaving a truncated subtask set.
78
+ * Idempotent on `(taskId, round)`: if this round already has Subtasks,
79
+ * returns them unchanged (a Workflow-step retry must not duplicate work); the
80
+ * unique `(task_id, ordinal)` index is the schema-level backstop. Enforces the
81
+ * 1..8 per-round bound as the durable guard.
82
+ *
83
+ * `ordinal` continues across rounds (it is the Task-wide position, and what
84
+ * the unique index is built on), so a later round's rows sort after the
85
+ * earlier ones in {@link list}.
86
+ */
87
+ createDecomposition(taskId, round, drafts) {
88
+ return db.transaction(() => {
89
+ const existing = listRound(taskId, round);
90
+ if (existing.length > 0)
91
+ return existing;
92
+ if (drafts.length < 1 || drafts.length > opts.maxSubtasks) {
93
+ throw new Error(`decomposition must have 1..${opts.maxSubtasks} subtasks, got ${drafts.length}`);
94
+ }
95
+ // References must match the persisted shape before we serialize them.
96
+ for (const d of drafts)
97
+ referencesSchema.parse(d.references);
98
+ const now = Date.now();
99
+ // Ordinals continue above every earlier round's rows — from the current
100
+ // maximum, not a row count, so a gap left by {@link cleanup} deleting part
101
+ // of a Task cannot hand a later round an ordinal that is already taken.
102
+ // Both read the `(task_id, ordinal)` index; only this one stays monotonic.
103
+ const highest = db
104
+ .select({ max: max(subtasks.ordinal) })
105
+ .from(subtasks)
106
+ .where(eq(subtasks.taskId, taskId))
107
+ .get()?.max ?? null;
108
+ const firstOrdinal = highest === null ? 0 : highest + 1;
109
+ drafts.forEach((d, offset) => {
110
+ db.insert(subtasks)
111
+ .values({
112
+ taskId,
113
+ round,
114
+ ordinal: firstOrdinal + offset,
115
+ type: d.type,
116
+ recipeId: null,
117
+ recipeVersion: null,
118
+ prompt: d.prompt,
119
+ referencesJson: JSON.stringify(d.references),
120
+ paramsJson: JSON.stringify(d.params ?? {}),
121
+ status: "pending",
122
+ resultPartsJson: null,
123
+ error: null,
124
+ createdAt: now,
125
+ updatedAt: now,
126
+ completedAt: null
127
+ })
128
+ .run();
129
+ });
130
+ return listRound(taskId, round);
131
+ });
132
+ },
133
+ /** Load one Subtask by id. */
134
+ get(id) {
135
+ const row = db.select().from(subtasks).where(eq(subtasks.id, id)).get();
136
+ return row ? rowToSubtask(row) : null;
137
+ },
138
+ /** List every round's Subtasks for a Task, in ordinal order. */
139
+ list(taskId) {
140
+ return list(taskId);
141
+ },
142
+ /**
143
+ * List one round's Subtasks, in ordinal order — what the Workflow's `scan`
144
+ * projects. It executes a single round at a time, so it must not see a
145
+ * sibling round's rows; a later round, by contrast, reads {@link list}
146
+ * across all rounds to reunite each `delegate` call with its results.
147
+ */
148
+ listRound(taskId, round) {
149
+ return listRound(taskId, round);
150
+ },
151
+ /**
152
+ * Begin execution: guarded `pending -> running`, recording the resolved
153
+ * Recipe id/version after-the-fact. Returns false if the Subtask was not
154
+ * pending (already started, terminal, or unknown).
155
+ */
156
+ start(id, recipe) {
157
+ return transition(id, "pending", {
158
+ status: "running",
159
+ recipeId: recipe.recipeId,
160
+ recipeVersion: recipe.recipeVersion
161
+ });
162
+ },
163
+ /**
164
+ * Persist a successful terminal result: guarded `running -> completed`.
165
+ * Requires at least one non-empty text result part (a successful Recipe
166
+ * output invariant).
167
+ */
168
+ complete(id, resultParts) {
169
+ const parts = resultPartsSchema.parse(resultParts);
170
+ if (!parts.some((p) => p.text.trim().length > 0)) {
171
+ throw new Error("completed subtask requires a non-empty text part");
172
+ }
173
+ return transition(id, "running", {
174
+ status: "completed",
175
+ resultPartsJson: JSON.stringify(parts),
176
+ completedAt: Date.now()
177
+ });
178
+ },
179
+ /**
180
+ * Persist a failure from either non-terminal status, with a diagnostic message.
181
+ *
182
+ * Both sides are reachable and both must land. A child's failed result arrives
183
+ * on a `running` row. The Workflow's last resort — `failSubtask`, once
184
+ * `execute:<id>` has exhausted every retry — can arrive on either:
185
+ * `executeSubtask` may throw before its `pending -> running` claim (an
186
+ * unresolvable recipe) or after it (a transient child fault). Leaving either
187
+ * behind strands the row in a non-terminal state that nobody is coming back
188
+ * to resolve.
189
+ *
190
+ * Returns false once the row is terminal — a late loser to the real result.
191
+ */
192
+ fail(id, error) {
193
+ return transition(id, ["running", "pending"], {
194
+ status: "failed",
195
+ error,
196
+ completedAt: Date.now()
197
+ });
198
+ },
199
+ /**
200
+ * Discard a late result after parent cancellation: guarded
201
+ * `running -> canceled`. The parent calls this when a child returned a
202
+ * terminal result but the Task was canceled while it ran — the result is
203
+ * dropped, and this leaves the row in a truthful terminal state instead of a
204
+ * `running` that never resolves. Returns false if the Subtask was not running.
205
+ */
206
+ cancelRunning(id) {
207
+ return transition(id, "running", {
208
+ status: "canceled",
209
+ completedAt: Date.now()
210
+ });
211
+ },
212
+ /**
213
+ * Cancel every still-pending Subtask of a Task (parent cancellation).
214
+ * Running Subtasks are left alone here — the parent transitions those with
215
+ * {@link cancelRunning} once their in-flight result comes back and is
216
+ * discarded. Returns the number canceled.
217
+ *
218
+ * Keyed on the Task rather than one id, so this is the one guarded
219
+ * transition that cannot go through {@link transition} and sets its own
220
+ * timestamps. `completedAt` is part of that and not optional: every other
221
+ * terminal write records it, and a terminal row without one reads as still
222
+ * in flight to anything measuring how long a Subtask took or when a Task
223
+ * actually stopped.
224
+ */
225
+ cancelPending(taskId) {
226
+ const now = Date.now();
227
+ const canceled = db
228
+ .update(subtasks)
229
+ .set({ status: "canceled", updatedAt: now, completedAt: now })
230
+ .where(and(eq(subtasks.taskId, taskId), eq(subtasks.status, "pending")))
231
+ .returning({ id: subtasks.id })
232
+ .all();
233
+ return canceled.length;
234
+ },
235
+ /** Delete Subtasks older than 30 days (called by the weekly maintenance cron). */
236
+ cleanup() {
237
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
238
+ db.delete(subtasks).where(lt(subtasks.createdAt, cutoff)).run();
239
+ }
240
+ };
241
+ }
@@ -0,0 +1,118 @@
1
+ import { Task, TaskState } from "@a2a-js/sdk";
2
+ import { type PlainTask } from "../../a2a/task.js";
3
+ import type { DB } from "../db.js";
4
+ /** Filters + paging for {@link makeTasks} `list` (the A2A `ListTasks` method). */
5
+ export interface TaskListQuery {
6
+ /** A2A context id to filter on; empty/absent means no filter. */
7
+ contextId?: string;
8
+ /** Task state to filter on, or `undefined` for any state. */
9
+ state?: TaskState;
10
+ /** Only tasks whose status was written at or after this epoch-ms instant. */
11
+ updatedAfter?: number;
12
+ /** Drop `artifacts` from the returned tasks (the spec's default). */
13
+ includeArtifacts: boolean;
14
+ /** Cap on each task's `history`; `undefined` keeps it whole, `0` drops it. */
15
+ historyLength?: number;
16
+ limit: number;
17
+ offset: number;
18
+ }
19
+ /**
20
+ * A task's state, tolerating the `status`-less task the SDK's generated type
21
+ * permits (`TaskStatus | undefined`). Nothing we build omits it, so an
22
+ * unspecified state means the row came from somewhere unexpected — and it
23
+ * compares equal to none of the states the callers switch on.
24
+ */
25
+ export declare function stateOf(task: Task): TaskState;
26
+ /**
27
+ * Query methods for the `notify_tasks` table (async A2A task state).
28
+ *
29
+ * Bound to a drizzle handle by {@link AgentDB} and reached as `db.tasks.*`.
30
+ * Migrations are owned by `AgentDB`, not this factory — it only issues queries.
31
+ *
32
+ * Rows hold the task in its **A2A wire form** (`Task.toJSON`), not the in-memory
33
+ * protobuf shape the SDK hands us. Those differ under v1.0 — enums are numbers
34
+ * in memory but `SCREAMING_SNAKE` strings on the wire, and `Part.content` is a
35
+ * `{ $case, value }` wrapper in memory but a bare named key on the wire — so a
36
+ * plain `JSON.stringify` would persist a shape that is neither valid A2A JSON
37
+ * nor stable across SDK versions. Encoding on write and decoding on read keeps
38
+ * the stored bytes the spec's own format, which is also exactly what
39
+ * `postNotification` puts on the wire.
40
+ */
41
+ export declare function makeTasks(db: DB): {
42
+ /**
43
+ * Accept a turn: return the `submitted` task for the given `messageId`,
44
+ * creating it on first sight. Idempotent on `messageId` (the gatekeeper's
45
+ * dedupe key, stable across dispatch retries).
46
+ */
47
+ begin(input: {
48
+ messageId: string;
49
+ taskId: string;
50
+ contextId: string;
51
+ }): PlainTask;
52
+ /** Load a task by id (for `GetTask` via the Worker's `DurableTaskStore`). */
53
+ get(taskId: string): PlainTask | null;
54
+ /**
55
+ * A page of this caller's tasks for `ListTasks`, newest first, with the
56
+ * total matching count so the store can decide whether a next page exists.
57
+ */
58
+ list(query: TaskListQuery): {
59
+ tasks: PlainTask[];
60
+ totalSize: number;
61
+ };
62
+ /**
63
+ * Upsert a task by id, preserving the `message_id` set by {@link begin}.
64
+ * Returns whether the write applied.
65
+ *
66
+ * Guarded exactly like {@link markWorking}, and for the same reason: a
67
+ * `canceled` row is terminal, so nothing may write a non-canceled state over
68
+ * it. That closes the window between a workflow's terminal build and its
69
+ * callback — the read-check-write is synchronous here, so a `CancelTask`
70
+ * landing mid-delivery makes this return `false` and the notify never fires.
71
+ *
72
+ * The reverse direction is guarded too: writing `canceled` over an already
73
+ * `completed`/`failed` row is refused, mirroring {@link cancel}'s own source
74
+ * guard. Without it, a cancellation landing between the Workflow's `complete`
75
+ * and `notify` steps — separate, independently-retried steps — could flip
76
+ * storage to canceled while `deliver()` still posts the cached completed
77
+ * task it already built, the exact race this guard exists to close. Writing
78
+ * `canceled` onto a `submitted`/`working` row, or re-writing it onto an
79
+ * already-`canceled` one, stays allowed: that is how the a2a-js handler's own
80
+ * cancel branch records the cancellation.
81
+ *
82
+ * **And no terminal row may be replaced by a *different* terminal state.**
83
+ * The two rules above were written about cancellation and between them left
84
+ * `completed → failed` wide open, which is not hypothetical: a workflow whose
85
+ * `notify` step exhausts its retries throws *after* `complete` durably saved
86
+ * a completed Task, and an abandoned-task recovery above it would then write
87
+ * a generic failure over a real answer and post a callback contradicting it.
88
+ * A turn that succeeded would be recorded as having failed because a webhook
89
+ * was flaky.
90
+ *
91
+ * Same terminal state re-written is still allowed, and must be: a Workflow
92
+ * replay legitimately re-runs `complete` and saves what it already saved, and
93
+ * refusing that would suppress the callback that replay exists to send.
94
+ */
95
+ save(task: Task): boolean;
96
+ /**
97
+ * Move a task to `working` (a turn workflow's first step). Returns
98
+ * `"canceled"` when the row is already canceled, which is the caller's
99
+ * signal to stop the turn; every other outcome — unknown task, or a task
100
+ * already past `submitted` — is a no-op reported as `"ok"`, because a
101
+ * workflow replay legitimately re-runs this step.
102
+ */
103
+ markWorking(taskId: string): "ok" | "canceled";
104
+ /**
105
+ * Flip the task to `canceled` and return it, or `null` if the row is not
106
+ * eligible — unknown, or already past `submitted`/`working`. Guarding the
107
+ * source state (not just the destination, as {@link save} does) matters
108
+ * because `complete`/`notify` are separate Workflow steps: without this, a
109
+ * cancellation landing between them would flip an already-`completed` or
110
+ * `failed` row to `canceled` right as `deliver()` posts the terminal
111
+ * callback it had already built, silently rewriting a delivered result.
112
+ * Terminal: once this lands, {@link save} refuses every non-canceled write,
113
+ * so no completed or failed callback can be built from this row afterwards.
114
+ */
115
+ cancel(taskId: string): PlainTask | null;
116
+ /** Delete all tasks older than 30 days (called by the maintenance cron). */
117
+ cleanup(): void;
118
+ };