@dsh-enhanced/assistant-automations 0.1.65 → 0.1.67

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 (50) hide show
  1. package/README.md +34 -1
  2. package/lib/coordinator.d.ts +15 -0
  3. package/lib/coordinator.d.ts.map +1 -1
  4. package/lib/coordinator.js +12 -0
  5. package/lib/coordinator.js.map +1 -1
  6. package/lib/host-agent-execution.d.ts +24 -0
  7. package/lib/host-agent-execution.d.ts.map +1 -0
  8. package/lib/host-agent-execution.js +248 -0
  9. package/lib/host-agent-execution.js.map +1 -0
  10. package/lib/host-executors.d.ts +10 -1
  11. package/lib/host-executors.d.ts.map +1 -1
  12. package/lib/host-executors.js +37 -7
  13. package/lib/host-executors.js.map +1 -1
  14. package/lib/index.d.ts +3 -2
  15. package/lib/index.d.ts.map +1 -1
  16. package/lib/index.js +2 -1
  17. package/lib/index.js.map +1 -1
  18. package/lib/native-agent-execution.d.ts +123 -0
  19. package/lib/native-agent-execution.d.ts.map +1 -0
  20. package/lib/native-agent-execution.js +922 -0
  21. package/lib/native-agent-execution.js.map +1 -0
  22. package/lib/operator.d.ts +56 -2
  23. package/lib/operator.d.ts.map +1 -1
  24. package/lib/operator.js +307 -8
  25. package/lib/operator.js.map +1 -1
  26. package/lib/runner.d.ts +2 -86
  27. package/lib/runner.d.ts.map +1 -1
  28. package/lib/runner.js +2 -777
  29. package/lib/runner.js.map +1 -1
  30. package/lib/schedule.d.ts +2 -0
  31. package/lib/schedule.d.ts.map +1 -1
  32. package/lib/schedule.js +8 -1
  33. package/lib/schedule.js.map +1 -1
  34. package/lib/service.d.ts +38 -4
  35. package/lib/service.d.ts.map +1 -1
  36. package/lib/service.js +112 -8
  37. package/lib/service.js.map +1 -1
  38. package/lib/sqlite.d.ts +1 -1
  39. package/lib/sqlite.d.ts.map +1 -1
  40. package/lib/sqlite.js +53 -2
  41. package/lib/sqlite.js.map +1 -1
  42. package/lib/store.d.ts +47 -1
  43. package/lib/store.d.ts.map +1 -1
  44. package/lib/store.js +221 -10
  45. package/lib/store.js.map +1 -1
  46. package/lib/types.d.ts +59 -0
  47. package/lib/types.d.ts.map +1 -1
  48. package/lib/version.d.ts +1 -1
  49. package/lib/version.js +1 -1
  50. package/package.json +13 -13
@@ -0,0 +1,922 @@
1
+ import { acceptanceDigest } from '@dsh-enhanced/task-acceptance-contract';
2
+ import { hostAgentToolSchemaDigest } from './host-agent-execution.js';
3
+ import { createHash } from 'node:crypto';
4
+ import { installModelSelection, } from '@deepseek-ai/dsh-agent';
5
+ import { ReasoningEffortId, createUserMessage } from '@deepseek-ai/dsh-llm';
6
+ import { appendPluginRecord, SessionId } from '@deepseek-ai/dsh-session';
7
+ import { resolveLlmRouteCapability } from '@dsh-enhanced/llm-route-capabilities';
8
+ import { HostAutomationExecutorRegistry, HostExecutorRegistryError, } from './host-executors.js';
9
+ import { AutomationRunnerAmbiguousError, AutomationRunnerFailureError, } from './coordinator.js';
10
+ import { isHostAutomationDefinition } from './types.js';
11
+ const usageFields = [
12
+ 'inputTokens',
13
+ 'outputTokens',
14
+ 'cacheReadTokens',
15
+ 'cacheWriteTokens',
16
+ 'reasoningTokens',
17
+ ];
18
+ function summarize(events, signal) {
19
+ let output = '';
20
+ // Seeded empty, not zeroed: execution evidence must distinguish a provider
21
+ // that reported zero from one that never supplied usage at all.
22
+ const usage = {};
23
+ let turnReason;
24
+ for (const event of events) {
25
+ if (event.type === 'assistant/message') {
26
+ const text = event.data.message.content
27
+ .filter(block => block.type === 'text')
28
+ .map(block => block.type === 'text' ? block.text : '')
29
+ .join('');
30
+ if (text !== '')
31
+ output = text;
32
+ if (event.data.usage !== undefined) {
33
+ for (const field of usageFields) {
34
+ const amount = event.data.usage[field];
35
+ if (amount !== undefined)
36
+ usage[field] = (usage[field] ?? 0) + amount;
37
+ }
38
+ }
39
+ }
40
+ if (event.type === 'turn/end')
41
+ turnReason = event.data.reason;
42
+ }
43
+ let outcome = 'unknown';
44
+ if (signal.aborted)
45
+ outcome = 'cancelled';
46
+ else if (typeof turnReason === 'object' && turnReason !== null && 'kind' in turnReason) {
47
+ const kind = turnReason.kind;
48
+ if (kind === 'completed' || kind === 'max-tokens')
49
+ outcome = 'succeeded';
50
+ else if (kind === 'aborted')
51
+ outcome = 'cancelled';
52
+ else if (kind === 'blocked' || kind === 'error')
53
+ outcome = 'failed';
54
+ }
55
+ const turnKind = typeof turnReason === 'object' && turnReason !== null && 'kind' in turnReason
56
+ ? String(turnReason.kind)
57
+ : undefined;
58
+ return { outcome, output, usage, ...(turnKind === undefined ? {} : { turnKind }) };
59
+ }
60
+ function stableFailureCode(error, phase) {
61
+ if (typeof error === 'object' && error !== null && 'code' in error) {
62
+ const code = error.code;
63
+ if (typeof code === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/u.test(code))
64
+ return code;
65
+ }
66
+ const defaults = {
67
+ preflight: 'preflight-failed',
68
+ 'artifact-write': 'artifact-write-failed',
69
+ 'budget-reservation': 'budget-reservation-failed',
70
+ 'preset-resolution': 'preset-resolution-failed',
71
+ 'agent-creation': 'agent-creation-failed',
72
+ 'agent-disposal': 'agent-disposal-failed',
73
+ 'agent-setup': 'agent-setup-failed',
74
+ 'prompt-submission': 'prompt-submission-failed',
75
+ 'model-execution': 'model-execution-failed',
76
+ 'session-flush': 'session-flush-failed',
77
+ 'budget-settlement': 'budget-settlement-failed',
78
+ };
79
+ return defaults[phase] ?? 'runner-failed';
80
+ }
81
+ function failureClass(phase, signal, error) {
82
+ if (signal.aborted)
83
+ return 'cancelled';
84
+ if (phase === 'budget-reservation' || phase === 'budget-settlement')
85
+ return 'budget';
86
+ if (typeof error === 'object' && error !== null && 'name' in error
87
+ && (error.name === 'AssistantPolicyError' || error.name === 'PolicyLedgerError'))
88
+ return 'policy';
89
+ if (phase === 'preflight' || phase === 'preset-resolution' || phase === 'agent-setup')
90
+ return 'configuration';
91
+ if (phase === 'agent-creation' || phase === 'agent-disposal' || phase === 'session-flush')
92
+ return 'infrastructure';
93
+ if (phase === 'prompt-submission' || phase === 'model-execution')
94
+ return 'provider';
95
+ return 'unknown';
96
+ }
97
+ function diagnostic(input) {
98
+ return Object.freeze({
99
+ schemaVersion: 1,
100
+ ...input,
101
+ retryability: input.failureClass === 'none'
102
+ ? input.sideEffectState === 'none' ? 'safe' : 'unsafe'
103
+ : input.sideEffectState === 'possible' || input.sideEffectState === 'unknown'
104
+ ? 'unsafe'
105
+ : input.failureClass === 'configuration' || input.failureClass === 'policy' || input.failureClass === 'budget'
106
+ ? 'after-intervention'
107
+ : 'safe',
108
+ });
109
+ }
110
+ export class AutomationBudgetReplayError extends AutomationRunnerFailureError {
111
+ reservationStatus;
112
+ code = 'automation-budget-reservation-replayed';
113
+ constructor(reservationStatus) {
114
+ super(`assistant-automations: budget reservation replay is ${reservationStatus}; refusing duplicate execution`, diagnostic({
115
+ failureClass: 'budget',
116
+ failurePhase: 'budget-reservation',
117
+ failureCode: 'automation-budget-reservation-replayed',
118
+ promptSubmissionState: reservationStatus === 'released' ? 'not-submitted' : 'unknown',
119
+ sideEffectState: reservationStatus === 'released' ? 'none' : 'unknown',
120
+ budgetSettlementState: reservationStatus,
121
+ }));
122
+ this.reservationStatus = reservationStatus;
123
+ this.name = 'AutomationBudgetReplayError';
124
+ }
125
+ }
126
+ export const AUTOMATION_RUN_BUDGET_METRIC = 'automation-runs';
127
+ const RECOVERY_HOST_EXECUTOR_ID = 'assistant-recovery';
128
+ /**
129
+ * Bind a Host reservation to the exact immutable execution snapshot. The
130
+ * digest avoids delimiter collisions in caller-controlled ids while keeping
131
+ * the Policy ledger key bounded and content-free.
132
+ */
133
+ function hostBudgetIdempotencyKey(input, immutableDefinitionHash, budgetId) {
134
+ const digest = createHash('sha256').update(JSON.stringify([
135
+ 'assistant-automations-host-budget/v1',
136
+ input.automation.id,
137
+ input.occurrence.id,
138
+ immutableDefinitionHash,
139
+ AUTOMATION_RUN_BUDGET_METRIC,
140
+ budgetId,
141
+ ])).digest('hex');
142
+ return `automation-budget:host:v1:${digest}`;
143
+ }
144
+ function exactHostRunnerInput(input) {
145
+ return input.occurrence.automationId === input.automation.id
146
+ && input.task.automationId === input.automation.id
147
+ && input.task.occurrenceId === input.occurrence.id
148
+ && input.task.status === 'running'
149
+ && Number.isSafeInteger(input.task.attemptCount)
150
+ && input.task.attemptCount >= 1;
151
+ }
152
+ function isAuthorizedRecoveryResume(input, definition) {
153
+ return input.task.attemptCount > 1
154
+ && input.task.attemptCount <= definition.maxRetries + 1
155
+ && definition.retrySafety === 'idempotent'
156
+ && definition.execution.executorId === RECOVERY_HOST_EXECUTOR_ID;
157
+ }
158
+ /** Prove the configured Policy budget uses the fixed per-run unit before reservation. */
159
+ export function assertAutomationRunBudget(policy, budgetId) {
160
+ const budget = policy.getBudgetConfig(budgetId);
161
+ if (budget === undefined) {
162
+ throw new Error(`assistant-automations: budget is not configured: ${budgetId}`);
163
+ }
164
+ if (budget.id !== budgetId || budget.metric !== AUTOMATION_RUN_BUDGET_METRIC) {
165
+ throw new Error(`assistant-automations: budget ${budgetId} must use ${AUTOMATION_RUN_BUDGET_METRIC}, got ${budget.metric}`);
166
+ }
167
+ return budget;
168
+ }
169
+ function requireAdapterToolCallProtocol(llm, provider, model, presetId, toolCount) {
170
+ if (toolCount === 0)
171
+ return;
172
+ if (resolveLlmRouteCapability(llm, provider, model)?.toolCalls === 'none') {
173
+ throw new Error(`assistant-automations: adapter ${provider}/${model} declares no DSH tool-call protocol for preset ${presetId}`);
174
+ }
175
+ }
176
+ class NativeAgentExecution {
177
+ ctx;
178
+ policy;
179
+ hostContract;
180
+ allowUnbudgetedExecution;
181
+ prepareAcceptance;
182
+ acceptanceEnabled;
183
+ beforePrompt;
184
+ maxModelCalls;
185
+ modelOnly;
186
+ constructor(ctx, policy, options = {}, hostContract) {
187
+ this.ctx = ctx;
188
+ this.policy = policy;
189
+ this.hostContract = hostContract;
190
+ this.allowUnbudgetedExecution = options.allowUnbudgetedExecution ?? false;
191
+ this.prepareAcceptance = options.prepareAcceptance;
192
+ this.acceptanceEnabled = options.acceptanceEnabled;
193
+ this.beforePrompt = options.beforePrompt;
194
+ if (options.maxModelCalls !== undefined && (!Number.isSafeInteger(options.maxModelCalls) || options.maxModelCalls < 1)) {
195
+ throw new Error('assistant-automations: maxModelCalls must be a positive safe integer');
196
+ }
197
+ this.maxModelCalls = options.maxModelCalls;
198
+ this.modelOnly = options.modelOnly ?? false;
199
+ }
200
+ async run(input) {
201
+ const definition = input.automation.definition;
202
+ if (isHostAutomationDefinition(definition)) {
203
+ throw new AutomationRunnerFailureError('assistant-automations: Host definitions require a registered Host executor', diagnostic({
204
+ failureClass: 'configuration', failurePhase: 'executor-availability',
205
+ failureCode: 'host-executor-router-missing', promptSubmissionState: 'not-applicable',
206
+ sideEffectState: 'none', budgetSettlementState: definition.budgetId === undefined
207
+ ? 'not-required'
208
+ : 'not-reserved',
209
+ }));
210
+ }
211
+ if (this.modelOnly && (definition.allowedTools.length !== 0 || definition.maxToolCalls !== 0)) {
212
+ throw new AutomationRunnerFailureError('assistant-automations: modelOnly runner requires an empty immutable tool contract', diagnostic({ failureClass: 'configuration', failurePhase: 'preflight', failureCode: 'model-only-tool-contract-invalid',
213
+ promptSubmissionState: 'not-submitted', sideEffectState: 'none', budgetSettlementState: definition.budgetId === undefined ? 'not-required' : 'not-reserved' }));
214
+ }
215
+ const budgetId = definition.budgetId;
216
+ let reservationId;
217
+ let reservationSettled = false;
218
+ let handle;
219
+ let setupAgent;
220
+ let agentCreationSubmitted = false;
221
+ let followupSubmitted = false;
222
+ let removeAbort;
223
+ let toolCalls = 0;
224
+ let phase = 'preflight';
225
+ let promptSubmissionState = 'not-submitted';
226
+ let sideEffectState = 'none';
227
+ let budgetSettlementState = budgetId === undefined
228
+ ? 'not-required'
229
+ : 'not-reserved';
230
+ try {
231
+ this.hostContract?.deadline.check();
232
+ const agents = this.ctx.get('agents');
233
+ const sessions = this.ctx.get('sessions');
234
+ const tools = this.ctx.get('tools');
235
+ const llm = this.ctx.get('llm');
236
+ if (agents === undefined || sessions === undefined || tools === undefined || llm === undefined) {
237
+ throw new Error('assistant-automations: agents, sessions, and tools services are required');
238
+ }
239
+ const allowed = new Set(input.occurrence.dryRun ? [] : definition.allowedTools);
240
+ if (input.signal.aborted)
241
+ throw new Error('assistant-automations: run was cancelled before Agent creation');
242
+ phase = 'budget-reservation';
243
+ if (budgetId === undefined && !this.allowUnbudgetedExecution) {
244
+ throw new Error('assistant-automations: a configured budget is required for unattended execution');
245
+ }
246
+ if (budgetId !== undefined)
247
+ assertAutomationRunBudget(this.policy, budgetId);
248
+ const agentPresets = (this.hostContract ? undefined : this.ctx.get('agentPresets'));
249
+ phase = 'preset-resolution';
250
+ const presetId = agentPresets === undefined
251
+ ? definition.agentPreset
252
+ : (await agentPresets.resolve(definition.agentPreset)).id;
253
+ if (budgetId !== undefined) {
254
+ phase = 'budget-reservation';
255
+ const reservation = this.policy.reserve({
256
+ budgetId,
257
+ subject: {
258
+ kind: 'background',
259
+ id: input.automation.id,
260
+ workspace: definition.workspace,
261
+ principal: definition.principal,
262
+ },
263
+ amount: definition.budgetAmount,
264
+ idempotencyKey: [
265
+ 'automation-budget', input.automation.id, input.occurrence.id, AUTOMATION_RUN_BUDGET_METRIC, budgetId,
266
+ ].join(':'),
267
+ });
268
+ if (reservation.replayed || reservation.status !== 'reserved') {
269
+ throw new AutomationBudgetReplayError(reservation.status);
270
+ }
271
+ reservationId = reservation.reservationId;
272
+ budgetSettlementState = 'reserved';
273
+ }
274
+ phase = 'preflight';
275
+ const globalNames = tools.schemas().map(schema => schema.name);
276
+ const execution = Object.freeze({
277
+ mode: input.occurrence.dryRun ? 'preview' : 'production',
278
+ automationId: input.automation.id,
279
+ occurrenceId: input.occurrence.id,
280
+ });
281
+ if (this.hostContract) {
282
+ this.hostContract.deadline.check();
283
+ const contract = this.hostContract.contract;
284
+ const resolved = await llm.resolveCallConfig({ provider: contract.provider, model: contract.model,
285
+ ...(contract.reasoningEffort === undefined ? {} : { reasoningEffort: ReasoningEffortId(contract.reasoningEffort) }), maxTokens: contract.maxOutputTokens }, input.signal);
286
+ if (resolved.provider !== contract.provider || resolved.model !== contract.model || resolved.reasoningEffort !== contract.reasoningEffort
287
+ || resolved.maxTokens !== contract.maxOutputTokens)
288
+ throw new Error('assistant-automations: resolved Host supplier differs from frozen contract');
289
+ this.hostContract.deadline.check();
290
+ }
291
+ phase = 'agent-creation';
292
+ agentCreationSubmitted = true;
293
+ handle = await agents.create({
294
+ sessionId: SessionId(input.sessionId),
295
+ meta: {
296
+ cwd: definition.workspace,
297
+ agentPreset: presetId,
298
+ },
299
+ agentOptions: {
300
+ provider: definition.provider,
301
+ model: definition.model,
302
+ maxTokens: definition.maxOutputTokens,
303
+ ...(this.hostContract?.contract.reasoningEffort === undefined ? {} : { reasoningEffort: ReasoningEffortId(this.hostContract.contract.reasoningEffort) }),
304
+ },
305
+ signal: input.signal,
306
+ setup: async (agentCtx, preparedAgent) => {
307
+ phase = 'agent-setup';
308
+ this.hostContract?.deadline.check();
309
+ const agent = preparedAgent;
310
+ if (agent === undefined) {
311
+ throw new Error('assistant-automations: unpublished Agent identity is missing during setup');
312
+ }
313
+ if (this.hostContract)
314
+ setupAgent = agent;
315
+ if (this.hostContract && agent.session.id !== input.sessionId)
316
+ throw new Error('assistant-automations: Host Agent session identity differs from occurrence');
317
+ if (agent.session.header.cwd !== definition.workspace
318
+ || agent.session.header.agentPreset !== presetId) {
319
+ throw new Error('assistant-automations: background Agent identity does not match the immutable definition');
320
+ }
321
+ agentCtx.provide('assistantAutomationExecution', execution);
322
+ let modelCalls = 0;
323
+ const maxModelCalls = this.maxModelCalls;
324
+ const beforePrompt = this.beforePrompt;
325
+ const strict = this.hostContract;
326
+ agentCtx.on('llm/stream', async function* (_options, next) {
327
+ if (strict && _options.sessionId !== agent.session.id) {
328
+ yield* next();
329
+ return;
330
+ }
331
+ if (strict) {
332
+ strict.deadline.check();
333
+ _options.signal?.throwIfAborted();
334
+ const contract = strict.contract;
335
+ if (_options.provider !== contract.provider || _options.model !== contract.model
336
+ || _options.reasoningEffort !== contract.reasoningEffort || _options.maxTokens !== contract.maxOutputTokens
337
+ || hostAgentToolSchemaDigest(_options.tools ?? []) !== contract.toolSchemaDigest
338
+ || hostAgentToolSchemaDigest(agentCtx.tools.schemas(agent)) !== contract.toolSchemaDigest) {
339
+ agent.cancel({ kind: 'hook', reason: 'host-agent-request-contract-drift' });
340
+ throw new Error('assistant-automations: actual Host request differs from frozen contract');
341
+ }
342
+ }
343
+ if (maxModelCalls !== undefined && modelCalls >= maxModelCalls) {
344
+ throw new Error('assistant-automations: immutable model-call budget exhausted');
345
+ }
346
+ beforePrompt?.();
347
+ strict?.deadline.authorize?.();
348
+ modelCalls += 1;
349
+ if (!strict) {
350
+ yield* next();
351
+ return;
352
+ }
353
+ let outputBytes = 0;
354
+ for await (const chunk of next()) {
355
+ if (strict) {
356
+ strict.deadline.check();
357
+ if (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta')
358
+ outputBytes += Buffer.byteLength(chunk.text);
359
+ else if (chunk.type === 'tool-call-delta')
360
+ outputBytes += Buffer.byteLength(chunk.argumentsDelta);
361
+ if (outputBytes > strict.contract.maxOutputTokens * 32 || chunk.type === 'usage' && (chunk.usage.outputTokens ?? 0) > strict.contract.maxOutputTokens) {
362
+ agent.cancel({ kind: 'hook', reason: 'host-agent-output-limit' });
363
+ throw new Error('assistant-automations: Host Agent output limit exceeded');
364
+ }
365
+ }
366
+ yield chunk;
367
+ }
368
+ strict?.deadline.check();
369
+ });
370
+ // Host tools must retain the principal frozen in the authorized
371
+ // occurrence. Ordinary DshRunner keeps its historical initiator.
372
+ const unbindInitiator = this.policy.bindInitiator(agent, 'background', strict ? definition.principal : undefined);
373
+ agentCtx.effect(() => unbindInitiator, 'assistant-automations.background-initiator');
374
+ const delivery = this.ctx.get('assistantDelivery');
375
+ // Approval delegation is independent from ordinary run-result
376
+ // delivery. Legacy definitions retain their historical route.
377
+ const bindingId = definition.approvalBindingId ?? definition.deliveryBindingId;
378
+ if (bindingId !== undefined && typeof delivery?.bindAgentApprovalRoute === 'function') {
379
+ const unbindApproval = delivery.bindAgentApprovalRoute(agent, { bindingId });
380
+ agentCtx.effect(() => unbindApproval, 'assistant-automations.approval-route');
381
+ }
382
+ installModelSelection(agentCtx, {
383
+ current: {
384
+ provider: definition.provider,
385
+ model: definition.model,
386
+ ...(this.hostContract?.contract.reasoningEffort === undefined ? {} : { reasoningEffort: ReasoningEffortId(this.hostContract.contract.reasoningEffort) }),
387
+ },
388
+ assembled: undefined,
389
+ });
390
+ if (!this.modelOnly)
391
+ await agentPresets?.mount(agentCtx, presetId);
392
+ const mountedNames = agentCtx.tools.schemas(agent).map(schema => schema.name);
393
+ for (const name of allowed) {
394
+ if (!mountedNames.includes(name)) {
395
+ throw new Error(`assistant-automations: unknown allowlist tool after preset mount: ${name}`);
396
+ }
397
+ }
398
+ // Scoped preset tools cannot be removed by DSH restrictions. Mask
399
+ // inherited global tools here and let the final immutable allowlist
400
+ // assertion fail closed for any scoped tool outside the contract.
401
+ const denied = globalNames.filter(name => !allowed.has(name));
402
+ if (denied.length > 0)
403
+ agentCtx.tools.restrict({ deny: denied });
404
+ const finalSchemas = agentCtx.tools.schemas(agent);
405
+ const outsideAllowlist = finalSchemas.map(schema => schema.name).filter(name => !allowed.has(name));
406
+ if (outsideAllowlist.length > 0) {
407
+ throw new Error(`assistant-automations: preset ${presetId} exposes tools outside the immutable allowlist: ${outsideAllowlist.join(', ')}`);
408
+ }
409
+ if (strict) {
410
+ if (finalSchemas.length !== allowed.size || hostAgentToolSchemaDigest(finalSchemas) !== strict.contract.toolSchemaDigest)
411
+ throw new Error('assistant-automations: Host tool schemas differ from frozen contract');
412
+ appendPluginRecord(agent.session, 'plugin:assistant-automations/host-agent-contract', {
413
+ protocol: 'assistant-automations/host-agent-contract/v1', contract: strict.contract, contractDigest: strict.digest,
414
+ automationId: execution.automationId, occurrenceId: execution.occurrenceId, sessionId: input.sessionId,
415
+ });
416
+ }
417
+ requireAdapterToolCallProtocol(llm, definition.provider, definition.model, presetId, finalSchemas.length);
418
+ agentCtx.tools.guard((execution) => {
419
+ if (strict && execution.agent !== agent)
420
+ return undefined;
421
+ if (strict) {
422
+ strict.deadline.authorize?.();
423
+ strict.deadline.check();
424
+ if (hostAgentToolSchemaDigest(agentCtx.tools.schemas(agent)) !== strict.contract.toolSchemaDigest)
425
+ return 'assistant-automations: Host tool schemas drifted';
426
+ }
427
+ if (!allowed.has(execution.name))
428
+ return 'assistant-automations: tool is outside the immutable allowlist';
429
+ if (toolCalls >= definition.maxToolCalls) {
430
+ return 'assistant-automations: immutable tool-call budget exhausted';
431
+ }
432
+ toolCalls += 1;
433
+ return undefined;
434
+ });
435
+ },
436
+ });
437
+ if (this.hostContract && (handle.agent !== setupAgent || handle.agent.session.id !== input.sessionId))
438
+ throw new Error('assistant-automations: returned Host Agent identity differs from setup');
439
+ if (this.hostContract && input.signal.aborted) {
440
+ await handle.dispose();
441
+ handle = undefined;
442
+ throw new Error('assistant-automations: late Host Agent creation rejected');
443
+ }
444
+ sideEffectState = 'possible';
445
+ const agent = handle.agent;
446
+ this.hostContract?.deadline.observe(() => {
447
+ // Only committed native assistant-message usage is counted: this is an observed lower bound, never a total bill.
448
+ const usage = { toolCalls };
449
+ for (const event of agent.session.snapshotEvents()) {
450
+ if (event.type !== 'assistant/message' || event.data.usage === undefined)
451
+ continue;
452
+ for (const field of usageFields) {
453
+ const amount = event.data.usage[field];
454
+ if (amount !== undefined && Number.isSafeInteger(amount) && amount >= 0) {
455
+ const sum = Number(usage[field] ?? 0) + amount;
456
+ if (Number.isSafeInteger(sum))
457
+ usage[field] = sum;
458
+ }
459
+ }
460
+ }
461
+ return { outcome: 'unknown', output: '', usage, sessionId: agent.session.id, quiescent: false };
462
+ });
463
+ const evolution = this.ctx.get('assistantEvolution');
464
+ if (!this.hostContract && execution.mode === 'production'
465
+ && typeof evolution?.injectAutomationGuidance === 'function') {
466
+ evolution.injectAutomationGuidance(agent, execution);
467
+ }
468
+ // Resolve the real Delivery owner only after setup installed the immutable
469
+ // approval binding. Never derive an acceptance principal from the
470
+ // definition's display principal or any model-controlled value.
471
+ if (execution.mode === 'production' && this.prepareAcceptance !== undefined && this.acceptanceEnabled?.() === true) {
472
+ const delivery = this.ctx.get('assistantDelivery');
473
+ let owner;
474
+ let binding;
475
+ if (typeof delivery?.prepareAgentApproval === 'function') {
476
+ try {
477
+ const route = delivery.prepareAgentApproval(agent, {
478
+ sourceId: 'dsh-enhanced-assistant-verifier',
479
+ });
480
+ if (route.routeVersion !== 2 || route.sourceId !== 'dsh-enhanced-assistant-verifier'
481
+ || route.workspace !== definition.workspace || route.bindingId.trim() === ''
482
+ || !Number.isSafeInteger(route.bindingVersion) || route.bindingVersion < 1
483
+ || !Number.isSafeInteger(route.bindingGeneration) || route.bindingGeneration < 1
484
+ || route.principalRecordId.trim() === '' || !Number.isSafeInteger(route.principalVersion)
485
+ || route.principalVersion < 1) {
486
+ throw new Error('assistant-automations: Delivery acceptance owner route is invalid');
487
+ }
488
+ owner = Object.freeze({ principalRecordId: route.principalRecordId, principalVersion: route.principalVersion });
489
+ binding = Object.freeze({ id: route.bindingId, version: route.bindingVersion, generation: route.bindingGeneration });
490
+ }
491
+ catch (error) {
492
+ // The sink decides whether a missing authenticated owner is an
493
+ // optional no-profile skip or a required fail-closed boundary.
494
+ if (typeof error !== 'object' || error === null
495
+ || !('code' in error) || error.code !== 'missing-binding')
496
+ throw error;
497
+ }
498
+ }
499
+ this.prepareAcceptance({ taskId: input.task.id, automationId: input.automation.id,
500
+ scope: Object.freeze({ workspace: definition.workspace, preset: presetId }), objective: definition.prompt,
501
+ ...(owner === undefined ? {} : { owner }), ...(binding === undefined ? {} : { binding }),
502
+ });
503
+ }
504
+ const abort = () => agent.cancel({ kind: 'hook', reason: 'assistant-automations-signal' });
505
+ input.signal.addEventListener('abort', abort, { once: true });
506
+ removeAbort = () => input.signal.removeEventListener('abort', abort);
507
+ this.beforePrompt?.();
508
+ this.hostContract?.deadline.authorize?.();
509
+ this.hostContract?.deadline.check();
510
+ phase = 'prompt-submission';
511
+ promptSubmissionState = 'unknown';
512
+ agent.followup(createUserMessage({
513
+ content: [{ type: 'text', text: definition.prompt }],
514
+ source: {
515
+ kind: 'assistant-automations',
516
+ form: 'notice',
517
+ summary: `Automation: ${definition.name}`,
518
+ },
519
+ }));
520
+ promptSubmissionState = 'submitted';
521
+ followupSubmitted = true;
522
+ phase = 'model-execution';
523
+ await agent.whenIdle();
524
+ this.hostContract?.deadline.check();
525
+ const summary = summarize(agent.session.snapshotEvents(), input.signal);
526
+ if (this.hostContract && summary.turnKind === 'max-tokens')
527
+ summary.outcome = 'failed';
528
+ sideEffectState = 'possible';
529
+ phase = 'session-flush';
530
+ await sessions.flush(agent.session);
531
+ if (reservationId !== undefined) {
532
+ phase = 'budget-settlement';
533
+ this.policy.finalize(reservationId, definition.budgetAmount);
534
+ reservationSettled = true;
535
+ budgetSettlementState = 'finalized';
536
+ }
537
+ const classified = summary.outcome === 'succeeded'
538
+ ? { failureClass: 'none', failurePhase: 'none', failureCode: 'none' }
539
+ : summary.outcome === 'cancelled'
540
+ ? { failureClass: 'cancelled', failurePhase: 'model-execution', failureCode: 'agent-turn-cancelled' }
541
+ : summary.outcome === 'unknown'
542
+ ? { failureClass: 'execution', failurePhase: 'model-execution', failureCode: 'agent-turn-unknown' }
543
+ : {
544
+ failureClass: 'execution',
545
+ failurePhase: 'model-execution',
546
+ failureCode: summary.turnKind === 'blocked' ? 'agent-turn-blocked' : 'agent-turn-failed',
547
+ };
548
+ const completed = {
549
+ outcome: summary.outcome,
550
+ output: summary.output,
551
+ usage: { ...summary.usage, toolCalls },
552
+ sessionId: input.sessionId,
553
+ diagnostic: diagnostic({
554
+ ...classified,
555
+ promptSubmissionState,
556
+ sideEffectState,
557
+ budgetSettlementState,
558
+ }),
559
+ quiescent: !input.signal.aborted,
560
+ };
561
+ removeAbort?.();
562
+ removeAbort = undefined;
563
+ phase = 'agent-disposal';
564
+ await handle?.dispose();
565
+ handle = undefined;
566
+ this.hostContract?.deadline.check();
567
+ return completed;
568
+ }
569
+ catch (error) {
570
+ if (error instanceof AutomationBudgetReplayError)
571
+ throw error;
572
+ let caught = error;
573
+ if (reservationId !== undefined && !reservationSettled) {
574
+ try {
575
+ if (!agentCreationSubmitted) {
576
+ this.policy.release(reservationId);
577
+ budgetSettlementState = 'released';
578
+ }
579
+ else {
580
+ this.policy.finalize(reservationId, definition.budgetAmount);
581
+ budgetSettlementState = 'finalized';
582
+ }
583
+ reservationSettled = true;
584
+ }
585
+ catch (settlementError) {
586
+ phase = 'budget-settlement';
587
+ budgetSettlementState = 'unknown';
588
+ sideEffectState = agentCreationSubmitted
589
+ ? 'possible'
590
+ : promptSubmissionState === 'not-submitted' ? 'none' : 'unknown';
591
+ caught = new AutomationRunnerAmbiguousError(`assistant-automations: conservative budget settlement failed: ${settlementError instanceof Error ? settlementError.message : String(settlementError)}`, {
592
+ cause: settlementError,
593
+ diagnostic: diagnostic({
594
+ failureClass: 'budget',
595
+ failurePhase: 'budget-settlement',
596
+ failureCode: stableFailureCode(settlementError, 'budget-settlement'),
597
+ promptSubmissionState,
598
+ sideEffectState,
599
+ budgetSettlementState,
600
+ }),
601
+ });
602
+ }
603
+ }
604
+ if (caught instanceof AutomationRunnerAmbiguousError)
605
+ throw caught;
606
+ sideEffectState = agentCreationSubmitted
607
+ ? 'possible'
608
+ : promptSubmissionState === 'not-submitted' ? 'none' : 'unknown';
609
+ const receipt = diagnostic({
610
+ failureClass: failureClass(phase, input.signal, caught),
611
+ failurePhase: phase,
612
+ failureCode: stableFailureCode(caught, phase),
613
+ promptSubmissionState,
614
+ sideEffectState,
615
+ budgetSettlementState,
616
+ });
617
+ if (followupSubmitted || promptSubmissionState !== 'not-submitted') {
618
+ throw new AutomationRunnerAmbiguousError(`assistant-automations: Agent execution may have produced side effects: ${caught instanceof Error ? caught.message : String(caught)}`, { cause: caught, diagnostic: receipt });
619
+ }
620
+ throw new AutomationRunnerFailureError(caught instanceof Error ? caught.message : String(caught), receipt, { cause: caught });
621
+ }
622
+ finally {
623
+ removeAbort?.();
624
+ try {
625
+ await handle?.dispose();
626
+ }
627
+ catch {
628
+ // Preserve the original typed failure; cleanup is secondary evidence.
629
+ }
630
+ }
631
+ }
632
+ }
633
+ /** Original public runner options and acceptance/guidance/settlement behavior. */
634
+ export class DshAutomationRunner extends NativeAgentExecution {
635
+ constructor(ctx, policy, options = {}) { super(ctx, policy, options); }
636
+ }
637
+ /** Internal Host path: outer HostRunner alone reserves and settles the occurrence. */
638
+ export function runHostNativeAgent(ctx, policy, input, contract, contractDigest, signal, deadline) {
639
+ if (!isHostAutomationDefinition(input.automation.definition) || acceptanceDigest(contract) !== contractDigest)
640
+ throw new Error('assistant-automations: invalid Host native execution');
641
+ const { execution: _execution, budgetId: _budgetId, budgetAmount: _budgetAmount, ...control } = input.automation.definition;
642
+ const definition = { ...control, provider: contract.provider, model: contract.model, prompt: contract.prompt,
643
+ allowedTools: contract.allowedTools, maxToolCalls: contract.maxToolCalls, maxOutputTokens: contract.maxOutputTokens };
644
+ return new NativeAgentExecution(ctx, policy, { allowUnbudgetedExecution: true, maxModelCalls: contract.maxModelCalls }, { contract, digest: contractDigest, deadline })
645
+ .run({ ...input, signal, automation: { ...input.automation, definition } });
646
+ }
647
+ /** Executes immutable Host contracts without creating an Agent or model turn. */
648
+ export class HostAutomationRunner {
649
+ registry;
650
+ policy;
651
+ allowUnbudgetedExecution;
652
+ #hostAgentKey;
653
+ constructor(registry, policy, options = {}, hostAgentKey) {
654
+ this.registry = registry;
655
+ this.policy = policy;
656
+ this.#hostAgentKey = hostAgentKey;
657
+ this.allowUnbudgetedExecution = options.allowUnbudgetedExecution ?? false;
658
+ }
659
+ async run(input) {
660
+ const definition = input.automation.definition;
661
+ if (!isHostAutomationDefinition(definition)) {
662
+ throw new AutomationRunnerFailureError('assistant-automations: Agent definition reached the Host runner', Object.freeze({
663
+ schemaVersion: 1, failureClass: 'configuration', failurePhase: 'preflight',
664
+ failureCode: 'host-runner-surface-mismatch', promptSubmissionState: 'not-applicable',
665
+ sideEffectState: 'none', retryability: 'after-intervention',
666
+ budgetSettlementState: 'not-required',
667
+ }));
668
+ }
669
+ const budgetSettlementStateBeforeReservation = definition.budgetId === undefined ? 'not-required' : 'not-reserved';
670
+ if (!exactHostRunnerInput(input)) {
671
+ throw new AutomationRunnerFailureError('assistant-automations: Host runner input identity is not exact', this.hostDiagnostic(definition, {
672
+ failureClass: 'configuration', failurePhase: 'preflight',
673
+ failureCode: 'host-runner-identity-mismatch', sideEffectState: 'none',
674
+ retryability: 'after-intervention',
675
+ budgetSettlementState: budgetSettlementStateBeforeReservation,
676
+ }));
677
+ }
678
+ const retryAttempt = input.task.attemptCount > 1;
679
+ const authorizedRecoveryResume = isAuthorizedRecoveryResume(input, definition);
680
+ if (retryAttempt && !authorizedRecoveryResume) {
681
+ throw new AutomationRunnerFailureError('assistant-automations: Host retry is not an exact idempotent Recovery resume', this.hostDiagnostic(definition, {
682
+ failureClass: 'configuration', failurePhase: 'preflight',
683
+ failureCode: 'host-recovery-resume-not-authorized', sideEffectState: 'none',
684
+ retryability: 'after-intervention',
685
+ budgetSettlementState: budgetSettlementStateBeforeReservation,
686
+ }));
687
+ }
688
+ const immutableDefinitionHash = definitionHash(definition);
689
+ const proof = this.registry.prove(definition.execution);
690
+ if (!proof.available || definitionHash(definition) !== immutableDefinitionHash) {
691
+ throw new AutomationRunnerFailureError('assistant-automations: exact Host executor is unavailable', this.hostDiagnostic(definition, {
692
+ failureClass: 'configuration', failurePhase: 'executor-availability',
693
+ failureCode: proof.available ? 'host-runner-identity-mismatch' : proof.reasonCode, sideEffectState: 'none', retryability: 'after-intervention',
694
+ budgetSettlementState: definition.budgetId === undefined ? 'not-required' : 'not-reserved',
695
+ }));
696
+ }
697
+ const budgetId = definition.budgetId;
698
+ let reservationId;
699
+ let budgetSettlementState = budgetId === undefined
700
+ ? 'not-required'
701
+ : 'not-reserved';
702
+ let replayedFinalizedRecoveryBudget = false;
703
+ let executionStarted = false;
704
+ let settlementAttempted = false;
705
+ let nativeResult;
706
+ try {
707
+ if (budgetId === undefined && !this.allowUnbudgetedExecution) {
708
+ throw new AutomationRunnerFailureError('assistant-automations: a configured budget is required for unattended Host execution', this.hostDiagnostic(definition, {
709
+ failureClass: 'budget', failurePhase: 'budget-reservation',
710
+ failureCode: 'automation-budget-required', sideEffectState: 'none',
711
+ retryability: 'after-intervention', budgetSettlementState,
712
+ }));
713
+ }
714
+ if (budgetId !== undefined) {
715
+ assertAutomationRunBudget(this.policy, budgetId);
716
+ const reservation = this.policy.reserve({
717
+ budgetId,
718
+ subject: {
719
+ kind: 'background', id: input.automation.id,
720
+ workspace: definition.workspace, principal: definition.principal,
721
+ },
722
+ amount: definition.budgetAmount,
723
+ idempotencyKey: hostBudgetIdempotencyKey(input, immutableDefinitionHash, budgetId),
724
+ });
725
+ if (reservation.status === 'finalized'
726
+ && reservation.replayed
727
+ && authorizedRecoveryResume) {
728
+ // Recovery owns an idempotent, durable operation ledger. A crash can
729
+ // happen after that operation and its exact budget were finalized but
730
+ // before Automations committed the terminal run. Invoke the exact
731
+ // executor again only to read/replay its durable terminal result; the
732
+ // already-finalized reservation must never be settled a second time.
733
+ replayedFinalizedRecoveryBudget = true;
734
+ budgetSettlementState = 'finalized';
735
+ }
736
+ else if (reservation.status !== 'reserved') {
737
+ const uncertain = reservation.status !== 'released';
738
+ throw new AutomationRunnerFailureError('assistant-automations: Host budget reservation was replayed', this.hostDiagnostic(definition, {
739
+ failureClass: 'budget', failurePhase: 'budget-reservation',
740
+ failureCode: 'automation-budget-reservation-replayed',
741
+ sideEffectState: uncertain ? 'unknown' : 'none',
742
+ retryability: uncertain ? 'unsafe' : 'after-intervention',
743
+ budgetSettlementState: reservation.status,
744
+ }));
745
+ }
746
+ else {
747
+ reservationId = reservation.reservationId;
748
+ budgetSettlementState = 'reserved';
749
+ }
750
+ if (!replayedFinalizedRecoveryBudget && reservation.replayed && !authorizedRecoveryResume) {
751
+ // A replay on the first attempt is concurrent/duplicate execution,
752
+ // not crash recovery. Keep the original reservation untouched.
753
+ reservationId = undefined;
754
+ throw new AutomationRunnerFailureError('assistant-automations: Host budget reservation was replayed outside Recovery resume', this.hostDiagnostic(definition, {
755
+ failureClass: 'budget', failurePhase: 'budget-reservation',
756
+ failureCode: 'automation-budget-reservation-replayed',
757
+ sideEffectState: 'unknown', retryability: 'unsafe',
758
+ budgetSettlementState: 'reserved',
759
+ }));
760
+ }
761
+ if (!replayedFinalizedRecoveryBudget && !reservation.replayed && authorizedRecoveryResume) {
762
+ // A recovered attempt must see the exact prior reservation. A fresh
763
+ // receipt proves the immutable definition/key changed (or no prior
764
+ // execution reached the budget boundary), so it cannot resume this
765
+ // occurrence. Release only the newly created reservation.
766
+ try {
767
+ const released = this.policy.release(reservation.reservationId);
768
+ if (released.status !== 'released') {
769
+ throw new Error('assistant-automations: fresh Recovery retry reservation was not released');
770
+ }
771
+ budgetSettlementState = 'released';
772
+ reservationId = undefined;
773
+ }
774
+ catch (error) {
775
+ budgetSettlementState = 'unknown';
776
+ reservationId = undefined;
777
+ throw new AutomationRunnerAmbiguousError('assistant-automations: fresh Recovery retry reservation could not be released', {
778
+ cause: error,
779
+ diagnostic: this.hostDiagnostic(definition, {
780
+ failureClass: 'budget', failurePhase: 'budget-settlement',
781
+ failureCode: 'budget-settlement-failed', sideEffectState: 'unknown',
782
+ retryability: 'unsafe', budgetSettlementState,
783
+ }),
784
+ });
785
+ }
786
+ throw new AutomationRunnerFailureError('assistant-automations: Recovery resume did not replay the exact immutable budget key', this.hostDiagnostic(definition, {
787
+ failureClass: 'budget', failurePhase: 'budget-reservation',
788
+ failureCode: 'host-recovery-resume-key-mismatch', sideEffectState: 'none',
789
+ retryability: 'after-intervention', budgetSettlementState,
790
+ }));
791
+ }
792
+ }
793
+ // Availability is proved again after claim and immediately before any
794
+ // Host execution. The reservation above is the only intervening effect.
795
+ const exactProof = this.registry.prove(definition.execution);
796
+ if (!exactProof.available
797
+ || !proof.available
798
+ || exactProof.registrationToken !== proof.registrationToken
799
+ || definitionHash(definition) !== immutableDefinitionHash) {
800
+ throw new HostExecutorRegistryError('stale-registration', 'Host executor changed between availability proof and execution');
801
+ }
802
+ executionStarted = true;
803
+ let hostResult = await this.registry.execute(exactProof, {
804
+ occurrenceId: input.occurrence.id,
805
+ automationId: input.automation.id,
806
+ definitionHash: immutableDefinitionHash,
807
+ executionMode: input.occurrence.dryRun ? 'preview' : 'production',
808
+ targetScope: definition.execution.targetScope,
809
+ principal: definition.principal,
810
+ ownerRouteId: definition.execution.ownerRouteId,
811
+ activationNonce: definition.execution.activationNonce,
812
+ catalogDigest: definition.execution.catalogDigest,
813
+ signal: input.signal,
814
+ }, this.#hostAgentKey === undefined ? undefined : { key: this.#hostAgentKey, input,
815
+ freshReservation: reservationId !== undefined && !replayedFinalizedRecoveryBudget, onNativeResult: result => { nativeResult = result; } });
816
+ if (nativeResult !== undefined && nativeResult.quiescent !== true)
817
+ throw new Error('assistant-automations: native Host execution remains ambiguous');
818
+ if (nativeResult?.outcome === 'unknown')
819
+ hostResult = Object.freeze({ outcome: 'unknown', failureClass: 'unknown',
820
+ failurePhase: 'host-execution', failureCode: 'host-agent-unknown', sideEffectState: 'unknown', retryability: 'unsafe' });
821
+ if (reservationId !== undefined) {
822
+ settlementAttempted = true;
823
+ this.policy.finalize(reservationId, definition.budgetAmount);
824
+ budgetSettlementState = 'finalized';
825
+ }
826
+ // Reserving/finalizing a durable budget is itself an external effect.
827
+ const sideEffectState = nativeResult?.outcome === 'unknown' ? 'unknown'
828
+ : reservationId === undefined && !replayedFinalizedRecoveryBudget ? hostResult.sideEffectState : 'possible';
829
+ const retryability = sideEffectState === 'none' ? hostResult.retryability : 'unsafe';
830
+ return {
831
+ outcome: hostResult.outcome,
832
+ output: `Host execution terminal code: ${hostResult.failureCode}`,
833
+ usage: nativeResult?.usage ?? {},
834
+ ...(nativeResult === undefined ? {} : { unverifiedObjective: true }),
835
+ ...(nativeResult?.sessionId === undefined ? {} : { sessionId: nativeResult.sessionId }),
836
+ ...(nativeResult?.quiescent === undefined ? {} : { quiescent: nativeResult.quiescent }),
837
+ diagnostic: Object.freeze({
838
+ schemaVersion: 1,
839
+ failureClass: hostResult.failureClass,
840
+ failurePhase: hostResult.failurePhase,
841
+ failureCode: hostResult.failureCode,
842
+ promptSubmissionState: 'not-applicable',
843
+ sideEffectState,
844
+ retryability,
845
+ budgetSettlementState,
846
+ }),
847
+ };
848
+ }
849
+ catch (error) {
850
+ if (!executionStarted && (error instanceof AutomationRunnerFailureError || error instanceof AutomationRunnerAmbiguousError))
851
+ throw error;
852
+ let settlementFailed = settlementAttempted && budgetSettlementState === 'reserved';
853
+ if (settlementFailed)
854
+ budgetSettlementState = 'unknown';
855
+ if (reservationId !== undefined && budgetSettlementState === 'reserved' && !settlementAttempted) {
856
+ try {
857
+ // Once the registry invocation boundary may have been crossed, settle
858
+ // conservatively so a crash/retry cannot obtain a second occurrence cap.
859
+ settlementAttempted = true;
860
+ if (executionStarted) {
861
+ this.policy.finalize(reservationId, definition.budgetAmount);
862
+ budgetSettlementState = 'finalized';
863
+ }
864
+ else {
865
+ this.policy.release(reservationId);
866
+ budgetSettlementState = 'released';
867
+ }
868
+ }
869
+ catch {
870
+ settlementFailed = true;
871
+ budgetSettlementState = 'unknown';
872
+ }
873
+ }
874
+ const sideEffectState = executionStarted || settlementFailed
875
+ ? 'unknown'
876
+ : budgetSettlementState === 'released' || budgetSettlementState === 'not-required'
877
+ ? 'none'
878
+ : 'unknown';
879
+ const receipt = this.hostDiagnostic(definition, {
880
+ failureClass: settlementFailed ? 'budget' : 'infrastructure',
881
+ failurePhase: settlementFailed ? 'budget-settlement' : 'host-execution',
882
+ failureCode: settlementFailed
883
+ ? 'budget-settlement-failed'
884
+ : error instanceof HostExecutorRegistryError ? error.code : 'host-executor-failed',
885
+ sideEffectState,
886
+ retryability: sideEffectState === 'none' ? 'safe' : 'unsafe',
887
+ budgetSettlementState,
888
+ });
889
+ if (nativeResult !== undefined) {
890
+ return { outcome: 'unknown', unverifiedObjective: true, output: 'Host execution remains unknown; native usage is an observed lower bound',
891
+ usage: Object.freeze({ ...nativeResult.usage, observedPartial: true }),
892
+ ...(nativeResult.sessionId === undefined ? {} : { sessionId: nativeResult.sessionId }),
893
+ quiescent: false, diagnostic: receipt };
894
+ }
895
+ if (sideEffectState !== 'none') {
896
+ throw new AutomationRunnerAmbiguousError('assistant-automations: Host execution may have produced external effects', { cause: error, diagnostic: receipt });
897
+ }
898
+ throw new AutomationRunnerFailureError('assistant-automations: Host execution failed before external effects', receipt, { cause: error });
899
+ }
900
+ }
901
+ hostDiagnostic(_definition, value) {
902
+ return Object.freeze({ schemaVersion: 1, promptSubmissionState: 'not-applicable', ...value });
903
+ }
904
+ }
905
+ /** Preserves the legacy Agent runner while routing Host definitions explicitly. */
906
+ export class RoutedAutomationRunner {
907
+ agent;
908
+ host;
909
+ constructor(agent, host) {
910
+ this.agent = agent;
911
+ this.host = host;
912
+ }
913
+ run(input) {
914
+ return isHostAutomationDefinition(input.automation.definition)
915
+ ? this.host.run(input)
916
+ : this.agent.run(input);
917
+ }
918
+ }
919
+ function definitionHash(definition) {
920
+ return createHash('sha256').update(JSON.stringify(definition)).digest('hex');
921
+ }
922
+ //# sourceMappingURL=native-agent-execution.js.map