@vercel/factory 0.0.17 → 0.0.18
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 +46 -0
- package/dist/api-contracts.d.mts +665 -715
- package/dist/api-contracts.mjs +91 -4
- package/dist/api-contracts.mjs.map +1 -1
- package/dist/api-task-graph.mjs +14 -5
- package/dist/api-task-graph.mjs.map +1 -1
- package/dist/api.d.mts +58 -11
- package/dist/api.mjs +715 -399
- package/dist/api.mjs.map +1 -1
- package/dist/approval-access.d.mts +1 -1
- package/dist/approval-contracts.d.mts +3 -4
- package/dist/approval-contracts.mjs +1 -2
- package/dist/approval-contracts.mjs.map +1 -1
- package/dist/blob/index.d.mts +1 -1
- package/dist/budget.mjs +20 -3
- package/dist/budget.mjs.map +1 -1
- package/dist/change-verification/brief.d.mts +1 -1
- package/dist/change-verification/dispatch.mjs +8 -1
- package/dist/change-verification/dispatch.mjs.map +1 -1
- package/dist/changes/eve-record-change.mjs +4 -1
- package/dist/changes/eve-record-change.mjs.map +1 -1
- package/dist/client-stream.d.mts +19 -2
- package/dist/client-stream.mjs +115 -53
- package/dist/client-stream.mjs.map +1 -1
- package/dist/client-transcript.d.mts +25 -2
- package/dist/client-transcript.mjs +95 -13
- package/dist/client-transcript.mjs.map +1 -1
- package/dist/client.d.mts +145 -19
- package/dist/client.mjs +89 -16
- 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 -2
- package/dist/code-review/eve-post-review.mjs +8 -0
- package/dist/code-review/eve-post-review.mjs.map +1 -1
- package/dist/code-review/github-reporter.d.mts +6 -2
- package/dist/code-review/github-reporter.mjs +86 -35
- package/dist/code-review/github-reporter.mjs.map +1 -1
- package/dist/code-review/preparation.d.mts +2 -1
- package/dist/code-review/preparation.mjs +2 -1
- package/dist/code-review/preparation.mjs.map +1 -1
- package/dist/code-review/stall.mjs +11 -2
- package/dist/code-review/stall.mjs.map +1 -1
- package/dist/connectors/github.d.mts +1 -1
- package/dist/connectors/index.d.mts +1 -1
- package/dist/connectors/slack.d.mts +1 -1
- package/dist/delivery-metrics/metrics.d.mts +1 -1
- package/dist/dispatch.mjs +3 -3
- package/dist/dispatch.mjs.map +1 -1
- package/dist/eve/index.d.mts +1 -1
- package/dist/eve/index.mjs +8 -1
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/task-execution.d.mts +4 -4
- package/dist/eve/transcript.d.mts +3 -1
- package/dist/eve/transcript.mjs.map +1 -1
- package/dist/finding-remediation/admission.d.mts +1 -1
- package/dist/finding-remediation/brief.d.mts +1 -1
- package/dist/github-app.d.mts +1 -1
- package/dist/github-publication.d.mts +1 -1
- package/dist/github.d.mts +9 -1
- package/dist/github.mjs +29 -11
- package/dist/github.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/intake-contracts.d.mts +1 -0
- package/dist/intake-contracts.mjs +2 -1
- package/dist/intake-contracts.mjs.map +1 -1
- package/dist/integrations/github-issues.d.mts +53 -0
- package/dist/integrations/github-issues.mjs +170 -0
- package/dist/integrations/github-issues.mjs.map +1 -0
- package/dist/integrations/github.d.mts +2 -1
- package/dist/integrations/github.mjs +2 -1
- package/dist/multi-repository-changes/coordination.mjs +1 -1
- package/dist/planning/reconcile.mjs +15 -2
- package/dist/planning/reconcile.mjs.map +1 -1
- package/dist/postgres/index.d.mts +2 -1
- package/dist/postgres/index.mjs +11 -5
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/postgres/indexed-codec.mjs +137 -0
- package/dist/postgres/indexed-codec.mjs.map +1 -0
- package/dist/postgres/indexed-schema.mjs +99 -0
- package/dist/postgres/indexed-schema.mjs.map +1 -0
- package/dist/postgres/indexed.d.mts +28 -0
- package/dist/postgres/indexed.mjs +277 -0
- package/dist/postgres/indexed.mjs.map +1 -0
- package/dist/postgres/record-predicates.mjs +23 -0
- package/dist/postgres/record-predicates.mjs.map +1 -0
- package/dist/presets/software-development/deepsec-brief.d.mts +1 -1
- package/dist/presets/software-development/dispatch.mjs +1 -1
- package/dist/presets/software-development/dispatch.mjs.map +1 -1
- package/dist/presets/software-development/recovery.mjs +9 -2
- package/dist/presets/software-development/recovery.mjs.map +1 -1
- package/dist/pull-requests/eve-fetch.d.mts +4 -1
- package/dist/pull-requests/eve-fetch.mjs +9 -0
- package/dist/pull-requests/eve-fetch.mjs.map +1 -1
- package/dist/pull-requests/github-publisher.d.mts +1 -1
- package/dist/pull-requests/github-publisher.mjs +19 -7
- package/dist/pull-requests/github-publisher.mjs.map +1 -1
- package/dist/repositories.mjs +20 -13
- package/dist/repositories.mjs.map +1 -1
- package/dist/sandbox/execution-run-ledger.d.mts +2 -2
- package/dist/sandbox/execution-run-ledger.mjs.map +1 -1
- package/dist/sandbox/execution.d.mts +2 -2
- package/dist/sandbox/execution.mjs.map +1 -1
- package/dist/schema/catalog.mjs +1 -1
- package/dist/schema/change.d.mts +1 -1
- package/dist/schema/communication.d.mts +2 -2
- package/dist/schema/factory-config.d.mts +1 -1
- package/dist/schema/outbox.d.mts +6 -6
- package/dist/schema/session-summary.d.mts +218 -0
- package/dist/schema/session-summary.mjs +68 -0
- package/dist/schema/session-summary.mjs.map +1 -0
- package/dist/schema/session.d.mts +39 -59
- package/dist/schema/session.mjs +6 -6
- package/dist/schema/session.mjs.map +1 -1
- package/dist/schema/task-graph.d.mts +24 -24
- package/dist/schema/transcript.d.mts +141 -3
- package/dist/schema/transcript.mjs +19 -3
- package/dist/schema/transcript.mjs.map +1 -1
- package/dist/schema/transitions.d.mts +1 -1
- package/dist/schema/transitions.mjs +1 -1
- package/dist/schema/transitions.mjs.map +1 -1
- package/dist/signals.d.mts +1 -1
- package/dist/stall.mjs +10 -2
- package/dist/stall.mjs.map +1 -1
- package/dist/storage.d.mts +10 -1
- package/dist/storage.mjs +7 -1
- package/dist/store/approvals.d.mts +14 -0
- package/dist/store/approvals.mjs +78 -0
- package/dist/store/approvals.mjs.map +1 -0
- package/dist/store/conversations.d.mts +59 -0
- package/dist/store/conversations.mjs +549 -0
- package/dist/store/conversations.mjs.map +1 -0
- package/dist/store/driver.d.mts +1 -1
- package/dist/store/driver.mjs.map +1 -1
- package/dist/store/engine.d.mts +38 -16
- package/dist/store/engine.mjs +229 -106
- package/dist/store/engine.mjs.map +1 -1
- package/dist/store/indexed-engine.d.mts +57 -0
- package/dist/store/indexed-engine.mjs +155 -0
- package/dist/store/indexed-engine.mjs.map +1 -0
- package/dist/store/indexed-memory.d.mts +7 -0
- package/dist/store/indexed-memory.mjs +200 -0
- package/dist/store/indexed-memory.mjs.map +1 -0
- package/dist/store/indexed-projections.d.mts +280 -0
- package/dist/store/indexed-projections.mjs +481 -0
- package/dist/store/indexed-projections.mjs.map +1 -0
- package/dist/store/indexed.d.mts +101 -0
- package/dist/store/indexed.mjs +19 -0
- package/dist/store/indexed.mjs.map +1 -0
- package/dist/store/iterate.d.mts +9 -0
- package/dist/store/iterate.mjs +23 -0
- package/dist/store/iterate.mjs.map +1 -0
- package/dist/store/query-scope.mjs +62 -0
- package/dist/store/query-scope.mjs.map +1 -0
- package/dist/store/query.d.mts +32 -0
- package/dist/store/query.mjs +103 -0
- package/dist/store/query.mjs.map +1 -0
- package/dist/store/session-history.mjs +24 -0
- package/dist/store/session-history.mjs.map +1 -0
- package/dist/store/slack-pr-notifications.d.mts +2 -2
- package/dist/store/slack-pr-notifications.mjs.map +1 -1
- package/dist/store/state.d.mts +26 -0
- package/dist/store/state.mjs +95 -0
- package/dist/store/state.mjs.map +1 -0
- package/dist/store/task-effects.d.mts +1 -1
- package/dist/store/task-graphs.d.mts +1 -1
- package/dist/store/task-messages.d.mts +1 -1
- package/dist/store/task-work.d.mts +1 -1
- package/dist/store/task-work.mjs +1 -1
- package/dist/store/transcripts.mjs +308 -0
- package/dist/store/transcripts.mjs.map +1 -0
- package/dist/sweep.d.mts +1 -1
- package/dist/sweep.mjs +12 -7
- package/dist/sweep.mjs.map +1 -1
- package/dist/task-communication.mjs +15 -1
- package/dist/task-communication.mjs.map +1 -1
- package/dist/task-protocols.d.mts +3 -3
- package/dist/tasks.d.mts +1 -1
- package/dist/vercel.d.mts +1 -1
- package/dist/work-triage/find-existing-work.d.mts +1 -1
- package/dist/work-triage/find-existing-work.mjs +12 -2
- package/dist/work-triage/find-existing-work.mjs.map +1 -1
- package/docs/index.md +4 -0
- package/docs/recipes/persistence-recovery.md +15 -9
- package/docs/recipes/postgres-indexed-engine.md +108 -0
- package/docs/recipes/postgres-indexed-storage.md +145 -0
- package/package.json +3 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { executionReferenceSchema } from "../schema/task.mjs";
|
|
2
|
+
import { RecordExistsError, VersionConflictError } from "./driver.mjs";
|
|
3
|
+
import { transcriptBlockSchema, transcriptSnapshotSchema } from "../schema/transcript.mjs";
|
|
4
|
+
import { projectIndexedRecord } from "./indexed-projections.mjs";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
//#region src/store/transcripts.ts
|
|
8
|
+
const partBytes = 49152;
|
|
9
|
+
const previewBytes = 4096;
|
|
10
|
+
const headSchema = z.strictObject({
|
|
11
|
+
schemaVersion: z.literal(1),
|
|
12
|
+
execution: executionReferenceSchema,
|
|
13
|
+
snapshot: transcriptSnapshotSchema,
|
|
14
|
+
blockCount: z.number().int().nonnegative(),
|
|
15
|
+
updatedAt: z.iso.datetime()
|
|
16
|
+
});
|
|
17
|
+
const blockSchema = z.strictObject({
|
|
18
|
+
executionId: z.string(),
|
|
19
|
+
blockId: z.string(),
|
|
20
|
+
digest: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
21
|
+
ordinal: z.number().int().nonnegative(),
|
|
22
|
+
parts: z.number().int().positive(),
|
|
23
|
+
bytes: z.number().int().positive(),
|
|
24
|
+
preview: transcriptBlockSchema
|
|
25
|
+
});
|
|
26
|
+
const chunkSchema = z.strictObject({ data: z.string() });
|
|
27
|
+
function jsonValue(value) {
|
|
28
|
+
return JSON.parse(JSON.stringify(value));
|
|
29
|
+
}
|
|
30
|
+
const transcriptPageQuerySchema = z.strictObject({
|
|
31
|
+
limit: z.number().int().min(1).max(100),
|
|
32
|
+
cursor: z.string().max(8192).optional()
|
|
33
|
+
});
|
|
34
|
+
function digest(value) {
|
|
35
|
+
return createHash("sha256").update(value).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
function executionId(execution) {
|
|
38
|
+
executionReferenceSchema.parse(execution);
|
|
39
|
+
return `transcript_${digest(JSON.stringify([execution.provider, execution.sessionId])).slice(0, 32)}`;
|
|
40
|
+
}
|
|
41
|
+
function blockKey(id, blockId) {
|
|
42
|
+
return `block_${digest(JSON.stringify([id, blockId]))}`;
|
|
43
|
+
}
|
|
44
|
+
function chunkKey(id, hash, part) {
|
|
45
|
+
return `chunk_${digest(JSON.stringify([
|
|
46
|
+
id,
|
|
47
|
+
hash,
|
|
48
|
+
part
|
|
49
|
+
]))}`;
|
|
50
|
+
}
|
|
51
|
+
function blockIndex(id) {
|
|
52
|
+
return `transcript-blocks:${id}`;
|
|
53
|
+
}
|
|
54
|
+
function transcriptBlockPreview(block) {
|
|
55
|
+
const bytes = Buffer.byteLength(JSON.stringify(block));
|
|
56
|
+
if (bytes <= previewBytes) return block;
|
|
57
|
+
const clipped = (text, length) => Array.from(text.replace(/[\uD800-\uDFFF]/gu, "�")).slice(0, length).join("");
|
|
58
|
+
const base = {
|
|
59
|
+
id: block.id,
|
|
60
|
+
title: clipped(block.title, 120),
|
|
61
|
+
text: clipped(block.text, 240),
|
|
62
|
+
createdAt: block.createdAt,
|
|
63
|
+
contentRef: {
|
|
64
|
+
blockId: block.id,
|
|
65
|
+
bytes
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return transcriptBlockSchema.parse(block.kind === "tool" ? {
|
|
69
|
+
...base,
|
|
70
|
+
kind: "tool",
|
|
71
|
+
tool: {
|
|
72
|
+
callId: block.tool.callId,
|
|
73
|
+
name: block.tool.name,
|
|
74
|
+
inputText: clipped(block.tool.inputText, 240),
|
|
75
|
+
state: block.tool.state
|
|
76
|
+
}
|
|
77
|
+
} : {
|
|
78
|
+
...base,
|
|
79
|
+
kind: block.kind,
|
|
80
|
+
...block.kind === "user" ? { attachments: block.attachments } : {}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function pageTranscriptSnapshot(execution, snapshot, input) {
|
|
84
|
+
const query = transcriptPageQuerySchema.parse(input);
|
|
85
|
+
const id = executionId(execution);
|
|
86
|
+
const before = query.cursor === void 0 ? snapshot.blocks.length : z.strictObject({
|
|
87
|
+
id: z.literal(id),
|
|
88
|
+
before: z.number().int().nonnegative()
|
|
89
|
+
}).parse(JSON.parse(Buffer.from(query.cursor, "base64url").toString("utf8"))).before;
|
|
90
|
+
const start = Math.max(0, before - query.limit);
|
|
91
|
+
return {
|
|
92
|
+
snapshot: {
|
|
93
|
+
...snapshot,
|
|
94
|
+
blocks: snapshot.blocks.slice(start, before).map(transcriptBlockPreview)
|
|
95
|
+
},
|
|
96
|
+
nextCursor: start > 0 ? Buffer.from(JSON.stringify({
|
|
97
|
+
id,
|
|
98
|
+
before: start
|
|
99
|
+
})).toString("base64url") : null,
|
|
100
|
+
totalBlocks: snapshot.blocks.length
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function createIndexedTranscriptStore(store, now) {
|
|
104
|
+
async function head(transaction, execution) {
|
|
105
|
+
const record = await transaction.get("transcripts", executionId(execution));
|
|
106
|
+
if (record === null) return null;
|
|
107
|
+
const value = headSchema.parse(record.value);
|
|
108
|
+
if (value.execution.provider !== execution.provider || value.execution.sessionId !== execution.sessionId) throw new Error("Transcript execution key does not match its stored value");
|
|
109
|
+
return {
|
|
110
|
+
value,
|
|
111
|
+
version: record.version
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function readBlock(transaction, id, block) {
|
|
115
|
+
if (block.executionId !== id) throw new Error("Transcript block execution mismatch");
|
|
116
|
+
const chunks = [];
|
|
117
|
+
for (let part = 0; part < block.parts; part++) {
|
|
118
|
+
const record = await transaction.get("transcriptChunks", chunkKey(id, block.digest, part));
|
|
119
|
+
if (record === null) throw new Error("Committed transcript content is missing");
|
|
120
|
+
chunks.push(Buffer.from(chunkSchema.parse(record.value).data, "base64"));
|
|
121
|
+
}
|
|
122
|
+
const encoded = Buffer.concat(chunks);
|
|
123
|
+
if (encoded.byteLength !== block.bytes || digest(encoded) !== block.digest) throw new Error("Transcript content checksum mismatch");
|
|
124
|
+
const value = transcriptBlockSchema.parse(JSON.parse(encoded.toString("utf8")));
|
|
125
|
+
if (value.id !== block.blockId || value.contentRef !== void 0) throw new Error("Transcript block identity mismatch");
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
async function readSnapshot(transaction, execution) {
|
|
129
|
+
const record = await head(transaction, execution);
|
|
130
|
+
if (record === null) return null;
|
|
131
|
+
const id = executionId(execution);
|
|
132
|
+
const blocks = [];
|
|
133
|
+
let cursor;
|
|
134
|
+
do {
|
|
135
|
+
const page = await transaction.query({
|
|
136
|
+
index: blockIndex(id),
|
|
137
|
+
limit: 100,
|
|
138
|
+
order: "asc",
|
|
139
|
+
cursor
|
|
140
|
+
});
|
|
141
|
+
for (const entry of page.entries) blocks.push(await readBlock(transaction, id, blockSchema.parse(entry.summary)));
|
|
142
|
+
cursor = page.nextCursor ?? void 0;
|
|
143
|
+
} while (cursor !== void 0);
|
|
144
|
+
if (blocks.length !== record.value.blockCount) throw new Error("Transcript block count mismatch");
|
|
145
|
+
return {
|
|
146
|
+
...record.value.snapshot,
|
|
147
|
+
blocks
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
get(execution) {
|
|
152
|
+
return store.transaction((transaction) => readSnapshot(transaction, execution));
|
|
153
|
+
},
|
|
154
|
+
async head(execution) {
|
|
155
|
+
const record = await head(store, execution);
|
|
156
|
+
return record === null ? null : {
|
|
157
|
+
...record.value.snapshot,
|
|
158
|
+
blockCount: record.value.blockCount
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
async page(execution, input) {
|
|
162
|
+
const query = transcriptPageQuerySchema.parse(input);
|
|
163
|
+
return store.transaction(async (transaction) => {
|
|
164
|
+
const record = await head(transaction, execution);
|
|
165
|
+
if (record === null) return null;
|
|
166
|
+
const id = executionId(execution);
|
|
167
|
+
const page = await transaction.query({
|
|
168
|
+
index: blockIndex(id),
|
|
169
|
+
...query,
|
|
170
|
+
order: "desc"
|
|
171
|
+
});
|
|
172
|
+
const blocks = page.entries.map((entry) => {
|
|
173
|
+
const block = blockSchema.parse(entry.summary);
|
|
174
|
+
if (block.executionId !== id) throw new Error("Transcript page execution mismatch");
|
|
175
|
+
return block.preview;
|
|
176
|
+
}).reverse();
|
|
177
|
+
return {
|
|
178
|
+
snapshot: {
|
|
179
|
+
...record.value.snapshot,
|
|
180
|
+
blocks
|
|
181
|
+
},
|
|
182
|
+
nextCursor: page.nextCursor,
|
|
183
|
+
totalBlocks: record.value.blockCount
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
block(execution, blockId) {
|
|
188
|
+
return store.transaction(async (transaction) => {
|
|
189
|
+
const id = executionId(execution);
|
|
190
|
+
if (await head(transaction, execution) === null) return null;
|
|
191
|
+
const record = await transaction.get("transcriptBlocks", blockKey(id, blockId));
|
|
192
|
+
if (record === null) return null;
|
|
193
|
+
const block = blockSchema.parse(record.value);
|
|
194
|
+
if (block.blockId !== blockId) throw new Error("Transcript block key mismatch");
|
|
195
|
+
return readBlock(transaction, id, block);
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
async put(execution, input) {
|
|
199
|
+
const snapshot = transcriptSnapshotSchema.parse(input);
|
|
200
|
+
const id = executionId(execution);
|
|
201
|
+
const seen = /* @__PURE__ */ new Set();
|
|
202
|
+
const encoded = snapshot.blocks.map((block, ordinal) => {
|
|
203
|
+
if (block.contentRef !== void 0) throw new Error("Cannot persist a transcript preview as full content");
|
|
204
|
+
if (seen.has(block.id)) throw new Error("Duplicate transcript block identity");
|
|
205
|
+
seen.add(block.id);
|
|
206
|
+
const bytes = Buffer.from(JSON.stringify(block));
|
|
207
|
+
return {
|
|
208
|
+
key: blockKey(id, block.id),
|
|
209
|
+
bytes,
|
|
210
|
+
header: blockSchema.parse(jsonValue({
|
|
211
|
+
executionId: id,
|
|
212
|
+
blockId: block.id,
|
|
213
|
+
digest: digest(bytes),
|
|
214
|
+
ordinal,
|
|
215
|
+
parts: Math.ceil(bytes.length / partBytes),
|
|
216
|
+
bytes: bytes.length,
|
|
217
|
+
preview: transcriptBlockPreview(block)
|
|
218
|
+
}))
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
for (let attempt = 0;; attempt++) try {
|
|
222
|
+
return await store.transaction(async (transaction) => {
|
|
223
|
+
const previous = await head(transaction, execution);
|
|
224
|
+
if (previous !== null && previous.value.snapshot.cursor >= snapshot.cursor) return await readSnapshot(transaction, execution);
|
|
225
|
+
const old = /* @__PURE__ */ new Map();
|
|
226
|
+
let cursor;
|
|
227
|
+
do {
|
|
228
|
+
const page = await transaction.query({
|
|
229
|
+
index: blockIndex(id),
|
|
230
|
+
limit: 100,
|
|
231
|
+
cursor
|
|
232
|
+
});
|
|
233
|
+
for (const entry of page.entries) old.set(entry.id, {
|
|
234
|
+
value: blockSchema.parse(entry.summary),
|
|
235
|
+
version: entry.version
|
|
236
|
+
});
|
|
237
|
+
cursor = page.nextCursor ?? void 0;
|
|
238
|
+
} while (cursor !== void 0);
|
|
239
|
+
for (const block of encoded) {
|
|
240
|
+
const current = old.get(block.key);
|
|
241
|
+
old.delete(block.key);
|
|
242
|
+
if (current?.value.digest === block.header.digest && current.value.ordinal === block.header.ordinal) continue;
|
|
243
|
+
if (current?.value.digest !== block.header.digest) for (let part = 0; part < block.header.parts; part++) {
|
|
244
|
+
const key = chunkKey(id, block.header.digest, part);
|
|
245
|
+
if (await transaction.get("transcriptChunks", key) === null) await transaction.write({
|
|
246
|
+
collection: "transcriptChunks",
|
|
247
|
+
id: key,
|
|
248
|
+
expectedVersion: 0,
|
|
249
|
+
operationId: randomUUID(),
|
|
250
|
+
value: { data: block.bytes.subarray(part * partBytes, (part + 1) * partBytes).toString("base64") },
|
|
251
|
+
indexes: []
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const inactive = current === void 0 ? await transaction.get("transcriptBlocks", block.key) : null;
|
|
255
|
+
await transaction.write({
|
|
256
|
+
collection: "transcriptBlocks",
|
|
257
|
+
id: block.key,
|
|
258
|
+
expectedVersion: current?.version ?? inactive?.version ?? 0,
|
|
259
|
+
operationId: randomUUID(),
|
|
260
|
+
value: block.header,
|
|
261
|
+
indexes: [{
|
|
262
|
+
index: blockIndex(id),
|
|
263
|
+
sortKey: String(block.header.ordinal).padStart(16, "0"),
|
|
264
|
+
summary: block.header
|
|
265
|
+
}]
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
for (const [key, previous] of old) await transaction.write({
|
|
269
|
+
collection: "transcriptBlocks",
|
|
270
|
+
id: key,
|
|
271
|
+
expectedVersion: previous.version,
|
|
272
|
+
operationId: randomUUID(),
|
|
273
|
+
value: previous.value,
|
|
274
|
+
indexes: []
|
|
275
|
+
});
|
|
276
|
+
const value = headSchema.parse(jsonValue({
|
|
277
|
+
schemaVersion: 1,
|
|
278
|
+
execution,
|
|
279
|
+
snapshot: {
|
|
280
|
+
...snapshot,
|
|
281
|
+
blocks: []
|
|
282
|
+
},
|
|
283
|
+
blockCount: snapshot.blocks.length,
|
|
284
|
+
updatedAt: now()
|
|
285
|
+
}));
|
|
286
|
+
await transaction.write({
|
|
287
|
+
collection: "transcripts",
|
|
288
|
+
id,
|
|
289
|
+
expectedVersion: previous?.version ?? 0,
|
|
290
|
+
operationId: randomUUID(),
|
|
291
|
+
value,
|
|
292
|
+
indexes: projectIndexedRecord("transcripts", id, value).map((entry) => ({
|
|
293
|
+
...entry,
|
|
294
|
+
summary: jsonValue(entry.summary)
|
|
295
|
+
}))
|
|
296
|
+
});
|
|
297
|
+
return snapshot;
|
|
298
|
+
});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (attempt >= 4 || !(error instanceof VersionConflictError || error instanceof RecordExistsError)) throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
export { createIndexedTranscriptStore, pageTranscriptSnapshot };
|
|
307
|
+
|
|
308
|
+
//# sourceMappingURL=transcripts.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcripts.mjs","names":[],"sources":["../../src/store/transcripts.ts"],"sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { z } from \"zod\";\nimport {\n transcriptBlockSchema,\n transcriptSnapshotSchema,\n type TranscriptBlock,\n type TranscriptPageQuery,\n type TranscriptSnapshot,\n} from \"../schema/transcript\";\nimport { executionReferenceSchema, type ExecutionReference } from \"../schema/task\";\nimport { RecordExistsError, VersionConflictError } from \"./driver\";\nimport type { FactoryStores } from \"./engine\";\nimport type { IndexedStore } from \"./indexed\";\nimport { projectIndexedRecord } from \"./indexed-projections\";\n\nconst partBytes = 48 * 1024;\nconst previewBytes = 4 * 1024;\nconst headSchema = z.strictObject({\n schemaVersion: z.literal(1),\n execution: executionReferenceSchema,\n snapshot: transcriptSnapshotSchema,\n blockCount: z.number().int().nonnegative(),\n updatedAt: z.iso.datetime(),\n});\nconst blockSchema = z.strictObject({\n executionId: z.string(),\n blockId: z.string(),\n digest: z.string().regex(/^[a-f0-9]{64}$/u),\n ordinal: z.number().int().nonnegative(),\n parts: z.number().int().positive(),\n bytes: z.number().int().positive(),\n preview: transcriptBlockSchema,\n});\nconst chunkSchema = z.strictObject({ data: z.string() });\nfunction jsonValue(value: unknown): unknown {\n return JSON.parse(JSON.stringify(value)) as unknown;\n}\nconst transcriptPageQuerySchema = z.strictObject({\n limit: z.number().int().min(1).max(100),\n cursor: z.string().max(8192).optional(),\n});\n\nfunction digest(value: string | Buffer): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction executionId(execution: ExecutionReference): string {\n executionReferenceSchema.parse(execution);\n return `transcript_${digest(JSON.stringify([execution.provider, execution.sessionId])).slice(0, 32)}`;\n}\n\nfunction blockKey(id: string, blockId: string): string {\n return `block_${digest(JSON.stringify([id, blockId]))}`;\n}\n\nfunction chunkKey(id: string, hash: string, part: number): string {\n return `chunk_${digest(JSON.stringify([id, hash, part]))}`;\n}\n\nfunction blockIndex(id: string): string {\n return `transcript-blocks:${id}`;\n}\n\nfunction transcriptBlockPreview(block: TranscriptBlock): TranscriptBlock {\n const bytes = Buffer.byteLength(JSON.stringify(block));\n if (bytes <= previewBytes) return block;\n const clipped = (text: string, length: number) =>\n Array.from(text.replace(/[\\uD800-\\uDFFF]/gu, \"�\"))\n .slice(0, length)\n .join(\"\");\n const base = {\n id: block.id,\n title: clipped(block.title, 120),\n text: clipped(block.text, 240),\n createdAt: block.createdAt,\n contentRef: { blockId: block.id, bytes },\n };\n return transcriptBlockSchema.parse(\n block.kind === \"tool\"\n ? {\n ...base,\n kind: \"tool\",\n tool: {\n callId: block.tool.callId,\n name: block.tool.name,\n inputText: clipped(block.tool.inputText, 240),\n state: block.tool.state,\n },\n }\n : {\n ...base,\n kind: block.kind,\n ...(block.kind === \"user\" ? { attachments: block.attachments } : {}),\n },\n );\n}\n\nexport function pageTranscriptSnapshot(\n execution: ExecutionReference,\n snapshot: TranscriptSnapshot,\n input: TranscriptPageQuery,\n) {\n const query = transcriptPageQuerySchema.parse(input);\n const id = executionId(execution);\n const before =\n query.cursor === undefined\n ? snapshot.blocks.length\n : z\n .strictObject({\n id: z.literal(id),\n before: z.number().int().nonnegative(),\n })\n .parse(JSON.parse(Buffer.from(query.cursor, \"base64url\").toString(\"utf8\"))).before;\n const start = Math.max(0, before - query.limit);\n return {\n snapshot: {\n ...snapshot,\n blocks: snapshot.blocks.slice(start, before).map(transcriptBlockPreview),\n },\n nextCursor:\n start > 0 ? Buffer.from(JSON.stringify({ id, before: start })).toString(\"base64url\") : null,\n totalBlocks: snapshot.blocks.length,\n };\n}\n\nexport function createIndexedTranscriptStore(\n store: IndexedStore,\n now: () => string,\n): FactoryStores[\"transcripts\"] {\n async function head(transaction: IndexedStore, execution: ExecutionReference) {\n const record = await transaction.get(\"transcripts\", executionId(execution));\n if (record === null) return null;\n const value = headSchema.parse(record.value);\n if (\n value.execution.provider !== execution.provider ||\n value.execution.sessionId !== execution.sessionId\n )\n throw new Error(\"Transcript execution key does not match its stored value\");\n return { value, version: record.version };\n }\n\n async function readBlock(\n transaction: IndexedStore,\n id: string,\n block: z.infer<typeof blockSchema>,\n ) {\n if (block.executionId !== id) throw new Error(\"Transcript block execution mismatch\");\n const chunks: Buffer[] = [];\n for (let part = 0; part < block.parts; part++) {\n const record = await transaction.get(\"transcriptChunks\", chunkKey(id, block.digest, part));\n if (record === null) throw new Error(\"Committed transcript content is missing\");\n chunks.push(Buffer.from(chunkSchema.parse(record.value).data, \"base64\"));\n }\n const encoded = Buffer.concat(chunks);\n if (encoded.byteLength !== block.bytes || digest(encoded) !== block.digest)\n throw new Error(\"Transcript content checksum mismatch\");\n const value = transcriptBlockSchema.parse(JSON.parse(encoded.toString(\"utf8\")));\n if (value.id !== block.blockId || value.contentRef !== undefined)\n throw new Error(\"Transcript block identity mismatch\");\n return value;\n }\n\n async function readSnapshot(\n transaction: IndexedStore,\n execution: ExecutionReference,\n ): Promise<TranscriptSnapshot | null> {\n const record = await head(transaction, execution);\n if (record === null) return null;\n const id = executionId(execution);\n const blocks: TranscriptBlock[] = [];\n let cursor: string | undefined;\n do {\n const page = await transaction.query({\n index: blockIndex(id),\n limit: 100,\n order: \"asc\",\n cursor,\n });\n for (const entry of page.entries)\n blocks.push(await readBlock(transaction, id, blockSchema.parse(entry.summary)));\n cursor = page.nextCursor ?? undefined;\n } while (cursor !== undefined);\n if (blocks.length !== record.value.blockCount)\n throw new Error(\"Transcript block count mismatch\");\n return { ...record.value.snapshot, blocks };\n }\n\n return {\n get(execution) {\n return store.transaction((transaction) => readSnapshot(transaction, execution));\n },\n async head(execution) {\n const record = await head(store, execution);\n return record === null\n ? null\n : { ...record.value.snapshot, blockCount: record.value.blockCount };\n },\n async page(execution, input) {\n const query = transcriptPageQuerySchema.parse(input);\n return store.transaction(async (transaction) => {\n const record = await head(transaction, execution);\n if (record === null) return null;\n const id = executionId(execution);\n const page = await transaction.query({ index: blockIndex(id), ...query, order: \"desc\" });\n const blocks = page.entries\n .map((entry) => {\n const block = blockSchema.parse(entry.summary);\n if (block.executionId !== id) throw new Error(\"Transcript page execution mismatch\");\n return block.preview;\n })\n .reverse();\n return {\n snapshot: { ...record.value.snapshot, blocks },\n nextCursor: page.nextCursor,\n totalBlocks: record.value.blockCount,\n };\n });\n },\n block(execution, blockId) {\n return store.transaction(async (transaction) => {\n const id = executionId(execution);\n if ((await head(transaction, execution)) === null) return null;\n const record = await transaction.get(\"transcriptBlocks\", blockKey(id, blockId));\n if (record === null) return null;\n const block = blockSchema.parse(record.value);\n if (block.blockId !== blockId) throw new Error(\"Transcript block key mismatch\");\n return readBlock(transaction, id, block);\n });\n },\n async put(execution, input) {\n const snapshot = transcriptSnapshotSchema.parse(input);\n const id = executionId(execution);\n const seen = new Set<string>();\n const encoded = snapshot.blocks.map((block, ordinal) => {\n if (block.contentRef !== undefined)\n throw new Error(\"Cannot persist a transcript preview as full content\");\n if (seen.has(block.id)) throw new Error(\"Duplicate transcript block identity\");\n seen.add(block.id);\n const bytes = Buffer.from(JSON.stringify(block));\n return {\n key: blockKey(id, block.id),\n bytes,\n header: blockSchema.parse(\n jsonValue({\n executionId: id,\n blockId: block.id,\n digest: digest(bytes),\n ordinal,\n parts: Math.ceil(bytes.length / partBytes),\n bytes: bytes.length,\n preview: transcriptBlockPreview(block),\n }),\n ),\n };\n });\n for (let attempt = 0; ; attempt++) {\n try {\n return await store.transaction(async (transaction) => {\n const previous = await head(transaction, execution);\n if (previous !== null && previous.value.snapshot.cursor >= snapshot.cursor)\n return (await readSnapshot(transaction, execution))!;\n const old = new Map<string, { value: z.infer<typeof blockSchema>; version: number }>();\n let cursor: string | undefined;\n do {\n const page = await transaction.query({ index: blockIndex(id), limit: 100, cursor });\n for (const entry of page.entries)\n old.set(entry.id, {\n value: blockSchema.parse(entry.summary),\n version: entry.version,\n });\n cursor = page.nextCursor ?? undefined;\n } while (cursor !== undefined);\n for (const block of encoded) {\n const current = old.get(block.key);\n old.delete(block.key);\n if (\n current?.value.digest === block.header.digest &&\n current.value.ordinal === block.header.ordinal\n )\n continue;\n if (current?.value.digest !== block.header.digest) {\n for (let part = 0; part < block.header.parts; part++) {\n const key = chunkKey(id, block.header.digest, part);\n if ((await transaction.get(\"transcriptChunks\", key)) === null)\n await transaction.write({\n collection: \"transcriptChunks\",\n id: key,\n expectedVersion: 0,\n operationId: randomUUID(),\n value: {\n data: block.bytes\n .subarray(part * partBytes, (part + 1) * partBytes)\n .toString(\"base64\"),\n },\n indexes: [],\n });\n }\n }\n const inactive =\n current === undefined ? await transaction.get(\"transcriptBlocks\", block.key) : null;\n await transaction.write({\n collection: \"transcriptBlocks\",\n id: block.key,\n expectedVersion: current?.version ?? inactive?.version ?? 0,\n operationId: randomUUID(),\n value: block.header,\n indexes: [\n {\n index: blockIndex(id),\n sortKey: String(block.header.ordinal).padStart(16, \"0\"),\n summary: block.header,\n },\n ],\n });\n }\n for (const [key, previous] of old)\n await transaction.write({\n collection: \"transcriptBlocks\",\n id: key,\n expectedVersion: previous.version,\n operationId: randomUUID(),\n value: previous.value,\n indexes: [],\n });\n const value = headSchema.parse(\n jsonValue({\n schemaVersion: 1,\n execution,\n snapshot: { ...snapshot, blocks: [] },\n blockCount: snapshot.blocks.length,\n updatedAt: now(),\n }),\n );\n await transaction.write({\n collection: \"transcripts\",\n id,\n expectedVersion: previous?.version ?? 0,\n operationId: randomUUID(),\n value,\n indexes: projectIndexedRecord(\"transcripts\", id, value).map((entry) => ({\n ...entry,\n summary: jsonValue(entry.summary),\n })),\n });\n return snapshot;\n });\n } catch (error) {\n if (\n attempt >= 4 ||\n !(error instanceof VersionConflictError || error instanceof RecordExistsError)\n )\n throw error;\n }\n }\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/sweep.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TaskId } from "./schema/id.mjs";
|
|
2
|
-
import { Repository } from "./schema/repository.mjs";
|
|
3
2
|
import { ExecutionReference, Task } from "./schema/task.mjs";
|
|
4
3
|
import { AgentRouteKey } from "./agent-routes.mjs";
|
|
4
|
+
import { Repository } from "./schema/repository.mjs";
|
|
5
5
|
import { FactoryStores } from "./store/engine.mjs";
|
|
6
6
|
import { DispatchBudget, DispatchRefusalCode } from "./dispatch.mjs";
|
|
7
7
|
//#region src/sweep.d.ts
|
package/dist/sweep.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { collectFactoryRecords } from "./store/iterate.mjs";
|
|
2
|
+
import { budgetTasks } from "./budget.mjs";
|
|
3
|
+
import { VersionConflictError } from "./store/driver.mjs";
|
|
1
4
|
import { InvalidTransitionError } from "./schema/transitions.mjs";
|
|
2
5
|
import { routeKey } from "./agent-routes.mjs";
|
|
3
|
-
import { VersionConflictError } from "./store/driver.mjs";
|
|
4
6
|
import { DispatchRefusedError, dispatchTask } from "./dispatch.mjs";
|
|
5
7
|
import { TaskAttemptConflictError } from "./store/engine.mjs";
|
|
6
8
|
//#region src/sweep.ts
|
|
@@ -61,9 +63,12 @@ async function sweepQueued(stores, options) {
|
|
|
61
63
|
const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;
|
|
62
64
|
if (!(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) || maxDispatches < 1) throw new Error("maxDispatches must be a positive integer");
|
|
63
65
|
let attempted = 0;
|
|
64
|
-
const taskSnapshot = options.tasks ?? await stores.tasks.list
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const taskSnapshot = options.tasks ?? await collectFactoryRecords(stores.tasks.list, {
|
|
67
|
+
limit: 100,
|
|
68
|
+
match: { state: "queued" }
|
|
69
|
+
});
|
|
70
|
+
const budgetSnapshot = options.budget?.tasks ?? (options.budget && options.budget.enforce !== false ? await budgetTasks(stores) : []);
|
|
71
|
+
const budgetTasks$1 = new Map(budgetSnapshot.map((task) => [task.id, task]));
|
|
67
72
|
const queued = taskSnapshot.filter((task) => task.state === "queued").sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));
|
|
68
73
|
for (const task of queued) {
|
|
69
74
|
const route = task.work.route;
|
|
@@ -130,15 +135,15 @@ async function sweepQueued(stores, options) {
|
|
|
130
135
|
},
|
|
131
136
|
budget: options.budget === void 0 ? void 0 : {
|
|
132
137
|
...options.budget,
|
|
133
|
-
tasks: [...budgetTasks.values()]
|
|
138
|
+
tasks: [...budgetTasks$1.values()]
|
|
134
139
|
}
|
|
135
140
|
});
|
|
136
|
-
budgetTasks.set(dispatched.id, dispatched);
|
|
141
|
+
budgetTasks$1.set(dispatched.id, dispatched);
|
|
137
142
|
result.dispatched.push(task.id);
|
|
138
143
|
} catch (error) {
|
|
139
144
|
if (options.budget !== void 0) {
|
|
140
145
|
const current = await stores.tasks.get(task.id).catch(() => null);
|
|
141
|
-
if (current !== null) budgetTasks.set(current.id, current);
|
|
146
|
+
if (current !== null) budgetTasks$1.set(current.id, current);
|
|
142
147
|
}
|
|
143
148
|
if (error instanceof DispatchRefusedError) {
|
|
144
149
|
result.skipped.push({
|
package/dist/sweep.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sweep.mjs","names":[],"sources":["../src/sweep.ts"],"sourcesContent":["import { routeKey, type AgentRouteKey } from \"./agent-routes\";\nimport { dispatchTask, DispatchRefusedError, type DispatchBudget } from \"./dispatch\";\nimport type { DispatchRefusalCode } from \"./dispatch\";\nimport type { TaskId } from \"./schema/id\";\nimport type { Repository } from \"./schema/repository\";\nimport type { ExecutionReference, Task } from \"./schema/task\";\nimport { InvalidTransitionError } from \"./schema/transitions\";\nimport { VersionConflictError } from \"./store/driver\";\nimport { TaskAttemptConflictError, type FactoryStores } from \"./store/engine\";\n\n/** Graph, receipt, repository, and Task capabilities required by queued-work dispatch. */\nexport type SweepQueuedStores = Pick<\n FactoryStores,\n \"graphs\" | \"receipts\" | \"repositories\" | \"tasks\"\n>;\n\n/**\n * Single-repository adapter for an exact agent route. Supplied by the app, which knows its\n * agents' addresses. Returns the provider's execution reference when it has one.\n *\n * @param task - Running Task already admitted by the dispatch gate.\n * @param repository - Exact enabled repository selected from the Task scope.\n * @returns Provider execution reference that dispatch can persist for recovery.\n */\nexport type TaskLauncher = (task: Task, repository: Repository) => Promise<ExecutionReference>;\n\n/** Exact-route launchers, budget gate, and pass bounds for queued Task dispatch. */\nexport interface SweepQueuedOptions {\n /** Single-repository agent adapters keyed by routeKey(binding), e.g. code-change@1. */\n launchers: Readonly<Partial<Record<AgentRouteKey, TaskLauncher>>>;\n /** Reserve-before-execution limits, passed through to every dispatch. */\n budget?: DispatchBudget;\n /** Maximum task launches attempted in one pass; omitted means no limit. */\n maxDispatches?: number;\n /** A dispatcher-wide snapshot avoids another complete task read. */\n tasks?: readonly Task[];\n}\n\n/** Tasks dispatched, deferred by a gate, or failed by one queued-work pass. */\nexport interface SweepQueuedResult {\n /** Task IDs whose provider execution reference was durably recorded. */\n readonly dispatched: readonly TaskId[];\n /** Tasks left in the queue because a declared gate may become eligible later. */\n readonly skipped: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepSkipReasonCode;\n readonly reason: string;\n }[];\n /** Tasks whose routing is invalid or whose launch threw. */\n readonly failed: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepFailureReasonCode;\n readonly reason: string;\n }[];\n}\n\n/** Stable reason a queued Task remained eligible for a later dispatch pass. */\nexport type SweepSkipReasonCode =\n | DispatchRefusalCode\n | \"workflow_owned\"\n | \"awaiting_approval\"\n | \"dispatch_limit\"\n | \"task_changed\";\n\n/** Stable reason a queued Task could not be routed or launched. */\nexport type SweepFailureReasonCode =\n | \"invalid_repository_scope\"\n | \"repository_not_found\"\n | \"launcher_not_registered\"\n | \"launch_failed\";\n\ntype MutableSweepQueuedResult = {\n dispatched: TaskId[];\n skipped: { taskId: TaskId; reasonCode: SweepSkipReasonCode; reason: string }[];\n failed: { taskId: TaskId; reasonCode: SweepFailureReasonCode; reason: string }[];\n};\n\nasync function failUnrouteable(\n stores: SweepQueuedStores,\n task: Task,\n reasonCode: SweepFailureReasonCode,\n reason: string,\n result: MutableSweepQueuedResult,\n): Promise<void> {\n try {\n await stores.tasks.transition(task.id, \"failed\", { reason, expectFrom: \"queued\" });\n result.failed.push({ taskId: task.id, reasonCode, reason });\n } catch (error) {\n if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed during routing\",\n });\n return;\n }\n throw error;\n }\n}\n\n/**\n * Runs one bounded pass over queued Tasks and dispatches each eligible exact route.\n *\n * @remarks\n * Candidates are ordered by newest attempt first and then creation time. Route-less work remains\n * queued for its workflow owner; unmet dependencies, approvals, and pass limits appear in\n * `skipped`. Invalid repository scope, missing repositories or launchers, and launch failures\n * appear in `failed`, with unrouteable Tasks transitioned to `failed` when this sweep wins the\n * race. A result entry describes this pass rather than guaranteeing the Task's lifecycle state:\n * an ambiguously accepted launch is reported as `launch_failed` while its Task intentionally stays\n * running for recovery. Callers should surface every failure, reread those Tasks before deciding\n * remediation, and schedule another pass for skipped work.\n *\n * Overlapping passes are safe: {@link dispatchTask} atomically admits one winner before invoking a\n * launcher. A supplied `tasks` snapshot bounds reads and candidate selection for this pass; it can\n * become stale, and corresponding races are returned as `task_changed` rather than hidden.\n * `maxDispatches` counts provider launches started during the pass, including launches that later\n * fail, so `dispatched.length` may be smaller than the limit.\n *\n * @param stores - Graph, receipt, repository, and Task stores used during the pass.\n * @param options - Exact-route launchers plus optional budget, Task snapshot, and dispatch limit.\n * @returns IDs dispatched and structured skipped/failed outcomes for every considered candidate.\n * @throws When `maxDispatches` is not a positive integer or an unexpected persistence error occurs.\n * @see The shipped `docs/recipes/retry-recovery.md` recipe for scheduled recovery.\n */\nexport async function sweepQueued(\n stores: SweepQueuedStores,\n options: SweepQueuedOptions,\n): Promise<SweepQueuedResult> {\n const result: MutableSweepQueuedResult = { dispatched: [], skipped: [], failed: [] };\n const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;\n if (\n !(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) ||\n maxDispatches < 1\n ) {\n throw new Error(\"maxDispatches must be a positive integer\");\n }\n let attempted = 0;\n const taskSnapshot = options.tasks ?? (await stores.tasks.list());\n const budgetSnapshot =\n options.budget?.tasks ??\n (options.budget && options.tasks ? await stores.tasks.list() : taskSnapshot);\n const budgetTasks = new Map(budgetSnapshot.map((task) => [task.id, task]));\n const queued = taskSnapshot\n .filter((task) => task.state === \"queued\")\n .sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));\n\n for (const task of queued) {\n const route = task.work.route;\n if (route === undefined) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"workflow_owned\",\n reason: \"no agent route; workflow owns execution\",\n });\n continue;\n }\n const [repositoryId] = task.repositoryIds;\n if (task.repositoryIds.length !== 1 || repositoryId === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"invalid_repository_scope\",\n \"sweep launcher adapter requires exactly one repository\",\n result,\n );\n continue;\n }\n if (task.approval === \"required\") {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"awaiting_approval\",\n reason: \"awaiting human approval\",\n });\n continue;\n }\n const key = routeKey(route);\n const launcher = Object.hasOwn(options.launchers, key) ? options.launchers[key] : undefined;\n if (launcher === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"launcher_not_registered\",\n `no registered launcher handles agent route ${key}`,\n result,\n );\n continue;\n }\n if (\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: task.rootTaskId,\n taskId: task.id,\n }))\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dependencies_unsatisfied\",\n reason: \"graph dependencies must all be succeeded before dispatch\",\n });\n continue;\n }\n if (attempted >= maxDispatches) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dispatch_limit\",\n reason: \"per-sweep dispatch limit reached\",\n });\n continue;\n }\n const repository = await stores.repositories.get(repositoryId);\n if (repository === null) {\n await failUnrouteable(stores, task, \"repository_not_found\", \"task has no repository\", result);\n continue;\n }\n let launchStarted = false;\n try {\n const dispatched = await dispatchTask(stores, {\n taskId: task.id,\n route,\n launch: (dispatched) => {\n attempted += 1;\n launchStarted = true;\n return launcher(dispatched, repository);\n },\n budget:\n options.budget === undefined\n ? undefined\n : { ...options.budget, tasks: [...budgetTasks.values()] },\n });\n budgetTasks.set(dispatched.id, dispatched);\n result.dispatched.push(task.id);\n } catch (error) {\n if (options.budget !== undefined) {\n const current = await stores.tasks.get(task.id).catch(() => null);\n if (current !== null) budgetTasks.set(current.id, current);\n }\n if (error instanceof DispatchRefusedError) {\n // Dependencies not ready, or another sweep won the race. Leave it.\n result.skipped.push({ taskId: task.id, reasonCode: error.code, reason: error.reason });\n continue;\n }\n if (\n !launchStarted &&\n (error instanceof InvalidTransitionError ||\n error instanceof TaskAttemptConflictError ||\n error instanceof VersionConflictError)\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed before dispatch\",\n });\n continue;\n }\n result.failed.push({\n taskId: task.id,\n reasonCode: \"launch_failed\",\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sweep.mjs","names":["readBudgetTasks","budgetTasks"],"sources":["../src/sweep.ts"],"sourcesContent":["import { budgetTasks as readBudgetTasks } from \"./budget\";\nimport { collectFactoryRecords } from \"./store/iterate\";\nimport { routeKey, type AgentRouteKey } from \"./agent-routes\";\nimport { dispatchTask, DispatchRefusedError, type DispatchBudget } from \"./dispatch\";\nimport type { DispatchRefusalCode } from \"./dispatch\";\nimport type { TaskId } from \"./schema/id\";\nimport type { Repository } from \"./schema/repository\";\nimport type { ExecutionReference, Task } from \"./schema/task\";\nimport { InvalidTransitionError } from \"./schema/transitions\";\nimport { VersionConflictError } from \"./store/driver\";\nimport { TaskAttemptConflictError, type FactoryStores } from \"./store/engine\";\n\n/** Graph, receipt, repository, and Task capabilities required by queued-work dispatch. */\nexport type SweepQueuedStores = Pick<\n FactoryStores,\n \"graphs\" | \"receipts\" | \"repositories\" | \"tasks\"\n>;\n\n/**\n * Single-repository adapter for an exact agent route. Supplied by the app, which knows its\n * agents' addresses. Returns the provider's execution reference when it has one.\n *\n * @param task - Running Task already admitted by the dispatch gate.\n * @param repository - Exact enabled repository selected from the Task scope.\n * @returns Provider execution reference that dispatch can persist for recovery.\n */\nexport type TaskLauncher = (task: Task, repository: Repository) => Promise<ExecutionReference>;\n\n/** Exact-route launchers, budget gate, and pass bounds for queued Task dispatch. */\nexport interface SweepQueuedOptions {\n /** Single-repository agent adapters keyed by routeKey(binding), e.g. code-change@1. */\n launchers: Readonly<Partial<Record<AgentRouteKey, TaskLauncher>>>;\n /** Reserve-before-execution limits, passed through to every dispatch. */\n budget?: DispatchBudget;\n /** Maximum task launches attempted in one pass; omitted means no limit. */\n maxDispatches?: number;\n /** A dispatcher-wide snapshot avoids another complete task read. */\n tasks?: readonly Task[];\n}\n\n/** Tasks dispatched, deferred by a gate, or failed by one queued-work pass. */\nexport interface SweepQueuedResult {\n /** Task IDs whose provider execution reference was durably recorded. */\n readonly dispatched: readonly TaskId[];\n /** Tasks left in the queue because a declared gate may become eligible later. */\n readonly skipped: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepSkipReasonCode;\n readonly reason: string;\n }[];\n /** Tasks whose routing is invalid or whose launch threw. */\n readonly failed: readonly {\n readonly taskId: TaskId;\n readonly reasonCode: SweepFailureReasonCode;\n readonly reason: string;\n }[];\n}\n\n/** Stable reason a queued Task remained eligible for a later dispatch pass. */\nexport type SweepSkipReasonCode =\n | DispatchRefusalCode\n | \"workflow_owned\"\n | \"awaiting_approval\"\n | \"dispatch_limit\"\n | \"task_changed\";\n\n/** Stable reason a queued Task could not be routed or launched. */\nexport type SweepFailureReasonCode =\n | \"invalid_repository_scope\"\n | \"repository_not_found\"\n | \"launcher_not_registered\"\n | \"launch_failed\";\n\ntype MutableSweepQueuedResult = {\n dispatched: TaskId[];\n skipped: { taskId: TaskId; reasonCode: SweepSkipReasonCode; reason: string }[];\n failed: { taskId: TaskId; reasonCode: SweepFailureReasonCode; reason: string }[];\n};\n\nasync function failUnrouteable(\n stores: SweepQueuedStores,\n task: Task,\n reasonCode: SweepFailureReasonCode,\n reason: string,\n result: MutableSweepQueuedResult,\n): Promise<void> {\n try {\n await stores.tasks.transition(task.id, \"failed\", { reason, expectFrom: \"queued\" });\n result.failed.push({ taskId: task.id, reasonCode, reason });\n } catch (error) {\n if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed during routing\",\n });\n return;\n }\n throw error;\n }\n}\n\n/**\n * Runs one bounded pass over queued Tasks and dispatches each eligible exact route.\n *\n * @remarks\n * Candidates are ordered by newest attempt first and then creation time. Route-less work remains\n * queued for its workflow owner; unmet dependencies, approvals, and pass limits appear in\n * `skipped`. Invalid repository scope, missing repositories or launchers, and launch failures\n * appear in `failed`, with unrouteable Tasks transitioned to `failed` when this sweep wins the\n * race. A result entry describes this pass rather than guaranteeing the Task's lifecycle state:\n * an ambiguously accepted launch is reported as `launch_failed` while its Task intentionally stays\n * running for recovery. Callers should surface every failure, reread those Tasks before deciding\n * remediation, and schedule another pass for skipped work.\n *\n * Overlapping passes are safe: {@link dispatchTask} atomically admits one winner before invoking a\n * launcher. A supplied `tasks` snapshot bounds reads and candidate selection for this pass; it can\n * become stale, and corresponding races are returned as `task_changed` rather than hidden.\n * `maxDispatches` counts provider launches started during the pass, including launches that later\n * fail, so `dispatched.length` may be smaller than the limit.\n *\n * @param stores - Graph, receipt, repository, and Task stores used during the pass.\n * @param options - Exact-route launchers plus optional budget, Task snapshot, and dispatch limit.\n * @returns IDs dispatched and structured skipped/failed outcomes for every considered candidate.\n * @throws When `maxDispatches` is not a positive integer or an unexpected persistence error occurs.\n * @see The shipped `docs/recipes/retry-recovery.md` recipe for scheduled recovery.\n */\nexport async function sweepQueued(\n stores: SweepQueuedStores,\n options: SweepQueuedOptions,\n): Promise<SweepQueuedResult> {\n const result: MutableSweepQueuedResult = { dispatched: [], skipped: [], failed: [] };\n const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;\n if (\n !(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) ||\n maxDispatches < 1\n ) {\n throw new Error(\"maxDispatches must be a positive integer\");\n }\n let attempted = 0;\n const taskSnapshot =\n options.tasks ??\n (await collectFactoryRecords(stores.tasks.list, { limit: 100, match: { state: \"queued\" } }));\n const budgetSnapshot =\n options.budget?.tasks ??\n (options.budget && options.budget.enforce !== false ? await readBudgetTasks(stores) : []);\n const budgetTasks = new Map(budgetSnapshot.map((task) => [task.id, task]));\n const queued = taskSnapshot\n .filter((task) => task.state === \"queued\")\n .sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));\n\n for (const task of queued) {\n const route = task.work.route;\n if (route === undefined) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"workflow_owned\",\n reason: \"no agent route; workflow owns execution\",\n });\n continue;\n }\n const [repositoryId] = task.repositoryIds;\n if (task.repositoryIds.length !== 1 || repositoryId === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"invalid_repository_scope\",\n \"sweep launcher adapter requires exactly one repository\",\n result,\n );\n continue;\n }\n if (task.approval === \"required\") {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"awaiting_approval\",\n reason: \"awaiting human approval\",\n });\n continue;\n }\n const key = routeKey(route);\n const launcher = Object.hasOwn(options.launchers, key) ? options.launchers[key] : undefined;\n if (launcher === undefined) {\n await failUnrouteable(\n stores,\n task,\n \"launcher_not_registered\",\n `no registered launcher handles agent route ${key}`,\n result,\n );\n continue;\n }\n if (\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: task.rootTaskId,\n taskId: task.id,\n }))\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dependencies_unsatisfied\",\n reason: \"graph dependencies must all be succeeded before dispatch\",\n });\n continue;\n }\n if (attempted >= maxDispatches) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"dispatch_limit\",\n reason: \"per-sweep dispatch limit reached\",\n });\n continue;\n }\n const repository = await stores.repositories.get(repositoryId);\n if (repository === null) {\n await failUnrouteable(stores, task, \"repository_not_found\", \"task has no repository\", result);\n continue;\n }\n let launchStarted = false;\n try {\n const dispatched = await dispatchTask(stores, {\n taskId: task.id,\n route,\n launch: (dispatched) => {\n attempted += 1;\n launchStarted = true;\n return launcher(dispatched, repository);\n },\n budget:\n options.budget === undefined\n ? undefined\n : { ...options.budget, tasks: [...budgetTasks.values()] },\n });\n budgetTasks.set(dispatched.id, dispatched);\n result.dispatched.push(task.id);\n } catch (error) {\n if (options.budget !== undefined) {\n const current = await stores.tasks.get(task.id).catch(() => null);\n if (current !== null) budgetTasks.set(current.id, current);\n }\n if (error instanceof DispatchRefusedError) {\n // Dependencies not ready, or another sweep won the race. Leave it.\n result.skipped.push({ taskId: task.id, reasonCode: error.code, reason: error.reason });\n continue;\n }\n if (\n !launchStarted &&\n (error instanceof InvalidTransitionError ||\n error instanceof TaskAttemptConflictError ||\n error instanceof VersionConflictError)\n ) {\n result.skipped.push({\n taskId: task.id,\n reasonCode: \"task_changed\",\n reason: \"task changed before dispatch\",\n });\n continue;\n }\n result.failed.push({\n taskId: task.id,\n reasonCode: \"launch_failed\",\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFactoryRecords } from "./store/iterate.mjs";
|
|
1
2
|
//#region src/task-communication.ts
|
|
2
3
|
/**
|
|
3
4
|
* Build the generic durable receipt reconciler around an application's event
|
|
@@ -6,7 +7,20 @@
|
|
|
6
7
|
*/
|
|
7
8
|
function createTaskCommunicationReconciler(projection) {
|
|
8
9
|
return async function reconcileTaskCommunications(stores, snapshot) {
|
|
9
|
-
const tasks = snapshot ?? await stores.tasks.list
|
|
10
|
+
const tasks = snapshot ?? [...new Map((await Promise.all([collectFactoryRecords(stores.tasks.list, {
|
|
11
|
+
limit: 100,
|
|
12
|
+
match: { state: "needs_human" },
|
|
13
|
+
fields: [{
|
|
14
|
+
path: ["question"],
|
|
15
|
+
test: "exists"
|
|
16
|
+
}]
|
|
17
|
+
}), collectFactoryRecords(stores.tasks.list, {
|
|
18
|
+
limit: 100,
|
|
19
|
+
fields: [{
|
|
20
|
+
path: ["pendingCommunicationReceiptIds"],
|
|
21
|
+
test: "notEmpty"
|
|
22
|
+
}]
|
|
23
|
+
})])).flat().map((task) => [task.id, task])).values()];
|
|
10
24
|
let enqueued = 0;
|
|
11
25
|
for (const task of tasks) {
|
|
12
26
|
const presentsQuestion = task.question !== void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-communication.mjs","names":[],"sources":["../src/task-communication.ts"],"sourcesContent":["import type { CommunicationEvent } from \"./schema/communication\";\nimport type { MaybePromise } from \"./schema/common\";\nimport type { Receipt } from \"./schema/receipt\";\nimport type { Task } from \"./schema/task\";\nimport type { FactoryStores } from \"./store/engine\";\n\n/** Outbox, receipt, and Task capabilities required by communication reconciliation. */\nexport type TaskCommunicationStores = Pick<FactoryStores, \"outbox\" | \"receipts\" | \"tasks\">;\n\n/** Number of connector-neutral events newly enqueued during reconciliation. */\nexport interface TaskCommunicationResult {\n enqueued: number;\n}\n\n/** Repairs pending Task communication projections against the durable outbox. */\nexport type TaskCommunicationReconciler<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> = (stores: Stores, snapshot?: readonly Task[]) => Promise<TaskCommunicationResult>;\n\n/** Durable Task and receipt context supplied to a lifecycle event projection. */\nexport interface TaskLifecycleProjectionInput<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> {\n stores: Stores;\n tasks: readonly Task[];\n task: Task;\n receipt: Receipt;\n}\n\n/** Application policy for projecting durable task state into connector-neutral events. */\nexport interface TaskCommunicationProjection<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> {\n question(task: Task): MaybePromise<readonly CommunicationEvent[]>;\n lifecycle(\n input: TaskLifecycleProjectionInput<Stores>,\n ): MaybePromise<readonly CommunicationEvent[]>;\n}\n\n/**\n * Build the generic durable receipt reconciler around an application's event\n * projection. Events are acknowledged only after every projected event has\n * reached the outbox.\n */\nexport function createTaskCommunicationReconciler<Stores extends TaskCommunicationStores>(\n projection: TaskCommunicationProjection<Stores>,\n): TaskCommunicationReconciler<Stores> {\n return async function reconcileTaskCommunications(\n stores: Stores,\n snapshot?: readonly Task[],\n ): Promise<TaskCommunicationResult> {\n const tasks = snapshot ?? (await stores.tasks.list());\n let enqueued = 0;\n for (const task of tasks) {\n const presentsQuestion = task.question !== undefined;\n // A connector may confirm before the parent starts waiting, or before\n // its delivery stamp is persisted.\n if (\n presentsQuestion &&\n task.state === \"needs_human\" &&\n task.needsHumanNotifiedAt !== undefined\n ) {\n await stores.tasks.recordNeedsHumanDelivery(task.id, { attempt: task.attempt });\n }\n if (\n presentsQuestion &&\n task.state === \"needs_human\" &&\n task.needsHumanNotifiedAt === undefined &&\n !task.pendingCommunicationReceiptIds?.length\n ) {\n for (const event of await projection.question(task)) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n }\n for (const receiptId of task.pendingCommunicationReceiptIds ?? []) {\n const receipt = await stores.receipts.get(receiptId);\n if (receipt === null || receipt.taskId !== task.id) {\n throw new Error(`Task ${task.id} references missing lifecycle receipt ${receiptId}.`);\n }\n if (receipt.state === \"needs_human\" && presentsQuestion) {\n if (task.state === \"needs_human\" && task.attempt === receipt.attempt) {\n for (const event of await projection.question(task)) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n }\n await stores.tasks.acknowledgeCommunication(task.id, receiptId);\n continue;\n }\n for (const event of await projection.lifecycle({ stores, tasks, task, receipt })) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n await stores.tasks.acknowledgeCommunication(task.id, receiptId);\n }\n }\n return { enqueued };\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"task-communication.mjs","names":[],"sources":["../src/task-communication.ts"],"sourcesContent":["import { collectFactoryRecords } from \"./store/iterate\";\nimport type { CommunicationEvent } from \"./schema/communication\";\nimport type { MaybePromise } from \"./schema/common\";\nimport type { Receipt } from \"./schema/receipt\";\nimport type { Task } from \"./schema/task\";\nimport type { FactoryStores } from \"./store/engine\";\n\n/** Outbox, receipt, and Task capabilities required by communication reconciliation. */\nexport type TaskCommunicationStores = Pick<FactoryStores, \"outbox\" | \"receipts\" | \"tasks\">;\n\n/** Number of connector-neutral events newly enqueued during reconciliation. */\nexport interface TaskCommunicationResult {\n enqueued: number;\n}\n\n/** Repairs pending Task communication projections against the durable outbox. */\nexport type TaskCommunicationReconciler<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> = (stores: Stores, snapshot?: readonly Task[]) => Promise<TaskCommunicationResult>;\n\n/** Durable Task and receipt context supplied to a lifecycle event projection. */\nexport interface TaskLifecycleProjectionInput<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> {\n stores: Stores;\n tasks: readonly Task[];\n task: Task;\n receipt: Receipt;\n}\n\n/** Application policy for projecting durable task state into connector-neutral events. */\nexport interface TaskCommunicationProjection<\n Stores extends TaskCommunicationStores = TaskCommunicationStores,\n> {\n question(task: Task): MaybePromise<readonly CommunicationEvent[]>;\n lifecycle(\n input: TaskLifecycleProjectionInput<Stores>,\n ): MaybePromise<readonly CommunicationEvent[]>;\n}\n\n/**\n * Build the generic durable receipt reconciler around an application's event\n * projection. Events are acknowledged only after every projected event has\n * reached the outbox.\n */\nexport function createTaskCommunicationReconciler<Stores extends TaskCommunicationStores>(\n projection: TaskCommunicationProjection<Stores>,\n): TaskCommunicationReconciler<Stores> {\n return async function reconcileTaskCommunications(\n stores: Stores,\n snapshot?: readonly Task[],\n ): Promise<TaskCommunicationResult> {\n const tasks = snapshot ?? [\n ...new Map(\n (\n await Promise.all([\n collectFactoryRecords(stores.tasks.list, {\n limit: 100,\n match: { state: \"needs_human\" },\n fields: [{ path: [\"question\"], test: \"exists\" }],\n }),\n collectFactoryRecords(stores.tasks.list, {\n limit: 100,\n fields: [{ path: [\"pendingCommunicationReceiptIds\"], test: \"notEmpty\" }],\n }),\n ])\n )\n .flat()\n .map((task) => [task.id, task]),\n ).values(),\n ];\n let enqueued = 0;\n for (const task of tasks) {\n const presentsQuestion = task.question !== undefined;\n // A connector may confirm before the parent starts waiting, or before\n // its delivery stamp is persisted.\n if (\n presentsQuestion &&\n task.state === \"needs_human\" &&\n task.needsHumanNotifiedAt !== undefined\n ) {\n await stores.tasks.recordNeedsHumanDelivery(task.id, { attempt: task.attempt });\n }\n if (\n presentsQuestion &&\n task.state === \"needs_human\" &&\n task.needsHumanNotifiedAt === undefined &&\n !task.pendingCommunicationReceiptIds?.length\n ) {\n for (const event of await projection.question(task)) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n }\n for (const receiptId of task.pendingCommunicationReceiptIds ?? []) {\n const receipt = await stores.receipts.get(receiptId);\n if (receipt === null || receipt.taskId !== task.id) {\n throw new Error(`Task ${task.id} references missing lifecycle receipt ${receiptId}.`);\n }\n if (receipt.state === \"needs_human\" && presentsQuestion) {\n if (task.state === \"needs_human\" && task.attempt === receipt.attempt) {\n for (const event of await projection.question(task)) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n }\n await stores.tasks.acknowledgeCommunication(task.id, receiptId);\n continue;\n }\n for (const event of await projection.lifecycle({ stores, tasks, task, receipt })) {\n await stores.outbox.enqueue(event);\n enqueued += 1;\n }\n await stores.tasks.acknowledgeCommunication(task.id, receiptId);\n }\n }\n return { enqueued };\n };\n}\n"],"mappings":";;;;;;;AA6CA,SAAgB,kCACd,YACqC;CACrC,OAAO,eAAe,4BACpB,QACA,UACkC;EAClC,MAAM,QAAQ,YAAY,CACxB,GAAG,IAAI,KAEH,MAAM,QAAQ,IAAI,CAChB,sBAAsB,OAAO,MAAM,MAAM;GACvC,OAAO;GACP,OAAO,EAAE,OAAO,cAAc;GAC9B,QAAQ,CAAC;IAAE,MAAM,CAAC,UAAU;IAAG,MAAM;GAAS,CAAC;EACjD,CAAC,GACD,sBAAsB,OAAO,MAAM,MAAM;GACvC,OAAO;GACP,QAAQ,CAAC;IAAE,MAAM,CAAC,gCAAgC;IAAG,MAAM;GAAW,CAAC;EACzE,CAAC,CACH,CAAC,EAAA,CAEA,KAAK,CAAC,CACN,KAAK,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAClC,CAAC,CAAC,OAAO,CACX;EACA,IAAI,WAAW;EACf,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,mBAAmB,KAAK,aAAa,KAAA;GAG3C,IACE,oBACA,KAAK,UAAU,iBACf,KAAK,yBAAyB,KAAA,GAE9B,MAAM,OAAO,MAAM,yBAAyB,KAAK,IAAI,EAAE,SAAS,KAAK,QAAQ,CAAC;GAEhF,IACE,oBACA,KAAK,UAAU,iBACf,KAAK,yBAAyB,KAAA,KAC9B,CAAC,KAAK,gCAAgC,QAEtC,KAAK,MAAM,SAAS,MAAM,WAAW,SAAS,IAAI,GAAG;IACnD,MAAM,OAAO,OAAO,QAAQ,KAAK;IACjC,YAAY;GACd;GAEF,KAAK,MAAM,aAAa,KAAK,kCAAkC,CAAC,GAAG;IACjE,MAAM,UAAU,MAAM,OAAO,SAAS,IAAI,SAAS;IACnD,IAAI,YAAY,QAAQ,QAAQ,WAAW,KAAK,IAC9C,MAAM,IAAI,MAAM,QAAQ,KAAK,GAAG,wCAAwC,UAAU,EAAE;IAEtF,IAAI,QAAQ,UAAU,iBAAiB,kBAAkB;KACvD,IAAI,KAAK,UAAU,iBAAiB,KAAK,YAAY,QAAQ,SAC3D,KAAK,MAAM,SAAS,MAAM,WAAW,SAAS,IAAI,GAAG;MACnD,MAAM,OAAO,OAAO,QAAQ,KAAK;MACjC,YAAY;KACd;KAEF,MAAM,OAAO,MAAM,yBAAyB,KAAK,IAAI,SAAS;KAC9D;IACF;IACA,KAAK,MAAM,SAAS,MAAM,WAAW,UAAU;KAAE;KAAQ;KAAO;KAAM;IAAQ,CAAC,GAAG;KAChF,MAAM,OAAO,OAAO,QAAQ,KAAK;KACjC,YAAY;IACd;IACA,MAAM,OAAO,MAAM,yBAAyB,KAAK,IAAI,SAAS;GAChE;EACF;EACA,OAAO,EAAE,SAAS;CACpB;AACF"}
|
|
@@ -13,8 +13,8 @@ declare const assignmentPayloadSchema: z.ZodObject<{
|
|
|
13
13
|
declare const handoffPayloadSchema: z.ZodObject<{
|
|
14
14
|
outcome: z.ZodEnum<{
|
|
15
15
|
failed: "failed";
|
|
16
|
-
completed: "completed";
|
|
17
16
|
needs_input: "needs_input";
|
|
17
|
+
completed: "completed";
|
|
18
18
|
}>;
|
|
19
19
|
summary: z.ZodString;
|
|
20
20
|
output: z.ZodOptional<z.ZodCustom<JsonValue, JsonValue>>;
|
|
@@ -38,8 +38,8 @@ declare const assignmentTaskMessageProtocol: Readonly<TaskMessageProtocol<z.ZodO
|
|
|
38
38
|
declare const handoffTaskMessageProtocol: Readonly<TaskMessageProtocol<z.ZodObject<{
|
|
39
39
|
outcome: z.ZodEnum<{
|
|
40
40
|
failed: "failed";
|
|
41
|
-
completed: "completed";
|
|
42
41
|
needs_input: "needs_input";
|
|
42
|
+
completed: "completed";
|
|
43
43
|
}>;
|
|
44
44
|
summary: z.ZodString;
|
|
45
45
|
output: z.ZodOptional<z.ZodCustom<JsonValue, JsonValue>>;
|
|
@@ -57,8 +57,8 @@ declare const standardTaskMessageProtocols: readonly (Readonly<TaskMessageProtoc
|
|
|
57
57
|
}, z.core.$strict>, "assignment", 1>> | Readonly<TaskMessageProtocol<z.ZodObject<{
|
|
58
58
|
outcome: z.ZodEnum<{
|
|
59
59
|
failed: "failed";
|
|
60
|
-
completed: "completed";
|
|
61
60
|
needs_input: "needs_input";
|
|
61
|
+
completed: "completed";
|
|
62
62
|
}>;
|
|
63
63
|
summary: z.ZodString;
|
|
64
64
|
output: z.ZodOptional<z.ZodCustom<JsonValue, JsonValue>>;
|
package/dist/tasks.d.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ApprovalPolicy, ApprovalPriority, ApprovalRule, ApprovalSubject, allowByDefaultApprovalPolicy, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, requiresApproval } from "./approval.mjs";
|
|
2
1
|
import { AnswerId, TaskId, answerIdSchema, taskIdSchema } from "./schema/id.mjs";
|
|
3
2
|
import { ExecutionReference, MessageWaitingTask, NeedsHumanTask, SucceededTask, Task, TaskApproval, TaskKind, TaskOrigin, TaskQuestion, TaskState, TaskUsage, executionReferenceSchema, harnessSchema, isMessageWaitingTask, isNeedsHumanTask, isSucceededTask, standardTaskKinds, taskApprovalSchema, taskExecutionRepositoryIdsSchema, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskUsageSchema } from "./schema/task.mjs";
|
|
4
3
|
import { MAX_TASK_INBOX_PENDING, SendTaskMessageInput, TaskInbox, TaskMessage, TaskMessageClaim, TaskMessageDiscard, TaskMessageForProtocol, TaskMessageId, TaskMessageOperationId, TaskMessagePrincipal, TaskMessageProcessing, TaskMessageProtocol, TaskMessageProtocolBinding, TaskMessageProtocolBindingInput, TaskMessageProtocolInput, TaskMessageProtocolInputPayload, TaskMessageProtocolStoredPayload, TaskMessageProtocols, defineTaskMessageProtocol, defineTaskMessageProtocols, parseTaskMessageProtocolBinding, sendTaskMessageInputSchema, taskInboxSchema, taskMessageClaimSchema, taskMessageDiscardSchema, taskMessageIdSchema, taskMessageInput, taskMessageOperationIdSchema, taskMessagePrincipalSchema, taskMessageProcessingSchema, taskMessageProtocolBindingSchema, taskMessageProtocolSchema, taskMessageReservationSchema, taskMessageSchema } from "./schema/task-message.mjs";
|
|
@@ -10,6 +9,7 @@ import { CreateTaskEffectStoreOptions, RecordTaskEffectInput, ResolveTaskEffectI
|
|
|
10
9
|
import { CompleteTaskWorkInput, CompleteWorkflowTaskWorkInput, CreateTaskWorkStoreOptions, ResumeTaskWorkInput, TaskWorkCompletionFence, TaskWorkStore, TaskWorkStores, TaskWorkUsage, WaitForTaskMessagesInput, createTaskWorkStore } from "./store/task-work.mjs";
|
|
11
10
|
import { AnswerEvent, answerEventSchema } from "./schema/answer.mjs";
|
|
12
11
|
import { AgentSessionBinding, AnswerEventConflictError, AnswerQuestionInput, AnswerQuestionResult, ApprovalDecision, ApprovalError, CreateTaskInput, RejectionDecision, RootTaskOrigin, TaskAttemptConflictError, TaskPatch, TaskTransitionArguments, TaskTransitionOptions } from "./store/engine.mjs";
|
|
12
|
+
import { ApprovalPolicy, ApprovalPriority, ApprovalRule, ApprovalSubject, allowByDefaultApprovalPolicy, approvalPolicySchema, approvalPrioritySchema, approvalRuleSchema, requiresApproval } from "./approval.mjs";
|
|
13
13
|
import { ApprovalAccess, ApprovalAccessDecisionInput, ApprovalAccessError, ApprovalAccessOptions, ApprovalAccessStores, createApprovalAccess } from "./approval-access.mjs";
|
|
14
14
|
import { taskInitialState, taskTransitions } from "./schema/transitions.mjs";
|
|
15
15
|
import { ProcessTaskMessageOptions, ProcessTaskMessageResult, TaskMessageProcessingStores, processTaskMessage, readTaskMessageDecision } from "./task-message-processing.mjs";
|
package/dist/vercel.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { RepositorySlug } from "./schema/repository.mjs";
|
|
2
1
|
import { WorkNotification, WorkNotifications } from "./store/work-notifications.mjs";
|
|
2
|
+
import { RepositorySlug } from "./schema/repository.mjs";
|
|
3
3
|
import { VercelManagedGitHubApi, VercelManagedGitHubApiAction, VercelManagedGitHubApiError, VercelManagedGitHubApiOptions, VercelManagedGitHubApiSandboxCreateInput, VercelManagedGitHubIssue, VercelManagedGitHubIssueInput, VercelManagedGitHubPublication, VercelManagedGitHubPublicationInput, VercelManagedGitHubPullRequest, VercelManagedGitHubPullRequestInput, createVercelManagedGitHubApi } from "./vercel-github-api.mjs";
|
|
4
4
|
import { VercelGitHubGitTransportOptions, VercelGitHubTokenInput, VercelManagedGitHubGitTransportOptions, VercelManagedGitHubSandboxCreateInput, createVercelGitHubGitTransport, createVercelManagedGitHubGitTransport } from "./vercel-git.mjs";
|
|
5
5
|
//#region src/vercel.d.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RepositoryId } from "../schema/id.mjs";
|
|
2
|
-
import { RepositorySlug } from "../schema/repository.mjs";
|
|
3
2
|
import { Task } from "../schema/task.mjs";
|
|
4
3
|
import { Change } from "../schema/change.mjs";
|
|
4
|
+
import { RepositorySlug } from "../schema/repository.mjs";
|
|
5
5
|
import { FactoryStores } from "../store/engine.mjs";
|
|
6
6
|
import { OpenPullRequest } from "../pull-requests/contracts.mjs";
|
|
7
7
|
//#region src/work-triage/find-existing-work.d.ts
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFactoryRecords } from "../store/iterate.mjs";
|
|
1
2
|
//#region src/work-triage/find-existing-work.ts
|
|
2
3
|
const openChangeStates = /* @__PURE__ */ new Set(["verifying", "ready"]);
|
|
3
4
|
const activeTaskStates = /* @__PURE__ */ new Set([
|
|
@@ -15,8 +16,17 @@ async function findExistingWork(input) {
|
|
|
15
16
|
};
|
|
16
17
|
const [openPullRequests, changes, tasks] = await Promise.all([
|
|
17
18
|
input.listOpenPullRequests(repository.slug),
|
|
18
|
-
input.stores.changes.list
|
|
19
|
-
|
|
19
|
+
collectFactoryRecords(input.stores.changes.list, {
|
|
20
|
+
limit: 100,
|
|
21
|
+
match: [...openChangeStates].map((state) => ({
|
|
22
|
+
repositoryId: input.repositoryId,
|
|
23
|
+
state
|
|
24
|
+
}))
|
|
25
|
+
}),
|
|
26
|
+
collectFactoryRecords(input.stores.tasks.list, {
|
|
27
|
+
limit: 100,
|
|
28
|
+
match: { repositoryIds: [input.repositoryId] }
|
|
29
|
+
})
|
|
20
30
|
]);
|
|
21
31
|
return {
|
|
22
32
|
found: true,
|