@vercel/factory 0.0.15 → 0.0.16
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/CHANGELOG.md +356 -0
- package/README.md +49 -261
- package/dist/agent-routes.d.mts +47 -3
- package/dist/agent-routes.mjs +28 -1
- package/dist/agent-routes.mjs.map +1 -1
- package/dist/api-contracts.d.mts +20 -1
- package/dist/api-contracts.mjs +2 -1
- package/dist/api-contracts.mjs.map +1 -1
- package/dist/api.d.mts +45 -2
- package/dist/api.mjs +199 -10
- package/dist/api.mjs.map +1 -1
- package/dist/approval-contracts.d.mts +6 -0
- package/dist/blob/index.d.mts +51 -14
- package/dist/blob/index.mjs +26 -10
- package/dist/blob/index.mjs.map +1 -1
- package/dist/budget.d.mts +7 -0
- package/dist/budget.mjs +6 -0
- package/dist/budget.mjs.map +1 -1
- package/dist/build-factory.d.mts +1 -0
- package/dist/change-verification/dispatch.d.mts +16 -3
- package/dist/change-verification/dispatch.mjs +45 -7
- package/dist/change-verification/dispatch.mjs.map +1 -1
- package/dist/change-verification/eve-tool.d.mts +2 -1
- package/dist/change-verification/eve-tool.mjs +35 -47
- package/dist/change-verification/eve-tool.mjs.map +1 -1
- package/dist/change-verification/result.mjs +130 -0
- package/dist/change-verification/result.mjs.map +1 -0
- package/dist/changes/eve-record-change.d.mts +2 -2
- package/dist/changes/eve-record-change.mjs +43 -9
- package/dist/changes/eve-record-change.mjs.map +1 -1
- package/dist/changes.d.mts +4 -3
- package/dist/changes.mjs +2 -2
- package/dist/changes.mjs.map +1 -1
- package/dist/client-events.d.mts +10 -3
- package/dist/client-events.mjs +6 -2
- package/dist/client-events.mjs.map +1 -1
- package/dist/client-stream.mjs +8 -2
- package/dist/client-stream.mjs.map +1 -1
- package/dist/client-transcript.mjs +5 -1
- package/dist/client-transcript.mjs.map +1 -1
- package/dist/client.d.mts +121 -12
- package/dist/client.mjs +117 -9
- package/dist/client.mjs.map +1 -1
- package/dist/code-review/contracts.d.mts +1 -0
- package/dist/code-review/eve-post-review.d.mts +4 -4
- package/dist/code-review/eve-post-review.mjs +29 -17
- package/dist/code-review/eve-post-review.mjs.map +1 -1
- package/dist/code-review/eve-review-comments.d.mts +13 -2
- package/dist/code-review/eve-review-comments.mjs +45 -11
- package/dist/code-review/eve-review-comments.mjs.map +1 -1
- package/dist/code-review/github-reporter.d.mts +2 -0
- package/dist/code-review/github-reporter.mjs +7 -5
- package/dist/code-review/github-reporter.mjs.map +1 -1
- package/dist/code-review.d.mts +3 -2
- package/dist/deepsec/eve-tool.mjs +3 -1
- package/dist/deepsec/eve-tool.mjs.map +1 -1
- package/dist/dispatch.d.mts +79 -8
- package/dist/dispatch.mjs +68 -9
- package/dist/dispatch.mjs.map +1 -1
- package/dist/eve/index.d.mts +70 -10
- package/dist/eve/index.mjs +93 -22
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/invoke.mjs +24 -11
- package/dist/eve/invoke.mjs.map +1 -1
- package/dist/eve/session-client.d.mts +122 -4
- package/dist/eve/session-client.mjs +127 -13
- package/dist/eve/session-client.mjs.map +1 -1
- package/dist/eve/task-execution.d.mts +380 -0
- package/dist/eve/task-execution.mjs +57 -2
- package/dist/eve/task-execution.mjs.map +1 -1
- package/dist/eve/task-session.d.mts +44 -2
- package/dist/eve/task-session.mjs +44 -2
- package/dist/eve/task-session.mjs.map +1 -1
- package/dist/eve/transcript.mjs +5 -1
- package/dist/eve/transcript.mjs.map +1 -1
- package/dist/execution.d.mts +117 -9
- package/dist/execution.mjs +76 -6
- package/dist/execution.mjs.map +1 -1
- package/dist/finding-remediation/admission.d.mts +2 -0
- package/dist/finding-remediation/admission.mjs +4 -1
- package/dist/finding-remediation/admission.mjs.map +1 -1
- package/dist/findings.d.mts +1 -0
- package/dist/github-publication.d.mts +1 -0
- package/dist/github-publication.mjs +97 -84
- package/dist/github-publication.mjs.map +1 -1
- package/dist/github-transfer.d.mts +15 -6
- package/dist/github-transfer.mjs +214 -66
- package/dist/github-transfer.mjs.map +1 -1
- package/dist/github.d.mts +51 -11
- package/dist/github.mjs +126 -24
- package/dist/github.mjs.map +1 -1
- package/dist/inbox-activity.d.mts +53 -0
- package/dist/inbox-activity.mjs +41 -0
- package/dist/inbox-activity.mjs.map +1 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -2
- package/dist/intake-contracts.d.mts +0 -1
- package/dist/integrations/deepsec.d.mts +1 -0
- package/dist/integrations/github.d.mts +2 -2
- package/dist/integrations/github.mjs +2 -2
- package/dist/integrations/slack.d.mts +3 -1
- package/dist/integrations/slack.mjs +3 -1
- package/dist/integrations/vercel.d.mts +4 -2
- package/dist/integrations/vercel.mjs +3 -2
- package/dist/merge-resolution/eve-tools.d.mts +1 -0
- package/dist/merge-resolution/eve-tools.mjs +7 -2
- package/dist/merge-resolution/eve-tools.mjs.map +1 -1
- package/dist/model-settings.d.mts +41 -0
- package/dist/model-settings.mjs +35 -0
- package/dist/model-settings.mjs.map +1 -0
- package/dist/planning/reconcile.mjs +6 -0
- package/dist/planning/reconcile.mjs.map +1 -1
- package/dist/postgres/index.d.mts +43 -2
- package/dist/postgres/index.mjs +40 -2
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/presets/software-development/dispatch.d.mts +4 -1
- package/dist/presets/software-development/dispatch.mjs +2 -1
- package/dist/presets/software-development/dispatch.mjs.map +1 -1
- package/dist/presets/software-development/task-communication.d.mts +1 -0
- package/dist/presets/software-development/task-communication.mjs +48 -11
- package/dist/presets/software-development/task-communication.mjs.map +1 -1
- package/dist/presets/software-development.d.mts +1 -0
- package/dist/pull-requests/github-publisher.d.mts +15 -1
- package/dist/pull-requests/github-publisher.mjs +61 -1
- package/dist/pull-requests/github-publisher.mjs.map +1 -1
- package/dist/pull-requests.d.mts +1 -0
- package/dist/sandbox/index.d.mts +1 -0
- package/dist/schema/agent-route.d.mts +19 -1
- package/dist/schema/agent-route.mjs +19 -1
- package/dist/schema/agent-route.mjs.map +1 -1
- package/dist/schema/factory-config.d.mts +27 -0
- package/dist/schema/factory-config.mjs +33 -3
- package/dist/schema/factory-config.mjs.map +1 -1
- package/dist/schema/repository.d.mts +4 -0
- package/dist/schema/repository.mjs +5 -1
- package/dist/schema/repository.mjs.map +1 -1
- package/dist/schema/session.d.mts +1 -0
- package/dist/schema/session.mjs +1 -0
- package/dist/schema/session.mjs.map +1 -1
- package/dist/schema/slack-pr-notifications.d.mts +12 -0
- package/dist/schema/slack-pr-notifications.mjs +11 -0
- package/dist/schema/slack-pr-notifications.mjs.map +1 -0
- package/dist/schema/task-graph.d.mts +39 -0
- package/dist/schema/task.d.mts +1 -0
- package/dist/schema/task.mjs +2 -1
- package/dist/schema/task.mjs.map +1 -1
- package/dist/schema/transcript.d.mts +6 -0
- package/dist/schema/transcript.mjs +2 -1
- package/dist/schema/transcript.mjs.map +1 -1
- package/dist/schema/work.d.mts +52 -3
- package/dist/schema/work.mjs.map +1 -1
- package/dist/session-previews.d.mts +76 -0
- package/dist/session-previews.mjs +55 -0
- package/dist/session-previews.mjs.map +1 -0
- package/dist/session-review.d.mts +120 -0
- package/dist/session-review.mjs +79 -0
- package/dist/session-review.mjs.map +1 -0
- package/dist/signal-triage.mjs +1 -1
- package/dist/signals.d.mts +1 -0
- package/dist/stall.d.mts +4 -1
- package/dist/stall.mjs +6 -2
- package/dist/stall.mjs.map +1 -1
- package/dist/store/driver.d.mts +1 -1
- package/dist/store/driver.mjs.map +1 -1
- package/dist/store/engine.d.mts +206 -8
- package/dist/store/engine.mjs +147 -13
- package/dist/store/engine.mjs.map +1 -1
- package/dist/store/memory.d.mts +18 -1
- package/dist/store/memory.mjs +18 -1
- package/dist/store/memory.mjs.map +1 -1
- package/dist/store/slack-pr-notifications.d.mts +44 -0
- package/dist/store/slack-pr-notifications.mjs +121 -0
- package/dist/store/slack-pr-notifications.mjs.map +1 -0
- package/dist/store/task-work.d.mts +121 -6
- package/dist/store/task-work.mjs +7 -4
- package/dist/store/task-work.mjs.map +1 -1
- package/dist/sweep.d.mts +28 -6
- package/dist/sweep.mjs +34 -6
- package/dist/sweep.mjs.map +1 -1
- package/dist/task-graph-view.d.mts +3 -0
- package/dist/tasks.d.mts +3 -3
- package/dist/tasks.mjs +3 -3
- package/dist/vercel-git.d.mts +35 -3
- package/dist/vercel-git.mjs +265 -33
- package/dist/vercel-git.mjs.map +1 -1
- package/dist/vercel-github-api.d.mts +103 -0
- package/dist/vercel-github-api.mjs +363 -0
- package/dist/vercel-github-api.mjs.map +1 -0
- package/dist/vercel.d.mts +3 -2
- package/dist/vercel.mjs +3 -2
- package/dist/vercel.mjs.map +1 -1
- package/dist/work-triage.d.mts +1 -0
- package/dist/workflows.d.mts +102 -4
- package/dist/workflows.mjs +55 -2
- package/dist/workflows.mjs.map +1 -1
- package/dist/workspace-files-git.d.mts +15 -0
- package/dist/workspace-files-git.mjs +61 -0
- package/dist/workspace-files-git.mjs.map +1 -0
- package/dist/workspace-files.d.mts +107 -0
- package/dist/workspace-files.mjs +74 -0
- package/dist/workspace-files.mjs.map +1 -0
- package/docs/getting-started.md +104 -0
- package/docs/index.md +100 -0
- package/docs/recipes/cancellation.md +215 -0
- package/docs/recipes/custom-workflow.md +153 -0
- package/docs/recipes/dependent-tasks.md +207 -0
- package/docs/recipes/eve-agent.md +277 -0
- package/docs/recipes/human-input.md +204 -0
- package/docs/recipes/persistence-recovery.md +268 -0
- package/docs/recipes/retry-recovery.md +241 -0
- package/docs/recipes/task-messaging.md +215 -0
- package/docs/recipes/typed-eve-result.md +161 -0
- package/docs/runtime-integration.md +137 -0
- package/package.json +17 -6
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Process a typed Task message
|
|
2
|
+
|
|
3
|
+
Task messages are trusted application APIs with deny-by-default authorization. This recipe sends a
|
|
4
|
+
typed parent-to-child message, stores a decision before applying its effect, and acknowledges the
|
|
5
|
+
message only after the effect commits.
|
|
6
|
+
|
|
7
|
+
Task messaging coordinates durable work around Eve sessions; it is not another Eve transport. An
|
|
8
|
+
application exposes narrowly authorized message operations through its own tools or workflow code,
|
|
9
|
+
and the Eve agent remains bound to its current Task attempt. Start with
|
|
10
|
+
[Run an Eve agent as a Factory Task](./eve-agent.md) before adding inter-Task messages.
|
|
11
|
+
|
|
12
|
+
## Run the recipe
|
|
13
|
+
|
|
14
|
+
Save this as `task-messaging.mts` in an ESM project with `@vercel/factory`, `eve`, and `zod`
|
|
15
|
+
installed.
|
|
16
|
+
|
|
17
|
+
<!-- runnable-example:start -->
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import type { ToolContext } from "eve/tools";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import {
|
|
23
|
+
factoryTaskAttemptAttribute,
|
|
24
|
+
factoryTaskAttribute,
|
|
25
|
+
requireTaskExecution,
|
|
26
|
+
} from "@vercel/factory";
|
|
27
|
+
import { createInMemoryDriver, createStores } from "@vercel/factory/storage";
|
|
28
|
+
import {
|
|
29
|
+
defineTaskMessageProtocol,
|
|
30
|
+
defineTaskMessageProtocols,
|
|
31
|
+
processTaskMessage,
|
|
32
|
+
taskBoundMessagePolicy,
|
|
33
|
+
taskMessageInput,
|
|
34
|
+
} from "@vercel/factory/tasks";
|
|
35
|
+
import { defineWorkflow, defineWorkflows, taskWork } from "@vercel/factory/workflows";
|
|
36
|
+
|
|
37
|
+
const noteProtocol = defineTaskMessageProtocol({
|
|
38
|
+
id: "review-note",
|
|
39
|
+
version: 1,
|
|
40
|
+
payload: z.strictObject({ note: z.string().min(1) }),
|
|
41
|
+
});
|
|
42
|
+
const messageWorkflow = defineWorkflow({
|
|
43
|
+
id: "process-review-note",
|
|
44
|
+
version: 1,
|
|
45
|
+
input: z.null(),
|
|
46
|
+
output: z.strictObject({ handled: z.boolean() }),
|
|
47
|
+
});
|
|
48
|
+
const driver = createInMemoryDriver();
|
|
49
|
+
let nowMs = Date.parse("2026-09-10T12:00:00.000Z");
|
|
50
|
+
const openStores = () =>
|
|
51
|
+
createStores({
|
|
52
|
+
driver,
|
|
53
|
+
now: () => new Date(nowMs).toISOString(),
|
|
54
|
+
workflows: defineWorkflows([messageWorkflow]),
|
|
55
|
+
messages: {
|
|
56
|
+
protocols: defineTaskMessageProtocols([noteProtocol]),
|
|
57
|
+
authorize: taskBoundMessagePolicy,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const stores = openStores();
|
|
61
|
+
const base = {
|
|
62
|
+
repositoryIds: ["repo_recipe05"] as const,
|
|
63
|
+
kind: "example",
|
|
64
|
+
origin: { operator: "local:recipe" },
|
|
65
|
+
replyTo: { channel: "local", address: "task-messaging" },
|
|
66
|
+
work: taskWork({ title: "Process a review note", input: null, workflow: messageWorkflow }),
|
|
67
|
+
} as const;
|
|
68
|
+
const root = await stores.tasks.create({ ...base, dedupeKey: "message-root" });
|
|
69
|
+
const child = await stores.tasks.create({
|
|
70
|
+
...base,
|
|
71
|
+
parentTaskId: root.id,
|
|
72
|
+
dedupeKey: "message-child",
|
|
73
|
+
});
|
|
74
|
+
await stores.tasks.transition(root.id, "running");
|
|
75
|
+
await stores.tasks.transition(child.id, "running");
|
|
76
|
+
const runningRoot = await stores.tasks.recordExecution(root.id, {
|
|
77
|
+
execution: { provider: "eve", sessionId: "ses_message_root" },
|
|
78
|
+
});
|
|
79
|
+
const runningChild = await stores.tasks.recordExecution(child.id, {
|
|
80
|
+
execution: { provider: "eve", sessionId: "ses_message_child" },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function toolContext(task: typeof runningRoot): ToolContext {
|
|
84
|
+
if (task.execution?.provider !== "eve") throw new Error("Expected an Eve execution");
|
|
85
|
+
return {
|
|
86
|
+
session: {
|
|
87
|
+
id: task.execution.sessionId,
|
|
88
|
+
auth: {
|
|
89
|
+
initiator: {
|
|
90
|
+
attributes: {
|
|
91
|
+
[factoryTaskAttribute]: task.id,
|
|
92
|
+
[factoryTaskAttemptAttribute]: String(task.attempt),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
} as unknown as ToolContext;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const rootContext = toolContext(runningRoot);
|
|
101
|
+
const authenticatedRoot = await requireTaskExecution(stores, rootContext, runningRoot.id);
|
|
102
|
+
const rootPrincipal = {
|
|
103
|
+
id: `eve:${rootContext.session.id}`,
|
|
104
|
+
taskId: authenticatedRoot.id,
|
|
105
|
+
};
|
|
106
|
+
const childContext = toolContext(runningChild);
|
|
107
|
+
const authenticatedChild = await requireTaskExecution(stores, childContext, runningChild.id);
|
|
108
|
+
const childPrincipal = {
|
|
109
|
+
id: `eve:${childContext.session.id}`,
|
|
110
|
+
taskId: authenticatedChild.id,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const sendInput = taskMessageInput(noteProtocol, {
|
|
114
|
+
operationId: "review-note-1",
|
|
115
|
+
fromTaskId: root.id,
|
|
116
|
+
toTaskId: child.id,
|
|
117
|
+
expectAttempt: root.attempt,
|
|
118
|
+
payload: { note: "Check the retry boundary" },
|
|
119
|
+
});
|
|
120
|
+
const sent = await stores.messages.send({
|
|
121
|
+
principal: rootPrincipal,
|
|
122
|
+
message: sendInput,
|
|
123
|
+
});
|
|
124
|
+
const duplicate = await stores.messages.send({
|
|
125
|
+
principal: rootPrincipal,
|
|
126
|
+
message: sendInput,
|
|
127
|
+
});
|
|
128
|
+
if (duplicate.id !== sent.id) throw new Error("Expected idempotent message admission");
|
|
129
|
+
|
|
130
|
+
let unauthorizedReadRejected = false;
|
|
131
|
+
try {
|
|
132
|
+
await stores.messages.pending({
|
|
133
|
+
principal: rootPrincipal,
|
|
134
|
+
toTaskId: child.id,
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
unauthorizedReadRejected = true;
|
|
138
|
+
}
|
|
139
|
+
const externalEffects = new Set<string>();
|
|
140
|
+
let decisionCalls = 0;
|
|
141
|
+
let applyCalls = 0;
|
|
142
|
+
let interruptAfterCommit = true;
|
|
143
|
+
const processNext = (currentStores: typeof stores) =>
|
|
144
|
+
processTaskMessage({
|
|
145
|
+
stores: currentStores,
|
|
146
|
+
principal: childPrincipal,
|
|
147
|
+
task: { taskId: authenticatedChild.id, attempt: authenticatedChild.attempt },
|
|
148
|
+
leaseMs: 1_000,
|
|
149
|
+
workflow: messageWorkflow.binding,
|
|
150
|
+
protocol: noteProtocol,
|
|
151
|
+
decisionSchema: z.strictObject({ handled: z.boolean() }),
|
|
152
|
+
now: () => new Date(nowMs).toISOString(),
|
|
153
|
+
async decide({ message }) {
|
|
154
|
+
decisionCalls += 1;
|
|
155
|
+
return { handled: message.payload.note === "Check the retry boundary" };
|
|
156
|
+
},
|
|
157
|
+
async apply({ decision, effectKey }) {
|
|
158
|
+
if (!decision.handled) throw new Error("Expected the note to be handled");
|
|
159
|
+
applyCalls += 1;
|
|
160
|
+
externalEffects.add(effectKey);
|
|
161
|
+
if (interruptAfterCommit) {
|
|
162
|
+
interruptAfterCommit = false;
|
|
163
|
+
throw new Error("process interrupted after the external effect committed");
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
let interruptionObserved = false;
|
|
169
|
+
try {
|
|
170
|
+
await processNext(stores);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
interruptionObserved =
|
|
173
|
+
error instanceof Error && error.message.includes("interrupted after the external effect");
|
|
174
|
+
}
|
|
175
|
+
const restartedStores = openStores();
|
|
176
|
+
const stillLeased = await processNext(restartedStores);
|
|
177
|
+
nowMs += 1_001;
|
|
178
|
+
const processed = await processNext(restartedStores);
|
|
179
|
+
const replay = await processNext(restartedStores);
|
|
180
|
+
if (
|
|
181
|
+
!unauthorizedReadRejected ||
|
|
182
|
+
!interruptionObserved ||
|
|
183
|
+
stillLeased.status !== "idle" ||
|
|
184
|
+
processed.status !== "processed" ||
|
|
185
|
+
processed.decision.handled !== true ||
|
|
186
|
+
decisionCalls !== 1 ||
|
|
187
|
+
applyCalls !== 2 ||
|
|
188
|
+
externalEffects.size !== 1 ||
|
|
189
|
+
replay.status !== "idle"
|
|
190
|
+
) {
|
|
191
|
+
throw new Error("Expected authorized, interruption-safe message processing");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(JSON.stringify({ messageId: sent.id, status: processed.status }));
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
<!-- runnable-example:end -->
|
|
198
|
+
|
|
199
|
+
## What it proves
|
|
200
|
+
|
|
201
|
+
The supplied policy allows direct parent-child sends and recipient-local reads and processing. A
|
|
202
|
+
stable source operation ID makes retries return the same immutable message. The example interrupts
|
|
203
|
+
processing after the external effect commits, opens the stores again, waits for the claim lease to
|
|
204
|
+
expire, and recovers without recomputing the durable decision. `apply` repeats with the same effect
|
|
205
|
+
key, so the simulated external system records one effect.
|
|
206
|
+
|
|
207
|
+
Leases do not make external systems exactly-once: `apply` may repeat after interruption. Production
|
|
208
|
+
effects must remain idempotent by `effectKey`. An Eve-facing wrapper should call
|
|
209
|
+
`requireTaskExecution` and derive its message principal from that authenticated Task and session,
|
|
210
|
+
as the recipe does; never accept the principal from model input.
|
|
211
|
+
|
|
212
|
+
Message admission does not wake an idle recipient by itself. After a successful send, publish an
|
|
213
|
+
application-owned queue wakeup or run message processing on a schedule. If the recipient is
|
|
214
|
+
deliberately inactive, use `stores.work.waitForMessages` and `stores.work.resume` around that
|
|
215
|
+
workflow phase.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Submit a typed workflow result from Eve
|
|
2
|
+
|
|
3
|
+
The generic `finish_task` tool records `{ summary }` for the default `task@1` workflow. When an
|
|
4
|
+
Eve agent must return a domain-specific result, define a narrow Eve tool that authenticates the
|
|
5
|
+
current execution with `requireTaskExecution` and completes the exact registered workflow.
|
|
6
|
+
|
|
7
|
+
## Run the recipe
|
|
8
|
+
|
|
9
|
+
Save this as `typed-eve-result.mts` in an ESM project with `@vercel/factory`, `eve`, and `zod`
|
|
10
|
+
installed.
|
|
11
|
+
|
|
12
|
+
<!-- runnable-example:start -->
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { defineTool, type ToolContext } from "eve/tools";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import {
|
|
18
|
+
factoryTaskAttemptAttribute,
|
|
19
|
+
factoryTaskAttribute,
|
|
20
|
+
requireTaskExecution,
|
|
21
|
+
startEveSession,
|
|
22
|
+
taskHeaders,
|
|
23
|
+
} from "@vercel/factory";
|
|
24
|
+
import { dispatchTask } from "@vercel/factory/execution";
|
|
25
|
+
import { createInMemoryDriver, createStores } from "@vercel/factory/storage";
|
|
26
|
+
import { taskIdSchema } from "@vercel/factory/tasks";
|
|
27
|
+
import {
|
|
28
|
+
defineWorkflow,
|
|
29
|
+
defineWorkflows,
|
|
30
|
+
parseAgentRouteBinding,
|
|
31
|
+
taskWork,
|
|
32
|
+
} from "@vercel/factory/workflows";
|
|
33
|
+
|
|
34
|
+
const greetingWorkflow = defineWorkflow({
|
|
35
|
+
id: "greeting",
|
|
36
|
+
version: 1,
|
|
37
|
+
input: z.strictObject({ name: z.string().min(1) }),
|
|
38
|
+
output: z.strictObject({
|
|
39
|
+
greeting: z.string().min(1),
|
|
40
|
+
language: z.enum(["en", "de"]),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
const route = parseAgentRouteBinding({ id: "greeter", version: 1 });
|
|
44
|
+
const stores = createStores({
|
|
45
|
+
driver: createInMemoryDriver(),
|
|
46
|
+
workflows: defineWorkflows([greetingWorkflow]),
|
|
47
|
+
});
|
|
48
|
+
const base = {
|
|
49
|
+
repositoryIds: ["repo_typed01"] as const,
|
|
50
|
+
kind: "example",
|
|
51
|
+
origin: { operator: "local:recipe" },
|
|
52
|
+
replyTo: { channel: "local", address: "typed-eve-result" },
|
|
53
|
+
} as const;
|
|
54
|
+
const task = await stores.tasks.create({
|
|
55
|
+
...base,
|
|
56
|
+
work: {
|
|
57
|
+
...taskWork({
|
|
58
|
+
title: "Create a German greeting",
|
|
59
|
+
input: { name: "Factory" },
|
|
60
|
+
workflow: greetingWorkflow,
|
|
61
|
+
}),
|
|
62
|
+
route,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const otherTask = await stores.tasks.create({
|
|
66
|
+
...base,
|
|
67
|
+
work: {
|
|
68
|
+
...taskWork({
|
|
69
|
+
title: "A different greeting",
|
|
70
|
+
input: { name: "Eve" },
|
|
71
|
+
workflow: greetingWorkflow,
|
|
72
|
+
}),
|
|
73
|
+
route,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const running = await dispatchTask(stores, {
|
|
78
|
+
taskId: task.id,
|
|
79
|
+
route,
|
|
80
|
+
launch: (claimed) =>
|
|
81
|
+
startEveSession({
|
|
82
|
+
agentUrl: "https://factory.example/eve/agents/greeter",
|
|
83
|
+
message: `Greet ${greetingWorkflow.input.parse(claimed.work.input).name}`,
|
|
84
|
+
operationId: `factory-task:${claimed.id}:${claimed.attempt}`,
|
|
85
|
+
headers: taskHeaders({ taskId: claimed.id, attempt: claimed.attempt }),
|
|
86
|
+
fetch: async () => Response.json({ sessionId: "ses_typed_greeting" }),
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
if (running.execution?.provider !== "eve") throw new Error("Expected an Eve execution");
|
|
90
|
+
const context = {
|
|
91
|
+
session: {
|
|
92
|
+
id: running.execution.sessionId,
|
|
93
|
+
auth: {
|
|
94
|
+
initiator: {
|
|
95
|
+
attributes: {
|
|
96
|
+
[factoryTaskAttribute]: running.id,
|
|
97
|
+
[factoryTaskAttemptAttribute]: String(running.attempt),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
} as unknown as ToolContext;
|
|
103
|
+
|
|
104
|
+
const submitGreeting = defineTool({
|
|
105
|
+
description: "Validate and submit the greeting produced for your current Factory Task.",
|
|
106
|
+
inputSchema: z.strictObject({
|
|
107
|
+
taskId: taskIdSchema,
|
|
108
|
+
greeting: greetingWorkflow.output.shape.greeting,
|
|
109
|
+
language: greetingWorkflow.output.shape.language,
|
|
110
|
+
}),
|
|
111
|
+
async execute({ taskId, greeting, language }, ctx) {
|
|
112
|
+
const authenticated = await requireTaskExecution(stores, ctx, taskId);
|
|
113
|
+
if (authenticated.execution?.provider !== "eve") {
|
|
114
|
+
throw new Error("Expected the authenticated Eve execution");
|
|
115
|
+
}
|
|
116
|
+
const completed = await stores.work.completeWorkflow({
|
|
117
|
+
task: { taskId: authenticated.id, attempt: authenticated.attempt },
|
|
118
|
+
workflow: greetingWorkflow.binding,
|
|
119
|
+
output: { greeting, language },
|
|
120
|
+
fence: { from: "running", execution: authenticated.execution },
|
|
121
|
+
});
|
|
122
|
+
return { submitted: true as const, taskId: completed.id };
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
let foreignTaskRejected = false;
|
|
127
|
+
try {
|
|
128
|
+
await submitGreeting.execute(
|
|
129
|
+
{ taskId: otherTask.id, greeting: "Hallo, Eve!", language: "de" },
|
|
130
|
+
context,
|
|
131
|
+
);
|
|
132
|
+
} catch {
|
|
133
|
+
foreignTaskRejected = true;
|
|
134
|
+
}
|
|
135
|
+
await submitGreeting.execute(
|
|
136
|
+
{ taskId: running.id, greeting: "Hallo, Factory!", language: "de" },
|
|
137
|
+
context,
|
|
138
|
+
);
|
|
139
|
+
const completed = await stores.tasks.get(running.id);
|
|
140
|
+
const output = greetingWorkflow.output.parse(completed?.workResult?.output);
|
|
141
|
+
if (
|
|
142
|
+
!foreignTaskRejected ||
|
|
143
|
+
completed?.state !== "succeeded" ||
|
|
144
|
+
output.greeting !== "Hallo, Factory!"
|
|
145
|
+
) {
|
|
146
|
+
throw new Error("Expected authenticated, schema-validated workflow completion");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(JSON.stringify({ state: completed.state, output }));
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
<!-- runnable-example:end -->
|
|
153
|
+
|
|
154
|
+
Export the tool from the target agent's `tools/` directory, alongside `get_task`. Its input
|
|
155
|
+
schema tells the model exactly what result to return, while the workflow registry validates the
|
|
156
|
+
persisted output again. The completion fence checks the authenticated Task's running state and Eve
|
|
157
|
+
execution atomically with the success transition, so a concurrent pause, cancellation, retry, or
|
|
158
|
+
replacement execution cannot accept a stale result.
|
|
159
|
+
|
|
160
|
+
Keep each historical workflow version registered while stored Tasks can still reference it. Create
|
|
161
|
+
a new workflow and tool version when the result contract changes incompatibly.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Runtime composition
|
|
2
|
+
|
|
3
|
+
`@vercel/factory` provides typed contracts and policy injection points. An application still owns
|
|
4
|
+
its configured repositories, credentials, agent routes, budgets, prompts, approval policy, and
|
|
5
|
+
provider accounts.
|
|
6
|
+
|
|
7
|
+
## Kernel and supplied policy
|
|
8
|
+
|
|
9
|
+
The package root owns common configuration, repository admission, credential-redacted diagnostics,
|
|
10
|
+
generic Eve session integration, and declarations with no narrower capability owner. Kernel
|
|
11
|
+
capabilities live on their own paths, including Tasks, workflows, execution, planning, budgets,
|
|
12
|
+
signals, receipts, storage, and communication.
|
|
13
|
+
|
|
14
|
+
The supplied software-development behavior is an explicit composition under
|
|
15
|
+
`@vercel/factory/presets/software-development`. It combines approval, completion, communication,
|
|
16
|
+
recovery, dispatch, and delegation policy with capability-owned Task and Change tools. Applications
|
|
17
|
+
inject repository policy, planner behavior, launchers, credentials, budget, wakeup publication,
|
|
18
|
+
and retry and batch limits.
|
|
19
|
+
|
|
20
|
+
## Durable work
|
|
21
|
+
|
|
22
|
+
Every Task contains explicit work: a title, bounded JSON input, completion criteria, an exact
|
|
23
|
+
workflow ID and version, and an optional exact agent route. `taskWork` selects the generic `task@1`
|
|
24
|
+
JSON contract. Custom input and output contracts require `defineWorkflow` and registration through
|
|
25
|
+
`defineWorkflows`.
|
|
26
|
+
|
|
27
|
+
Task kind is descriptive. It does not select execution or determine success. Canonical dependencies
|
|
28
|
+
belong to `stores.graphs`, and `stores.work.complete` records validated output. A route-less Task is
|
|
29
|
+
owned by local workflow code; agent work records an exact route and must recover that same binding.
|
|
30
|
+
|
|
31
|
+
Task messages are trusted application APIs rather than HTTP endpoints. Message access defaults to
|
|
32
|
+
deny. `taskBoundMessagePolicy` permits parent-child sends and recipient-local operations when an
|
|
33
|
+
application explicitly configures the supplied protocols. Durable decisions are stored before
|
|
34
|
+
idempotent effects, but leases cannot fence external systems or remove cancellation races.
|
|
35
|
+
The runnable [Task messaging recipe](recipes/task-messaging.md) demonstrates that boundary and its
|
|
36
|
+
effect-key requirement.
|
|
37
|
+
|
|
38
|
+
## Where Eve fits
|
|
39
|
+
|
|
40
|
+
Eve is the normal agent runtime. Factory does not implement the model loop or replace an Eve
|
|
41
|
+
agent's `agent.ts`; it records the durable contract around each run and connects that contract to
|
|
42
|
+
the Eve session.
|
|
43
|
+
|
|
44
|
+
| Task execution mode | Use it for | What advances the Task |
|
|
45
|
+
| ------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| Exact agent route | Normal Eve agent work | `dispatchTask` starts an Eve session; authenticated Factory tools and hooks complete or recover the attempt |
|
|
47
|
+
| Workflow owned | Deterministic application or orchestrator logic with no provider session | Application code transitions and completes the Task |
|
|
48
|
+
| Execution adapter | Coding harnesses, sandboxes, and other provider sessions | The adapter returns evidence; workflow policy interprets it and completes or retries the Task |
|
|
49
|
+
|
|
50
|
+
For an Eve route, the application calls `startEveSession` with `taskHeaders`. The agent's Eve
|
|
51
|
+
channel wraps its authentication with `withFactoryTask`, and its filesystem mounts tools from
|
|
52
|
+
`createTaskTools` plus the hook from `createFactoryHooks`. The session is thereby bound to one Task
|
|
53
|
+
ID and attempt. See the runnable [Eve agent integration recipe](recipes/eve-agent.md), followed by
|
|
54
|
+
the [typed Eve result](recipes/typed-eve-result.md), [human input](recipes/human-input.md), and
|
|
55
|
+
[dependent Eve agents](recipes/dependent-tasks.md) recipes.
|
|
56
|
+
|
|
57
|
+
## Assemble the production loop
|
|
58
|
+
|
|
59
|
+
A deployable factory needs an application-owned loop around the package primitives. Keep each
|
|
60
|
+
boundary explicit so recovery can repeat one operation without guessing what happened elsewhere.
|
|
61
|
+
|
|
62
|
+
| Boundary | Production responsibility |
|
|
63
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| Persistence | Construct one `createStores` instance with Blob or Postgres; register every workflow and message protocol version referenced by stored Tasks |
|
|
65
|
+
| Route launchers | Map each exact `routeKey` to a protected Eve agent URL; merge `taskHeaders` with deployment identity |
|
|
66
|
+
| Dispatch | Run `sweepQueued` from a durable schedule and after events that can unblock work; pass budget and approval policy before launch |
|
|
67
|
+
| Agent completion | Mount `withFactoryTask`, `createFactoryHooks`, and only the Task or domain tools that role needs |
|
|
68
|
+
| Questions | Deliver persisted QuestionTasks through Foreman, authenticate replies to the original `replyTo`, then wake dispatch |
|
|
69
|
+
| Recovery | Let hooks requeue bounded Eve failures; sweep the exact stored route and retain attempt-derived operation IDs |
|
|
70
|
+
| Cancellation | Persist Task cancellation first, request provider cancellation second, and retry ambiguous provider stops |
|
|
71
|
+
| Messages and effects | Derive principals from authenticated sessions, publish recipient wakeups, and make external effects idempotent by effect key |
|
|
72
|
+
| Operations | Monitor failed launches, exhausted Tasks, unanswered questions, queue age, budget refusal, and outbox delivery |
|
|
73
|
+
|
|
74
|
+
The dispatcher can run concurrently: its queued-to-running admission has one winner. Invoke it on
|
|
75
|
+
a short schedule for repair and also from queue wakeups for latency. A useful wakeup contains no
|
|
76
|
+
authority; the worker rereads canonical state, runs one bounded `sweepQueued` pass, and records or
|
|
77
|
+
alerts on every `failed` result. Treat `skipped` results as named gates such as dependencies,
|
|
78
|
+
approval, or workflow ownership rather than launch failures. Budget exhaustion fails the Task and
|
|
79
|
+
appears as a `launch_failed` result, so operations should surface its persisted refusal reason.
|
|
80
|
+
|
|
81
|
+
Budget reservation and approval happen before a route launcher creates an Eve session. Configure
|
|
82
|
+
the policy in trusted application code, pass the current budget snapshot to dispatch, and expose
|
|
83
|
+
approval actions only through an authenticated operator boundary. The agent should receive the
|
|
84
|
+
approved Task contract and role-specific tools, never provider credentials or the ability to widen
|
|
85
|
+
repository scope.
|
|
86
|
+
|
|
87
|
+
## Storage and execution
|
|
88
|
+
|
|
89
|
+
`@vercel/factory/storage` exposes the validating store engine and a non-durable in-memory driver.
|
|
90
|
+
Persistent adapters are opt-in through `@vercel/factory/storage/blob` and
|
|
91
|
+
`@vercel/factory/storage/postgres`.
|
|
92
|
+
|
|
93
|
+
Recovery has three separate meanings. Reopening a persistent driver only restores access to the
|
|
94
|
+
records. `stores.work.recover(rootTaskId)` then reads the canonical graph and repairs Task snapshots
|
|
95
|
+
whose admitted graph nodes survived an interrupted creation write. It does not launch or resume a
|
|
96
|
+
provider. Execution recovery separately inspects the Task's recorded provider execution and either
|
|
97
|
+
reattaches when it is confirmed active or applies workflow-owned retry policy. The runnable
|
|
98
|
+
[persistence and restart recovery recipe](recipes/persistence-recovery.md) demonstrates the first
|
|
99
|
+
two boundaries with workflow-owned work; the retry recipe covers provider execution recovery.
|
|
100
|
+
|
|
101
|
+
`@vercel/factory/execution` owns provider-neutral dispatch and execution lifecycle contracts.
|
|
102
|
+
`@vercel/factory/sandbox` supplies retry-safe execution records, abort propagation, harness
|
|
103
|
+
continuation, workspace contracts, and scoped Vercel Sandbox lifecycle. The application injects
|
|
104
|
+
prompt construction, evidence interpretation, pricing, credentials, repository seeding, and its
|
|
105
|
+
model and harness registry.
|
|
106
|
+
|
|
107
|
+
The [custom workflow recipe](recipes/custom-workflow.md) demonstrates the adapter path for
|
|
108
|
+
non-Eve execution. The [cancellation](recipes/cancellation.md) recipe covers adapter and Eve
|
|
109
|
+
cancellation, while [retry recovery](recipes/retry-recovery.md) exercises Eve lifecycle hooks and
|
|
110
|
+
scheduled replacement attempts.
|
|
111
|
+
|
|
112
|
+
## Provider adapters
|
|
113
|
+
|
|
114
|
+
External-provider APIs stay opt-in under `@vercel/factory/integrations/<provider>`:
|
|
115
|
+
|
|
116
|
+
- GitHub provides intake normalization, App-token scope validation, checkout and publication
|
|
117
|
+
primitives, communication delivery, review reporting, labels, and work triage.
|
|
118
|
+
- Slack delivers communication events through an injected client.
|
|
119
|
+
- DeepSec normalizes scanner output and provides its Eve tool.
|
|
120
|
+
- Vercel provides Queue wakeups and credential-isolated Git transport.
|
|
121
|
+
|
|
122
|
+
Applications remain responsible for selecting credentials and authorizing repositories. Connector
|
|
123
|
+
installation access never expands the application's repository allowlist.
|
|
124
|
+
The Vercel-managed transport uses fresh trusted Sandboxes for reads and signed writes. They execute
|
|
125
|
+
no repository code or model. Publication consumes credential-free bundles, applies exact remote-head
|
|
126
|
+
leases, rechecks application authority at the mutation boundary, and returns the signed remote SHA.
|
|
127
|
+
Do not attach managed Git fields to agent or harness Sandboxes. Applications still choose their
|
|
128
|
+
pull-request creation adapter. In Vercel mode, initial Git Data and pull-request publication use one
|
|
129
|
+
fresh repository-scoped managed Sandbox; Connect mode retains the installation-token publication path.
|
|
130
|
+
|
|
131
|
+
## Operator boundary
|
|
132
|
+
|
|
133
|
+
`@vercel/factory/client` is browser-safe and exposes `FactoryClient`, request and response schemas,
|
|
134
|
+
Task states, transcripts, and session streaming. Server handlers and their dependencies live under
|
|
135
|
+
`@vercel/factory/api`; Next.js integration lives under `@vercel/factory/next`.
|
|
136
|
+
|
|
137
|
+
Return to the [consumer documentation index](index.md) to find exact import paths and declarations.
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/factory",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "Schema, state machines, and kernel primitives for Agent Factory.",
|
|
5
|
+
"homepage": "https://github.com/vercel-labs/agent-factory/tree/main/packages/factory",
|
|
6
|
+
"bugs": "https://github.com/vercel-labs/agent-factory/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/vercel-labs/agent-factory.git",
|
|
10
|
+
"directory": "packages/factory"
|
|
11
|
+
},
|
|
5
12
|
"files": [
|
|
6
|
-
"
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"dist",
|
|
15
|
+
"docs"
|
|
7
16
|
],
|
|
8
17
|
"type": "module",
|
|
9
18
|
"sideEffects": false,
|
|
@@ -124,6 +133,8 @@
|
|
|
124
133
|
"./package.json": "./package.json"
|
|
125
134
|
},
|
|
126
135
|
"dependencies": {
|
|
136
|
+
"@types/json-schema": "^7.0.15",
|
|
137
|
+
"@types/node": "24.x",
|
|
127
138
|
"diff": "8.0.2"
|
|
128
139
|
},
|
|
129
140
|
"devDependencies": {
|
|
@@ -131,13 +142,13 @@
|
|
|
131
142
|
"@ai-sdk/harness-claude-code": "1.0.112",
|
|
132
143
|
"@ai-sdk/sandbox-vercel": "1.0.108",
|
|
133
144
|
"@electric-sql/pglite": "^0.5.7",
|
|
134
|
-
"@types/node": "24.x",
|
|
135
145
|
"@vercel/blob": "^2.8.0",
|
|
136
146
|
"@vercel/sandbox": "2.10.0-beta.0",
|
|
137
147
|
"drizzle-orm": "^0.45.2",
|
|
138
|
-
"eve": "^0.
|
|
148
|
+
"eve": "^0.57.0",
|
|
139
149
|
"tsdown": "^0.22.14",
|
|
140
150
|
"typescript": "5.9.3",
|
|
151
|
+
"typescript-current": "npm:typescript@7.0.2",
|
|
141
152
|
"vitest": "4.1.9",
|
|
142
153
|
"zod": "4.4.3"
|
|
143
154
|
},
|
|
@@ -147,7 +158,7 @@
|
|
|
147
158
|
"@vercel/blob": "^2.6.0",
|
|
148
159
|
"@vercel/sandbox": ">=2.10.0-beta.0 <4",
|
|
149
160
|
"drizzle-orm": "^0.45.0",
|
|
150
|
-
"eve": "^0.
|
|
161
|
+
"eve": "^0.57.0",
|
|
151
162
|
"zod": "^4.4.3"
|
|
152
163
|
},
|
|
153
164
|
"peerDependenciesMeta": {
|
|
@@ -177,6 +188,6 @@
|
|
|
177
188
|
"lint": "oxlint --config ../../.oxlintrc.json --max-warnings 0 .",
|
|
178
189
|
"lint:logs": "node ../../scripts/check-structured-logging.mjs src",
|
|
179
190
|
"test": "vitest run",
|
|
180
|
-
"typecheck": "tsc"
|
|
191
|
+
"typecheck": "node node_modules/typescript/bin/tsc"
|
|
181
192
|
}
|
|
182
193
|
}
|