@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.
- package/README.md +37 -7
- package/lib/coordinator.d.ts +45 -5
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +418 -86
- package/lib/coordinator.js.map +1 -1
- package/lib/growth.d.ts +120 -0
- package/lib/growth.d.ts.map +1 -0
- package/lib/growth.js +383 -0
- package/lib/growth.js.map +1 -0
- package/lib/host-executors.d.ts +25 -0
- package/lib/host-executors.d.ts.map +1 -0
- package/lib/host-executors.js +181 -0
- package/lib/host-executors.js.map +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/operator.d.ts +2 -0
- package/lib/operator.d.ts.map +1 -1
- package/lib/operator.js +6 -2
- package/lib/operator.js.map +1 -1
- package/lib/proposals.d.ts +2 -2
- package/lib/proposals.d.ts.map +1 -1
- package/lib/proposals.js +34 -22
- package/lib/proposals.js.map +1 -1
- package/lib/runner.d.ts +28 -2
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +524 -73
- package/lib/runner.js.map +1 -1
- package/lib/service.d.ts +109 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +987 -16
- package/lib/service.js.map +1 -1
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +369 -41
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +175 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +1889 -71
- package/lib/store.js.map +1 -1
- package/lib/tools.js +1 -1
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +354 -11
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +13 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.d.ts.map +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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:
|
|
263
|
+
cwd: definition.workspace,
|
|
144
264
|
agentPreset: presetId,
|
|
145
265
|
},
|
|
146
266
|
agentOptions: {
|
|
147
|
-
provider:
|
|
148
|
-
model:
|
|
149
|
-
maxTokens:
|
|
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 !==
|
|
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
|
-
|
|
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:
|
|
171
|
-
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,
|
|
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 >=
|
|
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
|
-
|
|
336
|
+
phase = 'prompt-submission';
|
|
337
|
+
promptSubmissionState = 'unknown';
|
|
207
338
|
agent.followup(createUserMessage({
|
|
208
|
-
content: [{ type: 'text', text:
|
|
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: ${
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
356
|
+
phase = 'budget-settlement';
|
|
357
|
+
this.policy.finalize(reservationId, definition.budgetAmount);
|
|
225
358
|
reservationSettled = true;
|
|
359
|
+
budgetSettlementState = 'finalized';
|
|
226
360
|
}
|
|
227
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|