@abelxiaoxing/cadence 1.0.13 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -13
- package/agents/contract-reviewer.md +2 -2
- package/agents/design-explorer.md +1 -1
- package/agents/diagnosis-worker.md +6 -3
- package/agents/implementation-worker.md +14 -9
- package/config/.env.example +5 -17
- package/config/routes.example.json +42 -0
- package/package.json +3 -3
- package/prompts/abel-design.md +150 -228
- package/prompts/abel-diagnose.md +37 -18
- package/prompts/abel-implement.md +111 -178
- package/prompts/abel-init.md +36 -33
- package/skills/abel-workflow/SKILL.md +156 -168
- package/src/apply-transaction.ts +1427 -0
- package/src/artifact-store.ts +204 -0
- package/src/child-session.ts +136 -17
- package/src/contracts.ts +105 -696
- package/src/control-contracts.ts +259 -0
- package/src/delivery-compiler.ts +1088 -0
- package/src/file-snapshot.ts +5 -0
- package/src/implement-graph.ts +11 -4
- package/src/index.ts +1465 -133
- package/src/isolation-backend.ts +269 -0
- package/src/packet-runtime.ts +533 -0
- package/src/parent-provider.ts +23 -4
- package/src/route-policy.ts +483 -0
- package/src/run-state.ts +393 -0
- package/src/run-store.ts +918 -0
- package/src/safe-path.ts +11 -0
- package/src/scoped-tools.ts +36 -0
- package/src/state-root.ts +215 -0
- package/src/subagent-activity.ts +526 -27
- package/src/submit-tool.ts +274 -0
- package/src/task-ledger.ts +1380 -0
- package/src/verification-capability.ts +529 -42
- package/src/worker-broker.ts +703 -0
- package/src/workflow-engine.ts +7391 -0
- package/src/workspace-store.ts +544 -0
- package/src/candidate-preflight.ts +0 -1618
- package/src/drain.ts +0 -20
- package/src/patch.ts +0 -505
- package/src/result-store.ts +0 -312
- package/src/runtime.ts +0 -2436
- package/src/scheduler.ts +0 -384
- package/src/subagent-endpoint.ts +0 -158
- package/src/worker.ts +0 -204
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
18
|
+
|
|
19
|
+
export interface ArtifactIdentity {
|
|
20
|
+
hash: string;
|
|
21
|
+
bytes: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function digest(bytes: Uint8Array): string {
|
|
25
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireHash(hash: string): void {
|
|
29
|
+
if (!SHA256.test(hash)) throw new Error("artifact-hash-invalid");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ensureDirectory(directory: string): void {
|
|
33
|
+
const stat = lstatSync(directory, { throwIfNoEntry: false });
|
|
34
|
+
if (stat) {
|
|
35
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
36
|
+
throw new Error("artifact-store-path-unsafe");
|
|
37
|
+
}
|
|
38
|
+
chmodSync(directory, 0o700);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let cursor = path.dirname(directory);
|
|
42
|
+
while (cursor !== path.dirname(cursor)) {
|
|
43
|
+
const ancestor = lstatSync(cursor, { throwIfNoEntry: false });
|
|
44
|
+
if (ancestor) {
|
|
45
|
+
if (ancestor.isSymbolicLink() || !ancestor.isDirectory()) {
|
|
46
|
+
throw new Error("artifact-store-path-unsafe");
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
cursor = path.dirname(cursor);
|
|
51
|
+
}
|
|
52
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
53
|
+
const created = lstatSync(directory);
|
|
54
|
+
if (created.isSymbolicLink() || !created.isDirectory()) {
|
|
55
|
+
throw new Error("artifact-store-path-unsafe");
|
|
56
|
+
}
|
|
57
|
+
chmodSync(directory, 0o700);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function syncDirectory(directory: string): void {
|
|
61
|
+
const descriptor = openSync(directory, constants.O_RDONLY);
|
|
62
|
+
try {
|
|
63
|
+
fsyncSync(descriptor);
|
|
64
|
+
} finally {
|
|
65
|
+
closeSync(descriptor);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeAtomic(target: string, bytes: Uint8Array): void {
|
|
70
|
+
const directory = path.dirname(target);
|
|
71
|
+
ensureDirectory(directory);
|
|
72
|
+
const temporary = path.join(
|
|
73
|
+
directory,
|
|
74
|
+
`.${path.basename(target)}.${process.pid}.${randomUUID()}.tmp`,
|
|
75
|
+
);
|
|
76
|
+
const descriptor = openSync(
|
|
77
|
+
temporary,
|
|
78
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
|
|
79
|
+
0o600,
|
|
80
|
+
);
|
|
81
|
+
try {
|
|
82
|
+
writeFileSync(descriptor, bytes);
|
|
83
|
+
fsyncSync(descriptor);
|
|
84
|
+
} finally {
|
|
85
|
+
closeSync(descriptor);
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
renameSync(temporary, target);
|
|
89
|
+
chmodSync(target, 0o600);
|
|
90
|
+
syncDirectory(directory);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
try {
|
|
93
|
+
unlinkSync(temporary);
|
|
94
|
+
} catch {
|
|
95
|
+
// The rename may already have consumed the temporary file.
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Durable content-addressed bytes used by workspace revisions and sealed results. */
|
|
102
|
+
export class ArtifactStore {
|
|
103
|
+
readonly root: string;
|
|
104
|
+
readonly #blobs: string;
|
|
105
|
+
readonly #references: string;
|
|
106
|
+
|
|
107
|
+
constructor(root: string) {
|
|
108
|
+
if (!path.isAbsolute(root)) throw new Error("artifact-store-root-invalid");
|
|
109
|
+
this.root = path.resolve(root);
|
|
110
|
+
this.#blobs = path.join(this.root, "blobs");
|
|
111
|
+
this.#references = path.join(this.root, "references");
|
|
112
|
+
ensureDirectory(this.root);
|
|
113
|
+
ensureDirectory(this.#blobs);
|
|
114
|
+
ensureDirectory(this.#references);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#blobPath(hash: string): string {
|
|
118
|
+
requireHash(hash);
|
|
119
|
+
return path.join(this.#blobs, hash.slice(0, 2), hash);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#referencePath(hash: string): string {
|
|
123
|
+
requireHash(hash);
|
|
124
|
+
return path.join(this.#references, hash.slice(0, 2), `${hash}.ref`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
put(input: Uint8Array): ArtifactIdentity {
|
|
128
|
+
if (!(input instanceof Uint8Array))
|
|
129
|
+
throw new Error("artifact-bytes-invalid");
|
|
130
|
+
const bytes = Buffer.from(input);
|
|
131
|
+
const hash = digest(bytes);
|
|
132
|
+
const target = this.#blobPath(hash);
|
|
133
|
+
const existing = lstatSync(target, { throwIfNoEntry: false });
|
|
134
|
+
if (existing) {
|
|
135
|
+
if (existing.isSymbolicLink() || !existing.isFile()) {
|
|
136
|
+
throw new Error("artifact-store-path-unsafe");
|
|
137
|
+
}
|
|
138
|
+
const current = readFileSync(target);
|
|
139
|
+
if (current.length !== bytes.length || digest(current) !== hash) {
|
|
140
|
+
throw new Error("artifact-integrity-invalid");
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
writeAtomic(target, bytes);
|
|
144
|
+
const stored = readFileSync(target);
|
|
145
|
+
if (stored.length !== bytes.length || digest(stored) !== hash) {
|
|
146
|
+
throw new Error("artifact-integrity-invalid");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze({ hash, bytes: bytes.length });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
read(hash: string): Uint8Array {
|
|
153
|
+
const target = this.#blobPath(hash);
|
|
154
|
+
const stat = lstatSync(target, { throwIfNoEntry: false });
|
|
155
|
+
if (!stat?.isFile() || stat.isSymbolicLink()) {
|
|
156
|
+
throw new Error("artifact-unavailable");
|
|
157
|
+
}
|
|
158
|
+
const bytes = readFileSync(target);
|
|
159
|
+
if (digest(bytes) !== hash) throw new Error("artifact-integrity-invalid");
|
|
160
|
+
return Buffer.from(bytes);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
referenceCount(hash: string): number {
|
|
164
|
+
requireHash(hash);
|
|
165
|
+
const referencePath = this.#referencePath(hash);
|
|
166
|
+
const stat = lstatSync(referencePath, { throwIfNoEntry: false });
|
|
167
|
+
if (!stat) return 0;
|
|
168
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
169
|
+
throw new Error("artifact-reference-invalid");
|
|
170
|
+
}
|
|
171
|
+
const raw = readFileSync(referencePath, "utf8");
|
|
172
|
+
if (!/^(?:0|[1-9][0-9]*)\n$/u.test(raw)) {
|
|
173
|
+
throw new Error("artifact-reference-invalid");
|
|
174
|
+
}
|
|
175
|
+
const count = Number(raw.trim());
|
|
176
|
+
if (!Number.isSafeInteger(count))
|
|
177
|
+
throw new Error("artifact-reference-invalid");
|
|
178
|
+
return count;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#setReferenceCount(hash: string, count: number): number {
|
|
182
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
183
|
+
throw new Error("artifact-reference-invalid");
|
|
184
|
+
}
|
|
185
|
+
writeAtomic(this.#referencePath(hash), Buffer.from(`${count}\n`));
|
|
186
|
+
return count;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
retain(hash: string): number {
|
|
190
|
+
this.read(hash);
|
|
191
|
+
const current = this.referenceCount(hash);
|
|
192
|
+
if (current === Number.MAX_SAFE_INTEGER) {
|
|
193
|
+
throw new Error("artifact-reference-overflow");
|
|
194
|
+
}
|
|
195
|
+
return this.#setReferenceCount(hash, current + 1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
release(hash: string): number {
|
|
199
|
+
this.read(hash);
|
|
200
|
+
const current = this.referenceCount(hash);
|
|
201
|
+
if (current === 0) throw new Error("artifact-reference-underflow");
|
|
202
|
+
return this.#setReferenceCount(hash, current - 1);
|
|
203
|
+
}
|
|
204
|
+
}
|
package/src/child-session.ts
CHANGED
|
@@ -16,12 +16,21 @@ import type {
|
|
|
16
16
|
SafeFailureDetails,
|
|
17
17
|
} from "./contracts.ts";
|
|
18
18
|
import { EmptyResourceLoader } from "./empty-resource-loader.ts";
|
|
19
|
-
import { createScopedTools, TOOL_LIMITS } from "./scoped-tools.ts";
|
|
20
19
|
import {
|
|
20
|
+
createScopedTools,
|
|
21
|
+
type Observation,
|
|
22
|
+
ScopedObservationCollector,
|
|
23
|
+
TOOL_LIMITS,
|
|
24
|
+
} from "./scoped-tools.ts";
|
|
25
|
+
import {
|
|
26
|
+
type CandidateArtifactSubmission,
|
|
27
|
+
type CandidateArtifactSubmissionResult,
|
|
28
|
+
createCandidateArtifactTool,
|
|
21
29
|
createSubmitTool,
|
|
22
30
|
type FinalCategory,
|
|
23
31
|
type SubmitClassification,
|
|
24
32
|
} from "./submit-tool.ts";
|
|
33
|
+
import { serializeTaskLedgerProjection } from "./task-ledger.ts";
|
|
25
34
|
|
|
26
35
|
function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
27
36
|
signal.throwIfAborted();
|
|
@@ -106,6 +115,7 @@ function wrapScopedTools(
|
|
|
106
115
|
roots: string[],
|
|
107
116
|
cwd: string,
|
|
108
117
|
allowedPaths?: string[],
|
|
118
|
+
observer?: (observation: Observation) => void,
|
|
109
119
|
) {
|
|
110
120
|
const order = ["read", "grep", "find", "ls"];
|
|
111
121
|
return createScopedTools({
|
|
@@ -117,6 +127,7 @@ function wrapScopedTools(
|
|
|
117
127
|
),
|
|
118
128
|
}
|
|
119
129
|
: {}),
|
|
130
|
+
...(observer ? { observer } : {}),
|
|
120
131
|
})
|
|
121
132
|
.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name))
|
|
122
133
|
.map((scoped) =>
|
|
@@ -175,6 +186,20 @@ function finalDeliveryContent(message: AssistantMessage | undefined) {
|
|
|
175
186
|
);
|
|
176
187
|
}
|
|
177
188
|
|
|
189
|
+
function hasStructuralSubmit(message: AssistantMessage | undefined) {
|
|
190
|
+
return finalDeliveryContent(message).some(
|
|
191
|
+
(content) =>
|
|
192
|
+
content.type === "toolCall" && content.name === "abel_submit_result",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function structuralSubmitCount(message: AssistantMessage | undefined) {
|
|
197
|
+
return finalDeliveryContent(message).filter(
|
|
198
|
+
(content) =>
|
|
199
|
+
content.type === "toolCall" && content.name === "abel_submit_result",
|
|
200
|
+
).length;
|
|
201
|
+
}
|
|
202
|
+
|
|
178
203
|
function isSingleStructuralSubmit(message: AssistantMessage | undefined) {
|
|
179
204
|
const content = finalDeliveryContent(message);
|
|
180
205
|
return (
|
|
@@ -251,12 +276,13 @@ function withSubmitDetails(
|
|
|
251
276
|
export type ChildSessionResult =
|
|
252
277
|
| {
|
|
253
278
|
ok: true;
|
|
254
|
-
result: EvidenceResult | DiffResult;
|
|
279
|
+
result: EvidenceResult | DiffResult | CandidateArtifactSubmissionResult;
|
|
255
280
|
toolNames: string[];
|
|
256
281
|
submitCount: number;
|
|
257
282
|
disposeCount: number;
|
|
258
283
|
usage: Usage;
|
|
259
284
|
classification: SubmitClassification;
|
|
285
|
+
observations?: { observations: Observation[]; truncated: boolean };
|
|
260
286
|
}
|
|
261
287
|
| {
|
|
262
288
|
ok: false;
|
|
@@ -267,6 +293,7 @@ export type ChildSessionResult =
|
|
|
267
293
|
disposeCount: number;
|
|
268
294
|
usage: Usage;
|
|
269
295
|
classification: SubmitClassification;
|
|
296
|
+
observations?: { observations: Observation[]; truncated: boolean };
|
|
270
297
|
};
|
|
271
298
|
|
|
272
299
|
export async function runChildSession(input: {
|
|
@@ -284,19 +311,49 @@ export async function runChildSession(input: {
|
|
|
284
311
|
timeoutMs: number;
|
|
285
312
|
signal?: AbortSignal;
|
|
286
313
|
failureOverride?: () => ChildFailure | undefined;
|
|
314
|
+
ledgerProjection?: unknown;
|
|
315
|
+
captureObservations?: boolean;
|
|
316
|
+
candidateArtifact?: CandidateArtifactSubmission;
|
|
317
|
+
onStreamStart?: () => void;
|
|
318
|
+
onStreamProgress?: () => void;
|
|
287
319
|
}): Promise<ChildSessionResult> {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
const segmentedCandidate = input.candidateArtifact !== undefined;
|
|
321
|
+
const submit = input.candidateArtifact
|
|
322
|
+
? createCandidateArtifactTool(input.candidateArtifact)
|
|
323
|
+
: createSubmitTool({
|
|
324
|
+
requestId: input.requestId,
|
|
325
|
+
taskId: input.taskId,
|
|
326
|
+
role: input.role,
|
|
327
|
+
phase: input.phase ?? "red",
|
|
328
|
+
output: input.output,
|
|
329
|
+
});
|
|
330
|
+
const observationCollector = input.captureObservations
|
|
331
|
+
? new ScopedObservationCollector()
|
|
332
|
+
: undefined;
|
|
333
|
+
const readTools = wrapScopedTools(
|
|
334
|
+
input.roots,
|
|
335
|
+
input.cwd,
|
|
336
|
+
input.allowedPaths,
|
|
337
|
+
observationCollector?.observe,
|
|
338
|
+
);
|
|
296
339
|
const customTools = [...readTools, submit.tool];
|
|
297
340
|
const toolNames = customTools.map((tool) => tool.name);
|
|
298
341
|
let disposeCount = 0;
|
|
299
342
|
const usage = new UsageAggregator();
|
|
343
|
+
const effectivePrompt =
|
|
344
|
+
input.ledgerProjection === undefined
|
|
345
|
+
? input.systemPrompt
|
|
346
|
+
: [
|
|
347
|
+
input.systemPrompt,
|
|
348
|
+
"",
|
|
349
|
+
"<abel-task-ledger-projection>",
|
|
350
|
+
serializeTaskLedgerProjection(input.ledgerProjection),
|
|
351
|
+
"</abel-task-ledger-projection>",
|
|
352
|
+
].join("\n");
|
|
353
|
+
const observationMetadata = () =>
|
|
354
|
+
observationCollector
|
|
355
|
+
? { observations: observationCollector.projection() }
|
|
356
|
+
: {};
|
|
300
357
|
const abort = new AbortController();
|
|
301
358
|
const forwardCancellation = () =>
|
|
302
359
|
abort.abort(input.signal?.reason ?? new Error("child phase cancelled"));
|
|
@@ -316,6 +373,24 @@ export async function runChildSession(input: {
|
|
|
316
373
|
let unsubscribe: (() => void) | undefined;
|
|
317
374
|
let abortSettlement: Promise<void> | undefined;
|
|
318
375
|
let assistantSequence = 0;
|
|
376
|
+
let streamStarted = false;
|
|
377
|
+
const notifyStreamStart = () => {
|
|
378
|
+
if (streamStarted) return;
|
|
379
|
+
streamStarted = true;
|
|
380
|
+
try {
|
|
381
|
+
input.onStreamStart?.();
|
|
382
|
+
} catch {
|
|
383
|
+
// Stream observation cannot influence child execution.
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const notifyStreamProgress = () => {
|
|
387
|
+
notifyStreamStart();
|
|
388
|
+
try {
|
|
389
|
+
input.onStreamProgress?.();
|
|
390
|
+
} catch {
|
|
391
|
+
// Stream observation cannot influence child execution.
|
|
392
|
+
}
|
|
393
|
+
};
|
|
319
394
|
const disposeOnce = () => {
|
|
320
395
|
if (session) {
|
|
321
396
|
session.dispose();
|
|
@@ -332,7 +407,9 @@ export async function runChildSession(input: {
|
|
|
332
407
|
finalCategory = "no-final-assistant";
|
|
333
408
|
} else if (isSingleStructuralSubmit(last)) {
|
|
334
409
|
finalCategory =
|
|
335
|
-
submit.getAttempts() > 1
|
|
410
|
+
!segmentedCandidate && submit.getAttempts() > 1
|
|
411
|
+
? "multiple-submit"
|
|
412
|
+
: "single-submit-only";
|
|
336
413
|
} else {
|
|
337
414
|
const content = finalDeliveryContent(last);
|
|
338
415
|
finalCategory =
|
|
@@ -383,8 +460,11 @@ export async function runChildSession(input: {
|
|
|
383
460
|
}
|
|
384
461
|
return undefined;
|
|
385
462
|
};
|
|
386
|
-
const noStructuralSubmit = (): ChildFailure =>
|
|
387
|
-
|
|
463
|
+
const noStructuralSubmit = (): ChildFailure => {
|
|
464
|
+
const final = session?.messages
|
|
465
|
+
.filter((message) => message.role === "assistant")
|
|
466
|
+
.at(-1);
|
|
467
|
+
return submit.getAttempts() === 0 && !hasStructuralSubmit(final)
|
|
388
468
|
? {
|
|
389
469
|
kind: "artifact",
|
|
390
470
|
code: "child-no-structural-submit",
|
|
@@ -395,6 +475,7 @@ export async function runChildSession(input: {
|
|
|
395
475
|
code: "invalid-structural-result",
|
|
396
476
|
stage: "structural-submit",
|
|
397
477
|
};
|
|
478
|
+
};
|
|
398
479
|
try {
|
|
399
480
|
abort.signal.throwIfAborted();
|
|
400
481
|
const creation = createAgentSession({
|
|
@@ -404,7 +485,7 @@ export async function runChildSession(input: {
|
|
|
404
485
|
thinkingLevel: "off",
|
|
405
486
|
tools: toolNames,
|
|
406
487
|
customTools,
|
|
407
|
-
resourceLoader: new EmptyResourceLoader(
|
|
488
|
+
resourceLoader: new EmptyResourceLoader(effectivePrompt),
|
|
408
489
|
sessionManager: SessionManager.inMemory(input.cwd),
|
|
409
490
|
settingsManager: SettingsManager.inMemory({
|
|
410
491
|
compaction: { enabled: false },
|
|
@@ -419,7 +500,33 @@ export async function runChildSession(input: {
|
|
|
419
500
|
.catch(() => undefined);
|
|
420
501
|
throw error;
|
|
421
502
|
}
|
|
503
|
+
const childAgent = session.agent;
|
|
504
|
+
if (childAgent) {
|
|
505
|
+
const previousShouldStopAfterTurn = childAgent.shouldStopAfterTurn;
|
|
506
|
+
childAgent.shouldStopAfterTurn = async (turn, signal) => {
|
|
507
|
+
if (
|
|
508
|
+
segmentedCandidate
|
|
509
|
+
? submit.getResult() !== undefined
|
|
510
|
+
: hasStructuralSubmit(turn.message)
|
|
511
|
+
) {
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
return (await previousShouldStopAfterTurn?.(turn, signal)) ?? false;
|
|
515
|
+
};
|
|
516
|
+
}
|
|
422
517
|
unsubscribe = session.subscribe((event) => {
|
|
518
|
+
if (
|
|
519
|
+
event.type === "message_start" &&
|
|
520
|
+
event.message.role === "assistant"
|
|
521
|
+
) {
|
|
522
|
+
notifyStreamStart();
|
|
523
|
+
}
|
|
524
|
+
if (
|
|
525
|
+
event.type === "message_update" &&
|
|
526
|
+
event.message.role === "assistant"
|
|
527
|
+
) {
|
|
528
|
+
notifyStreamProgress();
|
|
529
|
+
}
|
|
423
530
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
424
531
|
usage.add(`assistant:${assistantSequence++}`, event.message.usage);
|
|
425
532
|
}
|
|
@@ -438,7 +545,7 @@ export async function runChildSession(input: {
|
|
|
438
545
|
throw abort.signal.reason;
|
|
439
546
|
}
|
|
440
547
|
await Promise.race([
|
|
441
|
-
session.prompt(
|
|
548
|
+
session.prompt(effectivePrompt, { expandPromptTemplates: false }),
|
|
442
549
|
new Promise<never>((_, reject) =>
|
|
443
550
|
abort.signal.addEventListener(
|
|
444
551
|
"abort",
|
|
@@ -453,7 +560,11 @@ export async function runChildSession(input: {
|
|
|
453
560
|
const result = submit.getResult();
|
|
454
561
|
const attempts = submit.getAttempts();
|
|
455
562
|
const classification = classifySession();
|
|
456
|
-
|
|
563
|
+
const segmentedComplete =
|
|
564
|
+
segmentedCandidate &&
|
|
565
|
+
result !== undefined &&
|
|
566
|
+
(result.kind === "sealed-candidate" || result.kind === "context-request");
|
|
567
|
+
if (!result || (segmentedCandidate ? !segmentedComplete : attempts !== 1)) {
|
|
457
568
|
const failure = withSubmitDetails(
|
|
458
569
|
submit.getFailure() ??
|
|
459
570
|
input.failureOverride?.() ??
|
|
@@ -472,11 +583,16 @@ export async function runChildSession(input: {
|
|
|
472
583
|
disposeCount,
|
|
473
584
|
usage: usage.total(),
|
|
474
585
|
classification,
|
|
586
|
+
...observationMetadata(),
|
|
475
587
|
};
|
|
476
588
|
}
|
|
477
589
|
const assistants = session.messages.filter((m) => m.role === "assistant");
|
|
478
590
|
const final = assistants.at(-1);
|
|
479
|
-
if (
|
|
591
|
+
if (
|
|
592
|
+
!(segmentedCandidate
|
|
593
|
+
? segmentedComplete && structuralSubmitCount(final) === 1
|
|
594
|
+
: isSingleStructuralSubmit(final))
|
|
595
|
+
) {
|
|
480
596
|
const failure = {
|
|
481
597
|
kind: "artifact",
|
|
482
598
|
code: "invalid-structural-result",
|
|
@@ -493,6 +609,7 @@ export async function runChildSession(input: {
|
|
|
493
609
|
disposeCount,
|
|
494
610
|
usage: usage.total(),
|
|
495
611
|
classification,
|
|
612
|
+
...observationMetadata(),
|
|
496
613
|
};
|
|
497
614
|
}
|
|
498
615
|
disposeOnce();
|
|
@@ -504,6 +621,7 @@ export async function runChildSession(input: {
|
|
|
504
621
|
disposeCount,
|
|
505
622
|
usage: usage.total(),
|
|
506
623
|
classification,
|
|
624
|
+
...observationMetadata(),
|
|
507
625
|
};
|
|
508
626
|
} catch (error) {
|
|
509
627
|
if (abort.signal.aborted && session) {
|
|
@@ -553,6 +671,7 @@ export async function runChildSession(input: {
|
|
|
553
671
|
disposeCount,
|
|
554
672
|
usage: usage.total(),
|
|
555
673
|
classification,
|
|
674
|
+
...observationMetadata(),
|
|
556
675
|
};
|
|
557
676
|
} finally {
|
|
558
677
|
clearTimeout(timer);
|