stitchkit 0.65.1 → 0.66.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/dist/agent-runtime/coordinator.d.ts +8 -5
- package/dist/agent-runtime/coordinator.d.ts.map +1 -1
- package/dist/agent-runtime/events.d.ts +36 -30
- package/dist/agent-runtime/events.d.ts.map +1 -1
- package/dist/agent-runtime/injection.d.ts +28 -0
- package/dist/agent-runtime/injection.d.ts.map +1 -0
- package/dist/agent-runtime/observability.d.ts +22 -20
- package/dist/agent-runtime/observability.d.ts.map +1 -1
- package/dist/agent-runtime/prompt.d.ts +11 -1
- package/dist/agent-runtime/prompt.d.ts.map +1 -1
- package/dist/agent-runtime/run-execution.d.ts +11 -0
- package/dist/agent-runtime/run-execution.d.ts.map +1 -1
- package/dist/agent-runtime/runtime-internals.d.ts.map +1 -1
- package/dist/agent-runtime/runtime.d.ts.map +1 -1
- package/dist/agent-runtime/schemas.d.ts +75 -21
- package/dist/agent-runtime/schemas.d.ts.map +1 -1
- package/dist/agent-runtime/store-driver.d.ts +21 -15
- package/dist/agent-runtime/store-driver.d.ts.map +1 -1
- package/dist/agent-runtime/store.d.ts +280 -46
- package/dist/agent-runtime/store.d.ts.map +1 -1
- package/dist/agent-runtime/terminal-commit.d.ts +33 -3
- package/dist/agent-runtime/terminal-commit.d.ts.map +1 -1
- package/dist/agent-runtime/terminal-status.d.ts.map +1 -1
- package/dist/agent-runtime-openrouter.js +1 -1
- package/dist/agent-runtime.d.ts +1 -1
- package/dist/agent-runtime.d.ts.map +1 -1
- package/dist/agent-runtime.js +384 -97
- package/dist/{index-sbkyvacf.js → index-fhsmrzj7.js} +45 -5
- package/dist/testing/agent-store-conformance.d.ts.map +1 -1
- package/dist/testing.js +149 -1
- package/llms-full.txt +251 -18
- package/package.json +1 -1
|
@@ -114,6 +114,7 @@ var AgentTerminalReasonSchema = z.enum([
|
|
|
114
114
|
"shutdown",
|
|
115
115
|
"provider_failure",
|
|
116
116
|
"context_overflow",
|
|
117
|
+
"absorbed",
|
|
117
118
|
"abandoned"
|
|
118
119
|
]);
|
|
119
120
|
function runStateForTerminalReason(reason) {
|
|
@@ -122,7 +123,7 @@ function runStateForTerminalReason(reason) {
|
|
|
122
123
|
}
|
|
123
124
|
if (reason === "interrupted")
|
|
124
125
|
return "interrupted";
|
|
125
|
-
if (reason === "superseded")
|
|
126
|
+
if (reason === "superseded" || reason === "absorbed")
|
|
126
127
|
return "superseded";
|
|
127
128
|
if (reason === "cancelled" || reason === "shutdown" || reason === "timeout") {
|
|
128
129
|
return "cancelled";
|
|
@@ -131,14 +132,31 @@ function runStateForTerminalReason(reason) {
|
|
|
131
132
|
return "abandoned";
|
|
132
133
|
return "failed";
|
|
133
134
|
}
|
|
135
|
+
var AgentProvenanceSchema = z.enum([
|
|
136
|
+
"provider-reported",
|
|
137
|
+
"measured",
|
|
138
|
+
"computed",
|
|
139
|
+
"estimated",
|
|
140
|
+
"unavailable"
|
|
141
|
+
]);
|
|
134
142
|
var AgentUsageValueSchema = z.object({
|
|
135
|
-
value: z.
|
|
136
|
-
provenance:
|
|
143
|
+
value: z.int().nonnegative().optional(),
|
|
144
|
+
provenance: AgentProvenanceSchema.extract([
|
|
145
|
+
"provider-reported",
|
|
146
|
+
"computed",
|
|
147
|
+
"estimated",
|
|
148
|
+
"unavailable"
|
|
149
|
+
])
|
|
137
150
|
});
|
|
138
151
|
var AgentCostValueSchema = z.object({
|
|
139
152
|
value: z.number().nonnegative().optional(),
|
|
140
153
|
currency: z.string().length(3).optional(),
|
|
141
|
-
provenance:
|
|
154
|
+
provenance: AgentProvenanceSchema.extract([
|
|
155
|
+
"provider-reported",
|
|
156
|
+
"computed",
|
|
157
|
+
"estimated",
|
|
158
|
+
"unavailable"
|
|
159
|
+
])
|
|
142
160
|
});
|
|
143
161
|
var AgentUsageSchema = z.object({
|
|
144
162
|
inputTokens: AgentUsageValueSchema,
|
|
@@ -160,11 +178,19 @@ var AgentRunFieldsSchema = z.object({
|
|
|
160
178
|
fencingToken: AgentRecordVersionSchema.optional(),
|
|
161
179
|
terminalReason: AgentTerminalReasonSchema.optional(),
|
|
162
180
|
terminalPolicyName: z.string().min(1).optional(),
|
|
181
|
+
absorbedIntoRunId: AgentRecordIdSchema.optional(),
|
|
163
182
|
usage: AgentUsageSchema.optional(),
|
|
164
183
|
createdAt: AgentTimestampSchema,
|
|
165
184
|
updatedAt: AgentTimestampSchema
|
|
166
185
|
});
|
|
167
186
|
var AgentRunSchema = AgentRunFieldsSchema.superRefine((run, ctx) => {
|
|
187
|
+
if (run.terminalReason !== "absorbed" && run.absorbedIntoRunId !== undefined) {
|
|
188
|
+
ctx.addIssue({
|
|
189
|
+
code: "custom",
|
|
190
|
+
path: ["absorbedIntoRunId"],
|
|
191
|
+
message: "Only an absorbed run names the run that answered it"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
168
194
|
if (run.terminalReason === undefined) {
|
|
169
195
|
if (TERMINAL_RUN_STATES.has(run.state)) {
|
|
170
196
|
ctx.addIssue({
|
|
@@ -190,6 +216,20 @@ var AgentRunSchema = AgentRunFieldsSchema.superRefine((run, ctx) => {
|
|
|
190
216
|
message: "A policy stop names the policy that stopped the run"
|
|
191
217
|
});
|
|
192
218
|
}
|
|
219
|
+
if (run.terminalReason === "absorbed" && run.absorbedIntoRunId === undefined) {
|
|
220
|
+
ctx.addIssue({
|
|
221
|
+
code: "custom",
|
|
222
|
+
path: ["absorbedIntoRunId"],
|
|
223
|
+
message: "An absorbed run names the run that answered its input"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (run.absorbedIntoRunId === run.id) {
|
|
227
|
+
ctx.addIssue({
|
|
228
|
+
code: "custom",
|
|
229
|
+
path: ["absorbedIntoRunId"],
|
|
230
|
+
message: "A run cannot absorb itself"
|
|
231
|
+
});
|
|
232
|
+
}
|
|
193
233
|
});
|
|
194
234
|
var TERMINAL_RUN_STATES = new Set([
|
|
195
235
|
"completed",
|
|
@@ -213,4 +253,4 @@ var AgentRunMetricsSchema = z.object({
|
|
|
213
253
|
ttftMs: z.number().nonnegative().optional()
|
|
214
254
|
});
|
|
215
255
|
|
|
216
|
-
export { AgentRecordIdSchema, AgentRecordVersionSchema, AgentTimestampSchema, AgentJsonObjectSchema, AgentProviderEnvelopeSchema, AgentTextPartSchema, AgentReasoningPartSchema, AgentFilePartSchema, AgentSourcePartSchema, AgentToolCallPartSchema, AgentToolResultPartSchema, AgentOpaquePartSchema, AgentControlPartSchema, AgentMessagePartSchema, AgentMessageRoleSchema, AgentMessageStatusSchema, AgentMessageSchema, AgentAssistantPlaceholderSchema, AgentRunStateSchema, AgentTerminalReasonSchema, runStateForTerminalReason, AgentUsageValueSchema, AgentCostValueSchema, AgentUsageSchema, AgentRunSchema, AgentSnapshotSchema, AgentRunMetricsSchema };
|
|
256
|
+
export { AgentRecordIdSchema, AgentRecordVersionSchema, AgentTimestampSchema, AgentJsonObjectSchema, AgentProviderEnvelopeSchema, AgentTextPartSchema, AgentReasoningPartSchema, AgentFilePartSchema, AgentSourcePartSchema, AgentToolCallPartSchema, AgentToolResultPartSchema, AgentOpaquePartSchema, AgentControlPartSchema, AgentMessagePartSchema, AgentMessageRoleSchema, AgentMessageStatusSchema, AgentMessageSchema, AgentAssistantPlaceholderSchema, AgentRunStateSchema, AgentTerminalReasonSchema, runStateForTerminalReason, AgentProvenanceSchema, AgentUsageValueSchema, AgentCostValueSchema, AgentUsageSchema, AgentRunSchema, AgentSnapshotSchema, AgentRunMetricsSchema };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-store-conformance.d.ts","sourceRoot":"","sources":["../../src/testing/agent-store-conformance.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,MAAM,WAAW,2BAA2B;IAC1C,WAAW,IAAI,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/D;AAwCD,gFAAgF;AAChF,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"agent-store-conformance.d.ts","sourceRoot":"","sources":["../../src/testing/agent-store-conformance.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,MAAM,WAAW,2BAA2B;IAC1C,WAAW,IAAI,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/D;AAwCD,gFAAgF;AAChF,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,IAAI,CAAC,CAobf"}
|
package/dist/testing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AgentMessageSchema,
|
|
3
3
|
AgentRunSchema
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-fhsmrzj7.js";
|
|
5
5
|
import {
|
|
6
6
|
createApplication
|
|
7
7
|
} from "./index-eabpd4tb.js";
|
|
@@ -352,6 +352,50 @@ async function runAgentStoreConformance(config) {
|
|
|
352
352
|
if (foreignOwner.outcome !== "conflict") {
|
|
353
353
|
throw new Error("A checkpoint from another owner must conflict");
|
|
354
354
|
}
|
|
355
|
+
const liveView = await store.loadRun({ conversationId, runId: running.id });
|
|
356
|
+
if (!liveView)
|
|
357
|
+
throw new Error("loadRun must find a run of this conversation");
|
|
358
|
+
if (liveView.run.id !== running.id || liveView.run.conversationId !== conversationId) {
|
|
359
|
+
throw new Error("loadRun returned a run it was not asked for");
|
|
360
|
+
}
|
|
361
|
+
if (liveView.run.revision !== checkpointedRun.revision) {
|
|
362
|
+
throw new Error("loadRun must return the run as the last mutation left it");
|
|
363
|
+
}
|
|
364
|
+
if (liveView.run.usage?.cost?.value !== 0.25) {
|
|
365
|
+
throw new Error("loadRun must carry the figure the run has spent so far");
|
|
366
|
+
}
|
|
367
|
+
if (liveView.snapshotVersion !== checkpoint.snapshot.version) {
|
|
368
|
+
throw new Error("loadRun must report the conversation version it read at");
|
|
369
|
+
}
|
|
370
|
+
if (liveView.assistant !== undefined) {
|
|
371
|
+
throw new Error("loadRun must not report a terminal answer for a live run");
|
|
372
|
+
}
|
|
373
|
+
if (await store.loadRun({ conversationId, runId: "no-such-run" })) {
|
|
374
|
+
throw new Error("loadRun must return undefined for an unknown run");
|
|
375
|
+
}
|
|
376
|
+
if (await store.loadRun({ conversationId: "no-such-conversation", runId: running.id })) {
|
|
377
|
+
throw new Error("loadRun must not cross conversation boundaries");
|
|
378
|
+
}
|
|
379
|
+
const activeRuns = await store.listActiveRuns(conversationId);
|
|
380
|
+
if (!activeRuns.some((run) => run.id === running.id)) {
|
|
381
|
+
throw new Error("listActiveRuns must report a run that is in flight");
|
|
382
|
+
}
|
|
383
|
+
if (activeRuns.some((run) => run.terminalReason !== undefined)) {
|
|
384
|
+
throw new Error("listActiveRuns must not report a run that has ended");
|
|
385
|
+
}
|
|
386
|
+
for (let index = 1;index < activeRuns.length; index += 1) {
|
|
387
|
+
const previous = activeRuns[index - 1];
|
|
388
|
+
const current = activeRuns[index];
|
|
389
|
+
if (!previous || !current)
|
|
390
|
+
continue;
|
|
391
|
+
const ordered = previous.createdAt === current.createdAt ? previous.id.localeCompare(current.id) < 0 : previous.createdAt < current.createdAt;
|
|
392
|
+
if (!ordered) {
|
|
393
|
+
throw new Error("listActiveRuns must order by createdAt and then by id");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if ((await store.listActiveRuns("no-such-conversation")).length !== 0) {
|
|
397
|
+
throw new Error("listActiveRuns must be empty for an unknown conversation");
|
|
398
|
+
}
|
|
355
399
|
const recoverable = await store.scanRecoverable({ limit: 10 });
|
|
356
400
|
if (!recoverable.items.some((item) => item.run.id === running.id)) {
|
|
357
401
|
throw new Error("A running run must appear in a recoverable scan");
|
|
@@ -409,6 +453,16 @@ async function runAgentStoreConformance(config) {
|
|
|
409
453
|
if (settledRun?.usage?.cost?.value !== 1.5) {
|
|
410
454
|
throw new Error("Terminal commit did not persist the run usage it was given");
|
|
411
455
|
}
|
|
456
|
+
const terminalView = await store.loadRun({ conversationId, runId: running.id });
|
|
457
|
+
if (terminalView?.run.terminalReason !== "success") {
|
|
458
|
+
throw new Error("loadRun must report the terminal reason a settled run ended with");
|
|
459
|
+
}
|
|
460
|
+
if (JSON.stringify(terminalView.assistant) !== JSON.stringify(terminalAssistant)) {
|
|
461
|
+
throw new Error("loadRun must retain the answer a settled run produced");
|
|
462
|
+
}
|
|
463
|
+
if ((await store.listActiveRuns(conversationId)).some((run) => run.id === running.id)) {
|
|
464
|
+
throw new Error("listActiveRuns must drop a run once it has ended");
|
|
465
|
+
}
|
|
412
466
|
const compactedTerminal = await store.replaceCompactedRange({
|
|
413
467
|
conversationId,
|
|
414
468
|
expectedVersion: terminalApplied.snapshot.version,
|
|
@@ -468,6 +522,100 @@ async function runAgentStoreConformance(config) {
|
|
|
468
522
|
if (terminalRun?.state !== "abandoned" || terminalMessage?.status !== "failed") {
|
|
469
523
|
throw new Error("Abandon recovery did not atomically terminalize its assistant record");
|
|
470
524
|
}
|
|
525
|
+
await assertAbsorptionIsAtomic(store);
|
|
526
|
+
}
|
|
527
|
+
async function assertAbsorptionIsAtomic(store) {
|
|
528
|
+
const conversationId = `conformance-absorb-${crypto.randomUUID()}`;
|
|
529
|
+
const leadInput = userMessage(conversationId, "absorb-input-1");
|
|
530
|
+
const leadRun = queuedRun(conversationId, leadInput.id, "absorb-run-1");
|
|
531
|
+
const lead = await store.acceptInputAndAssignRun({
|
|
532
|
+
idempotencyKey: "absorb-request-1",
|
|
533
|
+
input: leadInput,
|
|
534
|
+
run: leadRun
|
|
535
|
+
});
|
|
536
|
+
requireOutcome(lead, "applied");
|
|
537
|
+
const successorInput = userMessage(conversationId, "absorb-input-2");
|
|
538
|
+
const successorRun = queuedRun(conversationId, successorInput.id, "absorb-run-2");
|
|
539
|
+
const successor = await store.acceptInputAndAssignRun({
|
|
540
|
+
idempotencyKey: "absorb-request-2",
|
|
541
|
+
input: successorInput,
|
|
542
|
+
run: successorRun
|
|
543
|
+
});
|
|
544
|
+
requireOutcome(successor, "applied");
|
|
545
|
+
const acquired = await store.acquireRun({
|
|
546
|
+
conversationId,
|
|
547
|
+
runId: leadRun.id,
|
|
548
|
+
expectedRevision: 0,
|
|
549
|
+
ownerId: "absorb-owner"
|
|
550
|
+
});
|
|
551
|
+
requireOutcome(acquired, "applied");
|
|
552
|
+
const running = acquired.snapshot.runs.find((run) => run.id === leadRun.id);
|
|
553
|
+
if (!running)
|
|
554
|
+
throw new Error("Absorbing run disappeared after acquisition");
|
|
555
|
+
const answer = AgentMessageSchema.parse({
|
|
556
|
+
schemaVersion: 1,
|
|
557
|
+
id: running.assistantMessageId,
|
|
558
|
+
conversationId,
|
|
559
|
+
runId: running.id,
|
|
560
|
+
role: "assistant",
|
|
561
|
+
status: "completed",
|
|
562
|
+
parts: [{ type: "text", text: "answered both" }],
|
|
563
|
+
createdAt: "2026-08-26T00:00:00.000Z",
|
|
564
|
+
updatedAt: "2026-08-26T00:00:02.000Z"
|
|
565
|
+
});
|
|
566
|
+
await store.commitRunTerminal({
|
|
567
|
+
conversationId,
|
|
568
|
+
runId: running.id,
|
|
569
|
+
expectedRevision: running.revision,
|
|
570
|
+
ownerId: "absorb-owner",
|
|
571
|
+
assistant: AgentMessageSchema.parse({ ...answer, status: "interrupted" }),
|
|
572
|
+
reason: "interrupted",
|
|
573
|
+
absorb: [{ runId: successorRun.id, inputMessageIds: [successorInput.id] }]
|
|
574
|
+
}).then(() => {
|
|
575
|
+
throw new Error("A non-completing run absorbed a queued successor");
|
|
576
|
+
}, (error) => {
|
|
577
|
+
if (!(error instanceof TypeError))
|
|
578
|
+
throw error;
|
|
579
|
+
});
|
|
580
|
+
const committed = await store.commitRunTerminal({
|
|
581
|
+
conversationId,
|
|
582
|
+
runId: running.id,
|
|
583
|
+
expectedRevision: running.revision,
|
|
584
|
+
ownerId: "absorb-owner",
|
|
585
|
+
assistant: answer,
|
|
586
|
+
reason: "success",
|
|
587
|
+
absorb: [{ runId: successorRun.id, inputMessageIds: [successorInput.id] }]
|
|
588
|
+
});
|
|
589
|
+
requireOutcome(committed, "applied");
|
|
590
|
+
const absorbingView = await store.loadRun({ conversationId, runId: leadRun.id });
|
|
591
|
+
if (absorbingView?.run.inputMessageIds.join(",") !== "absorb-input-1,absorb-input-2") {
|
|
592
|
+
throw new Error("An absorbing run must record the inputs it answered");
|
|
593
|
+
}
|
|
594
|
+
const absorbedView = await store.loadRun({ conversationId, runId: successorRun.id });
|
|
595
|
+
if (absorbedView?.run.terminalReason !== "absorbed") {
|
|
596
|
+
throw new Error("An absorbed successor must be terminal in the same transaction");
|
|
597
|
+
}
|
|
598
|
+
if (absorbedView.run.absorbedIntoRunId !== leadRun.id) {
|
|
599
|
+
throw new Error("An absorbed run must name the run that answered its input");
|
|
600
|
+
}
|
|
601
|
+
if (absorbedView.assistant !== undefined) {
|
|
602
|
+
throw new Error("An absorbed run produced no answer and must retain none");
|
|
603
|
+
}
|
|
604
|
+
if ((await store.listActiveRuns(conversationId)).length !== 0) {
|
|
605
|
+
throw new Error("An absorbed successor must leave the active listing");
|
|
606
|
+
}
|
|
607
|
+
const retried = await store.acceptInputAndAssignRun({
|
|
608
|
+
idempotencyKey: "absorb-request-2",
|
|
609
|
+
input: userMessage(conversationId, "absorb-discarded"),
|
|
610
|
+
run: queuedRun(conversationId, "absorb-discarded", "absorb-discarded-run")
|
|
611
|
+
});
|
|
612
|
+
requireOutcome(retried, "duplicate");
|
|
613
|
+
if (retried.runId !== leadRun.id || retried.assistant?.id !== answer.id) {
|
|
614
|
+
throw new Error("A retried absorbed key must resolve to the run that answered it");
|
|
615
|
+
}
|
|
616
|
+
if (retried.inputMessageId !== successorInput.id) {
|
|
617
|
+
throw new Error("A retried absorbed key must still name its own input");
|
|
618
|
+
}
|
|
471
619
|
}
|
|
472
620
|
// src/testing/managed-resource-conformance-contract.ts
|
|
473
621
|
import { z } from "zod";
|
package/llms-full.txt
CHANGED
|
@@ -58,7 +58,7 @@ own, recorded as an ADR.
|
|
|
58
58
|
| `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
|
|
59
59
|
| `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
|
|
60
60
|
| `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
|
|
61
|
-
| `stitchkit/agent-runtime` | server | evolving<br>_redefined in
|
|
61
|
+
| `stitchkit/agent-runtime` | server | evolving<br>_redefined in 9 of the 11 minors since 0.56.2, most recently 0.66.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
|
|
62
62
|
| `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
|
|
63
63
|
| `stitchkit/application` | server | evolving | managed resource graph, readiness, admission, schedules and bounded shutdown |
|
|
64
64
|
| `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
|
|
@@ -3683,15 +3683,14 @@ input + queued run → running → execution settled → terminal CAS → succes
|
|
|
3683
3683
|
|
|
3684
3684
|
## What happens to a run when new input arrives
|
|
3685
3685
|
|
|
3686
|
-
`runs.inputPolicy` decides. It takes
|
|
3687
|
-
the fourth row is a behaviour that shipped and was withdrawn:
|
|
3686
|
+
`runs.inputPolicy` decides. It takes four values, or a function returning one:
|
|
3688
3687
|
|
|
3689
3688
|
| policy | the run in flight | what it already produced |
|
|
3690
3689
|
|--------|-------------------|--------------------------|
|
|
3691
3690
|
| `queue` (default) | finishes first | kept |
|
|
3691
|
+
| `inject` | continues, and answers the new input too | kept, and built on |
|
|
3692
3692
|
| `interrupt` | ends | kept, and marked as cut off |
|
|
3693
3693
|
| `supersede` | ends | discarded from the prompt, kept in the record |
|
|
3694
|
-
| _inject_ | continues | withdrawn in 0.65.0 — see below |
|
|
3695
3694
|
|
|
3696
3695
|
`interrupt` and `supersede` differ in exactly one thing, and the question that
|
|
3697
3696
|
picks between them is **not** "was the run interrupted" but **"did anyone see
|
|
@@ -3775,19 +3774,53 @@ const { decisions } = await projectAgentHistoryDetailed(snapshot.messages)
|
|
|
3775
3774
|
process-local escape hatch chooses the reason, so a caller that knows the answer
|
|
3776
3775
|
was never delivered can discard it without a newer input arriving.
|
|
3777
3776
|
|
|
3778
|
-
###
|
|
3777
|
+
### An input that joins a run in flight
|
|
3778
|
+
|
|
3779
|
+
`inject` is right when the new input **refines** rather than redirects, and the
|
|
3780
|
+
steps already taken are still worth something: *"summarise this thread… actually,
|
|
3781
|
+
in bullet points"* should not throw away the reading the first message paid for.
|
|
3782
|
+
|
|
3783
|
+
What happens:
|
|
3784
|
+
|
|
3785
|
+
1. The input is admitted exactly like any other — a committed user message and a
|
|
3786
|
+
**queued run**, durable before anything else happens.
|
|
3787
|
+
2. At the running loop's next step boundary, that input joins its prompt. Only
|
|
3788
|
+
that input: an unrelated queued submission is never carried in.
|
|
3789
|
+
3. When the run finishes, its terminal commit — **one transaction** — records
|
|
3790
|
+
the input as one the run answered and settles the queued successor with
|
|
3791
|
+
`terminalReason: 'absorbed'` and `absorbedIntoRunId` naming the run that
|
|
3792
|
+
answered it. Every ticket for that successor resolves to the same answer.
|
|
3793
|
+
|
|
3794
|
+
Nothing durable happens between 1 and 3, and that is the design (→ ADR 0113).
|
|
3795
|
+
A run that crashes, is closed, or is interrupted after taking an input on
|
|
3796
|
+
commits no absorption at all, so what is left behind is an ordinary queued
|
|
3797
|
+
successor — the state every other policy already produces and recovery already
|
|
3798
|
+
handles. **There is no ordering in which an accepted input becomes
|
|
3799
|
+
unanswerable.** Only a run that *completes* may absorb; an interrupted one took
|
|
3800
|
+
the input into its prompt and then stopped, and does not get to say it answered
|
|
3801
|
+
it.
|
|
3802
|
+
|
|
3803
|
+
An absorbed run has **no assistant message of its own** — it produced none, and
|
|
3804
|
+
writing an empty one would be a record claiming otherwise. Its answer is
|
|
3805
|
+
reachable through `absorbedIntoRunId`, and the store follows that pointer itself
|
|
3806
|
+
when a submission arrives on the absorbed run's idempotency key, so a retry
|
|
3807
|
+
after a restart returns the answer rather than an empty terminal record.
|
|
3779
3808
|
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
downstream of that ordering was wrong: an accepted input could end up in a state
|
|
3784
|
-
that was neither active, recoverable nor terminal, so `close()` could report
|
|
3785
|
-
`settled: true` while leaving it permanently unanswerable, and a duplicate
|
|
3786
|
-
submission of the same idempotency key was refused forever.
|
|
3809
|
+
It does publish one more `run-state` event, carrying `'superseded'`. It never
|
|
3810
|
+
enters the run executor, so that event is the only thing that tells a delivery
|
|
3811
|
+
surface following its `runId` that it is no longer queued.
|
|
3787
3812
|
|
|
3788
|
-
|
|
3789
|
-
absorption
|
|
3790
|
-
|
|
3813
|
+
With `coalescePending`, one successor can carry several inputs, and an
|
|
3814
|
+
absorption covers a successor **whole or not at all** — a partial one would
|
|
3815
|
+
leave a terminal run with inputs nobody answered. Inputs that coalesce before
|
|
3816
|
+
the absorbing run's last step boundary join the same absorption. One that
|
|
3817
|
+
arrives after it cancels the absorption: the successor then answers all of its
|
|
3818
|
+
inputs itself, and the absorbing run has answered one of them too. A duplicate
|
|
3819
|
+
answer, never a missing one.
|
|
3820
|
+
|
|
3821
|
+
(0.63.0 shipped a version of this that committed the absorption at the step
|
|
3822
|
+
boundary, before the answer existed, and it was withdrawn in 0.65.0. ADR 0113
|
|
3823
|
+
records what that ordering broke.)
|
|
3791
3824
|
|
|
3792
3825
|
With `runs.coalescePending: true`, an active lane has at most one queued
|
|
3793
3826
|
successor. Every later accepted input is atomically appended to that successor;
|
|
@@ -3850,7 +3883,7 @@ problem, and omitting a run's cost is an operator's.
|
|
|
3850
3883
|
## Store operations
|
|
3851
3884
|
|
|
3852
3885
|
`AgentRuntimeStore` remains the runtime-facing aggregate. Application adapters
|
|
3853
|
-
implement the smaller `AgentRuntimeStoreDriver` rather than these
|
|
3886
|
+
implement the smaller `AgentRuntimeStoreDriver` rather than these eleven members:
|
|
3854
3887
|
|
|
3855
3888
|
- `acceptInputAndAssignRun`
|
|
3856
3889
|
- `acquireRun`
|
|
@@ -3859,11 +3892,37 @@ implement the smaller `AgentRuntimeStoreDriver` rather than these nine members:
|
|
|
3859
3892
|
- `recoverRun`
|
|
3860
3893
|
- `commitRunTerminal`
|
|
3861
3894
|
- `replaceCompactedRange`
|
|
3862
|
-
- `loadSnapshot`
|
|
3895
|
+
- `loadSnapshot` — the whole conversation; see **Reading a conversation** below
|
|
3896
|
+
- `loadRun` — one run by id, with the answer it produced if it has ended
|
|
3897
|
+
- `listActiveRuns` — the runs of one conversation that have not ended
|
|
3863
3898
|
- `scanRecoverable` — one **bounded page** of recoverable runs; `recover()`
|
|
3864
3899
|
calls this and nothing else, so an adapter that implements the interface has
|
|
3865
3900
|
everything recovery needs
|
|
3866
3901
|
|
|
3902
|
+
### Reading a conversation
|
|
3903
|
+
|
|
3904
|
+
Two shapes of read, and the difference matters as a conversation grows.
|
|
3905
|
+
|
|
3906
|
+
**Bounded.** `loadRun` and `listActiveRuns` read run records and the
|
|
3907
|
+
conversation head. Neither touches history, so neither grows with the length of
|
|
3908
|
+
the conversation. `loadRun` is how you resolve the `runId` that
|
|
3909
|
+
`submit().admission` hands back — it returns the run, the version it was read
|
|
3910
|
+
at, and, once the run is terminal, the answer it produced.
|
|
3911
|
+
|
|
3912
|
+
**Unbounded.** `loadSnapshot` returns every message and every run, and so does
|
|
3913
|
+
every mutation result: the store's reducer validates its invariants against the
|
|
3914
|
+
whole conversation, and the runtime builds the next prompt from the snapshot the
|
|
3915
|
+
mutation returns. Ask for it when you need the conversation — composing a
|
|
3916
|
+
prompt, or compacting — and not to look one run up.
|
|
3917
|
+
|
|
3918
|
+
So the cost of a run scales with the length of its conversation, not with the
|
|
3919
|
+
length of the turn. **Configure compaction** (see below) for anything
|
|
3920
|
+
long-running: it is the only thing in the framework that makes a conversation
|
|
3921
|
+
smaller, and without it a year-old assistant thread is read in full on every
|
|
3922
|
+
turn. This is a known limit, held deliberately rather than by omission — paged
|
|
3923
|
+
history would have to change what a snapshot *is*, and the store's invariants
|
|
3924
|
+
with it, and that is a decision on its own (→ ADR 0112).
|
|
3925
|
+
|
|
3867
3926
|
Every mutation carries an expected run revision or snapshot version. Input
|
|
3868
3927
|
assignment additionally carries an idempotency identity. A conflict is a
|
|
3869
3928
|
control outcome; stale data is never silently overwritten.
|
|
@@ -7844,6 +7903,149 @@ implement `AgentRuntimeStoreDriver` and compose the aggregate with
|
|
|
7844
7903
|
runtime): bootstrap the server, one HTTP request, and any feature you rely on
|
|
7845
7904
|
(Socket.IO connect, an MCP tool call, a multipart upload, …).
|
|
7846
7905
|
|
|
7906
|
+
## Released migration: 0.66.0
|
|
7907
|
+
|
|
7908
|
+
Three changes, and only one of them is a feature. The other two are shapes that
|
|
7909
|
+
would have had to break later: a vocabulary that described one fact with two
|
|
7910
|
+
words, and a store whose only read was the whole conversation.
|
|
7911
|
+
|
|
7912
|
+
The compiler points at most of it. Two things change with no compile error —
|
|
7913
|
+
what a **total** says about its own provenance, and the fact that token counts
|
|
7914
|
+
are now validated where they were not.
|
|
7915
|
+
|
|
7916
|
+
### An input that joins a run in flight
|
|
7917
|
+
|
|
7918
|
+
#### If you matched exhaustively on `AgentTerminalReason`
|
|
7919
|
+
|
|
7920
|
+
`'absorbed'` is new. A run ends this way when a run already in flight took its
|
|
7921
|
+
input on and answered it; its state is `'superseded'`, and `absorbedIntoRunId`
|
|
7922
|
+
names the run that has the answer.
|
|
7923
|
+
|
|
7924
|
+
```ts
|
|
7925
|
+
// after
|
|
7926
|
+
switch (run.terminalReason) {
|
|
7927
|
+
// …
|
|
7928
|
+
case 'absorbed':
|
|
7929
|
+
// no assistant message of its own — follow run.absorbedIntoRunId
|
|
7930
|
+
break
|
|
7931
|
+
}
|
|
7932
|
+
```
|
|
7933
|
+
|
|
7934
|
+
A run record with `terminalReason: 'absorbed'` and no `absorbedIntoRunId` is
|
|
7935
|
+
refused at parse time, and so is `absorbedIntoRunId` on any other reason.
|
|
7936
|
+
|
|
7937
|
+
#### If you render or export runs
|
|
7938
|
+
|
|
7939
|
+
An absorbed run has **no assistant message**. Anything that assumes "every
|
|
7940
|
+
terminal run has one" needs the `absorbed` case — the answer is on the run
|
|
7941
|
+
`absorbedIntoRunId` names, and `store.loadRun` resolves it.
|
|
7942
|
+
|
|
7943
|
+
#### If you want the policy
|
|
7944
|
+
|
|
7945
|
+
```ts
|
|
7946
|
+
// after
|
|
7947
|
+
runs: { inputPolicy: 'inject' }
|
|
7948
|
+
```
|
|
7949
|
+
|
|
7950
|
+
It was withdrawn in 0.65.0 and is back with the ordering corrected: the
|
|
7951
|
+
absorption commits with the terminal record, not at the step boundary. Read
|
|
7952
|
+
*An input that joins a run in flight* in the agent-runtime guide before turning
|
|
7953
|
+
it on — in particular what happens when the absorbing run does not complete, and
|
|
7954
|
+
how it composes with `coalescePending`.
|
|
7955
|
+
|
|
7956
|
+
#### If you implement a store driver
|
|
7957
|
+
|
|
7958
|
+
Nothing to do, but know what changed underneath: one terminal commit can now
|
|
7959
|
+
save **two** run records, and they must land in the same transaction. A driver
|
|
7960
|
+
that persists one of the pair fails `runAgentStoreConformance`.
|
|
7961
|
+
|
|
7962
|
+
### A run read without its conversation
|
|
7963
|
+
|
|
7964
|
+
#### If you implement `AgentRuntimeStore` by hand
|
|
7965
|
+
|
|
7966
|
+
Two members to add. **If your adapter is an `AgentRuntimeStoreDriver` passed to
|
|
7967
|
+
`createAgentRuntimeStore`, there is nothing to do** — the driver already had
|
|
7968
|
+
everything both need.
|
|
7969
|
+
|
|
7970
|
+
```ts
|
|
7971
|
+
// after
|
|
7972
|
+
loadRun(input: { conversationId: string; runId: string }): Promise<AgentRunView | undefined>
|
|
7973
|
+
listActiveRuns(conversationId: string): Promise<readonly AgentRun[]>
|
|
7974
|
+
```
|
|
7975
|
+
|
|
7976
|
+
`AgentRunView` is `{ snapshotVersion, run, assistant? }`. `assistant` is the
|
|
7977
|
+
retained terminal answer, so it is present exactly when the run has ended and
|
|
7978
|
+
absent while it is live — a store that returns a live run's draft here lets the
|
|
7979
|
+
terminal path resolve a run that has not finished. `listActiveRuns` orders by
|
|
7980
|
+
`createdAt` then `id`, and must not report a run that has ended.
|
|
7981
|
+
`runAgentStoreConformance` covers both, including the boundary cases.
|
|
7982
|
+
|
|
7983
|
+
#### If you have a store **double** in your tests
|
|
7984
|
+
|
|
7985
|
+
The runtime now reads `loadRun` where it used to read `loadSnapshot`. A double
|
|
7986
|
+
that simulates a condition — a run drifting to another owner, a stale fencing
|
|
7987
|
+
token — must apply it to both reads, or the code under test will not see it.
|
|
7988
|
+
This is not hypothetical: it turned one of this repository's own fixtures into
|
|
7989
|
+
an infinite retry loop, which is how the bounded retry below was found.
|
|
7990
|
+
|
|
7991
|
+
#### Nothing else changes
|
|
7992
|
+
|
|
7993
|
+
`loadSnapshot` behaves exactly as before, and still returns the whole
|
|
7994
|
+
conversation — as does every mutation result. What it costs, and what bounds it,
|
|
7995
|
+
is now written down in *Reading a conversation* in the agent-runtime guide.
|
|
7996
|
+
|
|
7997
|
+
### One provenance vocabulary, and integral tokens
|
|
7998
|
+
|
|
7999
|
+
#### If you match on `'measured'`
|
|
8000
|
+
|
|
8001
|
+
A **total** now says `computed`, because it is a sum this code performed rather
|
|
8002
|
+
than a count it took — the same rule `AgentUsage` has always applied to a run's
|
|
8003
|
+
spend. Two values change with no compile error:
|
|
8004
|
+
|
|
8005
|
+
```ts
|
|
8006
|
+
// before
|
|
8007
|
+
if (result.totalTokens.provenance === 'measured') { /* exact */ }
|
|
8008
|
+
// after
|
|
8009
|
+
if (result.totalTokens.provenance === 'computed') { /* exact, and derived */ }
|
|
8010
|
+
```
|
|
8011
|
+
|
|
8012
|
+
It affects `AgentHistoryBudgetResult.totalTokens` and
|
|
8013
|
+
`ComposedAgentPrompt.instructionTokens`. A per-message or per-section count is
|
|
8014
|
+
still `measured` — only the totals moved. When any part was estimated the total
|
|
8015
|
+
is still `estimated`: an estimate survives arithmetic, and that is the weaker
|
|
8016
|
+
claim, so it wins.
|
|
8017
|
+
|
|
8018
|
+
#### If you produce token counts
|
|
8019
|
+
|
|
8020
|
+
They are validated now, in the places they were not. A fractional count throws
|
|
8021
|
+
where it used to flow into the context-window arithmetic:
|
|
8022
|
+
|
|
8023
|
+
```ts
|
|
8024
|
+
// refused from this release on
|
|
8025
|
+
estimateTokens: (text) => ({ value: text.length / 4, provenance: 'estimated' })
|
|
8026
|
+
// after
|
|
8027
|
+
estimateTokens: (text) => ({ value: Math.ceil(text.length / 4), provenance: 'estimated' })
|
|
8028
|
+
```
|
|
8029
|
+
|
|
8030
|
+
The same applies to `ComposeAgentPromptOptions.estimateFallback` and
|
|
8031
|
+
`.historyTokens`, to `AgentPromptBudget.toolSchemas` / `.attachments` /
|
|
8032
|
+
`.providerOverhead`, and to `AgentPromptBudget.contextWindow` /
|
|
8033
|
+
`.reservedOutput`, which must now be non-negative safe integers.
|
|
8034
|
+
|
|
8035
|
+
`AgentUsageValue.value` is `z.int()` too, which matters if you build usage
|
|
8036
|
+
records by hand or in a store double. A figure arriving from a **provider** is
|
|
8037
|
+
not thrown — `normalizeSdkUsage` and the OpenRouter adapter turn a non-integer
|
|
8038
|
+
into `{ provenance: 'unavailable' }`, so a run that already answered is not
|
|
8039
|
+
failed over its own bookkeeping.
|
|
8040
|
+
|
|
8041
|
+
#### The new export
|
|
8042
|
+
|
|
8043
|
+
`AgentProvenanceSchema` / `AgentProvenance` is the whole vocabulary:
|
|
8044
|
+
`provider-reported`, `measured`, `computed`, `estimated`, `unavailable`. Each
|
|
8045
|
+
surface declares its subset, so nothing widened — `AgentUsageValue` still refuses
|
|
8046
|
+
`measured` and `AgentTokenCount` still refuses `provider-reported`. Use it when
|
|
8047
|
+
you want one switch over the question instead of two.
|
|
8048
|
+
|
|
7847
8049
|
## Released migration: 0.65.0
|
|
7848
8050
|
|
|
7849
8051
|
The largest migration of the pre-1.0 line, and most of it is the compiler
|
|
@@ -10511,10 +10713,41 @@ Canonical protocol exports are `AgentProtocol`, `AgentProtocolConfig`, `AgentRec
|
|
|
10511
10713
|
`AgentUsageValueSchema`, `AgentCostValueSchema`, `AgentUsageSchema`, `AgentUsage` and
|
|
10512
10714
|
`AgentRunMetrics`.
|
|
10513
10715
|
|
|
10716
|
+
`AgentProvenanceSchema` / `AgentProvenance` is the entrypoint's single vocabulary for **how a
|
|
10717
|
+
number came to be known**: `provider-reported` (the provider stated it about a request it served),
|
|
10718
|
+
`measured` (this process counted it exactly, before any request was made), `computed` (arithmetic
|
|
10719
|
+
over other values — a sum of exact numbers is still `computed`), `estimated` (a heuristic) and
|
|
10720
|
+
`unavailable` (not known, so `value` is absent, which is a different fact from a reported zero).
|
|
10721
|
+
Each surface declares the subset it can produce: `AgentUsageValueSchema` and `AgentCostValueSchema`
|
|
10722
|
+
describe a request that has already happened and never say `measured`; `AgentTokenCountSchema`
|
|
10723
|
+
describes a prompt being composed and never says `provider-reported`. Every token count is an
|
|
10724
|
+
integer — `AgentUsageValueSchema` and `AgentTokenCountSchema` refuse a fractional `value`, and a
|
|
10725
|
+
provider figure that is not a whole number is normalised to `unavailable` rather than thrown.
|
|
10726
|
+
`AgentCostValueSchema.value` stays fractional, because money is.
|
|
10727
|
+
|
|
10728
|
+
`runs.inputPolicy` takes `queue` (default), `inject`, `interrupt` or `supersede`, or a function of
|
|
10729
|
+
the raw input returning one. `inject` lets a run in flight take a newly arrived input into its prompt
|
|
10730
|
+
at a step boundary and answer it too; the absorption is committed in the **same transaction** as that
|
|
10731
|
+
run's terminal record, via `CommitRunTerminal.absorb`, so a run that ends any other way leaves an
|
|
10732
|
+
ordinary queued successor. The absorbed run ends with `terminalReason: 'absorbed'`, run state
|
|
10733
|
+
`'superseded'`, `absorbedIntoRunId` naming the run that answered it, and **no assistant message of
|
|
10734
|
+
its own**; a submission on its idempotency key resolves through that pointer to the answer
|
|
10735
|
+
(→ ADR 0113).
|
|
10736
|
+
|
|
10737
|
+
`AgentRuntimeStore` has two **bounded** reads beside `loadSnapshot`:
|
|
10738
|
+
`loadRun({ conversationId, runId })` returns an `AgentRunView` — the run, the conversation version it
|
|
10739
|
+
was read at, and the retained answer once the run is terminal — or `undefined`; `listActiveRuns(conversationId)`
|
|
10740
|
+
returns the runs that have not ended, ordered by `createdAt` then `id`. Neither reads history, so
|
|
10741
|
+
neither grows with the length of the conversation, and neither needs anything new from
|
|
10742
|
+
`AgentRuntimeStoreDriver`. `loadSnapshot` and every mutation result still carry the whole
|
|
10743
|
+
conversation — that is what the store's reducer validates against, and what the runtime builds a
|
|
10744
|
+
prompt from (→ ADR 0112).
|
|
10745
|
+
|
|
10514
10746
|
Store command/result exports are `AcceptInputAndAssignRun`, `AcceptInputAndAssignRunSchema`,
|
|
10515
10747
|
`AcquireAgentRun`, `AcquireAgentRunSchema`, `CheckpointRunAssistant`,
|
|
10516
10748
|
`CheckpointRunAssistantSchema`, `CommitRunTerminal`, `CommitRunTerminalSchema`,
|
|
10517
|
-
`RequestRunInterrupt`, `RequestRunInterruptSchema`, `
|
|
10749
|
+
`RequestRunInterrupt`, `RequestRunInterruptSchema`, `AgentRunView`, `AgentRunViewSchema`,
|
|
10750
|
+
`runStateForTerminalReason`,
|
|
10518
10751
|
`ACTIVE_AGENT_RUN_STATES`, `RecoverAgentRun`, `ReplaceCompactedRange`,
|
|
10519
10752
|
`ReplaceCompactedRangeSchema`, `AgentStoreMutationResult`, `AgentStoreMutationResultSchema`,
|
|
10520
10753
|
`AgentStoreAppliedSchema`, `AgentStoreConflictSchema`, `AgentStoreDuplicateSchema`,
|
package/package.json
CHANGED