@dsh-enhanced/assistant-automations 0.1.7 → 0.1.12

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 (52) hide show
  1. package/README.md +37 -7
  2. package/lib/coordinator.d.ts +45 -5
  3. package/lib/coordinator.d.ts.map +1 -1
  4. package/lib/coordinator.js +418 -86
  5. package/lib/coordinator.js.map +1 -1
  6. package/lib/growth.d.ts +120 -0
  7. package/lib/growth.d.ts.map +1 -0
  8. package/lib/growth.js +383 -0
  9. package/lib/growth.js.map +1 -0
  10. package/lib/host-executors.d.ts +25 -0
  11. package/lib/host-executors.d.ts.map +1 -0
  12. package/lib/host-executors.js +181 -0
  13. package/lib/host-executors.js.map +1 -0
  14. package/lib/index.d.ts +1 -1
  15. package/lib/index.d.ts.map +1 -1
  16. package/lib/index.js +1 -1
  17. package/lib/index.js.map +1 -1
  18. package/lib/operator.d.ts +2 -0
  19. package/lib/operator.d.ts.map +1 -1
  20. package/lib/operator.js +6 -2
  21. package/lib/operator.js.map +1 -1
  22. package/lib/proposals.d.ts +2 -2
  23. package/lib/proposals.d.ts.map +1 -1
  24. package/lib/proposals.js +34 -22
  25. package/lib/proposals.js.map +1 -1
  26. package/lib/runner.d.ts +28 -2
  27. package/lib/runner.d.ts.map +1 -1
  28. package/lib/runner.js +524 -73
  29. package/lib/runner.js.map +1 -1
  30. package/lib/service.d.ts +109 -4
  31. package/lib/service.d.ts.map +1 -1
  32. package/lib/service.js +987 -16
  33. package/lib/service.js.map +1 -1
  34. package/lib/sqlite.d.ts +1 -1
  35. package/lib/sqlite.d.ts.map +1 -1
  36. package/lib/sqlite.js +369 -41
  37. package/lib/sqlite.js.map +1 -1
  38. package/lib/store.d.ts +175 -1
  39. package/lib/store.d.ts.map +1 -1
  40. package/lib/store.js +1889 -71
  41. package/lib/store.js.map +1 -1
  42. package/lib/tools.js +1 -1
  43. package/lib/tools.js.map +1 -1
  44. package/lib/types.d.ts +354 -11
  45. package/lib/types.d.ts.map +1 -1
  46. package/lib/types.js +13 -1
  47. package/lib/types.js.map +1 -1
  48. package/lib/version.d.ts +1 -1
  49. package/lib/version.d.ts.map +1 -1
  50. package/lib/version.js +1 -1
  51. package/lib/version.js.map +1 -1
  52. package/package.json +10 -8
package/lib/runner.js CHANGED
@@ -1,8 +1,11 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { installModelSelection, } from '@deepseek-ai/dsh-agent';
2
3
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
3
4
  import { SessionId } from '@deepseek-ai/dsh-session';
4
5
  import { resolveLlmRouteCapability } from '@dsh-enhanced/llm-route-capabilities';
5
- import { AutomationRunnerAmbiguousError, } from './coordinator.js';
6
+ import { HostAutomationExecutorRegistry, HostExecutorRegistryError, } from './host-executors.js';
7
+ import { AutomationRunnerAmbiguousError, AutomationRunnerFailureError, } from './coordinator.js';
8
+ import { isHostAutomationDefinition } from './types.js';
6
9
  const usageFields = [
7
10
  'inputTokens',
8
11
  'outputTokens',
@@ -47,18 +50,109 @@ function summarize(events, signal) {
47
50
  else if (kind === 'blocked' || kind === 'error')
48
51
  outcome = 'failed';
49
52
  }
50
- return { outcome, output, usage };
53
+ const turnKind = typeof turnReason === 'object' && turnReason !== null && 'kind' in turnReason
54
+ ? String(turnReason.kind)
55
+ : undefined;
56
+ return { outcome, output, usage, ...(turnKind === undefined ? {} : { turnKind }) };
51
57
  }
52
- export class AutomationBudgetReplayError extends Error {
58
+ function stableFailureCode(error, phase) {
59
+ if (typeof error === 'object' && error !== null && 'code' in error) {
60
+ const code = error.code;
61
+ if (typeof code === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/u.test(code))
62
+ return code;
63
+ }
64
+ const defaults = {
65
+ preflight: 'preflight-failed',
66
+ 'artifact-write': 'artifact-write-failed',
67
+ 'budget-reservation': 'budget-reservation-failed',
68
+ 'preset-resolution': 'preset-resolution-failed',
69
+ 'agent-creation': 'agent-creation-failed',
70
+ 'agent-disposal': 'agent-disposal-failed',
71
+ 'agent-setup': 'agent-setup-failed',
72
+ 'prompt-submission': 'prompt-submission-failed',
73
+ 'model-execution': 'model-execution-failed',
74
+ 'session-flush': 'session-flush-failed',
75
+ 'budget-settlement': 'budget-settlement-failed',
76
+ };
77
+ return defaults[phase] ?? 'runner-failed';
78
+ }
79
+ function failureClass(phase, signal, error) {
80
+ if (signal.aborted)
81
+ return 'cancelled';
82
+ if (phase === 'budget-reservation' || phase === 'budget-settlement')
83
+ return 'budget';
84
+ if (typeof error === 'object' && error !== null && 'name' in error
85
+ && (error.name === 'AssistantPolicyError' || error.name === 'PolicyLedgerError'))
86
+ return 'policy';
87
+ if (phase === 'preflight' || phase === 'preset-resolution' || phase === 'agent-setup')
88
+ return 'configuration';
89
+ if (phase === 'agent-creation' || phase === 'agent-disposal' || phase === 'session-flush')
90
+ return 'infrastructure';
91
+ if (phase === 'prompt-submission' || phase === 'model-execution')
92
+ return 'provider';
93
+ return 'unknown';
94
+ }
95
+ function diagnostic(input) {
96
+ return Object.freeze({
97
+ schemaVersion: 1,
98
+ ...input,
99
+ retryability: input.failureClass === 'none'
100
+ ? input.sideEffectState === 'none' ? 'safe' : 'unsafe'
101
+ : input.sideEffectState === 'possible' || input.sideEffectState === 'unknown'
102
+ ? 'unsafe'
103
+ : input.failureClass === 'configuration' || input.failureClass === 'policy' || input.failureClass === 'budget'
104
+ ? 'after-intervention'
105
+ : 'safe',
106
+ });
107
+ }
108
+ export class AutomationBudgetReplayError extends AutomationRunnerFailureError {
53
109
  reservationStatus;
54
110
  code = 'automation-budget-reservation-replayed';
55
111
  constructor(reservationStatus) {
56
- super(`assistant-automations: budget reservation replay is ${reservationStatus}; refusing duplicate execution`);
112
+ super(`assistant-automations: budget reservation replay is ${reservationStatus}; refusing duplicate execution`, diagnostic({
113
+ failureClass: 'budget',
114
+ failurePhase: 'budget-reservation',
115
+ failureCode: 'automation-budget-reservation-replayed',
116
+ promptSubmissionState: reservationStatus === 'released' ? 'not-submitted' : 'unknown',
117
+ sideEffectState: reservationStatus === 'released' ? 'none' : 'unknown',
118
+ budgetSettlementState: reservationStatus,
119
+ }));
57
120
  this.reservationStatus = reservationStatus;
58
121
  this.name = 'AutomationBudgetReplayError';
59
122
  }
60
123
  }
61
124
  export const AUTOMATION_RUN_BUDGET_METRIC = 'automation-runs';
125
+ const RECOVERY_HOST_EXECUTOR_ID = 'assistant-recovery';
126
+ /**
127
+ * Bind a Host reservation to the exact immutable execution snapshot. The
128
+ * digest avoids delimiter collisions in caller-controlled ids while keeping
129
+ * the Policy ledger key bounded and content-free.
130
+ */
131
+ function hostBudgetIdempotencyKey(input, immutableDefinitionHash, budgetId) {
132
+ const digest = createHash('sha256').update(JSON.stringify([
133
+ 'assistant-automations-host-budget/v1',
134
+ input.automation.id,
135
+ input.occurrence.id,
136
+ immutableDefinitionHash,
137
+ AUTOMATION_RUN_BUDGET_METRIC,
138
+ budgetId,
139
+ ])).digest('hex');
140
+ return `automation-budget:host:v1:${digest}`;
141
+ }
142
+ function exactHostRunnerInput(input) {
143
+ return input.occurrence.automationId === input.automation.id
144
+ && input.task.automationId === input.automation.id
145
+ && input.task.occurrenceId === input.occurrence.id
146
+ && input.task.status === 'running'
147
+ && Number.isSafeInteger(input.task.attemptCount)
148
+ && input.task.attemptCount >= 1;
149
+ }
150
+ function isAuthorizedRecoveryResume(input, definition) {
151
+ return input.task.attemptCount > 1
152
+ && input.task.attemptCount <= definition.maxRetries + 1
153
+ && definition.retrySafety === 'idempotent'
154
+ && definition.execution.executorId === RECOVERY_HOST_EXECUTOR_ID;
155
+ }
62
156
  /** Prove the configured Policy budget uses the fixed per-run unit before reservation. */
63
157
  export function assertAutomationRunBudget(policy, budgetId) {
64
158
  const budget = policy.getBudgetConfig(budgetId);
@@ -87,88 +181,118 @@ export class DshAutomationRunner {
87
181
  this.allowUnbudgetedExecution = options.allowUnbudgetedExecution ?? false;
88
182
  }
89
183
  async run(input) {
90
- const agents = this.ctx.get('agents');
91
- const sessions = this.ctx.get('sessions');
92
- const tools = this.ctx.get('tools');
93
- const llm = this.ctx.get('llm');
94
- if (agents === undefined || sessions === undefined || tools === undefined || llm === undefined) {
95
- throw new Error('assistant-automations: agents, sessions, and tools services are required');
96
- }
97
- const allowed = new Set(input.occurrence.dryRun ? [] : input.automation.definition.allowedTools);
98
- if (input.signal.aborted)
99
- throw new Error('assistant-automations: run was cancelled before Agent creation');
100
- const budgetId = input.automation.definition.budgetId;
101
- if (budgetId === undefined && !this.allowUnbudgetedExecution) {
102
- throw new Error('assistant-automations: a configured budget is required for unattended execution');
184
+ const definition = input.automation.definition;
185
+ if (isHostAutomationDefinition(definition)) {
186
+ throw new AutomationRunnerFailureError('assistant-automations: Host definitions require a registered Host executor', diagnostic({
187
+ failureClass: 'configuration', failurePhase: 'executor-availability',
188
+ failureCode: 'host-executor-router-missing', promptSubmissionState: 'not-applicable',
189
+ sideEffectState: 'none', budgetSettlementState: definition.budgetId === undefined
190
+ ? 'not-required'
191
+ : 'not-reserved',
192
+ }));
103
193
  }
104
- if (budgetId !== undefined)
105
- assertAutomationRunBudget(this.policy, budgetId);
106
- const agentPresets = this.ctx.get('agentPresets');
107
- const presetId = agentPresets === undefined
108
- ? input.automation.definition.agentPreset
109
- : (await agentPresets.resolve(input.automation.definition.agentPreset)).id;
194
+ const budgetId = definition.budgetId;
110
195
  let reservationId;
111
196
  let reservationSettled = false;
112
- if (budgetId !== undefined) {
113
- const reservation = this.policy.reserve({
114
- budgetId,
115
- subject: {
116
- kind: 'background',
117
- id: input.automation.id,
118
- workspace: input.automation.definition.workspace,
119
- principal: input.automation.definition.principal,
120
- },
121
- amount: input.automation.definition.budgetAmount,
122
- idempotencyKey: [
123
- 'automation-budget', input.automation.id, input.occurrence.id, AUTOMATION_RUN_BUDGET_METRIC, budgetId,
124
- ].join(':'),
125
- });
126
- if (reservation.replayed || reservation.status !== 'reserved') {
127
- throw new AutomationBudgetReplayError(reservation.status);
128
- }
129
- reservationId = reservation.reservationId;
130
- }
131
197
  let handle;
132
198
  let agentCreationSubmitted = false;
133
199
  let followupSubmitted = false;
134
200
  let removeAbort;
135
- let result;
136
201
  let toolCalls = 0;
202
+ let phase = 'preflight';
203
+ let promptSubmissionState = 'not-submitted';
204
+ let sideEffectState = 'none';
205
+ let budgetSettlementState = budgetId === undefined
206
+ ? 'not-required'
207
+ : 'not-reserved';
137
208
  try {
209
+ const agents = this.ctx.get('agents');
210
+ const sessions = this.ctx.get('sessions');
211
+ const tools = this.ctx.get('tools');
212
+ const llm = this.ctx.get('llm');
213
+ if (agents === undefined || sessions === undefined || tools === undefined || llm === undefined) {
214
+ throw new Error('assistant-automations: agents, sessions, and tools services are required');
215
+ }
216
+ const allowed = new Set(input.occurrence.dryRun ? [] : definition.allowedTools);
217
+ if (input.signal.aborted)
218
+ throw new Error('assistant-automations: run was cancelled before Agent creation');
219
+ phase = 'budget-reservation';
220
+ if (budgetId === undefined && !this.allowUnbudgetedExecution) {
221
+ throw new Error('assistant-automations: a configured budget is required for unattended execution');
222
+ }
223
+ if (budgetId !== undefined)
224
+ assertAutomationRunBudget(this.policy, budgetId);
225
+ const agentPresets = this.ctx.get('agentPresets');
226
+ phase = 'preset-resolution';
227
+ const presetId = agentPresets === undefined
228
+ ? definition.agentPreset
229
+ : (await agentPresets.resolve(definition.agentPreset)).id;
230
+ if (budgetId !== undefined) {
231
+ phase = 'budget-reservation';
232
+ const reservation = this.policy.reserve({
233
+ budgetId,
234
+ subject: {
235
+ kind: 'background',
236
+ id: input.automation.id,
237
+ workspace: definition.workspace,
238
+ principal: definition.principal,
239
+ },
240
+ amount: definition.budgetAmount,
241
+ idempotencyKey: [
242
+ 'automation-budget', input.automation.id, input.occurrence.id, AUTOMATION_RUN_BUDGET_METRIC, budgetId,
243
+ ].join(':'),
244
+ });
245
+ if (reservation.replayed || reservation.status !== 'reserved') {
246
+ throw new AutomationBudgetReplayError(reservation.status);
247
+ }
248
+ reservationId = reservation.reservationId;
249
+ budgetSettlementState = 'reserved';
250
+ }
251
+ phase = 'preflight';
138
252
  const globalNames = tools.schemas().map(schema => schema.name);
253
+ const execution = Object.freeze({
254
+ mode: input.occurrence.dryRun ? 'preview' : 'production',
255
+ automationId: input.automation.id,
256
+ occurrenceId: input.occurrence.id,
257
+ });
258
+ phase = 'agent-creation';
139
259
  agentCreationSubmitted = true;
140
260
  handle = await agents.create({
141
261
  sessionId: SessionId(input.sessionId),
142
262
  meta: {
143
- cwd: input.automation.definition.workspace,
263
+ cwd: definition.workspace,
144
264
  agentPreset: presetId,
145
265
  },
146
266
  agentOptions: {
147
- provider: input.automation.definition.provider,
148
- model: input.automation.definition.model,
149
- maxTokens: input.automation.definition.maxOutputTokens,
267
+ provider: definition.provider,
268
+ model: definition.model,
269
+ maxTokens: definition.maxOutputTokens,
150
270
  },
151
271
  signal: input.signal,
152
272
  setup: async (agentCtx) => {
273
+ phase = 'agent-setup';
153
274
  if (agentCtx.agent === undefined) {
154
275
  throw new Error('assistant-automations: unpublished Agent identity is missing during setup');
155
276
  }
156
- if (agentCtx.agent.session.header.cwd !== input.automation.definition.workspace
277
+ if (agentCtx.agent.session.header.cwd !== definition.workspace
157
278
  || agentCtx.agent.session.header.agentPreset !== presetId) {
158
279
  throw new Error('assistant-automations: background Agent identity does not match the immutable definition');
159
280
  }
281
+ agentCtx.provide('assistantAutomationExecution', execution);
160
282
  const unbindInitiator = this.policy.bindInitiator(agentCtx.agent, 'background');
161
283
  agentCtx.effect(() => unbindInitiator, 'assistant-automations.background-initiator');
162
284
  const delivery = this.ctx.get('assistantDelivery');
163
- const bindingId = input.automation.definition.deliveryBindingId;
285
+ // Approval delegation is independent from ordinary run-result
286
+ // delivery. Legacy definitions retain their historical route.
287
+ const bindingId = definition.approvalBindingId ?? definition.deliveryBindingId;
164
288
  if (bindingId !== undefined && typeof delivery?.bindAgentApprovalRoute === 'function') {
165
289
  const unbindApproval = delivery.bindAgentApprovalRoute(agentCtx.agent, { bindingId });
166
290
  agentCtx.effect(() => unbindApproval, 'assistant-automations.approval-route');
167
291
  }
168
292
  installModelSelection(agentCtx, {
169
293
  current: {
170
- provider: input.automation.definition.provider,
171
- model: input.automation.definition.model,
294
+ provider: definition.provider,
295
+ model: definition.model,
172
296
  },
173
297
  assembled: undefined,
174
298
  });
@@ -187,11 +311,11 @@ export class DshAutomationRunner {
187
311
  if (outsideAllowlist.length > 0) {
188
312
  throw new Error(`assistant-automations: preset ${presetId} exposes tools outside the immutable allowlist: ${outsideAllowlist.join(', ')}`);
189
313
  }
190
- requireAdapterToolCallProtocol(llm, input.automation.definition.provider, input.automation.definition.model, presetId, finalSchemas.length);
314
+ requireAdapterToolCallProtocol(llm, definition.provider, definition.model, presetId, finalSchemas.length);
191
315
  agentCtx.tools.guard((execution) => {
192
316
  if (!allowed.has(execution.name))
193
317
  return 'assistant-automations: tool is outside the immutable allowlist';
194
- if (toolCalls >= input.automation.definition.maxToolCalls) {
318
+ if (toolCalls >= definition.maxToolCalls) {
195
319
  return 'assistant-automations: immutable tool-call budget exhausted';
196
320
  }
197
321
  toolCalls += 1;
@@ -199,59 +323,386 @@ export class DshAutomationRunner {
199
323
  });
200
324
  },
201
325
  });
326
+ sideEffectState = 'possible';
202
327
  const agent = handle.agent;
328
+ const evolution = this.ctx.get('assistantEvolution');
329
+ if (execution.mode === 'production'
330
+ && typeof evolution?.injectAutomationGuidance === 'function') {
331
+ evolution.injectAutomationGuidance(agent, execution);
332
+ }
203
333
  const abort = () => agent.cancel({ kind: 'hook', reason: 'assistant-automations-signal' });
204
334
  input.signal.addEventListener('abort', abort, { once: true });
205
335
  removeAbort = () => input.signal.removeEventListener('abort', abort);
206
- followupSubmitted = true;
336
+ phase = 'prompt-submission';
337
+ promptSubmissionState = 'unknown';
207
338
  agent.followup(createUserMessage({
208
- content: [{ type: 'text', text: input.automation.definition.prompt }],
339
+ content: [{ type: 'text', text: definition.prompt }],
209
340
  source: {
210
341
  kind: 'plugin',
211
342
  plugin: '@dsh-enhanced/assistant-automations',
212
343
  form: 'notice',
213
- summary: `Automation: ${input.automation.definition.name}`,
344
+ summary: `Automation: ${definition.name}`,
214
345
  },
215
346
  }));
347
+ promptSubmissionState = 'submitted';
348
+ followupSubmitted = true;
349
+ phase = 'model-execution';
216
350
  await agent.whenIdle();
217
- result = {
218
- ...summarize(agent.session.events, input.signal),
219
- sessionId: input.sessionId,
220
- };
221
- result = { ...result, usage: { ...result.usage, toolCalls } };
351
+ const summary = summarize(agent.session.events, input.signal);
352
+ sideEffectState = 'possible';
353
+ phase = 'session-flush';
222
354
  await sessions.flush(agent.session);
223
355
  if (reservationId !== undefined) {
224
- this.policy.finalize(reservationId, input.automation.definition.budgetAmount);
356
+ phase = 'budget-settlement';
357
+ this.policy.finalize(reservationId, definition.budgetAmount);
225
358
  reservationSettled = true;
359
+ budgetSettlementState = 'finalized';
226
360
  }
227
- return result;
361
+ const classified = summary.outcome === 'succeeded'
362
+ ? { failureClass: 'none', failurePhase: 'none', failureCode: 'none' }
363
+ : summary.outcome === 'cancelled'
364
+ ? { failureClass: 'cancelled', failurePhase: 'model-execution', failureCode: 'agent-turn-cancelled' }
365
+ : summary.outcome === 'unknown'
366
+ ? { failureClass: 'execution', failurePhase: 'model-execution', failureCode: 'agent-turn-unknown' }
367
+ : {
368
+ failureClass: 'execution',
369
+ failurePhase: 'model-execution',
370
+ failureCode: summary.turnKind === 'blocked' ? 'agent-turn-blocked' : 'agent-turn-failed',
371
+ };
372
+ const completed = {
373
+ outcome: summary.outcome,
374
+ output: summary.output,
375
+ usage: { ...summary.usage, toolCalls },
376
+ sessionId: input.sessionId,
377
+ diagnostic: diagnostic({
378
+ ...classified,
379
+ promptSubmissionState,
380
+ sideEffectState,
381
+ budgetSettlementState,
382
+ }),
383
+ };
384
+ removeAbort?.();
385
+ removeAbort = undefined;
386
+ phase = 'agent-disposal';
387
+ await handle?.dispose();
388
+ handle = undefined;
389
+ return completed;
228
390
  }
229
391
  catch (error) {
392
+ if (error instanceof AutomationBudgetReplayError)
393
+ throw error;
230
394
  let caught = error;
231
395
  if (reservationId !== undefined && !reservationSettled) {
232
396
  try {
233
- if (!agentCreationSubmitted)
397
+ if (!agentCreationSubmitted) {
234
398
  this.policy.release(reservationId);
235
- else
236
- this.policy.finalize(reservationId, input.automation.definition.budgetAmount);
399
+ budgetSettlementState = 'released';
400
+ }
401
+ else {
402
+ this.policy.finalize(reservationId, definition.budgetAmount);
403
+ budgetSettlementState = 'finalized';
404
+ }
237
405
  reservationSettled = true;
238
406
  }
239
407
  catch (settlementError) {
240
- caught = new AutomationRunnerAmbiguousError(`assistant-automations: conservative budget settlement failed: ${settlementError instanceof Error ? settlementError.message : String(settlementError)}`, { cause: settlementError });
408
+ phase = 'budget-settlement';
409
+ budgetSettlementState = 'unknown';
410
+ sideEffectState = agentCreationSubmitted
411
+ ? 'possible'
412
+ : promptSubmissionState === 'not-submitted' ? 'none' : 'unknown';
413
+ caught = new AutomationRunnerAmbiguousError(`assistant-automations: conservative budget settlement failed: ${settlementError instanceof Error ? settlementError.message : String(settlementError)}`, {
414
+ cause: settlementError,
415
+ diagnostic: diagnostic({
416
+ failureClass: 'budget',
417
+ failurePhase: 'budget-settlement',
418
+ failureCode: stableFailureCode(settlementError, 'budget-settlement'),
419
+ promptSubmissionState,
420
+ sideEffectState,
421
+ budgetSettlementState,
422
+ }),
423
+ });
241
424
  }
242
425
  }
243
- if (caught instanceof AutomationBudgetReplayError) {
426
+ if (caught instanceof AutomationRunnerAmbiguousError)
244
427
  throw caught;
428
+ sideEffectState = agentCreationSubmitted
429
+ ? 'possible'
430
+ : promptSubmissionState === 'not-submitted' ? 'none' : 'unknown';
431
+ const receipt = diagnostic({
432
+ failureClass: failureClass(phase, input.signal, caught),
433
+ failurePhase: phase,
434
+ failureCode: stableFailureCode(caught, phase),
435
+ promptSubmissionState,
436
+ sideEffectState,
437
+ budgetSettlementState,
438
+ });
439
+ if (followupSubmitted || promptSubmissionState !== 'not-submitted') {
440
+ throw new AutomationRunnerAmbiguousError(`assistant-automations: Agent execution may have produced side effects: ${caught instanceof Error ? caught.message : String(caught)}`, { cause: caught, diagnostic: receipt });
245
441
  }
246
- if (followupSubmitted) {
247
- throw new AutomationRunnerAmbiguousError(`assistant-automations: Agent execution may have produced side effects: ${caught instanceof Error ? caught.message : String(caught)}`, { cause: caught });
248
- }
249
- throw caught;
442
+ throw new AutomationRunnerFailureError(caught instanceof Error ? caught.message : String(caught), receipt, { cause: caught });
250
443
  }
251
444
  finally {
252
445
  removeAbort?.();
253
- await handle?.dispose();
446
+ try {
447
+ await handle?.dispose();
448
+ }
449
+ catch {
450
+ // Preserve the original typed failure; cleanup is secondary evidence.
451
+ }
254
452
  }
255
453
  }
256
454
  }
455
+ /** Executes immutable Host contracts without creating an Agent or model turn. */
456
+ export class HostAutomationRunner {
457
+ registry;
458
+ policy;
459
+ allowUnbudgetedExecution;
460
+ constructor(registry, policy, options = {}) {
461
+ this.registry = registry;
462
+ this.policy = policy;
463
+ this.allowUnbudgetedExecution = options.allowUnbudgetedExecution ?? false;
464
+ }
465
+ async run(input) {
466
+ const definition = input.automation.definition;
467
+ if (!isHostAutomationDefinition(definition)) {
468
+ throw new AutomationRunnerFailureError('assistant-automations: Agent definition reached the Host runner', Object.freeze({
469
+ schemaVersion: 1, failureClass: 'configuration', failurePhase: 'preflight',
470
+ failureCode: 'host-runner-surface-mismatch', promptSubmissionState: 'not-applicable',
471
+ sideEffectState: 'none', retryability: 'after-intervention',
472
+ budgetSettlementState: 'not-required',
473
+ }));
474
+ }
475
+ const budgetSettlementStateBeforeReservation = definition.budgetId === undefined ? 'not-required' : 'not-reserved';
476
+ if (!exactHostRunnerInput(input)) {
477
+ throw new AutomationRunnerFailureError('assistant-automations: Host runner input identity is not exact', this.hostDiagnostic(definition, {
478
+ failureClass: 'configuration', failurePhase: 'preflight',
479
+ failureCode: 'host-runner-identity-mismatch', sideEffectState: 'none',
480
+ retryability: 'after-intervention',
481
+ budgetSettlementState: budgetSettlementStateBeforeReservation,
482
+ }));
483
+ }
484
+ const retryAttempt = input.task.attemptCount > 1;
485
+ const authorizedRecoveryResume = isAuthorizedRecoveryResume(input, definition);
486
+ if (retryAttempt && !authorizedRecoveryResume) {
487
+ throw new AutomationRunnerFailureError('assistant-automations: Host retry is not an exact idempotent Recovery resume', this.hostDiagnostic(definition, {
488
+ failureClass: 'configuration', failurePhase: 'preflight',
489
+ failureCode: 'host-recovery-resume-not-authorized', sideEffectState: 'none',
490
+ retryability: 'after-intervention',
491
+ budgetSettlementState: budgetSettlementStateBeforeReservation,
492
+ }));
493
+ }
494
+ const proof = this.registry.prove(definition.execution);
495
+ if (!proof.available) {
496
+ throw new AutomationRunnerFailureError('assistant-automations: exact Host executor is unavailable', this.hostDiagnostic(definition, {
497
+ failureClass: 'configuration', failurePhase: 'executor-availability',
498
+ failureCode: proof.reasonCode, sideEffectState: 'none', retryability: 'after-intervention',
499
+ budgetSettlementState: definition.budgetId === undefined ? 'not-required' : 'not-reserved',
500
+ }));
501
+ }
502
+ const budgetId = definition.budgetId;
503
+ const immutableDefinitionHash = definitionHash(definition);
504
+ let reservationId;
505
+ let budgetSettlementState = budgetId === undefined
506
+ ? 'not-required'
507
+ : 'not-reserved';
508
+ let replayedFinalizedRecoveryBudget = false;
509
+ let executionStarted = false;
510
+ try {
511
+ if (budgetId === undefined && !this.allowUnbudgetedExecution) {
512
+ throw new AutomationRunnerFailureError('assistant-automations: a configured budget is required for unattended Host execution', this.hostDiagnostic(definition, {
513
+ failureClass: 'budget', failurePhase: 'budget-reservation',
514
+ failureCode: 'automation-budget-required', sideEffectState: 'none',
515
+ retryability: 'after-intervention', budgetSettlementState,
516
+ }));
517
+ }
518
+ if (budgetId !== undefined) {
519
+ assertAutomationRunBudget(this.policy, budgetId);
520
+ const reservation = this.policy.reserve({
521
+ budgetId,
522
+ subject: {
523
+ kind: 'background', id: input.automation.id,
524
+ workspace: definition.workspace, principal: definition.principal,
525
+ },
526
+ amount: definition.budgetAmount,
527
+ idempotencyKey: hostBudgetIdempotencyKey(input, immutableDefinitionHash, budgetId),
528
+ });
529
+ if (reservation.status === 'finalized'
530
+ && reservation.replayed
531
+ && authorizedRecoveryResume) {
532
+ // Recovery owns an idempotent, durable operation ledger. A crash can
533
+ // happen after that operation and its exact budget were finalized but
534
+ // before Automations committed the terminal run. Invoke the exact
535
+ // executor again only to read/replay its durable terminal result; the
536
+ // already-finalized reservation must never be settled a second time.
537
+ replayedFinalizedRecoveryBudget = true;
538
+ budgetSettlementState = 'finalized';
539
+ }
540
+ else if (reservation.status !== 'reserved') {
541
+ const uncertain = reservation.status !== 'released';
542
+ throw new AutomationRunnerFailureError('assistant-automations: Host budget reservation was replayed', this.hostDiagnostic(definition, {
543
+ failureClass: 'budget', failurePhase: 'budget-reservation',
544
+ failureCode: 'automation-budget-reservation-replayed',
545
+ sideEffectState: uncertain ? 'unknown' : 'none',
546
+ retryability: uncertain ? 'unsafe' : 'after-intervention',
547
+ budgetSettlementState: reservation.status,
548
+ }));
549
+ }
550
+ else {
551
+ reservationId = reservation.reservationId;
552
+ budgetSettlementState = 'reserved';
553
+ }
554
+ if (!replayedFinalizedRecoveryBudget && reservation.replayed && !authorizedRecoveryResume) {
555
+ // A replay on the first attempt is concurrent/duplicate execution,
556
+ // not crash recovery. Keep the original reservation untouched.
557
+ reservationId = undefined;
558
+ throw new AutomationRunnerFailureError('assistant-automations: Host budget reservation was replayed outside Recovery resume', this.hostDiagnostic(definition, {
559
+ failureClass: 'budget', failurePhase: 'budget-reservation',
560
+ failureCode: 'automation-budget-reservation-replayed',
561
+ sideEffectState: 'unknown', retryability: 'unsafe',
562
+ budgetSettlementState: 'reserved',
563
+ }));
564
+ }
565
+ if (!replayedFinalizedRecoveryBudget && !reservation.replayed && authorizedRecoveryResume) {
566
+ // A recovered attempt must see the exact prior reservation. A fresh
567
+ // receipt proves the immutable definition/key changed (or no prior
568
+ // execution reached the budget boundary), so it cannot resume this
569
+ // occurrence. Release only the newly created reservation.
570
+ try {
571
+ const released = this.policy.release(reservation.reservationId);
572
+ if (released.status !== 'released') {
573
+ throw new Error('assistant-automations: fresh Recovery retry reservation was not released');
574
+ }
575
+ budgetSettlementState = 'released';
576
+ reservationId = undefined;
577
+ }
578
+ catch (error) {
579
+ budgetSettlementState = 'unknown';
580
+ reservationId = undefined;
581
+ throw new AutomationRunnerAmbiguousError('assistant-automations: fresh Recovery retry reservation could not be released', {
582
+ cause: error,
583
+ diagnostic: this.hostDiagnostic(definition, {
584
+ failureClass: 'budget', failurePhase: 'budget-settlement',
585
+ failureCode: 'budget-settlement-failed', sideEffectState: 'unknown',
586
+ retryability: 'unsafe', budgetSettlementState,
587
+ }),
588
+ });
589
+ }
590
+ throw new AutomationRunnerFailureError('assistant-automations: Recovery resume did not replay the exact immutable budget key', this.hostDiagnostic(definition, {
591
+ failureClass: 'budget', failurePhase: 'budget-reservation',
592
+ failureCode: 'host-recovery-resume-key-mismatch', sideEffectState: 'none',
593
+ retryability: 'after-intervention', budgetSettlementState,
594
+ }));
595
+ }
596
+ }
597
+ // Availability is proved again after claim and immediately before any
598
+ // Host execution. The reservation above is the only intervening effect.
599
+ const exactProof = this.registry.prove(definition.execution);
600
+ if (!exactProof.available
601
+ || !proof.available
602
+ || exactProof.registrationToken !== proof.registrationToken) {
603
+ throw new HostExecutorRegistryError('stale-registration', 'Host executor changed between availability proof and execution');
604
+ }
605
+ executionStarted = true;
606
+ const hostResult = await this.registry.execute(exactProof, {
607
+ occurrenceId: input.occurrence.id,
608
+ automationId: input.automation.id,
609
+ definitionHash: immutableDefinitionHash,
610
+ executionMode: input.occurrence.dryRun ? 'preview' : 'production',
611
+ targetScope: definition.execution.targetScope,
612
+ principal: definition.principal,
613
+ ownerRouteId: definition.execution.ownerRouteId,
614
+ activationNonce: definition.execution.activationNonce,
615
+ catalogDigest: definition.execution.catalogDigest,
616
+ signal: input.signal,
617
+ });
618
+ if (reservationId !== undefined) {
619
+ this.policy.finalize(reservationId, definition.budgetAmount);
620
+ budgetSettlementState = 'finalized';
621
+ }
622
+ // Reserving/finalizing a durable budget is itself an external effect.
623
+ const sideEffectState = reservationId === undefined
624
+ && !replayedFinalizedRecoveryBudget
625
+ ? hostResult.sideEffectState
626
+ : 'possible';
627
+ const retryability = sideEffectState === 'none' ? hostResult.retryability : 'unsafe';
628
+ return {
629
+ outcome: hostResult.outcome,
630
+ output: `Host execution terminal code: ${hostResult.failureCode}`,
631
+ usage: {},
632
+ diagnostic: Object.freeze({
633
+ schemaVersion: 1,
634
+ failureClass: hostResult.failureClass,
635
+ failurePhase: hostResult.failurePhase,
636
+ failureCode: hostResult.failureCode,
637
+ promptSubmissionState: 'not-applicable',
638
+ sideEffectState,
639
+ retryability,
640
+ budgetSettlementState,
641
+ }),
642
+ };
643
+ }
644
+ catch (error) {
645
+ if (error instanceof AutomationRunnerFailureError || error instanceof AutomationRunnerAmbiguousError)
646
+ throw error;
647
+ let settlementFailed = false;
648
+ if (reservationId !== undefined && budgetSettlementState === 'reserved') {
649
+ try {
650
+ // Once the registry invocation boundary may have been crossed, settle
651
+ // conservatively so a crash/retry cannot obtain a second occurrence cap.
652
+ if (executionStarted) {
653
+ this.policy.finalize(reservationId, definition.budgetAmount);
654
+ budgetSettlementState = 'finalized';
655
+ }
656
+ else {
657
+ this.policy.release(reservationId);
658
+ budgetSettlementState = 'released';
659
+ }
660
+ }
661
+ catch {
662
+ settlementFailed = true;
663
+ budgetSettlementState = 'unknown';
664
+ }
665
+ }
666
+ const sideEffectState = executionStarted || settlementFailed
667
+ ? 'unknown'
668
+ : budgetSettlementState === 'released' || budgetSettlementState === 'not-required'
669
+ ? 'none'
670
+ : 'unknown';
671
+ const receipt = this.hostDiagnostic(definition, {
672
+ failureClass: settlementFailed ? 'budget' : 'infrastructure',
673
+ failurePhase: settlementFailed ? 'budget-settlement' : 'host-execution',
674
+ failureCode: settlementFailed
675
+ ? 'budget-settlement-failed'
676
+ : error instanceof HostExecutorRegistryError ? error.code : 'host-executor-failed',
677
+ sideEffectState,
678
+ retryability: sideEffectState === 'none' ? 'safe' : 'unsafe',
679
+ budgetSettlementState,
680
+ });
681
+ if (sideEffectState !== 'none') {
682
+ throw new AutomationRunnerAmbiguousError('assistant-automations: Host execution may have produced external effects', { cause: error, diagnostic: receipt });
683
+ }
684
+ throw new AutomationRunnerFailureError('assistant-automations: Host execution failed before external effects', receipt, { cause: error });
685
+ }
686
+ }
687
+ hostDiagnostic(_definition, value) {
688
+ return Object.freeze({ schemaVersion: 1, promptSubmissionState: 'not-applicable', ...value });
689
+ }
690
+ }
691
+ /** Preserves the legacy Agent runner while routing Host definitions explicitly. */
692
+ export class RoutedAutomationRunner {
693
+ agent;
694
+ host;
695
+ constructor(agent, host) {
696
+ this.agent = agent;
697
+ this.host = host;
698
+ }
699
+ run(input) {
700
+ return isHostAutomationDefinition(input.automation.definition)
701
+ ? this.host.run(input)
702
+ : this.agent.run(input);
703
+ }
704
+ }
705
+ function definitionHash(definition) {
706
+ return createHash('sha256').update(JSON.stringify(definition)).digest('hex');
707
+ }
257
708
  //# sourceMappingURL=runner.js.map