@dsh-enhanced/assistant-automations 0.1.64 → 0.1.66
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 +34 -1
- package/lib/coordinator.d.ts +15 -0
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +12 -0
- package/lib/coordinator.js.map +1 -1
- package/lib/host-agent-execution.d.ts +24 -0
- package/lib/host-agent-execution.d.ts.map +1 -0
- package/lib/host-agent-execution.js +248 -0
- package/lib/host-agent-execution.js.map +1 -0
- package/lib/host-executors.d.ts +10 -1
- package/lib/host-executors.d.ts.map +1 -1
- package/lib/host-executors.js +37 -7
- package/lib/host-executors.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/native-agent-execution.d.ts +123 -0
- package/lib/native-agent-execution.d.ts.map +1 -0
- package/lib/native-agent-execution.js +922 -0
- package/lib/native-agent-execution.js.map +1 -0
- package/lib/operator.d.ts +56 -2
- package/lib/operator.d.ts.map +1 -1
- package/lib/operator.js +307 -8
- package/lib/operator.js.map +1 -1
- package/lib/runner.d.ts +2 -86
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +2 -777
- package/lib/runner.js.map +1 -1
- package/lib/schedule.d.ts +2 -0
- package/lib/schedule.d.ts.map +1 -1
- package/lib/schedule.js +8 -1
- package/lib/schedule.js.map +1 -1
- package/lib/service.d.ts +38 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +112 -8
- 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 +53 -2
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +47 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +221 -10
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +59 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +13 -13
package/lib/runner.js
CHANGED
|
@@ -1,778 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
4
|
-
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
5
|
-
import { resolveLlmRouteCapability } from '@dsh-enhanced/llm-route-capabilities';
|
|
6
|
-
import { HostAutomationExecutorRegistry, HostExecutorRegistryError, } from './host-executors.js';
|
|
7
|
-
import { AutomationRunnerAmbiguousError, AutomationRunnerFailureError, } from './coordinator.js';
|
|
8
|
-
import { isHostAutomationDefinition } from './types.js';
|
|
9
|
-
const usageFields = [
|
|
10
|
-
'inputTokens',
|
|
11
|
-
'outputTokens',
|
|
12
|
-
'cacheReadTokens',
|
|
13
|
-
'cacheWriteTokens',
|
|
14
|
-
'reasoningTokens',
|
|
15
|
-
];
|
|
16
|
-
function summarize(events, signal) {
|
|
17
|
-
let output = '';
|
|
18
|
-
// Seeded empty, not zeroed: execution evidence must distinguish a provider
|
|
19
|
-
// that reported zero from one that never supplied usage at all.
|
|
20
|
-
const usage = {};
|
|
21
|
-
let turnReason;
|
|
22
|
-
for (const event of events) {
|
|
23
|
-
if (event.type === 'assistant/message') {
|
|
24
|
-
const text = event.data.message.content
|
|
25
|
-
.filter(block => block.type === 'text')
|
|
26
|
-
.map(block => block.type === 'text' ? block.text : '')
|
|
27
|
-
.join('');
|
|
28
|
-
if (text !== '')
|
|
29
|
-
output = text;
|
|
30
|
-
if (event.data.usage !== undefined) {
|
|
31
|
-
for (const field of usageFields) {
|
|
32
|
-
const amount = event.data.usage[field];
|
|
33
|
-
if (amount !== undefined)
|
|
34
|
-
usage[field] = (usage[field] ?? 0) + amount;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (event.type === 'turn/end')
|
|
39
|
-
turnReason = event.data.reason;
|
|
40
|
-
}
|
|
41
|
-
let outcome = 'unknown';
|
|
42
|
-
if (signal.aborted)
|
|
43
|
-
outcome = 'cancelled';
|
|
44
|
-
else if (typeof turnReason === 'object' && turnReason !== null && 'kind' in turnReason) {
|
|
45
|
-
const kind = turnReason.kind;
|
|
46
|
-
if (kind === 'completed' || kind === 'max-tokens')
|
|
47
|
-
outcome = 'succeeded';
|
|
48
|
-
else if (kind === 'aborted')
|
|
49
|
-
outcome = 'cancelled';
|
|
50
|
-
else if (kind === 'blocked' || kind === 'error')
|
|
51
|
-
outcome = 'failed';
|
|
52
|
-
}
|
|
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 }) };
|
|
57
|
-
}
|
|
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 {
|
|
109
|
-
reservationStatus;
|
|
110
|
-
code = 'automation-budget-reservation-replayed';
|
|
111
|
-
constructor(reservationStatus) {
|
|
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
|
-
}));
|
|
120
|
-
this.reservationStatus = reservationStatus;
|
|
121
|
-
this.name = 'AutomationBudgetReplayError';
|
|
122
|
-
}
|
|
123
|
-
}
|
|
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
|
-
}
|
|
156
|
-
/** Prove the configured Policy budget uses the fixed per-run unit before reservation. */
|
|
157
|
-
export function assertAutomationRunBudget(policy, budgetId) {
|
|
158
|
-
const budget = policy.getBudgetConfig(budgetId);
|
|
159
|
-
if (budget === undefined) {
|
|
160
|
-
throw new Error(`assistant-automations: budget is not configured: ${budgetId}`);
|
|
161
|
-
}
|
|
162
|
-
if (budget.id !== budgetId || budget.metric !== AUTOMATION_RUN_BUDGET_METRIC) {
|
|
163
|
-
throw new Error(`assistant-automations: budget ${budgetId} must use ${AUTOMATION_RUN_BUDGET_METRIC}, got ${budget.metric}`);
|
|
164
|
-
}
|
|
165
|
-
return budget;
|
|
166
|
-
}
|
|
167
|
-
function requireAdapterToolCallProtocol(llm, provider, model, presetId, toolCount) {
|
|
168
|
-
if (toolCount === 0)
|
|
169
|
-
return;
|
|
170
|
-
if (resolveLlmRouteCapability(llm, provider, model)?.toolCalls === 'none') {
|
|
171
|
-
throw new Error(`assistant-automations: adapter ${provider}/${model} declares no DSH tool-call protocol for preset ${presetId}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
export class DshAutomationRunner {
|
|
175
|
-
ctx;
|
|
176
|
-
policy;
|
|
177
|
-
allowUnbudgetedExecution;
|
|
178
|
-
prepareAcceptance;
|
|
179
|
-
acceptanceEnabled;
|
|
180
|
-
beforePrompt;
|
|
181
|
-
maxModelCalls;
|
|
182
|
-
modelOnly;
|
|
183
|
-
constructor(ctx, policy, options = {}) {
|
|
184
|
-
this.ctx = ctx;
|
|
185
|
-
this.policy = policy;
|
|
186
|
-
this.allowUnbudgetedExecution = options.allowUnbudgetedExecution ?? false;
|
|
187
|
-
this.prepareAcceptance = options.prepareAcceptance;
|
|
188
|
-
this.acceptanceEnabled = options.acceptanceEnabled;
|
|
189
|
-
this.beforePrompt = options.beforePrompt;
|
|
190
|
-
if (options.maxModelCalls !== undefined && (!Number.isSafeInteger(options.maxModelCalls) || options.maxModelCalls < 1)) {
|
|
191
|
-
throw new Error('assistant-automations: maxModelCalls must be a positive safe integer');
|
|
192
|
-
}
|
|
193
|
-
this.maxModelCalls = options.maxModelCalls;
|
|
194
|
-
this.modelOnly = options.modelOnly ?? false;
|
|
195
|
-
}
|
|
196
|
-
async run(input) {
|
|
197
|
-
const definition = input.automation.definition;
|
|
198
|
-
if (isHostAutomationDefinition(definition)) {
|
|
199
|
-
throw new AutomationRunnerFailureError('assistant-automations: Host definitions require a registered Host executor', diagnostic({
|
|
200
|
-
failureClass: 'configuration', failurePhase: 'executor-availability',
|
|
201
|
-
failureCode: 'host-executor-router-missing', promptSubmissionState: 'not-applicable',
|
|
202
|
-
sideEffectState: 'none', budgetSettlementState: definition.budgetId === undefined
|
|
203
|
-
? 'not-required'
|
|
204
|
-
: 'not-reserved',
|
|
205
|
-
}));
|
|
206
|
-
}
|
|
207
|
-
if (this.modelOnly && (definition.allowedTools.length !== 0 || definition.maxToolCalls !== 0)) {
|
|
208
|
-
throw new AutomationRunnerFailureError('assistant-automations: modelOnly runner requires an empty immutable tool contract', diagnostic({ failureClass: 'configuration', failurePhase: 'preflight', failureCode: 'model-only-tool-contract-invalid',
|
|
209
|
-
promptSubmissionState: 'not-submitted', sideEffectState: 'none', budgetSettlementState: definition.budgetId === undefined ? 'not-required' : 'not-reserved' }));
|
|
210
|
-
}
|
|
211
|
-
const budgetId = definition.budgetId;
|
|
212
|
-
let reservationId;
|
|
213
|
-
let reservationSettled = false;
|
|
214
|
-
let handle;
|
|
215
|
-
let agentCreationSubmitted = false;
|
|
216
|
-
let followupSubmitted = false;
|
|
217
|
-
let removeAbort;
|
|
218
|
-
let toolCalls = 0;
|
|
219
|
-
let phase = 'preflight';
|
|
220
|
-
let promptSubmissionState = 'not-submitted';
|
|
221
|
-
let sideEffectState = 'none';
|
|
222
|
-
let budgetSettlementState = budgetId === undefined
|
|
223
|
-
? 'not-required'
|
|
224
|
-
: 'not-reserved';
|
|
225
|
-
try {
|
|
226
|
-
const agents = this.ctx.get('agents');
|
|
227
|
-
const sessions = this.ctx.get('sessions');
|
|
228
|
-
const tools = this.ctx.get('tools');
|
|
229
|
-
const llm = this.ctx.get('llm');
|
|
230
|
-
if (agents === undefined || sessions === undefined || tools === undefined || llm === undefined) {
|
|
231
|
-
throw new Error('assistant-automations: agents, sessions, and tools services are required');
|
|
232
|
-
}
|
|
233
|
-
const allowed = new Set(input.occurrence.dryRun ? [] : definition.allowedTools);
|
|
234
|
-
if (input.signal.aborted)
|
|
235
|
-
throw new Error('assistant-automations: run was cancelled before Agent creation');
|
|
236
|
-
phase = 'budget-reservation';
|
|
237
|
-
if (budgetId === undefined && !this.allowUnbudgetedExecution) {
|
|
238
|
-
throw new Error('assistant-automations: a configured budget is required for unattended execution');
|
|
239
|
-
}
|
|
240
|
-
if (budgetId !== undefined)
|
|
241
|
-
assertAutomationRunBudget(this.policy, budgetId);
|
|
242
|
-
const agentPresets = this.ctx.get('agentPresets');
|
|
243
|
-
phase = 'preset-resolution';
|
|
244
|
-
const presetId = agentPresets === undefined
|
|
245
|
-
? definition.agentPreset
|
|
246
|
-
: (await agentPresets.resolve(definition.agentPreset)).id;
|
|
247
|
-
if (budgetId !== undefined) {
|
|
248
|
-
phase = 'budget-reservation';
|
|
249
|
-
const reservation = this.policy.reserve({
|
|
250
|
-
budgetId,
|
|
251
|
-
subject: {
|
|
252
|
-
kind: 'background',
|
|
253
|
-
id: input.automation.id,
|
|
254
|
-
workspace: definition.workspace,
|
|
255
|
-
principal: definition.principal,
|
|
256
|
-
},
|
|
257
|
-
amount: definition.budgetAmount,
|
|
258
|
-
idempotencyKey: [
|
|
259
|
-
'automation-budget', input.automation.id, input.occurrence.id, AUTOMATION_RUN_BUDGET_METRIC, budgetId,
|
|
260
|
-
].join(':'),
|
|
261
|
-
});
|
|
262
|
-
if (reservation.replayed || reservation.status !== 'reserved') {
|
|
263
|
-
throw new AutomationBudgetReplayError(reservation.status);
|
|
264
|
-
}
|
|
265
|
-
reservationId = reservation.reservationId;
|
|
266
|
-
budgetSettlementState = 'reserved';
|
|
267
|
-
}
|
|
268
|
-
phase = 'preflight';
|
|
269
|
-
const globalNames = tools.schemas().map(schema => schema.name);
|
|
270
|
-
const execution = Object.freeze({
|
|
271
|
-
mode: input.occurrence.dryRun ? 'preview' : 'production',
|
|
272
|
-
automationId: input.automation.id,
|
|
273
|
-
occurrenceId: input.occurrence.id,
|
|
274
|
-
});
|
|
275
|
-
phase = 'agent-creation';
|
|
276
|
-
agentCreationSubmitted = true;
|
|
277
|
-
handle = await agents.create({
|
|
278
|
-
sessionId: SessionId(input.sessionId),
|
|
279
|
-
meta: {
|
|
280
|
-
cwd: definition.workspace,
|
|
281
|
-
agentPreset: presetId,
|
|
282
|
-
},
|
|
283
|
-
agentOptions: {
|
|
284
|
-
provider: definition.provider,
|
|
285
|
-
model: definition.model,
|
|
286
|
-
maxTokens: definition.maxOutputTokens,
|
|
287
|
-
},
|
|
288
|
-
signal: input.signal,
|
|
289
|
-
setup: async (agentCtx, preparedAgent) => {
|
|
290
|
-
phase = 'agent-setup';
|
|
291
|
-
const agent = preparedAgent;
|
|
292
|
-
if (agent === undefined) {
|
|
293
|
-
throw new Error('assistant-automations: unpublished Agent identity is missing during setup');
|
|
294
|
-
}
|
|
295
|
-
if (agent.session.header.cwd !== definition.workspace
|
|
296
|
-
|| agent.session.header.agentPreset !== presetId) {
|
|
297
|
-
throw new Error('assistant-automations: background Agent identity does not match the immutable definition');
|
|
298
|
-
}
|
|
299
|
-
agentCtx.provide('assistantAutomationExecution', execution);
|
|
300
|
-
let modelCalls = 0;
|
|
301
|
-
const maxModelCalls = this.maxModelCalls;
|
|
302
|
-
const beforePrompt = this.beforePrompt;
|
|
303
|
-
agentCtx.on('llm/stream', async function* (_options, next) {
|
|
304
|
-
if (maxModelCalls !== undefined && modelCalls >= maxModelCalls) {
|
|
305
|
-
throw new Error('assistant-automations: immutable model-call budget exhausted');
|
|
306
|
-
}
|
|
307
|
-
beforePrompt?.();
|
|
308
|
-
modelCalls += 1;
|
|
309
|
-
yield* next();
|
|
310
|
-
});
|
|
311
|
-
const unbindInitiator = this.policy.bindInitiator(agent, 'background');
|
|
312
|
-
agentCtx.effect(() => unbindInitiator, 'assistant-automations.background-initiator');
|
|
313
|
-
const delivery = this.ctx.get('assistantDelivery');
|
|
314
|
-
// Approval delegation is independent from ordinary run-result
|
|
315
|
-
// delivery. Legacy definitions retain their historical route.
|
|
316
|
-
const bindingId = definition.approvalBindingId ?? definition.deliveryBindingId;
|
|
317
|
-
if (bindingId !== undefined && typeof delivery?.bindAgentApprovalRoute === 'function') {
|
|
318
|
-
const unbindApproval = delivery.bindAgentApprovalRoute(agent, { bindingId });
|
|
319
|
-
agentCtx.effect(() => unbindApproval, 'assistant-automations.approval-route');
|
|
320
|
-
}
|
|
321
|
-
installModelSelection(agentCtx, {
|
|
322
|
-
current: {
|
|
323
|
-
provider: definition.provider,
|
|
324
|
-
model: definition.model,
|
|
325
|
-
},
|
|
326
|
-
assembled: undefined,
|
|
327
|
-
});
|
|
328
|
-
if (!this.modelOnly)
|
|
329
|
-
await agentPresets?.mount(agentCtx, presetId);
|
|
330
|
-
const mountedNames = agentCtx.tools.schemas(agent).map(schema => schema.name);
|
|
331
|
-
for (const name of allowed) {
|
|
332
|
-
if (!mountedNames.includes(name)) {
|
|
333
|
-
throw new Error(`assistant-automations: unknown allowlist tool after preset mount: ${name}`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// Scoped preset tools cannot be removed by DSH restrictions. Mask
|
|
337
|
-
// inherited global tools here and let the final immutable allowlist
|
|
338
|
-
// assertion fail closed for any scoped tool outside the contract.
|
|
339
|
-
const denied = globalNames.filter(name => !allowed.has(name));
|
|
340
|
-
if (denied.length > 0)
|
|
341
|
-
agentCtx.tools.restrict({ deny: denied });
|
|
342
|
-
const finalSchemas = agentCtx.tools.schemas(agent);
|
|
343
|
-
const outsideAllowlist = finalSchemas.map(schema => schema.name).filter(name => !allowed.has(name));
|
|
344
|
-
if (outsideAllowlist.length > 0) {
|
|
345
|
-
throw new Error(`assistant-automations: preset ${presetId} exposes tools outside the immutable allowlist: ${outsideAllowlist.join(', ')}`);
|
|
346
|
-
}
|
|
347
|
-
requireAdapterToolCallProtocol(llm, definition.provider, definition.model, presetId, finalSchemas.length);
|
|
348
|
-
agentCtx.tools.guard((execution) => {
|
|
349
|
-
if (!allowed.has(execution.name))
|
|
350
|
-
return 'assistant-automations: tool is outside the immutable allowlist';
|
|
351
|
-
if (toolCalls >= definition.maxToolCalls) {
|
|
352
|
-
return 'assistant-automations: immutable tool-call budget exhausted';
|
|
353
|
-
}
|
|
354
|
-
toolCalls += 1;
|
|
355
|
-
return undefined;
|
|
356
|
-
});
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
sideEffectState = 'possible';
|
|
360
|
-
const agent = handle.agent;
|
|
361
|
-
const evolution = this.ctx.get('assistantEvolution');
|
|
362
|
-
if (execution.mode === 'production'
|
|
363
|
-
&& typeof evolution?.injectAutomationGuidance === 'function') {
|
|
364
|
-
evolution.injectAutomationGuidance(agent, execution);
|
|
365
|
-
}
|
|
366
|
-
// Resolve the real Delivery owner only after setup installed the immutable
|
|
367
|
-
// approval binding. Never derive an acceptance principal from the
|
|
368
|
-
// definition's display principal or any model-controlled value.
|
|
369
|
-
if (execution.mode === 'production' && this.prepareAcceptance !== undefined && this.acceptanceEnabled?.() === true) {
|
|
370
|
-
const delivery = this.ctx.get('assistantDelivery');
|
|
371
|
-
let owner;
|
|
372
|
-
let binding;
|
|
373
|
-
if (typeof delivery?.prepareAgentApproval === 'function') {
|
|
374
|
-
try {
|
|
375
|
-
const route = delivery.prepareAgentApproval(agent, {
|
|
376
|
-
sourceId: 'dsh-enhanced-assistant-verifier',
|
|
377
|
-
});
|
|
378
|
-
if (route.routeVersion !== 2 || route.sourceId !== 'dsh-enhanced-assistant-verifier'
|
|
379
|
-
|| route.workspace !== definition.workspace || route.bindingId.trim() === ''
|
|
380
|
-
|| !Number.isSafeInteger(route.bindingVersion) || route.bindingVersion < 1
|
|
381
|
-
|| !Number.isSafeInteger(route.bindingGeneration) || route.bindingGeneration < 1
|
|
382
|
-
|| route.principalRecordId.trim() === '' || !Number.isSafeInteger(route.principalVersion)
|
|
383
|
-
|| route.principalVersion < 1) {
|
|
384
|
-
throw new Error('assistant-automations: Delivery acceptance owner route is invalid');
|
|
385
|
-
}
|
|
386
|
-
owner = Object.freeze({ principalRecordId: route.principalRecordId, principalVersion: route.principalVersion });
|
|
387
|
-
binding = Object.freeze({ id: route.bindingId, version: route.bindingVersion, generation: route.bindingGeneration });
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
390
|
-
// The sink decides whether a missing authenticated owner is an
|
|
391
|
-
// optional no-profile skip or a required fail-closed boundary.
|
|
392
|
-
if (typeof error !== 'object' || error === null
|
|
393
|
-
|| !('code' in error) || error.code !== 'missing-binding')
|
|
394
|
-
throw error;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
this.prepareAcceptance({ taskId: input.task.id, automationId: input.automation.id,
|
|
398
|
-
scope: Object.freeze({ workspace: definition.workspace, preset: presetId }), objective: definition.prompt,
|
|
399
|
-
...(owner === undefined ? {} : { owner }), ...(binding === undefined ? {} : { binding }),
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
const abort = () => agent.cancel({ kind: 'hook', reason: 'assistant-automations-signal' });
|
|
403
|
-
input.signal.addEventListener('abort', abort, { once: true });
|
|
404
|
-
removeAbort = () => input.signal.removeEventListener('abort', abort);
|
|
405
|
-
this.beforePrompt?.();
|
|
406
|
-
phase = 'prompt-submission';
|
|
407
|
-
promptSubmissionState = 'unknown';
|
|
408
|
-
agent.followup(createUserMessage({
|
|
409
|
-
content: [{ type: 'text', text: definition.prompt }],
|
|
410
|
-
source: {
|
|
411
|
-
kind: 'assistant-automations',
|
|
412
|
-
form: 'notice',
|
|
413
|
-
summary: `Automation: ${definition.name}`,
|
|
414
|
-
},
|
|
415
|
-
}));
|
|
416
|
-
promptSubmissionState = 'submitted';
|
|
417
|
-
followupSubmitted = true;
|
|
418
|
-
phase = 'model-execution';
|
|
419
|
-
await agent.whenIdle();
|
|
420
|
-
const summary = summarize(agent.session.snapshotEvents(), input.signal);
|
|
421
|
-
sideEffectState = 'possible';
|
|
422
|
-
phase = 'session-flush';
|
|
423
|
-
await sessions.flush(agent.session);
|
|
424
|
-
if (reservationId !== undefined) {
|
|
425
|
-
phase = 'budget-settlement';
|
|
426
|
-
this.policy.finalize(reservationId, definition.budgetAmount);
|
|
427
|
-
reservationSettled = true;
|
|
428
|
-
budgetSettlementState = 'finalized';
|
|
429
|
-
}
|
|
430
|
-
const classified = summary.outcome === 'succeeded'
|
|
431
|
-
? { failureClass: 'none', failurePhase: 'none', failureCode: 'none' }
|
|
432
|
-
: summary.outcome === 'cancelled'
|
|
433
|
-
? { failureClass: 'cancelled', failurePhase: 'model-execution', failureCode: 'agent-turn-cancelled' }
|
|
434
|
-
: summary.outcome === 'unknown'
|
|
435
|
-
? { failureClass: 'execution', failurePhase: 'model-execution', failureCode: 'agent-turn-unknown' }
|
|
436
|
-
: {
|
|
437
|
-
failureClass: 'execution',
|
|
438
|
-
failurePhase: 'model-execution',
|
|
439
|
-
failureCode: summary.turnKind === 'blocked' ? 'agent-turn-blocked' : 'agent-turn-failed',
|
|
440
|
-
};
|
|
441
|
-
const completed = {
|
|
442
|
-
outcome: summary.outcome,
|
|
443
|
-
output: summary.output,
|
|
444
|
-
usage: { ...summary.usage, toolCalls },
|
|
445
|
-
sessionId: input.sessionId,
|
|
446
|
-
diagnostic: diagnostic({
|
|
447
|
-
...classified,
|
|
448
|
-
promptSubmissionState,
|
|
449
|
-
sideEffectState,
|
|
450
|
-
budgetSettlementState,
|
|
451
|
-
}),
|
|
452
|
-
quiescent: !input.signal.aborted,
|
|
453
|
-
};
|
|
454
|
-
removeAbort?.();
|
|
455
|
-
removeAbort = undefined;
|
|
456
|
-
phase = 'agent-disposal';
|
|
457
|
-
await handle?.dispose();
|
|
458
|
-
handle = undefined;
|
|
459
|
-
return completed;
|
|
460
|
-
}
|
|
461
|
-
catch (error) {
|
|
462
|
-
if (error instanceof AutomationBudgetReplayError)
|
|
463
|
-
throw error;
|
|
464
|
-
let caught = error;
|
|
465
|
-
if (reservationId !== undefined && !reservationSettled) {
|
|
466
|
-
try {
|
|
467
|
-
if (!agentCreationSubmitted) {
|
|
468
|
-
this.policy.release(reservationId);
|
|
469
|
-
budgetSettlementState = 'released';
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
this.policy.finalize(reservationId, definition.budgetAmount);
|
|
473
|
-
budgetSettlementState = 'finalized';
|
|
474
|
-
}
|
|
475
|
-
reservationSettled = true;
|
|
476
|
-
}
|
|
477
|
-
catch (settlementError) {
|
|
478
|
-
phase = 'budget-settlement';
|
|
479
|
-
budgetSettlementState = 'unknown';
|
|
480
|
-
sideEffectState = agentCreationSubmitted
|
|
481
|
-
? 'possible'
|
|
482
|
-
: promptSubmissionState === 'not-submitted' ? 'none' : 'unknown';
|
|
483
|
-
caught = new AutomationRunnerAmbiguousError(`assistant-automations: conservative budget settlement failed: ${settlementError instanceof Error ? settlementError.message : String(settlementError)}`, {
|
|
484
|
-
cause: settlementError,
|
|
485
|
-
diagnostic: diagnostic({
|
|
486
|
-
failureClass: 'budget',
|
|
487
|
-
failurePhase: 'budget-settlement',
|
|
488
|
-
failureCode: stableFailureCode(settlementError, 'budget-settlement'),
|
|
489
|
-
promptSubmissionState,
|
|
490
|
-
sideEffectState,
|
|
491
|
-
budgetSettlementState,
|
|
492
|
-
}),
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if (caught instanceof AutomationRunnerAmbiguousError)
|
|
497
|
-
throw caught;
|
|
498
|
-
sideEffectState = agentCreationSubmitted
|
|
499
|
-
? 'possible'
|
|
500
|
-
: promptSubmissionState === 'not-submitted' ? 'none' : 'unknown';
|
|
501
|
-
const receipt = diagnostic({
|
|
502
|
-
failureClass: failureClass(phase, input.signal, caught),
|
|
503
|
-
failurePhase: phase,
|
|
504
|
-
failureCode: stableFailureCode(caught, phase),
|
|
505
|
-
promptSubmissionState,
|
|
506
|
-
sideEffectState,
|
|
507
|
-
budgetSettlementState,
|
|
508
|
-
});
|
|
509
|
-
if (followupSubmitted || promptSubmissionState !== 'not-submitted') {
|
|
510
|
-
throw new AutomationRunnerAmbiguousError(`assistant-automations: Agent execution may have produced side effects: ${caught instanceof Error ? caught.message : String(caught)}`, { cause: caught, diagnostic: receipt });
|
|
511
|
-
}
|
|
512
|
-
throw new AutomationRunnerFailureError(caught instanceof Error ? caught.message : String(caught), receipt, { cause: caught });
|
|
513
|
-
}
|
|
514
|
-
finally {
|
|
515
|
-
removeAbort?.();
|
|
516
|
-
try {
|
|
517
|
-
await handle?.dispose();
|
|
518
|
-
}
|
|
519
|
-
catch {
|
|
520
|
-
// Preserve the original typed failure; cleanup is secondary evidence.
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
/** Executes immutable Host contracts without creating an Agent or model turn. */
|
|
526
|
-
export class HostAutomationRunner {
|
|
527
|
-
registry;
|
|
528
|
-
policy;
|
|
529
|
-
allowUnbudgetedExecution;
|
|
530
|
-
constructor(registry, policy, options = {}) {
|
|
531
|
-
this.registry = registry;
|
|
532
|
-
this.policy = policy;
|
|
533
|
-
this.allowUnbudgetedExecution = options.allowUnbudgetedExecution ?? false;
|
|
534
|
-
}
|
|
535
|
-
async run(input) {
|
|
536
|
-
const definition = input.automation.definition;
|
|
537
|
-
if (!isHostAutomationDefinition(definition)) {
|
|
538
|
-
throw new AutomationRunnerFailureError('assistant-automations: Agent definition reached the Host runner', Object.freeze({
|
|
539
|
-
schemaVersion: 1, failureClass: 'configuration', failurePhase: 'preflight',
|
|
540
|
-
failureCode: 'host-runner-surface-mismatch', promptSubmissionState: 'not-applicable',
|
|
541
|
-
sideEffectState: 'none', retryability: 'after-intervention',
|
|
542
|
-
budgetSettlementState: 'not-required',
|
|
543
|
-
}));
|
|
544
|
-
}
|
|
545
|
-
const budgetSettlementStateBeforeReservation = definition.budgetId === undefined ? 'not-required' : 'not-reserved';
|
|
546
|
-
if (!exactHostRunnerInput(input)) {
|
|
547
|
-
throw new AutomationRunnerFailureError('assistant-automations: Host runner input identity is not exact', this.hostDiagnostic(definition, {
|
|
548
|
-
failureClass: 'configuration', failurePhase: 'preflight',
|
|
549
|
-
failureCode: 'host-runner-identity-mismatch', sideEffectState: 'none',
|
|
550
|
-
retryability: 'after-intervention',
|
|
551
|
-
budgetSettlementState: budgetSettlementStateBeforeReservation,
|
|
552
|
-
}));
|
|
553
|
-
}
|
|
554
|
-
const retryAttempt = input.task.attemptCount > 1;
|
|
555
|
-
const authorizedRecoveryResume = isAuthorizedRecoveryResume(input, definition);
|
|
556
|
-
if (retryAttempt && !authorizedRecoveryResume) {
|
|
557
|
-
throw new AutomationRunnerFailureError('assistant-automations: Host retry is not an exact idempotent Recovery resume', this.hostDiagnostic(definition, {
|
|
558
|
-
failureClass: 'configuration', failurePhase: 'preflight',
|
|
559
|
-
failureCode: 'host-recovery-resume-not-authorized', sideEffectState: 'none',
|
|
560
|
-
retryability: 'after-intervention',
|
|
561
|
-
budgetSettlementState: budgetSettlementStateBeforeReservation,
|
|
562
|
-
}));
|
|
563
|
-
}
|
|
564
|
-
const proof = this.registry.prove(definition.execution);
|
|
565
|
-
if (!proof.available) {
|
|
566
|
-
throw new AutomationRunnerFailureError('assistant-automations: exact Host executor is unavailable', this.hostDiagnostic(definition, {
|
|
567
|
-
failureClass: 'configuration', failurePhase: 'executor-availability',
|
|
568
|
-
failureCode: proof.reasonCode, sideEffectState: 'none', retryability: 'after-intervention',
|
|
569
|
-
budgetSettlementState: definition.budgetId === undefined ? 'not-required' : 'not-reserved',
|
|
570
|
-
}));
|
|
571
|
-
}
|
|
572
|
-
const budgetId = definition.budgetId;
|
|
573
|
-
const immutableDefinitionHash = definitionHash(definition);
|
|
574
|
-
let reservationId;
|
|
575
|
-
let budgetSettlementState = budgetId === undefined
|
|
576
|
-
? 'not-required'
|
|
577
|
-
: 'not-reserved';
|
|
578
|
-
let replayedFinalizedRecoveryBudget = false;
|
|
579
|
-
let executionStarted = false;
|
|
580
|
-
try {
|
|
581
|
-
if (budgetId === undefined && !this.allowUnbudgetedExecution) {
|
|
582
|
-
throw new AutomationRunnerFailureError('assistant-automations: a configured budget is required for unattended Host execution', this.hostDiagnostic(definition, {
|
|
583
|
-
failureClass: 'budget', failurePhase: 'budget-reservation',
|
|
584
|
-
failureCode: 'automation-budget-required', sideEffectState: 'none',
|
|
585
|
-
retryability: 'after-intervention', budgetSettlementState,
|
|
586
|
-
}));
|
|
587
|
-
}
|
|
588
|
-
if (budgetId !== undefined) {
|
|
589
|
-
assertAutomationRunBudget(this.policy, budgetId);
|
|
590
|
-
const reservation = this.policy.reserve({
|
|
591
|
-
budgetId,
|
|
592
|
-
subject: {
|
|
593
|
-
kind: 'background', id: input.automation.id,
|
|
594
|
-
workspace: definition.workspace, principal: definition.principal,
|
|
595
|
-
},
|
|
596
|
-
amount: definition.budgetAmount,
|
|
597
|
-
idempotencyKey: hostBudgetIdempotencyKey(input, immutableDefinitionHash, budgetId),
|
|
598
|
-
});
|
|
599
|
-
if (reservation.status === 'finalized'
|
|
600
|
-
&& reservation.replayed
|
|
601
|
-
&& authorizedRecoveryResume) {
|
|
602
|
-
// Recovery owns an idempotent, durable operation ledger. A crash can
|
|
603
|
-
// happen after that operation and its exact budget were finalized but
|
|
604
|
-
// before Automations committed the terminal run. Invoke the exact
|
|
605
|
-
// executor again only to read/replay its durable terminal result; the
|
|
606
|
-
// already-finalized reservation must never be settled a second time.
|
|
607
|
-
replayedFinalizedRecoveryBudget = true;
|
|
608
|
-
budgetSettlementState = 'finalized';
|
|
609
|
-
}
|
|
610
|
-
else if (reservation.status !== 'reserved') {
|
|
611
|
-
const uncertain = reservation.status !== 'released';
|
|
612
|
-
throw new AutomationRunnerFailureError('assistant-automations: Host budget reservation was replayed', this.hostDiagnostic(definition, {
|
|
613
|
-
failureClass: 'budget', failurePhase: 'budget-reservation',
|
|
614
|
-
failureCode: 'automation-budget-reservation-replayed',
|
|
615
|
-
sideEffectState: uncertain ? 'unknown' : 'none',
|
|
616
|
-
retryability: uncertain ? 'unsafe' : 'after-intervention',
|
|
617
|
-
budgetSettlementState: reservation.status,
|
|
618
|
-
}));
|
|
619
|
-
}
|
|
620
|
-
else {
|
|
621
|
-
reservationId = reservation.reservationId;
|
|
622
|
-
budgetSettlementState = 'reserved';
|
|
623
|
-
}
|
|
624
|
-
if (!replayedFinalizedRecoveryBudget && reservation.replayed && !authorizedRecoveryResume) {
|
|
625
|
-
// A replay on the first attempt is concurrent/duplicate execution,
|
|
626
|
-
// not crash recovery. Keep the original reservation untouched.
|
|
627
|
-
reservationId = undefined;
|
|
628
|
-
throw new AutomationRunnerFailureError('assistant-automations: Host budget reservation was replayed outside Recovery resume', this.hostDiagnostic(definition, {
|
|
629
|
-
failureClass: 'budget', failurePhase: 'budget-reservation',
|
|
630
|
-
failureCode: 'automation-budget-reservation-replayed',
|
|
631
|
-
sideEffectState: 'unknown', retryability: 'unsafe',
|
|
632
|
-
budgetSettlementState: 'reserved',
|
|
633
|
-
}));
|
|
634
|
-
}
|
|
635
|
-
if (!replayedFinalizedRecoveryBudget && !reservation.replayed && authorizedRecoveryResume) {
|
|
636
|
-
// A recovered attempt must see the exact prior reservation. A fresh
|
|
637
|
-
// receipt proves the immutable definition/key changed (or no prior
|
|
638
|
-
// execution reached the budget boundary), so it cannot resume this
|
|
639
|
-
// occurrence. Release only the newly created reservation.
|
|
640
|
-
try {
|
|
641
|
-
const released = this.policy.release(reservation.reservationId);
|
|
642
|
-
if (released.status !== 'released') {
|
|
643
|
-
throw new Error('assistant-automations: fresh Recovery retry reservation was not released');
|
|
644
|
-
}
|
|
645
|
-
budgetSettlementState = 'released';
|
|
646
|
-
reservationId = undefined;
|
|
647
|
-
}
|
|
648
|
-
catch (error) {
|
|
649
|
-
budgetSettlementState = 'unknown';
|
|
650
|
-
reservationId = undefined;
|
|
651
|
-
throw new AutomationRunnerAmbiguousError('assistant-automations: fresh Recovery retry reservation could not be released', {
|
|
652
|
-
cause: error,
|
|
653
|
-
diagnostic: this.hostDiagnostic(definition, {
|
|
654
|
-
failureClass: 'budget', failurePhase: 'budget-settlement',
|
|
655
|
-
failureCode: 'budget-settlement-failed', sideEffectState: 'unknown',
|
|
656
|
-
retryability: 'unsafe', budgetSettlementState,
|
|
657
|
-
}),
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
throw new AutomationRunnerFailureError('assistant-automations: Recovery resume did not replay the exact immutable budget key', this.hostDiagnostic(definition, {
|
|
661
|
-
failureClass: 'budget', failurePhase: 'budget-reservation',
|
|
662
|
-
failureCode: 'host-recovery-resume-key-mismatch', sideEffectState: 'none',
|
|
663
|
-
retryability: 'after-intervention', budgetSettlementState,
|
|
664
|
-
}));
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
// Availability is proved again after claim and immediately before any
|
|
668
|
-
// Host execution. The reservation above is the only intervening effect.
|
|
669
|
-
const exactProof = this.registry.prove(definition.execution);
|
|
670
|
-
if (!exactProof.available
|
|
671
|
-
|| !proof.available
|
|
672
|
-
|| exactProof.registrationToken !== proof.registrationToken) {
|
|
673
|
-
throw new HostExecutorRegistryError('stale-registration', 'Host executor changed between availability proof and execution');
|
|
674
|
-
}
|
|
675
|
-
executionStarted = true;
|
|
676
|
-
const hostResult = await this.registry.execute(exactProof, {
|
|
677
|
-
occurrenceId: input.occurrence.id,
|
|
678
|
-
automationId: input.automation.id,
|
|
679
|
-
definitionHash: immutableDefinitionHash,
|
|
680
|
-
executionMode: input.occurrence.dryRun ? 'preview' : 'production',
|
|
681
|
-
targetScope: definition.execution.targetScope,
|
|
682
|
-
principal: definition.principal,
|
|
683
|
-
ownerRouteId: definition.execution.ownerRouteId,
|
|
684
|
-
activationNonce: definition.execution.activationNonce,
|
|
685
|
-
catalogDigest: definition.execution.catalogDigest,
|
|
686
|
-
signal: input.signal,
|
|
687
|
-
});
|
|
688
|
-
if (reservationId !== undefined) {
|
|
689
|
-
this.policy.finalize(reservationId, definition.budgetAmount);
|
|
690
|
-
budgetSettlementState = 'finalized';
|
|
691
|
-
}
|
|
692
|
-
// Reserving/finalizing a durable budget is itself an external effect.
|
|
693
|
-
const sideEffectState = reservationId === undefined
|
|
694
|
-
&& !replayedFinalizedRecoveryBudget
|
|
695
|
-
? hostResult.sideEffectState
|
|
696
|
-
: 'possible';
|
|
697
|
-
const retryability = sideEffectState === 'none' ? hostResult.retryability : 'unsafe';
|
|
698
|
-
return {
|
|
699
|
-
outcome: hostResult.outcome,
|
|
700
|
-
output: `Host execution terminal code: ${hostResult.failureCode}`,
|
|
701
|
-
usage: {},
|
|
702
|
-
diagnostic: Object.freeze({
|
|
703
|
-
schemaVersion: 1,
|
|
704
|
-
failureClass: hostResult.failureClass,
|
|
705
|
-
failurePhase: hostResult.failurePhase,
|
|
706
|
-
failureCode: hostResult.failureCode,
|
|
707
|
-
promptSubmissionState: 'not-applicable',
|
|
708
|
-
sideEffectState,
|
|
709
|
-
retryability,
|
|
710
|
-
budgetSettlementState,
|
|
711
|
-
}),
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
catch (error) {
|
|
715
|
-
if (error instanceof AutomationRunnerFailureError || error instanceof AutomationRunnerAmbiguousError)
|
|
716
|
-
throw error;
|
|
717
|
-
let settlementFailed = false;
|
|
718
|
-
if (reservationId !== undefined && budgetSettlementState === 'reserved') {
|
|
719
|
-
try {
|
|
720
|
-
// Once the registry invocation boundary may have been crossed, settle
|
|
721
|
-
// conservatively so a crash/retry cannot obtain a second occurrence cap.
|
|
722
|
-
if (executionStarted) {
|
|
723
|
-
this.policy.finalize(reservationId, definition.budgetAmount);
|
|
724
|
-
budgetSettlementState = 'finalized';
|
|
725
|
-
}
|
|
726
|
-
else {
|
|
727
|
-
this.policy.release(reservationId);
|
|
728
|
-
budgetSettlementState = 'released';
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
catch {
|
|
732
|
-
settlementFailed = true;
|
|
733
|
-
budgetSettlementState = 'unknown';
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
const sideEffectState = executionStarted || settlementFailed
|
|
737
|
-
? 'unknown'
|
|
738
|
-
: budgetSettlementState === 'released' || budgetSettlementState === 'not-required'
|
|
739
|
-
? 'none'
|
|
740
|
-
: 'unknown';
|
|
741
|
-
const receipt = this.hostDiagnostic(definition, {
|
|
742
|
-
failureClass: settlementFailed ? 'budget' : 'infrastructure',
|
|
743
|
-
failurePhase: settlementFailed ? 'budget-settlement' : 'host-execution',
|
|
744
|
-
failureCode: settlementFailed
|
|
745
|
-
? 'budget-settlement-failed'
|
|
746
|
-
: error instanceof HostExecutorRegistryError ? error.code : 'host-executor-failed',
|
|
747
|
-
sideEffectState,
|
|
748
|
-
retryability: sideEffectState === 'none' ? 'safe' : 'unsafe',
|
|
749
|
-
budgetSettlementState,
|
|
750
|
-
});
|
|
751
|
-
if (sideEffectState !== 'none') {
|
|
752
|
-
throw new AutomationRunnerAmbiguousError('assistant-automations: Host execution may have produced external effects', { cause: error, diagnostic: receipt });
|
|
753
|
-
}
|
|
754
|
-
throw new AutomationRunnerFailureError('assistant-automations: Host execution failed before external effects', receipt, { cause: error });
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
hostDiagnostic(_definition, value) {
|
|
758
|
-
return Object.freeze({ schemaVersion: 1, promptSubmissionState: 'not-applicable', ...value });
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
/** Preserves the legacy Agent runner while routing Host definitions explicitly. */
|
|
762
|
-
export class RoutedAutomationRunner {
|
|
763
|
-
agent;
|
|
764
|
-
host;
|
|
765
|
-
constructor(agent, host) {
|
|
766
|
-
this.agent = agent;
|
|
767
|
-
this.host = host;
|
|
768
|
-
}
|
|
769
|
-
run(input) {
|
|
770
|
-
return isHostAutomationDefinition(input.automation.definition)
|
|
771
|
-
? this.host.run(input)
|
|
772
|
-
: this.agent.run(input);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
function definitionHash(definition) {
|
|
776
|
-
return createHash('sha256').update(JSON.stringify(definition)).digest('hex');
|
|
777
|
-
}
|
|
1
|
+
// Compatibility surface: native execution mechanics are shared with Host-owned Agents.
|
|
2
|
+
export { DshAutomationRunner, HostAutomationRunner, RoutedAutomationRunner, AutomationBudgetReplayError, AUTOMATION_RUN_BUDGET_METRIC, assertAutomationRunBudget } from './native-agent-execution.js';
|
|
778
3
|
//# sourceMappingURL=runner.js.map
|