@tessar/core 0.1.0-alpha.0 → 0.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.
Files changed (147) hide show
  1. package/README.md +168 -31
  2. package/dist/agent.d.ts +37 -87
  3. package/dist/agent.js +101 -1116
  4. package/dist/agent.js.map +1 -1
  5. package/dist/controller.d.ts +13 -4
  6. package/dist/controller.js +184 -19
  7. package/dist/controller.js.map +1 -1
  8. package/dist/engine/call.d.ts +8 -0
  9. package/dist/engine/call.js +136 -0
  10. package/dist/engine/call.js.map +1 -0
  11. package/dist/engine/checkpoint.d.ts +11 -0
  12. package/dist/engine/checkpoint.js +187 -0
  13. package/dist/engine/checkpoint.js.map +1 -0
  14. package/dist/engine/delivery.js +6 -13
  15. package/dist/engine/delivery.js.map +1 -1
  16. package/dist/engine/execution.d.ts +5 -0
  17. package/dist/engine/execution.js +292 -0
  18. package/dist/engine/execution.js.map +1 -0
  19. package/dist/engine/frame.d.ts +77 -0
  20. package/dist/engine/frame.js +83 -0
  21. package/dist/engine/frame.js.map +1 -0
  22. package/dist/engine/hooks.d.ts +4 -0
  23. package/dist/engine/hooks.js +138 -0
  24. package/dist/engine/hooks.js.map +1 -0
  25. package/dist/engine/interrupt.d.ts +1 -1
  26. package/dist/engine/lineage.d.ts +1 -1
  27. package/dist/engine/recovery.d.ts +1 -2
  28. package/dist/engine/recovery.js +4 -2
  29. package/dist/engine/recovery.js.map +1 -1
  30. package/dist/engine/resume.d.ts +12 -1
  31. package/dist/engine/resume.js +189 -0
  32. package/dist/engine/resume.js.map +1 -1
  33. package/dist/engine/runtime.d.ts +16 -0
  34. package/dist/engine/runtime.js +29 -0
  35. package/dist/engine/runtime.js.map +1 -0
  36. package/dist/engine/services.d.ts +3 -0
  37. package/dist/engine/services.js +125 -0
  38. package/dist/engine/services.js.map +1 -0
  39. package/dist/engine/tool-executor.d.ts +8 -2
  40. package/dist/engine/tool-executor.js +266 -3
  41. package/dist/engine/tool-executor.js.map +1 -1
  42. package/dist/engine/tracked-effects.js +5 -6
  43. package/dist/engine/tracked-effects.js.map +1 -1
  44. package/dist/errors.js +24 -9
  45. package/dist/errors.js.map +1 -1
  46. package/dist/events/definition.d.ts +2 -0
  47. package/dist/events/definition.js +2 -0
  48. package/dist/events/definition.js.map +1 -1
  49. package/dist/events/dispatcher.d.ts +1 -1
  50. package/dist/events/dispatcher.js +35 -4
  51. package/dist/events/dispatcher.js.map +1 -1
  52. package/dist/events/observer.d.ts +3 -0
  53. package/dist/events/observer.js +13 -0
  54. package/dist/events/observer.js.map +1 -0
  55. package/dist/events/{catalog.d.ts → type.d.ts} +170 -47
  56. package/dist/events/type.js +175 -0
  57. package/dist/events/type.js.map +1 -0
  58. package/dist/identity.d.ts +1 -0
  59. package/dist/identity.js +23 -0
  60. package/dist/identity.js.map +1 -0
  61. package/dist/index.d.ts +4 -2
  62. package/dist/index.js +4 -2
  63. package/dist/index.js.map +1 -1
  64. package/dist/instructions/instruction.d.ts +2 -0
  65. package/dist/instructions/instruction.js +10 -3
  66. package/dist/instructions/instruction.js.map +1 -1
  67. package/dist/loop.d.ts +5 -5
  68. package/dist/loop.js +44 -204
  69. package/dist/loop.js.map +1 -1
  70. package/dist/messages/codec.js +4 -2
  71. package/dist/messages/codec.js.map +1 -1
  72. package/dist/messages/message.d.ts +2 -3
  73. package/dist/messages/message.js +3 -3
  74. package/dist/messages/message.js.map +1 -1
  75. package/dist/models/contract.d.ts +15 -0
  76. package/dist/models/options.d.ts +3 -0
  77. package/dist/models/options.js +13 -0
  78. package/dist/models/options.js.map +1 -0
  79. package/dist/models/registry.d.ts +2 -0
  80. package/dist/models/registry.js +18 -1
  81. package/dist/models/registry.js.map +1 -1
  82. package/dist/models/resolver.d.ts +4 -2
  83. package/dist/models/resolver.js +9 -4
  84. package/dist/models/resolver.js.map +1 -1
  85. package/dist/models/stream.d.ts +6 -0
  86. package/dist/models/stream.js +61 -2
  87. package/dist/models/stream.js.map +1 -1
  88. package/dist/plugins/context.d.ts +27 -2
  89. package/dist/plugins/host.d.ts +5 -1
  90. package/dist/plugins/host.js +33 -11
  91. package/dist/plugins/host.js.map +1 -1
  92. package/dist/plugins/registry.d.ts +1 -1
  93. package/dist/plugins/registry.js +18 -10
  94. package/dist/plugins/registry.js.map +1 -1
  95. package/dist/session.d.ts +16 -6
  96. package/dist/session.js +126 -55
  97. package/dist/session.js.map +1 -1
  98. package/dist/state/commit.d.ts +5 -0
  99. package/dist/state/commit.js +50 -5
  100. package/dist/state/commit.js.map +1 -1
  101. package/dist/state/fork.js +27 -6
  102. package/dist/state/fork.js.map +1 -1
  103. package/dist/state/memory-backend.js +307 -38
  104. package/dist/state/memory-backend.js.map +1 -1
  105. package/dist/state/plugin-state.js +73 -18
  106. package/dist/state/plugin-state.js.map +1 -1
  107. package/dist/state/queries.d.ts +14 -14
  108. package/dist/state/queries.js +150 -48
  109. package/dist/state/queries.js.map +1 -1
  110. package/dist/state/schema.d.ts +47 -6
  111. package/dist/state/schema.js +9 -1
  112. package/dist/state/schema.js.map +1 -1
  113. package/dist/state/scope.js +3 -2
  114. package/dist/state/scope.js.map +1 -1
  115. package/dist/state/sessions.d.ts +2 -0
  116. package/dist/state/sessions.js +21 -0
  117. package/dist/state/sessions.js.map +1 -0
  118. package/dist/state/storage-ports.d.ts +16 -2
  119. package/dist/state/threads.d.ts +1 -0
  120. package/dist/state/threads.js +77 -12
  121. package/dist/state/threads.js.map +1 -1
  122. package/dist/testing/harness.d.ts +2 -1
  123. package/dist/testing/harness.js +6 -4
  124. package/dist/testing/harness.js.map +1 -1
  125. package/dist/testing/scripted-provider.d.ts +1 -0
  126. package/dist/testing/scripted-provider.js +7 -0
  127. package/dist/testing/scripted-provider.js.map +1 -1
  128. package/dist/testing/storage-conformance.js +111 -0
  129. package/dist/testing/storage-conformance.js.map +1 -1
  130. package/dist/thread.d.ts +4 -2
  131. package/dist/thread.js +14 -6
  132. package/dist/thread.js.map +1 -1
  133. package/dist/tools/tool.d.ts +11 -12
  134. package/dist/tools/tool.js +27 -10
  135. package/dist/tools/tool.js.map +1 -1
  136. package/dist/trace/records.d.ts +11 -6
  137. package/dist/trace/records.js +28 -1
  138. package/dist/trace/records.js.map +1 -1
  139. package/package.json +7 -1
  140. package/dist/events/catalog.js +0 -89
  141. package/dist/events/catalog.js.map +0 -1
  142. package/dist/models/cache.d.ts +0 -12
  143. package/dist/models/cache.js +0 -17
  144. package/dist/models/cache.js.map +0 -1
  145. package/dist/session-events.d.ts +0 -60
  146. package/dist/session-events.js +0 -14
  147. package/dist/session-events.js.map +0 -1
package/dist/agent.js CHANGED
@@ -1,23 +1,17 @@
1
- import { loop } from "./loop.js";
2
- import { providerCall, streamModel } from "./models/stream.js";
1
+ import { stream as streamHooks } from "./events/type.js";
3
2
  import { createDelivery, } from "./engine/delivery.js";
4
- import { TessarError, errorInfo, errorFromInfo, fail, requireId, } from "./errors.js";
3
+ import { errorInfo, errorFromInfo, fail, requireId, } from "./errors.js";
5
4
  import { immutable, randomIds, systemClock, } from "./json.js";
6
5
  import { normalizeInstruction, } from "./instructions/instruction.js";
7
- import { AgentEvents, } from "./events/catalog.js";
8
- import {} from "./events/definition.js";
9
6
  import { MessageCodec } from "./messages/codec.js";
10
- import { ToolMessage } from "./messages/message.js";
11
- import { PluginHost } from "./plugins/host.js";
12
- import { CommitCoordinator } from "./state/commit.js";
13
- import { PluginStateAccess, } from "./state/plugin-state.js";
14
- import { aggregateUsage, normalizeUsage } from "./trace/usage.js";
15
- import { Suspension } from "./engine/interrupt.js";
16
- import { phaseForEvent, validateManifest, validateResume, } from "./engine/resume.js";
17
- import { canonicalJson } from "./state/commit.js";
18
- import { RecoveryService } from "./engine/recovery.js";
19
- import { EffectService, childStateDefinition, } from "./engine/tracked-effects.js";
20
- import { resolveChildAdmission, summarizeStreamUsage, } from "./engine/lineage.js";
7
+ import { Tool } from "./tools/tool.js";
8
+ import { execute, cancel } from "./engine/execution.js";
9
+ import { hook } from "./engine/hooks.js";
10
+ import { put, rows, commit, updateUsage, prepareMessage, appendMessage, finish, } from "./engine/checkpoint.js";
11
+ import { services } from "./engine/services.js";
12
+ import { callAgent, modelRef, readOutcome } from "./engine/call.js";
13
+ import { isModelAdapter } from "./models/stream.js";
14
+ import { reserveId } from "./identity.js";
21
15
  export function inputMessages(input) {
22
16
  const values = Array.isArray(input) ? input : [input];
23
17
  return values.map((value) => MessageCodec.encode(MessageCodec.decode(MessageCodec.encode(value))));
@@ -29,6 +23,8 @@ function validateLimits(limits) {
29
23
  }
30
24
  export class Agent {
31
25
  id;
26
+ tools;
27
+ /** @internal */ toolConcurrency;
32
28
  model;
33
29
  controller;
34
30
  /** @internal */ attachController(controller) {
@@ -52,14 +48,31 @@ export class Agent {
52
48
  limits;
53
49
  instructions;
54
50
  jobs = new Set();
51
+ outcomes = new WeakMap();
55
52
  active = new Map();
53
+ pendingModels = new Map();
56
54
  closed = false;
55
+ releaseId;
57
56
  resumeAdmissions = new Map();
58
57
  constructor(options) {
59
58
  this.id = requireId(options.id, "agent id");
60
- this.model = options.model
61
- ? immutable("ref" in options.model ? options.model.ref : options.model)
62
- : undefined;
59
+ if (!options.model)
60
+ fail("MissingModel", "Agent requires a default model");
61
+ const { concurrency, ...tools } = options.tools ?? {};
62
+ if (concurrency !== undefined &&
63
+ (!Number.isInteger(concurrency) || concurrency < 1))
64
+ fail("InvalidConfiguration", "Tool concurrency must be a positive integer");
65
+ const values = Object.values(tools);
66
+ if (values.some((tool) => !(tool instanceof Tool)))
67
+ fail("InvalidConfiguration", "Agent tools must be Tool instances");
68
+ this.tools = Object.freeze(values);
69
+ if (new Set(this.tools.map((tool) => tool.name)).size !== this.tools.length)
70
+ fail("RegistrationConflict", "Tool name already registered");
71
+ this.toolConcurrency = concurrency ?? Infinity;
72
+ this.model = ("ref" in options.model ? options.model : immutable(options.model));
73
+ const ref = modelRef(this.model);
74
+ requireId(ref.providerId, "provider id");
75
+ requireId(ref.modelId, "model id");
63
76
  this.ids = randomIds;
64
77
  this.clock = systemClock;
65
78
  this.instructions = normalizeInstruction(options.instructions);
@@ -70,1135 +83,107 @@ export class Agent {
70
83
  ...options.limits,
71
84
  });
72
85
  validateLimits(this.limits);
86
+ this.releaseId = reserveId("Agent", this.id, this);
73
87
  }
74
- stream(options) {
75
- const controller = this.runtime();
76
- controller.requireReady();
77
- const model = controller.resolveModel(options.model ?? this.model).ref;
78
- const threadId = this.ids.next("thread"), input = inputMessages(options.input);
79
- return this.admit({
80
- kind: "input",
81
- threadId,
82
- input,
83
- model,
84
- context: options.context,
85
- limits: options.limits,
86
- signal: options.signal,
87
- }, { kind: "iterator" }, () => controller
88
- .prepareThread({ scope: options.scope }, threadId)
89
- .then(() => { }));
88
+ stream(input, options = {}) {
89
+ return callAgent(this, input, options, this.controller);
90
90
  }
91
- async generate(options) {
92
- const stream = this.stream(options);
91
+ async generate(input, options = {}) {
92
+ const stream = this.stream(input, options);
93
93
  for await (const event of stream)
94
- if (event.is(AgentEvents.StreamError))
94
+ if (event.is(streamHooks.error))
95
95
  throw errorFromInfo(event.payload.error);
96
- if (!stream.id)
97
- fail("AdmissionFailed", "Execution was not admitted");
98
- const record = await this.runtime().queries.getStream(stream.threadId, stream.id);
99
- if (!record)
100
- fail("StorageUnavailable", "Committed execution missing");
101
- const cp = await this.runtime().queries.snapshot(stream.threadId), pending = cp.execution?.pendingInterruptId;
102
- return {
103
- threadId: stream.threadId,
104
- streamId: record.id,
105
- status: record.status,
106
- output: cp.state.messages.filter((m) => record.outputMessageIds.includes(m.id)),
107
- usage: record.directUsage,
108
- interrupt: pending
109
- ? await this.runtime().queries.getInterrupt(stream.threadId, pending)
110
- : null,
111
- error: record.error,
112
- };
96
+ return this.outcomes.get(stream);
113
97
  }
114
98
  /** @internal Shared admission for Thread, Session and testing. */
115
- admit(request, target = { kind: "iterator" }, prepare) {
116
- this.runtime().requireReady();
99
+ admit(request, target = { kind: "iterator" }, prepare, runtime, cleanup) {
100
+ const environment = runtime ?? this.runtime();
101
+ if (environment === this.controller)
102
+ this.controller.requireReady();
117
103
  if (this.closed)
118
104
  fail("Disposed", "Agent closed");
119
105
  if (request.kind === "input")
120
106
  validateLimits({ ...this.limits, ...request.limits });
121
- const sink = createDelivery(request.threadId, this.ids, target, request.signal, this.runtime().report), registry = this.registry.capture();
122
- const job = Promise.resolve()
123
- .then(async () => {
107
+ if (!request.adapter &&
108
+ isModelAdapter(this.model) &&
109
+ request.model?.providerId === this.model.ref.providerId &&
110
+ request.model.connectionId === this.model.ref.connectionId) {
111
+ request = {
112
+ ...request,
113
+ adapter: request.model.modelId === this.model.ref.modelId
114
+ ? this.model
115
+ : this.model.withModel?.(request.model.modelId),
116
+ };
117
+ }
118
+ const sink = createDelivery(request.threadId, this.ids, target, request.signal, environment.report);
119
+ const registry = environment.hooks.capture();
120
+ const outcome = (async () => {
121
+ let entered = false;
124
122
  try {
125
123
  await prepare?.();
126
124
  if (this.closed)
127
125
  fail("Disposed", "Agent closed");
126
+ entered = true;
127
+ const failure = await execute(this.executionContext(environment), request, sink, registry);
128
+ if (failure !== undefined && errorInfo(failure).source === "provider")
129
+ throw failure;
130
+ return await readOutcome(environment, sink.stream);
128
131
  }
129
132
  catch (error) {
130
- await registry.release();
133
+ if (!entered)
134
+ await registry.release();
135
+ environment.report(errorInfo(error));
136
+ sink.emit(streamHooks.error, {
137
+ error: errorInfo(error),
138
+ lastKnownCheckpointId: null,
139
+ persistedStatus: null,
140
+ }, {});
141
+ sink.rejected(errorInfo(error));
142
+ sink.close();
131
143
  throw error;
132
144
  }
133
- await this.execute(request, sink, registry);
134
- })
135
- .catch((error) => {
136
- this.runtime().report(errorInfo(error));
137
- sink.emit(AgentEvents.StreamError, {
138
- error: errorInfo(error),
139
- lastKnownCheckpointId: null,
140
- persistedStatus: null,
141
- }, {});
142
- sink.rejected(errorInfo(error));
143
- sink.close();
144
- });
145
+ finally {
146
+ await cleanup?.();
147
+ }
148
+ })();
149
+ this.outcomes.set(sink.stream, outcome);
150
+ const job = outcome.then(() => { }, () => { });
145
151
  this.jobs.add(job);
146
152
  void job.then(() => this.jobs.delete(job));
147
153
  return sink.stream;
148
154
  }
149
- async execute(request, sink, registry) {
150
- let frame;
151
- let pendingLease;
152
- try {
153
- if (sink.controller.signal.aborted)
154
- fail("Cancelled", "Execution cancelled before admission");
155
- if (request.kind === "resume") {
156
- frame = await this.prepareResume(request, sink, registry);
157
- this.active.set(frame.stream.id, frame);
158
- this.watchLease(frame);
159
- sink.admitted();
160
- await this.hook(frame, AgentEvents.StreamResumed, {
161
- checkpointId: frame.cp.id,
162
- interruptId: request.interruptId,
163
- reason: "response",
164
- });
165
- const cursor = frame.cp.execution.handlerCursor;
166
- if (cursor?.eventId === AgentEvents.BeforeStream) {
167
- const result = await this.hook(frame, AgentEvents.BeforeStream, cursor.payload);
168
- if (result?.decision === "deny" || result?.decision === "stop") {
169
- await this.finish(frame, "stopped", result.reason ?? "PolicyStopped");
170
- return;
171
- }
172
- }
173
- if (cursor?.eventId === AgentEvents.ModelRecover) {
174
- const result = await this.hook(frame, AgentEvents.ModelRecover, cursor.payload);
175
- if (result?.decision !== "retry")
176
- fail("ProviderError", "Recovery policy declined retry");
177
- frame.cp.execution.phase = "BeforeModel";
178
- await this.commit(frame, "ExecutionTransition");
179
- }
180
- await this.loop(frame);
181
- return;
182
- }
183
- if (request.kind === "recover") {
184
- frame = await this.prepareRecovery(request, sink, registry);
185
- this.active.set(frame.stream.id, frame);
186
- this.watchLease(frame);
187
- sink.admitted();
188
- if (frame.cp.execution?.pendingInterruptId) {
189
- const interrupt = await this.backend.executions.get("interrupts", frame.cp.execution.pendingInterruptId, { threadId: request.threadId });
190
- await this.hook(frame, AgentEvents.StreamSuspended, {
191
- checkpointId: frame.cp.id,
192
- interrupt: interrupt,
193
- });
194
- return;
195
- }
196
- await this.hook(frame, AgentEvents.StreamResumed, {
197
- checkpointId: frame.cp.id,
198
- interruptId: null,
199
- reason: "recovery",
200
- });
201
- await this.loop(frame);
202
- return;
203
- }
204
- const head = await this.backend.threads.get(request.threadId);
205
- if (!head)
206
- fail("ThreadNotFound", "Thread not found");
207
- if (head.activeStreamId)
208
- fail("ThreadBusy", "Thread already has unfinished execution");
209
- const lease = await this.backend.ownership.acquire({
210
- threadId: request.threadId,
211
- ownerToken: this.ids.next("owner"),
212
- ttlMs: 30000,
213
- });
214
- pendingLease = lease;
215
- const cp = structuredClone((await this.backend.checkpoints.get(head.currentCheckpointId)));
216
- const streamId = request.origin
217
- ? await resolveChildAdmission(this.backend, request.origin, request.threadId)
218
- : this.ids.next("stream"), usage = aggregateUsage([]);
219
- const stream = {
220
- id: streamId,
221
- threadId: request.threadId,
222
- scope: head.scope,
223
- parentStreamId: request.origin?.parentStreamId ?? null,
224
- parentToolExecutionId: request.origin?.parentToolExecutionId ?? null,
225
- triggerMessageId: null,
226
- status: "running",
227
- stopReason: null,
228
- startedAt: this.clock.now(),
229
- endedAt: null,
230
- durationMs: null,
231
- error: null,
232
- directUsage: usage,
233
- inclusiveUsage: usage,
234
- outputMessageIds: [],
235
- };
236
- const records = new Map();
237
- for (const kind of [
238
- "streams",
239
- "turns",
240
- "model_attempts",
241
- "tool_executions",
242
- "interrupts",
243
- "plugin_states",
244
- ])
245
- for (const row of await this.backend.executions.listRecords(kind, request.threadId))
246
- records.set(kind + ":" + ("id" in row ? row.id : ""), {
247
- kind,
248
- id: "id" in row ? row.id : "",
249
- threadId: request.threadId,
250
- data: structuredClone(row),
251
- });
252
- const state = new PluginStateAccess([
253
- childStateDefinition,
254
- ...registry.list("state"),
255
- ], { type: "thread", id: request.threadId }, cp.state.pluginStates, async () => {
256
- await this.commit(frame, "PluginStateCommitted");
257
- return frame.cp.id;
258
- }, undefined, this.backend.ownedState);
259
- frame = {
260
- request,
261
- sink,
262
- registry,
263
- lease,
264
- cp,
265
- stream,
266
- records,
267
- state,
268
- admitted: false,
269
- sequence: 0,
270
- };
271
- this.put(frame, "streams", stream);
272
- sink.setId(streamId);
273
- this.active.set(streamId, frame);
274
- this.watchLease(frame);
275
- cp.execution = {
276
- streamId,
277
- turnId: null,
278
- phase: "BeforeTurn",
279
- modelRef: request.model,
280
- baseInstructions: this.instructions,
281
- pendingToolExecutionIds: [],
282
- pendingInterruptId: null,
283
- handlerCursor: null,
284
- budgets: {
285
- turns: 0,
286
- modelAttempts: 0,
287
- toolCalls: 0,
288
- limits: { ...this.limits, ...request.limits },
289
- },
290
- registryManifest: [
291
- ...registry.values.map((r) => ({
292
- pluginId: r.pluginId,
293
- kind: r.kind,
294
- id: r.id,
295
- version: r.version,
296
- pluginVersion: r.pluginVersion,
297
- configurationId: r.configurationId,
298
- requiredForContinuation: true,
299
- })),
300
- ...registry.handlers.map((r) => ({
301
- pluginId: r.pluginId,
302
- kind: "handler",
303
- id: r.id,
304
- version: r.version,
305
- pluginVersion: r.pluginVersion,
306
- configurationId: r.configurationId,
307
- requiredForContinuation: r.kind !== "notification",
308
- })),
309
- ],
310
- recordRevision: cp.revision,
311
- };
312
- const input = await this.hook(frame, AgentEvents.InputPrepare, {
313
- messages: request.input,
314
- });
315
- if (input.rejected)
316
- throw errorFromInfo(input.rejected);
317
- for (const message of input.messages)
318
- cp.state = {
319
- ...cp.state,
320
- messages: [
321
- ...cp.state.messages,
322
- await this.prepareMessage(frame, message),
323
- ],
324
- };
325
- stream.triggerMessageId = input.messages[0]?.id ?? null;
326
- await this.commit(frame, "InputCommitted");
327
- frame.admitted = true;
328
- sink.admitted();
329
- await this.hook(frame, AgentEvents.StreamStarted, {
330
- modelRef: request.model,
331
- checkpointId: frame.cp.id,
332
- });
333
- await this.hook(frame, AgentEvents.InputReceived, {
334
- messages: input.messages,
335
- });
336
- if (input.handled) {
337
- for (const message of input.handled.output)
338
- await this.appendMessage(frame, message);
339
- await this.finish(frame, "completed", null);
340
- return;
341
- }
342
- const before = await this.hook(frame, AgentEvents.BeforeStream, {
343
- input: input.messages,
344
- });
345
- if (before?.decision === "deny" || before?.decision === "stop") {
346
- await this.finish(frame, "stopped", before.reason ?? "PolicyStopped");
347
- return;
348
- }
349
- await this.loop(frame);
350
- }
351
- catch (error) {
352
- if (error instanceof Suspension)
353
- return;
354
- if (frame?.ownershipError)
355
- error = new TessarError("OwnershipLost", "Execution ownership was lost");
356
- const info = errorInfo(error);
357
- if (frame?.admitted &&
358
- info.source !== "storage" &&
359
- ![
360
- "StorageUnavailable",
361
- "CommitConflict",
362
- "OwnershipLost",
363
- "RecoveryRequired",
364
- ].includes(info.code)) {
365
- try {
366
- await this.finish(frame, sink.controller.signal.aborted ? "cancelled" : "failed", info.code, info);
367
- return;
368
- }
369
- catch (finalizeError) {
370
- error = finalizeError;
371
- }
372
- }
373
- const finalError = errorInfo(error);
374
- this.runtime().report(finalError, this.eventContext(frame));
375
- sink.emit(AgentEvents.StreamError, {
376
- error: finalError,
377
- lastKnownCheckpointId: frame?.cp.id ?? null,
378
- persistedStatus: frame?.stream.status ?? null,
379
- }, this.eventContext(frame));
380
- sink.rejected(finalError);
381
- }
382
- finally {
383
- if (frame) {
384
- if (frame.heartbeat)
385
- clearInterval(frame.heartbeat);
386
- this.active.delete(frame.stream.id);
387
- try {
388
- await this.backend.ownership.release(frame.lease);
389
- }
390
- catch {
391
- /* recovery uses lease expiry */
392
- }
393
- }
394
- else if (pendingLease) {
395
- try {
396
- await this.backend.ownership.release(pendingLease);
397
- }
398
- catch {
399
- /* lease expiry remains authoritative */
400
- }
401
- }
402
- await registry.release();
403
- sink.close();
404
- }
405
- }
406
- eventContext(frame) {
407
- return {
408
- agentId: this.id,
409
- controllerId: this.runtime().id,
410
- ...(frame
411
- ? {
412
- scope: frame.cp.state.scope,
413
- streamId: frame.stream.id,
414
- threadId: frame.cp.threadId,
415
- sequence: ++frame.sequence,
416
- timestamp: this.clock.now(),
417
- }
418
- : {}),
419
- };
420
- }
421
- watchLease(frame) {
422
- if (frame.heartbeat)
423
- return;
424
- frame.heartbeat = setInterval(() => {
425
- void this.backend.ownership.renew(frame.lease, 30000).then((lease) => {
426
- frame.lease = lease;
427
- }, (error) => {
428
- frame.ownershipError = errorInfo(error, {
429
- source: "storage",
430
- backendId: this.backend.id,
431
- });
432
- frame.sink.controller.abort("OwnershipLost");
433
- });
434
- }, 10000);
435
- }
436
- async hook(frame, definition, payload) {
437
- const cursor = frame.cp.execution?.handlerCursor?.eventId === definition
438
- ? frame.cp.execution.handlerCursor
439
- : null;
440
- const decisions = cursor?.decisions.slice() ?? [];
441
- const kind = this.registry.kind(definition);
442
- const mutable = kind === "transformation" || kind === "decision";
443
- const context = {
444
- ...this.eventContext(frame),
445
- ...(mutable
446
- ? {
447
- ...this.services(frame, frame.currentToolExecutionId),
448
- state: frame.state,
449
- }
450
- : {}),
451
- signal: frame.sink.controller.signal,
452
- context: frame.request.kind === "input" ? frame.request.context : undefined,
453
- turnId: frame.cp.execution?.turnId,
454
- streamId: frame.stream.id,
455
- threadId: frame.cp.threadId,
456
- startIndex: cursor?.nextIndex ?? 0,
457
- initialDecision: decisions.at(-1) ?? undefined,
458
- allowedHandlerIds: frame.cp.execution?.registryManifest
459
- .filter((r) => r.kind === "handler")
460
- .map((r) => r.id),
461
- suspend: kind === "decision"
462
- ? (def, data) => {
463
- phaseForEvent(definition);
464
- const binding = frame.registry.find("interruption", def.id);
465
- if (!binding || binding.definition.version !== def.version)
466
- fail("ResumeIncompatible", "Interruption definition is not registered");
467
- return {
468
- decision: "suspend",
469
- definitionId: def.id,
470
- definitionVersion: def.version,
471
- pluginId: binding.pluginId,
472
- payload: immutable(def.payload ? def.payload.parse(data) : data),
473
- };
474
- }
475
- : undefined,
476
- onDecision: this.registry.kind(definition) === "decision"
477
- ? async (entry) => {
478
- const value = entry.decision;
479
- decisions.push(value ? immutable(value) : null);
480
- frame.cp.execution.handlerCursor = {
481
- eventId: definition,
482
- handlerId: entry.handlerId,
483
- nextIndex: entry.index + 1,
484
- payload: immutable(cursor?.payload ?? payload),
485
- decisions,
486
- };
487
- if (value?.decision !== "suspend")
488
- await this.commit(frame, "ExecutionTransition");
489
- }
490
- : undefined,
491
- };
492
- const value = await this.registry.dispatch(definition, payload, context, frame.registry);
493
- if (this.registry.kind(definition) === "decision") {
494
- if (value?.decision === "suspend")
495
- await this.suspend(frame, value);
496
- if (frame.cp.execution?.handlerCursor?.eventId === definition)
497
- frame.cp.execution.handlerCursor = null;
498
- }
499
- if (this.registry.kind(definition) === "notification")
500
- frame.sink.emit(definition, payload, context);
501
- return value;
502
- }
503
- put(frame, kind, data) {
504
- frame.records.set(kind + ":" + data.id, {
505
- kind,
506
- id: data.id,
507
- threadId: frame.cp.threadId,
508
- data,
509
- });
510
- }
511
- rows(frame, kind) {
512
- return [...frame.records.values()]
513
- .filter((r) => r.kind === kind)
514
- .map((r) => r.data);
515
- }
516
- services(frame, toolExecutionId) {
517
- const state = frame.state.withOwner({
518
- type: "stream",
519
- id: frame.stream.id,
520
- });
521
- frame.effectLocks ??= new Map();
522
- const effects = new EffectService((id) => frame.registry.find("effect", id), state, () => this.commit(frame, "PluginStateCommitted"), {
523
- signal: frame.sink.controller.signal,
524
- threadId: frame.cp.threadId,
525
- streamId: frame.stream.id,
526
- turnId: frame.cp.execution?.turnId ?? null,
527
- ...(toolExecutionId ? { toolExecutionId } : {}),
528
- }, () => this.ids.next("effect"), frame.effectLocks);
529
- const guard = () => {
530
- if (!frame.admitted || !frame.cp.execution)
531
- fail("UnsupportedBoundary", "Tracked execution requires admitted active work");
532
- };
533
- return {
534
- models: {
535
- stream: (options) => {
536
- guard();
537
- return this.auxiliary(frame, toolExecutionId, options);
538
- },
539
- },
540
- effects: {
541
- execute: (definition, input, options) => {
542
- guard();
543
- return effects.execute(definition, input, options);
544
- },
545
- },
546
- executions: {
547
- reserveChild: async (options) => {
548
- guard();
549
- if (options.threadId === frame.cp.threadId)
550
- fail("InvalidLineage", "Child requires a separate Thread");
551
- if (!(await this.backend.threads.get(options.threadId)))
552
- fail("ThreadNotFound", "Child Thread not found");
553
- const previous = state.get(childStateDefinition), old = previous[options.dispatchKey];
554
- if (old) {
555
- if (old.threadId !== options.threadId)
556
- fail("CommitConflict", "Dispatch key is bound to another Thread");
557
- await this.commit(frame, "PluginStateCommitted");
558
- return old;
559
- }
560
- const reservation = {
561
- threadId: options.threadId,
562
- streamId: this.ids.next("stream"),
563
- origin: {
564
- parentStreamId: frame.stream.id,
565
- ...(toolExecutionId
566
- ? { parentToolExecutionId: toolExecutionId }
567
- : {}),
568
- dispatchId: this.ids.next("dispatch"),
569
- },
570
- };
571
- state.update(childStateDefinition, {
572
- ...previous,
573
- [options.dispatchKey]: reservation,
574
- });
575
- await this.commit(frame, "PluginStateCommitted");
576
- return immutable(reservation);
577
- },
578
- },
579
- events: {
580
- emit: async (definition, payload) => {
581
- if (this.registry.kind(definition) !== "notification")
582
- fail("InvalidEvent", "Custom progress must be notification events");
583
- await this.hook(frame, definition, payload);
584
- },
585
- },
586
- };
587
- }
588
- async *auxiliary(frame, toolExecutionId, options) {
589
- const position = frame.cp.execution;
590
- if (!position.turnId)
591
- fail("UnsupportedBoundary", "Auxiliary calls require an active turn");
592
- if (position.budgets.modelAttempts >= position.budgets.limits.maxModelAttempts)
593
- fail("StreamLimit", "Model attempt budget exhausted");
594
- const provider = frame.registry.find("provider", options.model.providerId);
595
- if (!provider)
596
- fail("MissingProvider", "Auxiliary provider is missing");
597
- const adapter = providerCall(options.model, () => provider.create(options.model.modelId)), request = await this.hook(frame, AgentEvents.ModelRequest, immutable(options.request));
598
- providerCall(options.model, () => adapter.validate(request));
599
- const attempt = {
600
- id: this.ids.next("attempt"),
601
- turnId: position.turnId,
602
- streamId: frame.stream.id,
603
- scope: frame.cp.state.scope,
604
- callId: this.ids.next("call"),
605
- attemptIndex: 0,
606
- purpose: "auxiliary",
607
- parentToolExecutionId: toolExecutionId ?? null,
608
- providerId: options.model.providerId,
609
- modelId: options.model.modelId,
610
- providerRequestId: null,
611
- responseMessageId: null,
612
- contextRevision: frame.cp.revision,
613
- requestSnapshotRef: null,
614
- startedAt: this.clock.now(),
615
- firstOutputAt: null,
616
- endedAt: null,
617
- durationMs: null,
618
- timeToFirstOutputMs: null,
619
- status: "running",
620
- finishReason: null,
621
- error: null,
622
- usage: normalizeUsage({}),
623
- };
624
- position.budgets.modelAttempts++;
625
- this.put(frame, "model_attempts", attempt);
626
- await this.commit(frame, "ExecutionTransition");
627
- try {
628
- for await (const chunk of streamModel(adapter, request, {
629
- signal: frame.sink.controller.signal,
630
- })) {
631
- if (chunk.type === "usage")
632
- attempt.usage = chunk.usage;
633
- if (chunk.type === "response") {
634
- attempt.usage = chunk.response.usage ?? attempt.usage;
635
- attempt.status = "succeeded";
636
- attempt.finishReason = chunk.response.finishReason ?? null;
637
- attempt.providerRequestId = chunk.response.providerRequestId ?? null;
638
- }
639
- yield chunk;
640
- }
641
- }
642
- catch (error) {
643
- attempt.status = "failed";
644
- attempt.error = errorInfo(error);
645
- throw error;
646
- }
647
- finally {
648
- if (attempt.status === "running")
649
- attempt.status = "interrupted";
650
- attempt.endedAt = this.clock.now();
651
- await this.commit(frame, "ExecutionTransition");
652
- }
653
- }
654
- async updateUsage(frame) {
655
- const attempts = this.rows(frame, "model_attempts");
656
- const summary = await summarizeStreamUsage(this.backend, frame.stream.id, {
657
- threadId: frame.cp.threadId,
658
- attempts,
659
- states: frame.state.snapshots(),
660
- });
661
- frame.stream.directUsage = summary.direct;
662
- frame.stream.inclusiveUsage = summary.inclusive;
663
- for (const turn of this.rows(frame, "turns"))
664
- turn.usage = aggregateUsage(attempts.filter((a) => a.turnId === turn.id));
665
- }
666
- async commit(frame, cause) {
667
- const queued = (frame.commitQueue ?? Promise.resolve()).then(() => this.commitNow(frame, cause));
668
- frame.commitQueue = queued.catch(() => { });
669
- return queued;
670
- }
671
- async commitNow(frame, cause) {
672
- await this.updateUsage(frame);
673
- const cp = {
674
- ...frame.cp,
675
- id: this.ids.next("checkpoint"),
676
- parentCheckpointId: frame.cp.id,
677
- revision: frame.cp.revision + 1,
678
- createdAt: this.clock.now(),
679
- cause,
680
- state: { ...frame.cp.state, pluginStates: frame.state.snapshots() },
681
- execution: frame.cp.execution
682
- ? { ...frame.cp.execution, recordRevision: frame.cp.revision + 1 }
683
- : null,
684
- };
685
- if (cause === "Suspended" && cp.execution?.pendingInterruptId) {
686
- const record = frame.records.get("interrupts:" + cp.execution.pendingInterruptId);
687
- if (record.kind === "interrupts") {
688
- record.data.checkpointId = cp.id;
689
- record.data.revision = cp.revision;
690
- }
691
- }
692
- await new CommitCoordinator(this.backend).commit({
693
- commitId: this.ids.next("commit"),
694
- threadId: cp.threadId,
695
- expectedRevision: frame.cp.revision,
696
- checkpoint: cp,
697
- records: [...frame.records.values()],
698
- }, frame.lease);
699
- frame.cp = { ...structuredClone(cp), execution: frame.cp.execution };
700
- if (frame.cp.execution)
701
- frame.cp.execution.recordRevision = cp.revision;
702
- await this.hook(frame, AgentEvents.CheckpointCommitted, {
703
- checkpointId: cp.id,
704
- revision: cp.revision,
705
- });
706
- await this.hook(frame, AgentEvents.ThreadUpdated, {
707
- checkpointId: cp.id,
708
- revision: cp.revision,
709
- });
710
- }
711
- async prepareMessage(frame, message) {
712
- const value = await this.hook(frame, AgentEvents.MessagePrepare, immutable(message));
713
- if (value.id !== message.id ||
714
- value.role !== message.role ||
715
- value.toolCallId !== message.toolCallId)
716
- fail("InvalidMessage", "Message transform changed identity/role/correlation");
717
- const validated = MessageCodec.encode(MessageCodec.decode(value));
718
- await this.hook(frame, AgentEvents.MessageBeforeCommit, {
719
- message: validated,
720
- });
721
- return validated;
722
- }
723
- async appendMessage(frame, message) {
724
- const value = await this.prepareMessage(frame, message);
725
- frame.cp.state = {
726
- ...frame.cp.state,
727
- messages: [...frame.cp.state.messages, value],
728
- };
729
- if (value.role === "assistant" || value.role === "tool")
730
- frame.stream.outputMessageIds = [
731
- ...frame.stream.outputMessageIds,
732
- value.id,
733
- ];
734
- await this.commit(frame, "MessageCommitted");
735
- await this.hook(frame, AgentEvents.MessageCommitted, {
736
- message: value,
737
- checkpointId: frame.cp.id,
738
- });
739
- }
740
- loop(frame) {
741
- return loop(frame, {
155
+ executionContext(runtime = this.runtime()) {
156
+ const execution = {
157
+ agent: this,
158
+ runtime,
159
+ active: this.active,
160
+ pendingModels: this.pendingModels,
161
+ resumeAdmissions: this.resumeAdmissions,
742
162
  ids: this.ids,
743
163
  clock: this.clock,
744
- hook: this.hook.bind(this),
745
- put: this.put.bind(this),
746
- rows: this.rows.bind(this),
747
- commit: this.commit.bind(this),
748
- updateUsage: this.updateUsage.bind(this),
749
- appendMessage: this.appendMessage.bind(this),
750
- services: this.services.bind(this),
751
- finish: this.finish.bind(this),
752
- });
753
- }
754
- async restoreFrame(request, sink, registry, checkManifest = true) {
755
- const head = await this.backend.threads.get(request.threadId);
756
- if (!head)
757
- fail("ThreadNotFound", "Thread not found");
758
- const stored = await this.backend.executions.get("streams", request.streamId, { threadId: request.threadId });
759
- if (!stored)
760
- fail("StreamNotFound", "Execution not found");
761
- if (["completed", "failed", "stopped", "cancelled"].includes(stored.status))
762
- fail("AlreadyTerminal", "Execution already ended");
763
- const cp = structuredClone((await this.backend.checkpoints.get(head.currentCheckpointId)));
764
- if (cp.execution?.streamId !== request.streamId)
765
- fail("InvalidTransition", "Execution is not current thread work");
766
- if (checkManifest)
767
- validateManifest(cp.execution.registryManifest, registry, request.model);
768
- const lease = await this.backend.ownership.acquire({
769
- threadId: request.threadId,
770
- ownerToken: this.ids.next("owner"),
771
- ttlMs: 30000,
772
- });
773
- try {
774
- const records = new Map();
775
- for (const kind of [
776
- "streams",
777
- "turns",
778
- "model_attempts",
779
- "tool_executions",
780
- "interrupts",
781
- "plugin_states",
782
- ])
783
- for (const row of await this.backend.executions.listRecords(kind, request.threadId))
784
- records.set(kind + ":" + ("id" in row ? row.id : ""), {
785
- kind,
786
- id: "id" in row ? row.id : "",
787
- threadId: request.threadId,
788
- data: structuredClone(row),
789
- });
790
- const stream = records.get("streams:" + request.streamId).data;
791
- let frame;
792
- const state = new PluginStateAccess([
793
- childStateDefinition,
794
- ...registry.list("state"),
795
- ], { type: "thread", id: request.threadId }, cp.state.pluginStates, async () => {
796
- await this.commit(frame, "PluginStateCommitted");
797
- return frame.cp.id;
798
- }, undefined, this.backend.ownedState);
799
- frame = {
800
- request,
801
- sink,
802
- registry,
803
- lease,
804
- cp,
805
- stream,
806
- records,
807
- state,
808
- admitted: true,
809
- sequence: 0,
810
- };
811
- sink.setId(stream.id);
812
- if (request.model)
813
- cp.execution.modelRef = request.model;
814
- return frame;
815
- }
816
- catch (error) {
817
- await this.backend.ownership.release(lease);
818
- throw error;
819
- }
820
- }
821
- async prepareRecovery(request, sink, registry) {
822
- const frame = await this.restoreFrame(request, sink, registry);
823
- try {
824
- const plan = new RecoveryService().plan(frame.cp, this.rows(frame, "model_attempts"), this.rows(frame, "tool_executions"));
825
- if (plan.kind === "pending-interrupt")
826
- return frame;
827
- frame.stream.status = "interrupted";
828
- if (plan.kind === "incompatible")
829
- fail("ResumeIncompatible", "Invalid recovery cursor");
830
- if (plan.kind === "model") {
831
- plan.attempt.status = "interrupted";
832
- plan.attempt.endedAt = this.clock.now();
833
- plan.attempt.error = {
834
- source: "tessar",
835
- name: "TessarError",
836
- code: "InterruptedModelCall",
837
- message: "Provider outcome was not committed",
838
- retryable: true,
839
- };
840
- await this.commit(frame, "ExecutionTransition");
841
- const decision = await this.hook(frame, AgentEvents.ModelRecover, {
842
- attemptId: plan.attempt.id,
843
- error: plan.attempt.error,
844
- });
845
- if (decision?.decision !== "retry")
846
- fail("RecoveryRequired", "Model retry requires an explicit recovery policy");
847
- frame.cp.execution.phase = "BeforeModel";
848
- }
849
- if (plan.kind === "tool") {
850
- const record = plan.tool;
851
- record.status = "interrupted";
852
- await this.commit(frame, "ExecutionTransition");
853
- const tool = registry.find("tool", record.toolName);
854
- const result = tool?.recovery && record.idempotencyKey
855
- ? await tool.recovery.reconcile({
856
- id: record.id,
857
- input: tool.input.parse(record.arguments),
858
- key: record.idempotencyKey,
859
- })
860
- : { status: "unknown" };
861
- if (result.status === "unknown")
862
- fail("RecoveryRequired", "Tool dispatch outcome is unknown");
863
- if (result.status === "safeToRetry") {
864
- record.status = "pending";
865
- frame.cp.execution.phase = "BeforeTool";
866
- }
867
- if (result.status === "completed") {
868
- const transformed = await this.hook(frame, AgentEvents.ToolResult, result.result), message = MessageCodec.encode(new ToolMessage({
869
- toolCallId: record.toolCallId,
870
- content: transformed.content,
871
- metadata: transformed.metadata,
872
- }));
873
- record.status = "succeeded";
874
- record.result = transformed;
875
- record.resultMessageId = message.id;
876
- record.endedAt = this.clock.now();
877
- frame.cp.execution.pendingToolExecutionIds =
878
- frame.cp.execution.pendingToolExecutionIds.filter((id) => id !== record.id);
879
- frame.cp.execution.phase = frame.cp.execution
880
- .pendingToolExecutionIds.length
881
- ? "BeforeTool"
882
- : "TurnBoundary";
883
- await this.appendMessage(frame, message);
884
- }
885
- }
886
- frame.stream.status = "running";
887
- await this.commit(frame, "Resumed");
888
- return frame;
889
- }
890
- catch (error) {
891
- await this.backend.ownership.release(frame.lease);
892
- throw error;
893
- }
894
- }
895
- async suspend(frame, decision) {
896
- if (decision.decision !== "suspend")
897
- fail("InvalidTransition", "Expected suspension decision");
898
- const binding = frame.registry.find("interruption", decision.definitionId);
899
- const position = frame.cp.execution, id = this.ids.next("interrupt");
900
- const tool = position.pendingToolExecutionIds[0]
901
- ? this.rows(frame, "tool_executions").find((t) => t.id === position.pendingToolExecutionIds[0])
902
- : null;
903
- const interrupt = {
904
- id,
905
- threadId: frame.cp.threadId,
906
- streamId: frame.stream.id,
907
- checkpointId: frame.cp.id,
908
- definitionId: decision.definitionId,
909
- definitionVersion: decision.definitionVersion,
910
- pluginId: decision.pluginId,
911
- payload: decision.payload,
912
- responseSchema: binding.definition.response.jsonSchema,
913
- actionRef: tool
914
- ? {
915
- toolExecutionId: tool.id,
916
- toolName: tool.toolName,
917
- argumentsDigest: canonicalJson(tool.arguments),
918
- }
919
- : null,
920
- status: "pending",
921
- response: null,
922
- resolvedDecision: null,
923
- createdAt: this.clock.now(),
924
- resolvedAt: null,
925
- revision: frame.cp.revision,
164
+ toolConcurrency: this.toolConcurrency,
165
+ hook: (frame, definition, payload, toolExecutionId) => hook(execution, frame, definition, payload, toolExecutionId),
166
+ put,
167
+ rows,
168
+ commit: (frame, cause) => commit(execution, frame, cause),
169
+ updateUsage: (frame) => updateUsage(execution, frame),
170
+ prepareMessage: (frame, message) => prepareMessage(execution, frame, message),
171
+ appendMessage: (frame, message) => appendMessage(execution, frame, message),
172
+ services: (frame, toolExecutionId) => services(execution, frame, toolExecutionId),
173
+ finish: (frame, status, reason, error) => finish(execution, frame, status, reason, error),
926
174
  };
927
- this.put(frame, "interrupts", interrupt);
928
- position.pendingInterruptId = id;
929
- position.phase = "Suspended";
930
- frame.stream.status = "suspended";
931
- await this.commit(frame, "Suspended");
932
- await this.hook(frame, AgentEvents.StreamSuspended, {
933
- checkpointId: frame.cp.id,
934
- interrupt,
935
- });
936
- throw new Suspension();
937
- }
938
- async prepareResume(request, sink, registry) {
939
- const existing = this.resumeAdmissions.get(request.interruptId);
940
- if (existing) {
941
- await existing;
942
- fail("AlreadyResumed", "Another response was already committed");
943
- }
944
- const pending = this.prepareResumeFrame(request, sink, registry);
945
- this.resumeAdmissions.set(request.interruptId, pending);
946
- try {
947
- return await pending;
948
- }
949
- finally {
950
- this.resumeAdmissions.delete(request.interruptId);
951
- }
175
+ return execution;
952
176
  }
953
- async prepareResumeFrame(request, sink, registry) {
954
- await validateResume(this.backend, registry, request.threadId, request.interruptId, request.value);
955
- const head = await this.backend.threads.get(request.threadId);
956
- if (!head)
957
- fail("ThreadNotFound", "Thread not found");
958
- const stored = (await this.backend.checkpoints.get(head.currentCheckpointId));
959
- if (!stored.execution ||
960
- stored.execution.pendingInterruptId !== request.interruptId)
961
- fail("InvalidTransition", "Thread is not waiting for this interruption");
962
- validateManifest(stored.execution.registryManifest, registry, request.model);
963
- const lease = await this.backend.ownership.acquire({
964
- threadId: request.threadId,
965
- ownerToken: this.ids.next("owner"),
966
- ttlMs: 30000,
967
- });
968
- try {
969
- const { interrupt, decision } = await validateResume(this.backend, registry, request.threadId, request.interruptId, request.value);
970
- const cp = structuredClone(stored), records = new Map();
971
- for (const kind of [
972
- "streams",
973
- "turns",
974
- "model_attempts",
975
- "tool_executions",
976
- "interrupts",
977
- "plugin_states",
978
- ])
979
- for (const row of await this.backend.executions.listRecords(kind, request.threadId))
980
- records.set(kind + ":" + ("id" in row ? row.id : ""), {
981
- kind,
982
- id: "id" in row ? row.id : "",
983
- threadId: request.threadId,
984
- data: structuredClone(row),
985
- });
986
- const stream = records.get("streams:" + interrupt.streamId).data;
987
- if (interrupt.actionRef?.toolExecutionId) {
988
- const tool = this.rows({ records }, "tool_executions").find((t) => t.id === interrupt.actionRef.toolExecutionId);
989
- if (!tool ||
990
- canonicalJson(tool.arguments) !== interrupt.actionRef.argumentsDigest)
991
- fail("ResumeIncompatible", "Pending operation payload changed");
992
- }
993
- let frame;
994
- const state = new PluginStateAccess([
995
- childStateDefinition,
996
- ...registry.list("state"),
997
- ], { type: "thread", id: request.threadId }, cp.state.pluginStates, async () => {
998
- await this.commit(frame, "PluginStateCommitted");
999
- return frame.cp.id;
1000
- }, undefined, this.backend.ownedState);
1001
- frame = {
1002
- request,
1003
- sink,
1004
- registry,
1005
- lease,
1006
- cp,
1007
- stream,
1008
- records,
1009
- state,
1010
- admitted: true,
1011
- sequence: 0,
1012
- };
1013
- const position = cp.execution, cursor = position.handlerCursor;
1014
- if (!cursor)
1015
- fail("ResumeIncompatible", "Missing continuation cursor");
1016
- const previous = cursor.decisions.at(-2);
1017
- const resolved = previous && ["deny", "stop"].includes(previous.decision)
1018
- ? previous
1019
- : decision;
1020
- cursor.decisions = [
1021
- ...cursor.decisions.slice(0, -1),
1022
- resolved,
1023
- ];
1024
- position.phase = phaseForEvent(cursor.eventId);
1025
- position.pendingInterruptId = null;
1026
- if (request.model) {
1027
- position.modelRef = request.model;
1028
- position.registryManifest = position.registryManifest.filter((r) => r.kind !== "provider");
1029
- const provider = registry.values.find((r) => r.kind === "provider" && r.id === request.model.providerId);
1030
- if (!provider)
1031
- fail("MissingProvider", "Override model provider missing");
1032
- position.registryManifest = [
1033
- ...position.registryManifest,
1034
- {
1035
- pluginId: provider.pluginId,
1036
- kind: provider.kind,
1037
- id: provider.id,
1038
- version: provider.version,
1039
- requiredForContinuation: true,
1040
- },
1041
- ];
1042
- }
1043
- stream.status = "running";
1044
- this.put(frame, "interrupts", {
1045
- ...interrupt,
1046
- status: "resolved",
1047
- response: request.value,
1048
- resolvedDecision: resolved,
1049
- resolvedAt: this.clock.now(),
1050
- revision: cp.revision + 1,
1051
- });
1052
- sink.setId(stream.id);
1053
- await this.commit(frame, "Resumed");
1054
- return frame;
1055
- }
1056
- catch (error) {
1057
- await this.backend.ownership.release(lease);
1058
- throw error;
1059
- }
1060
- }
1061
- async finish(frame, status, reason, error = null) {
1062
- const pending = new Set();
1063
- for (const message of frame.cp.state.messages) {
1064
- for (const part of message.content)
1065
- if (part.type === "tool-call")
1066
- pending.add(part.toolCallId);
1067
- if (message.role === "tool")
1068
- pending.delete(message.toolCallId);
1069
- }
1070
- for (const toolCallId of pending) {
1071
- const message = MessageCodec.encode(new ToolMessage({ toolCallId, content: "Execution " + status }));
1072
- frame.cp.state = {
1073
- ...frame.cp.state,
1074
- messages: [...frame.cp.state.messages, message],
1075
- };
1076
- frame.stream.outputMessageIds = [
1077
- ...frame.stream.outputMessageIds,
1078
- message.id,
1079
- ];
1080
- const tool = this.rows(frame, "tool_executions").find((t) => t.toolCallId === toolCallId && t.streamId === frame.stream.id);
1081
- if (tool && tool.status === "pending") {
1082
- tool.status = "cancelled";
1083
- tool.resultMessageId = message.id;
1084
- tool.endedAt = this.clock.now();
1085
- }
1086
- }
1087
- frame.stream.status = status;
1088
- frame.stream.stopReason = reason;
1089
- frame.stream.error = error;
1090
- frame.stream.endedAt = this.clock.now();
1091
- await this.updateUsage(frame);
1092
- let outcome = {
1093
- status,
1094
- reason,
1095
- output: frame.cp.state.messages.filter((m) => frame.stream.outputMessageIds.includes(m.id)),
1096
- usage: frame.stream.directUsage,
1097
- error,
1098
- interrupt: null,
1099
- };
1100
- try {
1101
- await this.hook(frame, AgentEvents.StreamBeforeEnd, immutable(outcome));
1102
- }
1103
- catch (finalizationError) {
1104
- if (!error && status !== "cancelled") {
1105
- status = "failed";
1106
- error = errorInfo(finalizationError);
1107
- reason = "FinalizationFailed";
1108
- frame.stream.status = status;
1109
- frame.stream.error = error;
1110
- frame.stream.stopReason = reason;
1111
- outcome = { ...outcome, status, error, reason };
1112
- }
1113
- }
1114
- const activeTurns = this.rows(frame, "turns").filter((turn) => turn.status === "running");
1115
- for (const turn of activeTurns) {
1116
- turn.status = status;
1117
- turn.stopReason = reason;
1118
- turn.error = error;
1119
- turn.endedAt = this.clock.now();
1120
- turn.durationMs = Date.parse(turn.endedAt) - Date.parse(turn.startedAt);
1121
- }
1122
- frame.cp.execution = null;
1123
- await this.commit(frame, "Terminal");
1124
- for (const turn of activeTurns)
1125
- await this.hook(frame, AgentEvents.TurnEnd, {
1126
- turnId: turn.id,
1127
- usage: turn.usage,
1128
- });
1129
- await this.hook(frame, AgentEvents.StreamEnd, immutable(outcome));
1130
- }
1131
- async cancel(threadId, streamId, reason = "Cancelled", target = { kind: "observer", observe: () => { } }) {
1132
- const active = this.active.get(streamId);
1133
- if (active) {
1134
- if (active.cp.threadId !== threadId)
1135
- fail("InvalidTransition", "Execution belongs to a different thread");
1136
- await this.hook(active, AgentEvents.StreamCancel, { reason });
1137
- active.sink.controller.abort(reason);
1138
- return;
1139
- }
1140
- const registry = this.registry.capture(), sink = createDelivery(threadId, this.ids, target, undefined, this.runtime().report);
1141
- let frame;
1142
- try {
1143
- frame = await this.restoreFrame({ kind: "recover", threadId, streamId }, sink, registry, false);
1144
- this.watchLease(frame);
1145
- if (frame.cp.execution?.phase === "ToolPending") {
1146
- const toolRecord = this.rows(frame, "tool_executions").find((t) => t.id === frame.cp.execution.pendingToolExecutionIds[0]);
1147
- const tool = registry.find("tool", toolRecord.toolName);
1148
- const outcome = tool?.recovery && toolRecord.idempotencyKey
1149
- ? await tool.recovery.reconcile({
1150
- id: toolRecord.id,
1151
- input: tool.input.parse(toolRecord.arguments),
1152
- key: toolRecord.idempotencyKey,
1153
- })
1154
- : { status: "unknown" };
1155
- if (outcome.status === "unknown") {
1156
- frame.stream.status = "interrupted";
1157
- toolRecord.status = "interrupted";
1158
- await this.commit(frame, "ExecutionTransition");
1159
- fail("RecoveryRequired", "Cannot close unknown external tool outcome");
1160
- }
1161
- if (outcome.status === "completed") {
1162
- toolRecord.status = "succeeded";
1163
- toolRecord.result = outcome.result;
1164
- const message = MessageCodec.encode(new ToolMessage({
1165
- toolCallId: toolRecord.toolCallId,
1166
- content: outcome.result.content,
1167
- }));
1168
- toolRecord.resultMessageId = message.id;
1169
- await this.appendMessage(frame, message);
1170
- }
1171
- else
1172
- toolRecord.status = "pending";
1173
- }
1174
- for (const interrupt of this.rows(frame, "interrupts"))
1175
- if (interrupt.streamId === streamId && interrupt.status === "pending") {
1176
- interrupt.status = "cancelled";
1177
- interrupt.resolvedAt = this.clock.now();
1178
- }
1179
- for (const attempt of this.rows(frame, "model_attempts"))
1180
- if (attempt.streamId === streamId && attempt.status === "running") {
1181
- attempt.status = "cancelled";
1182
- attempt.endedAt = this.clock.now();
1183
- }
1184
- await this.hook(frame, AgentEvents.StreamCancel, { reason });
1185
- await this.finish(frame, "cancelled", reason);
1186
- }
1187
- finally {
1188
- if (frame) {
1189
- if (frame.heartbeat)
1190
- clearInterval(frame.heartbeat);
1191
- await this.backend.ownership.release(frame.lease);
1192
- }
1193
- await registry.release();
1194
- sink.close();
1195
- }
177
+ async cancel(threadId, streamId, reason = "Cancelled", target = { kind: "observer", observe: () => { } }, context = {}) {
178
+ return cancel(this.executionContext(), threadId, streamId, reason, target, context);
1196
179
  }
1197
180
  async shutdown() {
1198
181
  this.closed = true;
1199
182
  for (const frame of this.active.values())
1200
183
  frame.sink.controller.abort("Controller disposed");
1201
184
  await Promise.allSettled([...this.jobs]);
185
+ this.pendingModels.clear();
186
+ this.releaseId();
1202
187
  }
1203
188
  }
1204
189
  //# sourceMappingURL=agent.js.map