@dynamicagents/core 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +522 -0
- package/dist/a2a/agent-stub.d.ts +60 -0
- package/dist/a2a/agent-stub.js +1 -0
- package/dist/a2a/caller-token.d.ts +44 -0
- package/dist/a2a/caller-token.js +61 -0
- package/dist/a2a/caller.d.ts +23 -0
- package/dist/a2a/caller.js +33 -0
- package/dist/a2a/card.d.ts +158 -0
- package/dist/a2a/card.js +163 -0
- package/dist/a2a/context.d.ts +19 -0
- package/dist/a2a/context.js +61 -0
- package/dist/a2a/deliver.d.ts +159 -0
- package/dist/a2a/deliver.js +185 -0
- package/dist/a2a/executor.d.ts +84 -0
- package/dist/a2a/executor.js +106 -0
- package/dist/a2a/index.d.ts +35 -0
- package/dist/a2a/index.js +34 -0
- package/dist/a2a/notify.d.ts +106 -0
- package/dist/a2a/notify.js +180 -0
- package/dist/a2a/parts.d.ts +45 -0
- package/dist/a2a/parts.js +94 -0
- package/dist/a2a/push.d.ts +70 -0
- package/dist/a2a/push.js +53 -0
- package/dist/a2a/self-origin.d.ts +91 -0
- package/dist/a2a/self-origin.js +114 -0
- package/dist/a2a/task-store.d.ts +44 -0
- package/dist/a2a/task-store.js +99 -0
- package/dist/a2a/task.d.ts +85 -0
- package/dist/a2a/task.js +15 -0
- package/dist/a2a/verify.d.ts +80 -0
- package/dist/a2a/verify.js +143 -0
- package/dist/agent/budget.d.ts +46 -0
- package/dist/agent/budget.js +42 -0
- package/dist/agent/control.d.ts +109 -0
- package/dist/agent/control.js +115 -0
- package/dist/agent/errors.d.ts +85 -0
- package/dist/agent/errors.js +64 -0
- package/dist/agent/final-reply.d.ts +49 -0
- package/dist/agent/final-reply.js +68 -0
- package/dist/agent/history.d.ts +97 -0
- package/dist/agent/history.js +133 -0
- package/dist/agent/index.d.ts +29 -0
- package/dist/agent/index.js +29 -0
- package/dist/agent/inference.d.ts +110 -0
- package/dist/agent/inference.js +120 -0
- package/dist/agent/model.d.ts +90 -0
- package/dist/agent/model.js +1 -0
- package/dist/agent/session.d.ts +100 -0
- package/dist/agent/session.js +82 -0
- package/dist/agent/workers-ai/index.d.ts +23 -0
- package/dist/agent/workers-ai/index.js +23 -0
- package/dist/agent/workers-ai/runtime.d.ts +42 -0
- package/dist/agent/workers-ai/runtime.js +63 -0
- package/dist/alarm/index.d.ts +77 -0
- package/dist/alarm/index.js +116 -0
- package/dist/config.d.ts +202 -0
- package/dist/config.js +135 -0
- package/dist/contract/index.d.ts +9 -0
- package/dist/contract/index.js +8 -0
- package/dist/contract/plugin.d.ts +324 -0
- package/dist/contract/plugin.js +114 -0
- package/dist/contract/recipe.d.ts +180 -0
- package/dist/contract/recipe.js +1 -0
- package/dist/contract/validation.d.ts +91 -0
- package/dist/contract/validation.js +84 -0
- package/dist/db/db.d.ts +147 -0
- package/dist/db/db.js +90 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrations/index.d.ts +20 -0
- package/dist/db/migrations/index.js +60 -0
- package/dist/db/models/subtasks.d.ts +100 -0
- package/dist/db/models/subtasks.js +241 -0
- package/dist/db/models/tasks.d.ts +118 -0
- package/dist/db/models/tasks.js +274 -0
- package/dist/db/schema.d.ts +468 -0
- package/dist/db/schema.js +88 -0
- package/dist/env.d.ts +53 -0
- package/dist/env.js +47 -0
- package/dist/host/agent.d.ts +305 -0
- package/dist/host/agent.js +400 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/index.js +19 -0
- package/dist/host/plugin-host.d.ts +42 -0
- package/dist/host/plugin-host.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/job/index.d.ts +21 -0
- package/dist/job/index.js +21 -0
- package/dist/job/lifecycle.d.ts +176 -0
- package/dist/job/lifecycle.js +230 -0
- package/dist/job/state.d.ts +92 -0
- package/dist/job/state.js +40 -0
- package/dist/platform.d.ts +138 -0
- package/dist/platform.js +140 -0
- package/dist/round/agent.d.ts +271 -0
- package/dist/round/agent.js +678 -0
- package/dist/round/index.d.ts +25 -0
- package/dist/round/index.js +23 -0
- package/dist/round/policy.d.ts +98 -0
- package/dist/round/policy.js +1 -0
- package/dist/round/subagent.d.ts +87 -0
- package/dist/round/subagent.js +108 -0
- package/dist/round/turn.d.ts +249 -0
- package/dist/round/turn.js +564 -0
- package/dist/round/workflow.d.ts +147 -0
- package/dist/round/workflow.js +387 -0
- package/dist/runtime/index.d.ts +127 -0
- package/dist/runtime/index.js +186 -0
- package/dist/runtime/tool-families.d.ts +32 -0
- package/dist/runtime/tool-families.js +61 -0
- package/dist/subagent/fingerprint.d.ts +37 -0
- package/dist/subagent/fingerprint.js +92 -0
- package/dist/subagent/index.d.ts +169 -0
- package/dist/subagent/index.js +330 -0
- package/dist/subagent/prompt.d.ts +32 -0
- package/dist/subagent/prompt.js +37 -0
- package/dist/subagent/run.d.ts +157 -0
- package/dist/subagent/run.js +540 -0
- package/dist/subagent/workspace.d.ts +85 -0
- package/dist/subagent/workspace.js +127 -0
- package/dist/subtasks/catalog.d.ts +40 -0
- package/dist/subtasks/catalog.js +36 -0
- package/dist/subtasks/decomposition.d.ts +85 -0
- package/dist/subtasks/decomposition.js +156 -0
- package/dist/subtasks/delegate.d.ts +120 -0
- package/dist/subtasks/delegate.js +131 -0
- package/dist/subtasks/index.d.ts +9 -0
- package/dist/subtasks/index.js +9 -0
- package/dist/subtasks/subtask-types.d.ts +91 -0
- package/dist/subtasks/subtask-types.js +103 -0
- package/dist/subtasks/types.d.ts +295 -0
- package/dist/subtasks/types.js +15 -0
- package/dist/testing/auth.d.ts +34 -0
- package/dist/testing/auth.js +35 -0
- package/dist/testing/do.d.ts +29 -0
- package/dist/testing/do.js +25 -0
- package/dist/testing/fake-session.d.ts +26 -0
- package/dist/testing/fake-session.js +37 -0
- package/dist/testing/fixtures.d.ts +64 -0
- package/dist/testing/fixtures.js +104 -0
- package/dist/testing/harness.d.ts +97 -0
- package/dist/testing/harness.js +138 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.js +35 -0
- package/dist/testing/mock-model.d.ts +77 -0
- package/dist/testing/mock-model.js +136 -0
- package/dist/testing/node.d.ts +56 -0
- package/dist/testing/node.js +56 -0
- package/dist/testing/vcr-global-setup.d.ts +12 -0
- package/dist/testing/vcr-global-setup.js +15 -0
- package/dist/testing/vcr-shared.d.ts +38 -0
- package/dist/testing/vcr-shared.js +33 -0
- package/dist/testing/vcr-spec.d.ts +25 -0
- package/dist/testing/vcr-spec.js +124 -0
- package/dist/testing/vcr-store.d.ts +86 -0
- package/dist/testing/vcr-store.js +191 -0
- package/dist/testing/vcr.d.ts +117 -0
- package/dist/testing/vcr.js +275 -0
- package/dist/worker/define-agent.d.ts +123 -0
- package/dist/worker/define-agent.js +20 -0
- package/dist/worker/index.d.ts +218 -0
- package/dist/worker/index.js +369 -0
- package/eslint-rules/index.js +31 -0
- package/eslint-rules/no-deprecated-object-properties.js +81 -0
- package/package.json +178 -0
- package/scripts/generate-keys.mjs +48 -0
|
@@ -0,0 +1,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
|
+
};
|