stitchkit 0.84.0 → 0.85.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/agent-runtime/control-schema.d.ts +61 -0
  3. package/dist/agent-runtime/control-schema.d.ts.map +1 -1
  4. package/dist/agent-runtime/deferred-tool-search.d.ts.map +1 -1
  5. package/dist/agent-runtime/deferred-tool-selection.d.ts +17 -0
  6. package/dist/agent-runtime/deferred-tool-selection.d.ts.map +1 -1
  7. package/dist/agent-runtime/deferred-tool-types.d.ts.map +1 -1
  8. package/dist/agent-runtime/event-schema.d.ts +88 -1
  9. package/dist/agent-runtime/event-schema.d.ts.map +1 -1
  10. package/dist/agent-runtime/run-execution.d.ts.map +1 -1
  11. package/dist/agent-runtime/run-operation-lifecycle.d.ts +21 -0
  12. package/dist/agent-runtime/run-operation-lifecycle.d.ts.map +1 -0
  13. package/dist/agent-runtime/schemas.d.ts +67 -0
  14. package/dist/agent-runtime/schemas.d.ts.map +1 -1
  15. package/dist/agent-runtime/sqlite.d.ts.map +1 -1
  16. package/dist/agent-runtime/store-driver.d.ts +26 -1
  17. package/dist/agent-runtime/store-driver.d.ts.map +1 -1
  18. package/dist/agent-runtime/store.d.ts +208 -0
  19. package/dist/agent-runtime/store.d.ts.map +1 -1
  20. package/dist/agent-runtime/terminal-commit.d.ts +9 -0
  21. package/dist/agent-runtime/terminal-commit.d.ts.map +1 -1
  22. package/dist/agent-runtime-browser.js +11 -3
  23. package/dist/agent-runtime-harness.js +4 -4
  24. package/dist/agent-runtime-sqlite-bun.js +3 -3
  25. package/dist/agent-runtime-sqlite-node.js +3 -3
  26. package/dist/agent-runtime.d.ts +2 -2
  27. package/dist/agent-runtime.d.ts.map +1 -1
  28. package/dist/agent-runtime.js +32 -6
  29. package/dist/{index-85vfqd7m.js → index-4hk633vz.js} +11 -3
  30. package/dist/{index-cksjz4eg.js → index-66nxrgy0.js} +17 -16
  31. package/dist/{index-st8v8739.js → index-9merfjxf.js} +141 -14
  32. package/dist/{index-5s3zajp8.js → index-ff0kcqvp.js} +9 -3
  33. package/dist/{index-19eet1qx.js → index-fq492hg0.js} +31 -5
  34. package/dist/{index-x1th9s8c.js → index-x15ss2dx.js} +50 -1
  35. package/dist/testing.js +26 -2
  36. package/dist/tools/async-operation.d.ts +18 -1
  37. package/dist/tools/async-operation.d.ts.map +1 -1
  38. package/dist/tools.js +3 -1
  39. package/llms-full.txt +69 -8
  40. package/package.json +1 -1
@@ -5,11 +5,12 @@ import {
5
5
  AgentRecordIdSchema,
6
6
  AgentRecordVersionSchema,
7
7
  AgentRunMetricsSchema,
8
+ AgentRunOperationSchema,
8
9
  AgentRunSchema,
9
10
  AgentRunStateSchema,
10
11
  AgentTerminalReasonSchema,
11
12
  AgentTimestampSchema
12
- } from "./index-x1th9s8c.js";
13
+ } from "./index-x15ss2dx.js";
13
14
 
14
15
  // src/agent-runtime/event-schema.ts
15
16
  import { z } from "zod";
@@ -63,6 +64,12 @@ var AgentRunStateEventSchema = EventIdentitySchema.extend({
63
64
  snapshotVersion: AgentRecordVersionSchema,
64
65
  state: AgentRunStateSchema
65
66
  });
67
+ var AgentRunOperationEventSchema = EventIdentitySchema.extend({
68
+ type: z.literal("run-operation"),
69
+ eventId: AgentRecordIdSchema,
70
+ snapshotVersion: AgentRecordVersionSchema,
71
+ operation: AgentRunOperationSchema
72
+ });
66
73
  var AgentToolStatusEventSchema = EventIdentitySchema.extend({
67
74
  type: z.literal("tool-status"),
68
75
  runtimeEpoch: z.string().min(1),
@@ -90,6 +97,7 @@ var AgentRuntimeEventSchema = z.discriminatedUnion("type", [
90
97
  AgentReasoningEndEventSchema,
91
98
  AgentCheckpointEventSchema,
92
99
  AgentRunStateEventSchema,
100
+ AgentRunOperationEventSchema,
93
101
  AgentToolStatusEventSchema,
94
102
  AgentTerminalEventSchema
95
103
  ]);
@@ -100,7 +108,7 @@ var AgentRuntimeEventCursorSchema = z.object({
100
108
  sequence: z.int().nonnegative().optional()
101
109
  });
102
110
  function isDurableEvent(event) {
103
- return event.type === "admission" || event.type === "assistant-checkpoint" || event.type === "run-state" || event.type === "terminal";
111
+ return event.type === "admission" || event.type === "assistant-checkpoint" || event.type === "run-state" || event.type === "run-operation" || event.type === "terminal";
104
112
  }
105
113
  function advanceAgentRuntimeEventCursor(rawCursor, event) {
106
114
  const cursor = AgentRuntimeEventCursorSchema.parse(rawCursor);
@@ -132,4 +140,4 @@ function agentDurableEventId(type, runId, snapshotVersion) {
132
140
  return `${runId}:${type}:${snapshotVersion}`;
133
141
  }
134
142
 
135
- export { AgentAdmissionEventSchema, AgentTransientDeltaEventSchema, AgentReasoningStartEventSchema, AgentReasoningDeltaEventSchema, AgentReasoningEndEventSchema, AgentCheckpointEventSchema, AgentRunStateEventSchema, AgentToolStatusEventSchema, AgentTerminalEventSchema, AgentRuntimeEventSchema, AgentRuntimeEventCursorSchema, advanceAgentRuntimeEventCursor, agentDurableEventId };
143
+ export { AgentAdmissionEventSchema, AgentTransientDeltaEventSchema, AgentReasoningStartEventSchema, AgentReasoningDeltaEventSchema, AgentReasoningEndEventSchema, AgentCheckpointEventSchema, AgentRunStateEventSchema, AgentRunOperationEventSchema, AgentToolStatusEventSchema, AgentTerminalEventSchema, AgentRuntimeEventSchema, AgentRuntimeEventCursorSchema, advanceAgentRuntimeEventCursor, agentDurableEventId };
@@ -8,11 +8,11 @@ import {
8
8
  AgentRuntimeHeadSchema,
9
9
  AgentStoredRunSchema,
10
10
  createAgentRuntimeStore
11
- } from "./index-19eet1qx.js";
11
+ } from "./index-fq492hg0.js";
12
12
  import {
13
13
  AgentMessageSchema,
14
14
  AgentRunSchema
15
- } from "./index-x1th9s8c.js";
15
+ } from "./index-x15ss2dx.js";
16
16
 
17
17
  // src/agent-runtime/sqlite.ts
18
18
  import { z } from "zod";
@@ -244,19 +244,20 @@ function createSqliteAgentRuntimeStore(config) {
244
244
  });
245
245
  return result;
246
246
  };
247
+ const runTransaction = (access, work) => serial(async () => {
248
+ database.exec(access === "read" ? "BEGIN" : "BEGIN IMMEDIATE");
249
+ try {
250
+ const result = await work(database);
251
+ database.exec("COMMIT");
252
+ return result;
253
+ } catch (error) {
254
+ database.exec("ROLLBACK");
255
+ throw error;
256
+ }
257
+ });
247
258
  const driver = {
248
259
  conversations: sqliteConversationPurge(database),
249
- transaction: (work) => serial(async () => {
250
- database.exec("BEGIN IMMEDIATE");
251
- try {
252
- const result = await work(database);
253
- database.exec("COMMIT");
254
- return result;
255
- } catch (error) {
256
- database.exec("ROLLBACK");
257
- throw error;
258
- }
259
- }),
260
+ transaction: (work, options) => runTransaction(options?.access ?? "write", work),
260
261
  head: {
261
262
  async load(transaction, conversationId) {
262
263
  const value = transaction.prepare("SELECT version FROM stitchkit_agent_runtime_heads WHERE conversation_id = ?").get(conversationId);
@@ -437,7 +438,7 @@ function createSqliteAgentRuntimeStore(config) {
437
438
  `).run(message.conversationId, message.id, position, encodeJson(message));
438
439
  }
439
440
  },
440
- scanRecoverable: (input) => serial(async () => {
441
+ scanRecoverable: (input) => runTransaction("read", async () => {
441
442
  const cursor = input.cursor ? parseRecoveryCursor(input.cursor) : undefined;
442
443
  const values = cursor ? [cursor[0], cursor[0], cursor[1], input.limit + 1] : [input.limit + 1];
443
444
  const rows = database.prepare(`
@@ -466,7 +467,7 @@ function createSqliteAgentRuntimeStore(config) {
466
467
  return {
467
468
  store: createAgentRuntimeStore(driver),
468
469
  conversations: {
469
- list: (input) => serial(async () => {
470
+ list: (input) => runTransaction("read", async () => {
470
471
  if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 1000) {
471
472
  throw new TypeError("Conversation page limit must be between 1 and 1000");
472
473
  }
@@ -517,7 +518,7 @@ function createSqliteAgentRuntimeStore(config) {
517
518
  ...hasMore && last ? { nextCursor: conversationCursor(last.conversation_id) } : {}
518
519
  });
519
520
  }),
520
- messages: (input) => serial(async () => {
521
+ messages: (input) => runTransaction("read", async () => {
521
522
  if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 1000) {
522
523
  throw new TypeError("Conversation message page limit must be between 1 and 1000");
523
524
  }
@@ -12,17 +12,18 @@ import {
12
12
  import {
13
13
  AgentRuntimeEventSchema,
14
14
  agentDurableEventId
15
- } from "./index-85vfqd7m.js";
15
+ } from "./index-4hk633vz.js";
16
16
  import {
17
17
  AgentAssistantPlaceholderSchema,
18
18
  AgentJsonObjectSchema,
19
19
  AgentMessagePartSchema,
20
20
  AgentMessageSchema,
21
21
  AgentProvenanceSchema,
22
+ AgentRunOperationSchema,
22
23
  AgentRunSchema,
23
24
  AgentSnapshotSchema,
24
25
  runStateForTerminalReason
25
- } from "./index-x1th9s8c.js";
26
+ } from "./index-x15ss2dx.js";
26
27
  import {
27
28
  isAgentToolError
28
29
  } from "./index-3xnq72rz.js";
@@ -1074,6 +1075,73 @@ function createIdleDeadline(parent, timeoutMs) {
1074
1075
  };
1075
1076
  }
1076
1077
 
1078
+ // src/agent-runtime/run-operation-lifecycle.ts
1079
+ function createAgentRunOperationLifecycle(config) {
1080
+ let active;
1081
+ const record = async (operation) => {
1082
+ const current = config.currentRun();
1083
+ const snapshot = appliedSnapshot(await config.store.recordRunOperation({
1084
+ conversationId: current.conversationId,
1085
+ runId: current.id,
1086
+ expectedRevision: current.revision,
1087
+ ownerId: config.runtimeEpoch,
1088
+ ...current.fencingToken !== undefined && { fencingToken: current.fencingToken },
1089
+ operation
1090
+ }), "run operation");
1091
+ config.acceptSnapshot(snapshot);
1092
+ const run = findRun(snapshot.runs, current.id);
1093
+ active = operation;
1094
+ await config.publish({
1095
+ type: "run-operation",
1096
+ eventId: agentDurableEventId("run-operation", run.id, snapshot.version),
1097
+ conversationId: run.conversationId,
1098
+ runId: run.id,
1099
+ snapshotVersion: snapshot.version,
1100
+ operation,
1101
+ emittedAt: config.now().toISOString()
1102
+ });
1103
+ };
1104
+ return {
1105
+ startCompaction(operationId) {
1106
+ return record(AgentRunOperationSchema.parse({
1107
+ operationId,
1108
+ kind: "compaction",
1109
+ phase: "started",
1110
+ startedAt: config.now().toISOString()
1111
+ }));
1112
+ },
1113
+ startModelRequest(providerCallId, step) {
1114
+ return record(AgentRunOperationSchema.parse({
1115
+ operationId: `${providerCallId}:${step}`,
1116
+ kind: "model-request",
1117
+ phase: "started",
1118
+ step,
1119
+ startedAt: config.now().toISOString()
1120
+ }));
1121
+ },
1122
+ firstOutput() {
1123
+ if (active?.kind !== "model-request" || active.phase !== "started") {
1124
+ return Promise.resolve();
1125
+ }
1126
+ return record(AgentRunOperationSchema.parse({
1127
+ ...active,
1128
+ phase: "first-output",
1129
+ firstOutputAt: config.now().toISOString()
1130
+ }));
1131
+ },
1132
+ finish(phase) {
1133
+ if (!active || ["completed", "failed", "cancelled"].includes(active.phase)) {
1134
+ return Promise.resolve();
1135
+ }
1136
+ return record(AgentRunOperationSchema.parse({
1137
+ ...active,
1138
+ phase,
1139
+ finishedAt: config.now().toISOString()
1140
+ }));
1141
+ }
1142
+ };
1143
+ }
1144
+
1077
1145
  // src/agent-runtime/run-execution.ts
1078
1146
  function snapshotForRunPrompt(snapshot, runId) {
1079
1147
  const runIndex = snapshot.runs.findIndex((candidate) => candidate.id === runId);
@@ -1184,7 +1252,7 @@ function createRunExecutor(dependencies) {
1184
1252
  let observedVersion = snapshot.version;
1185
1253
  const parts = [];
1186
1254
  const absorbed = new Map;
1187
- let eventCount = 0;
1255
+ let eventsSinceCheckpoint = 0;
1188
1256
  let sequence = 0;
1189
1257
  let terminalReason = "success";
1190
1258
  let nonModelUsage = input.acceptedRun.usage;
@@ -1199,6 +1267,18 @@ function createRunExecutor(dependencies) {
1199
1267
  let terminalPolicyName;
1200
1268
  const idleDeadline = createIdleDeadline(input.signal, idleTimeoutMs);
1201
1269
  const executionSignal = idleDeadline.signal;
1270
+ const operationLifecycle = createAgentRunOperationLifecycle({
1271
+ store: config.store,
1272
+ runtimeEpoch,
1273
+ currentRun: () => run,
1274
+ acceptSnapshot: (next) => {
1275
+ snapshot = next;
1276
+ observedVersion = next.version;
1277
+ run = findRun(next.runs, run.id);
1278
+ },
1279
+ publish,
1280
+ now
1281
+ });
1202
1282
  const updateReasoning = (text, metadata) => {
1203
1283
  const provider = providerEnvelope(metadata);
1204
1284
  if (reasoningPartIndex === undefined) {
@@ -1256,14 +1336,30 @@ function createRunExecutor(dependencies) {
1256
1336
  };
1257
1337
  try {
1258
1338
  if (config.history?.compact) {
1259
- const compacted = await config.history.compact({
1260
- conversationId: run.conversationId,
1261
- store: config.store,
1262
- signal: executionSignal
1263
- });
1264
- snapshot = compacted.snapshot;
1265
- observedVersion = snapshot.version;
1266
- run = findRun(snapshot.runs, run.id);
1339
+ await operationLifecycle.startCompaction(generateId());
1340
+ let compacted;
1341
+ try {
1342
+ compacted = await config.history.compact({
1343
+ conversationId: run.conversationId,
1344
+ store: config.store,
1345
+ signal: executionSignal
1346
+ });
1347
+ snapshot = compacted.snapshot;
1348
+ observedVersion = snapshot.version;
1349
+ run = findRun(snapshot.runs, run.id);
1350
+ await operationLifecycle.finish("completed");
1351
+ } catch (error) {
1352
+ const latest = await config.store.loadRun({
1353
+ conversationId: run.conversationId,
1354
+ runId: run.id
1355
+ });
1356
+ if (latest?.run.ownerId === runtimeEpoch) {
1357
+ observedVersion = latest.snapshotVersion;
1358
+ run = latest.run;
1359
+ }
1360
+ await operationLifecycle.finish(executionSignal.aborted ? "cancelled" : "failed");
1361
+ throw error;
1362
+ }
1267
1363
  if (compacted.usage) {
1268
1364
  nonModelUsage = addUsage(nonModelUsage, compacted.usage);
1269
1365
  usage = modelUsage ? addUsage(nonModelUsage, modelUsage) : nonModelUsage;
@@ -1418,6 +1514,9 @@ function createRunExecutor(dependencies) {
1418
1514
  abortSignal: executionSignal,
1419
1515
  maxRetries: 0,
1420
1516
  stopWhen: stopConditions,
1517
+ onLanguageModelCallStart: async (event) => {
1518
+ await operationLifecycle.startModelRequest(event.callId, step);
1519
+ },
1421
1520
  repairToolCall: deferredToolRepair(config.loop?.prepareStep),
1422
1521
  ...config.loop?.toolApproval && {
1423
1522
  toolApproval: config.loop.toolApproval,
@@ -1428,6 +1527,13 @@ function createRunExecutor(dependencies) {
1428
1527
  },
1429
1528
  ...(config.loop?.prepareStep || injection) && {
1430
1529
  prepareStep: async (options) => {
1530
+ const previousStep = options.steps.at(-1);
1531
+ if (previousStep) {
1532
+ lastPromptTokens = selectedModel?.normalizeUsage?.({
1533
+ usage: previousStep.usage,
1534
+ providerMetadata: previousStep.providerMetadata
1535
+ })?.inputTokens ?? normalizeSdkUsage(previousStep.usage).inputTokens;
1536
+ }
1431
1537
  const prepared = await config.loop?.prepareStep?.({
1432
1538
  ...options,
1433
1539
  ...runtimeContext
@@ -1442,11 +1548,14 @@ function createRunExecutor(dependencies) {
1442
1548
  });
1443
1549
  for await (const part of result.stream) {
1444
1550
  idleDeadline.touch();
1445
- eventCount += 1;
1551
+ eventsSinceCheckpoint += 1;
1446
1552
  sequence += 1;
1447
- if (firstOutputAt === undefined && ["text-delta", "reasoning-delta", "tool-call", "file", "source"].includes(part.type)) {
1553
+ if (firstOutputAt === undefined && (part.type === "text-delta" && part.text.length > 0 || part.type === "reasoning-delta" && part.text.length > 0 || part.type === "tool-input-delta" && part.delta.length > 0 || part.type === "tool-call")) {
1448
1554
  firstOutputAt = performance.now();
1449
1555
  }
1556
+ if (part.type === "text-delta" && part.text.length > 0 || part.type === "reasoning-delta" && part.text.length > 0 || part.type === "tool-input-delta" && part.delta.length > 0 || part.type === "tool-call") {
1557
+ await operationLifecycle.firstOutput();
1558
+ }
1450
1559
  if (part.type === "text-delta") {
1451
1560
  appendText(parts, part.text);
1452
1561
  await publish({
@@ -1552,6 +1661,7 @@ function createRunExecutor(dependencies) {
1552
1661
  status: "interrupted",
1553
1662
  emittedAt: now().toISOString()
1554
1663
  });
1664
+ await checkpoint();
1555
1665
  throw part.error;
1556
1666
  }
1557
1667
  const output = isAgentToolError(part.error) ? jsonValue(part.error.output) : { message: "Tool execution failed" };
@@ -1661,6 +1771,7 @@ function createRunExecutor(dependencies) {
1661
1771
  terminalReason = part.error instanceof AgentContextOverflowError ? "context_overflow" : "provider_failure";
1662
1772
  internalCause = part.error;
1663
1773
  } else if (part.type === "finish-step") {
1774
+ await operationLifecycle.finish(part.finishReason === "error" ? "failed" : "completed");
1664
1775
  const stepTrace = trace ? config.observe?.rootTrace(trace) : undefined;
1665
1776
  const stepUsage = selectedModel.normalizeUsage?.({
1666
1777
  usage: part.usage,
@@ -1696,9 +1807,21 @@ function createRunExecutor(dependencies) {
1696
1807
  modelUsage = mergeModelTotals(normalizeSdkUsage(part.totalUsage), modelUsage);
1697
1808
  usage = nonModelUsage ? addUsage(nonModelUsage, modelUsage) : modelUsage;
1698
1809
  }
1699
- if (eventCount % checkpointEveryEvents === 0)
1810
+ const structuralBoundary = [
1811
+ "tool-call",
1812
+ "tool-result",
1813
+ "tool-error",
1814
+ "tool-output-denied",
1815
+ "tool-approval-request",
1816
+ "tool-approval-response",
1817
+ "finish-step"
1818
+ ].includes(part.type);
1819
+ if (structuralBoundary || eventsSinceCheckpoint >= checkpointEveryEvents) {
1700
1820
  await checkpoint();
1821
+ eventsSinceCheckpoint = 0;
1822
+ }
1701
1823
  }
1824
+ await operationLifecycle.finish(executionSignal.aborted ? "cancelled" : terminalReason === "provider_failure" ? "failed" : "completed");
1702
1825
  if (terminalPolicyName !== undefined)
1703
1826
  terminalReason = "policy_stop";
1704
1827
  if (executionSignal.aborted)
@@ -1732,6 +1855,10 @@ function createRunExecutor(dependencies) {
1732
1855
  observedVersion = latest.snapshotVersion;
1733
1856
  run = latestRun;
1734
1857
  }
1858
+ const staleOwner = isToolExecutionControlError(error) && error.reason === "stale_run";
1859
+ if (!staleOwner) {
1860
+ await operationLifecycle.finish(executionSignal.aborted || durableInterrupt ? "cancelled" : "failed");
1861
+ }
1735
1862
  if (isToolExecutionControlError(error) || executionSignal.aborted || durableInterrupt) {
1736
1863
  terminalReason = executionSignal.aborted ? abortTerminalReason(executionSignal) : "interrupted";
1737
1864
  parts.push(AgentMessagePartSchema.parse({
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  AgentRuntimeEventSchema
3
- } from "./index-85vfqd7m.js";
3
+ } from "./index-4hk633vz.js";
4
4
  import {
5
5
  AgentMessagePartSchema,
6
6
  AgentSnapshotSchema
7
- } from "./index-x1th9s8c.js";
7
+ } from "./index-x15ss2dx.js";
8
8
 
9
9
  // src/agent-runtime/control-schema.ts
10
10
  import { z } from "zod";
@@ -82,7 +82,13 @@ var AgentMultiSessionCursorSchema = z.object({
82
82
  }).strict())
83
83
  }).strict();
84
84
  function durable(event) {
85
- return ["admission", "assistant-checkpoint", "run-state", "terminal"].includes(event.type);
85
+ return [
86
+ "admission",
87
+ "assistant-checkpoint",
88
+ "run-state",
89
+ "run-operation",
90
+ "terminal"
91
+ ].includes(event.type);
86
92
  }
87
93
  function advanceAgentMultiSessionCursor(raw, event) {
88
94
  const cursor = AgentMultiSessionCursorSchema.parse(raw);
@@ -5,12 +5,13 @@ import {
5
5
  AgentMessageSchema,
6
6
  AgentRecordIdSchema,
7
7
  AgentRecordVersionSchema,
8
+ AgentRunOperationSchema,
8
9
  AgentRunSchema,
9
10
  AgentSnapshotSchema,
10
11
  AgentTerminalReasonSchema,
11
12
  AgentUsageSchema,
12
13
  runStateForTerminalReason
13
- } from "./index-x1th9s8c.js";
14
+ } from "./index-x15ss2dx.js";
14
15
 
15
16
  // src/agent-runtime/conversations.ts
16
17
  import { z } from "zod";
@@ -114,6 +115,14 @@ var CheckpointRunAssistantSchema = z3.object({
114
115
  assistant: AgentMessageSchema,
115
116
  usage: AgentUsageSchema.optional()
116
117
  });
118
+ var RecordRunOperationSchema = z3.object({
119
+ conversationId: AgentRecordIdSchema,
120
+ runId: AgentRecordIdSchema,
121
+ expectedRevision: AgentRecordVersionSchema,
122
+ ownerId: z3.string().min(1),
123
+ fencingToken: AgentRecordVersionSchema.optional(),
124
+ operation: AgentRunOperationSchema
125
+ });
117
126
  var CommitRunTerminalSchema = z3.object({
118
127
  conversationId: AgentRecordIdSchema,
119
128
  runId: AgentRecordIdSchema,
@@ -437,6 +446,19 @@ function reduceStore(current, operation) {
437
446
  historyMutations: [{ type: "upsert-assistant", message: input.assistant }]
438
447
  });
439
448
  }
449
+ if (operation.type === "operation" && run) {
450
+ const input = operation.input;
451
+ if (run.revision !== input.expectedRevision || run.state !== "running" && run.state !== "interrupt_requested" || run.ownerId !== input.ownerId || input.fencingToken !== undefined && run.fencingToken !== input.fencingToken) {
452
+ return conflict(run.revision);
453
+ }
454
+ const next = AgentRunSchema.parse({
455
+ ...run,
456
+ lastOperation: input.operation,
457
+ revision: run.revision + 1,
458
+ updatedAt: new Date().toISOString()
459
+ });
460
+ return applied(current, { runs: replaceRun(current.runs, next) }, { runRecords: [AgentStoredRunSchema.parse({ schemaVersion: 1, run: next })] });
461
+ }
440
462
  if (operation.type === "interrupt" && run) {
441
463
  if (run.revision !== operation.input.expectedRevision || run.state !== "running") {
442
464
  return conflict(run.revision);
@@ -640,7 +662,7 @@ function createAgentRuntimeStore(driver) {
640
662
  runIds: referencedRunIds(messages)
641
663
  });
642
664
  return snapshotOf(head, messages, mergeRunRecords(activeRecords, referencedRecords));
643
- });
665
+ }, { access: "read" });
644
666
  const loadRun = (input) => driver.transaction(async (transaction) => {
645
667
  const [stored, record] = await Promise.all([
646
668
  driver.head.load(transaction, input.conversationId),
@@ -658,7 +680,7 @@ function createAgentRuntimeStore(driver) {
658
680
  run: parsed.run,
659
681
  ...parsed.terminalAssistant && { assistant: parsed.terminalAssistant }
660
682
  });
661
- });
683
+ }, { access: "read" });
662
684
  const listActiveRuns = (conversationId) => driver.transaction(async (transaction) => {
663
685
  const [records, messages] = await Promise.all([
664
686
  driver.runs.listActive(transaction, conversationId),
@@ -674,7 +696,7 @@ function createAgentRuntimeStore(driver) {
674
696
  }
675
697
  }
676
698
  return orderRuns(messages, runs);
677
- });
699
+ }, { access: "read" });
678
700
  const mutate = (operation) => driver.transaction(async (transaction) => {
679
701
  const conversationId = operationConversationId(operation);
680
702
  if (await driver.conversations?.isPurged(transaction, conversationId)) {
@@ -794,6 +816,10 @@ function createAgentRuntimeStore(driver) {
794
816
  type: "checkpoint",
795
817
  input: CheckpointRunAssistantSchema.parse(input)
796
818
  }),
819
+ recordRunOperation: (input) => mutate({
820
+ type: "operation",
821
+ input: RecordRunOperationSchema.parse(input)
822
+ }),
797
823
  requestRunInterrupt: (input) => mutate({
798
824
  type: "interrupt",
799
825
  input: RequestRunInterruptSchema.parse(input)
@@ -988,4 +1014,4 @@ function createMemoryAgentRuntimeStore() {
988
1014
  return createAgentRuntimeStore(driver);
989
1015
  }
990
1016
 
991
- export { AgentConversationSummarySchema, AgentConversationPageSchema, AgentConversationMessagePageSchema, AgentConversationPurgeInputSchema, AgentConversationPurgeResultSchema, AgentConversationPurgedError, purgeAgentConversation, AgentStoreConflictSchema, AgentStoreNotFoundSchema, AgentStoreAppliedSchema, AgentStoreDuplicateSchema, AgentStoreMutationResultSchema, AgentRunViewSchema, AcceptInputAndAssignRunSchema, AcquireAgentRunSchema, CheckpointRunAssistantSchema, CommitRunTerminalSchema, RequestRunInterruptSchema, RecoverAgentRunSchema, ReplaceCompactedRangeSchema, AgentRecoverableDescriptorSchema, AgentRecoverablePageSchema, AgentRuntimeHeadSchema, AgentStoredRunSchema, AgentAdmissionReceiptSchema, AgentHistoryMutationSchema, ACTIVE_AGENT_RUN_STATES, createAgentRuntimeStore, createMemoryAgentRuntimeStore };
1017
+ export { AgentConversationSummarySchema, AgentConversationPageSchema, AgentConversationMessagePageSchema, AgentConversationPurgeInputSchema, AgentConversationPurgeResultSchema, AgentConversationPurgedError, purgeAgentConversation, AgentStoreConflictSchema, AgentStoreNotFoundSchema, AgentStoreAppliedSchema, AgentStoreDuplicateSchema, AgentStoreMutationResultSchema, AgentRunViewSchema, AcceptInputAndAssignRunSchema, AcquireAgentRunSchema, CheckpointRunAssistantSchema, RecordRunOperationSchema, CommitRunTerminalSchema, RequestRunInterruptSchema, RecoverAgentRunSchema, ReplaceCompactedRangeSchema, AgentRecoverableDescriptorSchema, AgentRecoverablePageSchema, AgentRuntimeHeadSchema, AgentStoredRunSchema, AgentAdmissionReceiptSchema, AgentHistoryMutationSchema, ACTIVE_AGENT_RUN_STATES, createAgentRuntimeStore, createMemoryAgentRuntimeStore };
@@ -125,6 +125,54 @@ var AgentRunStateSchema = z.enum([
125
125
  "abandoned"
126
126
  ]);
127
127
  var AgentRunQueuePrioritySchema = z.enum(["interrupt-next"]);
128
+ var AgentRunOperationKindSchema = z.enum(["model-request", "compaction"]);
129
+ var AgentRunOperationPhaseSchema = z.enum([
130
+ "started",
131
+ "first-output",
132
+ "completed",
133
+ "failed",
134
+ "cancelled"
135
+ ]);
136
+ var AgentRunOperationFieldsSchema = z.object({
137
+ operationId: AgentRecordIdSchema,
138
+ kind: AgentRunOperationKindSchema,
139
+ phase: AgentRunOperationPhaseSchema,
140
+ step: z.int().nonnegative().optional(),
141
+ startedAt: AgentTimestampSchema,
142
+ firstOutputAt: AgentTimestampSchema.optional(),
143
+ finishedAt: AgentTimestampSchema.optional()
144
+ });
145
+ var AgentRunOperationSchema = AgentRunOperationFieldsSchema.superRefine((operation, ctx) => {
146
+ if (operation.kind === "model-request" !== (operation.step !== undefined)) {
147
+ ctx.addIssue({
148
+ code: "custom",
149
+ path: ["step"],
150
+ message: "Only a model request has a step identity"
151
+ });
152
+ }
153
+ if (operation.kind === "compaction" && operation.phase === "first-output") {
154
+ ctx.addIssue({
155
+ code: "custom",
156
+ path: ["phase"],
157
+ message: "Compaction has no first-output phase"
158
+ });
159
+ }
160
+ if (operation.phase === "first-output" && operation.firstOutputAt === undefined || operation.phase === "started" && operation.firstOutputAt !== undefined || operation.kind === "compaction" && operation.firstOutputAt !== undefined) {
161
+ ctx.addIssue({
162
+ code: "custom",
163
+ path: ["firstOutputAt"],
164
+ message: "Only model output phases carry the observed first-output timestamp"
165
+ });
166
+ }
167
+ const terminal = ["completed", "failed", "cancelled"].includes(operation.phase);
168
+ if (terminal !== (operation.finishedAt !== undefined)) {
169
+ ctx.addIssue({
170
+ code: "custom",
171
+ path: ["finishedAt"],
172
+ message: "A terminal operation phase carries its observed finish timestamp"
173
+ });
174
+ }
175
+ });
128
176
  var AgentTerminalReasonSchema = z.enum([
129
177
  "success",
130
178
  "policy_stop",
@@ -204,6 +252,7 @@ var AgentRunFieldsSchema = z.object({
204
252
  terminalPolicyName: z.string().min(1).optional(),
205
253
  absorbedIntoRunId: AgentRecordIdSchema.optional(),
206
254
  usage: AgentUsageSchema.optional(),
255
+ lastOperation: AgentRunOperationSchema.optional(),
207
256
  createdAt: AgentTimestampSchema,
208
257
  updatedAt: AgentTimestampSchema
209
258
  });
@@ -277,4 +326,4 @@ var AgentRunMetricsSchema = z.object({
277
326
  ttftMs: z.number().nonnegative().optional()
278
327
  });
279
328
 
280
- export { AgentRecordIdSchema, AgentRecordVersionSchema, AgentTimestampSchema, AgentJsonObjectSchema, AgentProviderEnvelopeSchema, AgentTextPartSchema, AgentReasoningPartSchema, AgentFilePartSchema, AgentSourcePartSchema, AgentToolCallPartSchema, AgentToolResultPartSchema, AgentToolApprovalRequestPartSchema, AgentToolApprovalResponsePartSchema, AgentOpaquePartSchema, AgentControlPartSchema, AgentMessagePartSchema, AgentMessageRoleSchema, AgentMessageStatusSchema, AgentMessageSchema, AgentAssistantPlaceholderSchema, AgentRunStateSchema, AgentRunQueuePrioritySchema, AgentTerminalReasonSchema, runStateForTerminalReason, AgentProvenanceSchema, AgentUsageValueSchema, AgentCostValueSchema, AgentUsageSchema, AgentRunSchema, AgentSnapshotSchema, AgentRunMetricsSchema };
329
+ export { AgentRecordIdSchema, AgentRecordVersionSchema, AgentTimestampSchema, AgentJsonObjectSchema, AgentProviderEnvelopeSchema, AgentTextPartSchema, AgentReasoningPartSchema, AgentFilePartSchema, AgentSourcePartSchema, AgentToolCallPartSchema, AgentToolResultPartSchema, AgentToolApprovalRequestPartSchema, AgentToolApprovalResponsePartSchema, AgentOpaquePartSchema, AgentControlPartSchema, AgentMessagePartSchema, AgentMessageRoleSchema, AgentMessageStatusSchema, AgentMessageSchema, AgentAssistantPlaceholderSchema, AgentRunStateSchema, AgentRunQueuePrioritySchema, AgentRunOperationKindSchema, AgentRunOperationPhaseSchema, AgentRunOperationSchema, AgentTerminalReasonSchema, runStateForTerminalReason, AgentProvenanceSchema, AgentUsageValueSchema, AgentCostValueSchema, AgentUsageSchema, AgentRunSchema, AgentSnapshotSchema, AgentRunMetricsSchema };
package/dist/testing.js CHANGED
@@ -7,7 +7,7 @@ import"./index-3z73fh2c.js";
7
7
  import {
8
8
  AgentMessageSchema,
9
9
  AgentRunSchema
10
- } from "./index-x1th9s8c.js";
10
+ } from "./index-x15ss2dx.js";
11
11
  import {
12
12
  RealtimeRejectDirectionSchema,
13
13
  RealtimeRejectFaultSchema,
@@ -450,10 +450,34 @@ async function conformanceScenario(store, conversationIds) {
450
450
  if (!recoverable.items.some((item) => item.run.id === running.id)) {
451
451
  throw new Error("A running run must appear in a recoverable scan");
452
452
  }
453
+ const operation = await store.recordRunOperation({
454
+ conversationId,
455
+ runId: running.id,
456
+ expectedRevision: checkpointedRun.revision,
457
+ ownerId: "conformance-owner",
458
+ fencingToken: checkpointedRun.fencingToken,
459
+ operation: {
460
+ operationId: "model-call-1:0",
461
+ kind: "model-request",
462
+ phase: "first-output",
463
+ step: 0,
464
+ startedAt: "2026-08-22T00:00:01.800Z",
465
+ firstOutputAt: "2026-08-22T00:00:01.900Z"
466
+ }
467
+ });
468
+ requireOutcome(operation, "applied");
469
+ const operatedRun = operation.snapshot.runs.find((run) => run.id === running.id);
470
+ if (operatedRun?.lastOperation?.operationId !== "model-call-1:0") {
471
+ throw new Error("Run operation identity did not survive its durable mutation");
472
+ }
473
+ const operatedView = await store.loadRun({ conversationId, runId: running.id });
474
+ if (operatedView?.run.lastOperation?.startedAt !== "2026-08-22T00:00:01.800Z") {
475
+ throw new Error("loadRun did not retain the original operation timestamp");
476
+ }
453
477
  const interrupted = await store.requestRunInterrupt({
454
478
  conversationId,
455
479
  runId: running.id,
456
- expectedRevision: checkpointedRun.revision
480
+ expectedRevision: operatedRun.revision
457
481
  });
458
482
  requireOutcome(interrupted, "applied");
459
483
  const interruptedRun = interrupted.snapshot.runs.find((run) => run.id === running.id);
@@ -1,7 +1,7 @@
1
1
  import { type ZodObject, type ZodType, z } from 'zod';
2
2
  import { AsyncOperationCancelResultSchema, type AsyncOperationCapability } from './async-operation-contract.js';
3
3
  export { type AsyncOperationCancelResult, AsyncOperationCancelResultSchema, type AsyncOperationCapability, type AsyncOperationSnapshotSchema, type AsyncOperationSnapshotSchemaWithProgress, createAsyncOperationSnapshotSchema, } from './async-operation-contract.js';
4
- import type { ContractDef, EndpointDef, EndpointToolAnnotations, HttpMethod } from '../contract/index.js';
4
+ import type { ContractDef, EndpointDef, EndpointToolAnnotations, HttpMethod, Transport } from '../contract/index.js';
5
5
  import type { Handlers } from '../server/types.js';
6
6
  import { type RuntimeToolDefinition, type RuntimeToolDefinitionWithOutput, type RuntimeToolHandlerContext, type RuntimeToolTransport } from './runtime-tool.js';
7
7
  export interface AsyncOperationIdentity {
@@ -148,6 +148,23 @@ interface AsyncOperationContractBaseConfig<TStartInput extends ZodType, TId exte
148
148
  artifacts?: TArtifacts;
149
149
  descriptions?: Partial<Record<AsyncOperationCapability, string>>;
150
150
  scopes?: TScopes;
151
+ /**
152
+ * Which transports carry each capability. Absent, an endpoint is a tool on
153
+ * every transport, which is the framework default.
154
+ *
155
+ * It has to be declarable HERE because this contract is built inside the
156
+ * framework: an application that made tools opt-in with
157
+ * `createContractFactory({ toolExposure: 'explicit' })` set that default on
158
+ * its OWN factory, and these endpoints never pass through it. Without this
159
+ * field a consumer who had decided that agents get only the tools it names
160
+ * was still handed `start` and `cancel` — both effectful — with no way to say
161
+ * otherwise short of rebuilding the contract by hand.
162
+ *
163
+ * The default is unchanged on purpose. Unlike the tracking ingest, an async
164
+ * operation is a plausible thing for an agent to start and follow, so this
165
+ * says who decides rather than deciding for everyone.
166
+ */
167
+ expose?: Partial<Record<AsyncOperationCapability, readonly Transport[]>>;
151
168
  }
152
169
  export type AsyncOperationContractConfig<TStartInput extends ZodType, TId extends ZodType, TSnapshot extends ZodType, TCancel extends true | undefined = undefined, TResult extends ZodType | undefined = undefined, TArtifacts extends ZodType | undefined = undefined, TScope extends string = 'public', TScopes extends Partial<Record<AsyncOperationCapability, string>> = Record<never, never>> = AsyncOperationContractBaseConfig<TStartInput, TId, TSnapshot, TCancel, TResult, TArtifacts, TScope, TScopes> & {
153
170
  startOutput?: never;