@adjudicate/adapter-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/package.json +36 -0
- package/src/bridge.ts +111 -0
- package/src/decisions.ts +294 -0
- package/src/errors.ts +36 -0
- package/src/index.ts +98 -0
- package/src/loop.ts +569 -0
- package/src/persistence-redis.ts +158 -0
- package/src/persistence.ts +176 -0
- package/src/trace.ts +105 -0
- package/src/types.ts +236 -0
- package/tests/bridge.test.ts +112 -0
- package/tests/decisions.test.ts +194 -0
- package/tests/persistence-redis.test.ts +207 -0
- package/tests/persistence.test.ts +105 -0
- package/tests/trace.test.ts +223 -0
- package/tsconfig.json +7 -0
- package/vitest.config.ts +27 -0
package/src/loop.ts
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createAdjudicatedAgent` — the provider-neutral message-loop orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Wires the planner, renderer, provider bridge, kernel, and Decision
|
|
5
|
+
* translator into a single send/resume/confirm surface.
|
|
6
|
+
*
|
|
7
|
+
* Invariants the loop preserves:
|
|
8
|
+
* - `pack.planner.plan(state, context)` is called every iteration. State
|
|
9
|
+
* may change mid-turn (a refund executes, freeing a previously-locked
|
|
10
|
+
* tool); the visible-tool surface MUST update accordingly.
|
|
11
|
+
* - The Pack passed in MUST already be `safePlan` + `withBasisAudit`
|
|
12
|
+
* wrapped (Pack-author convention). The loop does NOT double-wrap.
|
|
13
|
+
* - Every intent envelope crosses `adjudicateAndAudit()` from
|
|
14
|
+
* `@adjudicate/core/kernel`. The loop never bypasses the kernel,
|
|
15
|
+
* never raises taint, and never short-circuits the guard ordering.
|
|
16
|
+
* - First non-continue Decision wins: subsequent tool_use blocks in the
|
|
17
|
+
* same assistant turn are surfaced as `not_processed_due_to_pause`.
|
|
18
|
+
* - History `H` is opaque. The bridge is the only thing that knows the
|
|
19
|
+
* conversation-history shape; the loop threads it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
noopAuditSink,
|
|
24
|
+
sha256Canonical,
|
|
25
|
+
type Decision,
|
|
26
|
+
type IntentEnvelope,
|
|
27
|
+
} from "@adjudicate/core";
|
|
28
|
+
import { adjudicateAndAudit } from "@adjudicate/core/kernel";
|
|
29
|
+
import { resumeDeferredIntent } from "@adjudicate/runtime";
|
|
30
|
+
import {
|
|
31
|
+
buildEnvelopeFromToolUse,
|
|
32
|
+
classifyIncomingToolUse,
|
|
33
|
+
} from "./bridge.js";
|
|
34
|
+
import {
|
|
35
|
+
makeOutOfPlanToolResult,
|
|
36
|
+
translateDecision,
|
|
37
|
+
type LoopAction,
|
|
38
|
+
} from "./decisions.js";
|
|
39
|
+
import { AdapterError, AdapterErrorCode } from "./errors.js";
|
|
40
|
+
import { noopTraceSink, type AdapterPauseReason } from "./trace.js";
|
|
41
|
+
import type {
|
|
42
|
+
AdjudicatedAgent,
|
|
43
|
+
AdjudicatedAgentOptions,
|
|
44
|
+
AgentEvent,
|
|
45
|
+
AgentOutcome,
|
|
46
|
+
AgentTurnResult,
|
|
47
|
+
ConfirmArgs,
|
|
48
|
+
ResumeArgs,
|
|
49
|
+
SendInput,
|
|
50
|
+
ToolResultBlock,
|
|
51
|
+
} from "./types.js";
|
|
52
|
+
|
|
53
|
+
const DEFAULT_MAX_ITERATIONS = 8;
|
|
54
|
+
|
|
55
|
+
export function createAdjudicatedAgent<K extends string, P, S, C, H>(
|
|
56
|
+
options: AdjudicatedAgentOptions<K, P, S, C, H>,
|
|
57
|
+
): AdjudicatedAgent<K, P, S, C, H> {
|
|
58
|
+
const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
59
|
+
const rk = options.rk ?? ((raw: string) => raw);
|
|
60
|
+
const deriveNonce =
|
|
61
|
+
options.deriveNonce ?? ((args) => args.toolUseId);
|
|
62
|
+
const bridge = options.bridge;
|
|
63
|
+
const traceSink = options.traceSink ?? noopTraceSink;
|
|
64
|
+
|
|
65
|
+
function pauseActionToReason(kind: LoopAction["kind"]): AdapterPauseReason | undefined {
|
|
66
|
+
switch (kind) {
|
|
67
|
+
case "pause_for_defer":
|
|
68
|
+
return "deferred";
|
|
69
|
+
case "pause_for_user_confirmation":
|
|
70
|
+
return "awaiting_confirmation";
|
|
71
|
+
case "complete_for_escalation":
|
|
72
|
+
return "escalated";
|
|
73
|
+
default:
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function runLoop(
|
|
79
|
+
sessionId: string,
|
|
80
|
+
initialHistory: H,
|
|
81
|
+
state: S,
|
|
82
|
+
context: C,
|
|
83
|
+
seedEvents: ReadonlyArray<AgentEvent>,
|
|
84
|
+
/**
|
|
85
|
+
* Optional pre-seeded Decision injected before the first provider
|
|
86
|
+
* call — used by `confirm()` and `resume()` to splice an authoritative
|
|
87
|
+
* Decision (from the user's confirmation or the resumed envelope) back
|
|
88
|
+
* into the conversation without consulting the LLM again first.
|
|
89
|
+
*/
|
|
90
|
+
seedDecision: SeedDecision<K, P> | null,
|
|
91
|
+
): Promise<AgentTurnResult<H>> {
|
|
92
|
+
const events: AgentEvent[] = [...seedEvents];
|
|
93
|
+
let history = initialHistory;
|
|
94
|
+
let lastDecision: Decision | null = null;
|
|
95
|
+
|
|
96
|
+
if (seedDecision !== null) {
|
|
97
|
+
const single = await processSingleDecision({
|
|
98
|
+
decision: seedDecision.decision,
|
|
99
|
+
envelope: seedDecision.envelope,
|
|
100
|
+
toolUseId: seedDecision.toolUseId,
|
|
101
|
+
sessionId,
|
|
102
|
+
state,
|
|
103
|
+
historySnapshot: history,
|
|
104
|
+
});
|
|
105
|
+
lastDecision = seedDecision.decision;
|
|
106
|
+
events.push(...single.events);
|
|
107
|
+
if (single.toolResult !== null) {
|
|
108
|
+
history = bridge.appendToolResults(history, [single.toolResult]);
|
|
109
|
+
}
|
|
110
|
+
if (single.loopAction.kind !== "continue") {
|
|
111
|
+
return {
|
|
112
|
+
events,
|
|
113
|
+
history,
|
|
114
|
+
outcome: pauseToOutcome(single.loopAction, lastDecision),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
120
|
+
traceSink.onTrace({
|
|
121
|
+
phase: "iteration_start",
|
|
122
|
+
sessionId,
|
|
123
|
+
iteration: iter + 1,
|
|
124
|
+
});
|
|
125
|
+
const plan = options.pack.planner.plan(state, context);
|
|
126
|
+
const rendered = options.renderer.render(state, context, plan);
|
|
127
|
+
|
|
128
|
+
const sent = await bridge.send(history, {
|
|
129
|
+
systemPrompt: rendered.systemPrompt,
|
|
130
|
+
maxTokens: rendered.maxTokens,
|
|
131
|
+
toolSchemas: rendered.toolSchemas,
|
|
132
|
+
});
|
|
133
|
+
history = sent.history;
|
|
134
|
+
|
|
135
|
+
for (const text of sent.turn.textBlocks) {
|
|
136
|
+
events.push({ kind: "assistant_text", text });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (sent.turn.toolUses.length === 0) {
|
|
140
|
+
traceSink.onTrace({
|
|
141
|
+
phase: "completed",
|
|
142
|
+
sessionId,
|
|
143
|
+
iteration: iter + 1,
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
events,
|
|
147
|
+
history,
|
|
148
|
+
outcome: {
|
|
149
|
+
kind: "completed",
|
|
150
|
+
assistantText: sent.turn.textBlocks.join(""),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const toolResults: ToolResultBlock[] = [];
|
|
156
|
+
let pauseAction: LoopAction | null = null;
|
|
157
|
+
|
|
158
|
+
for (const tu of sent.turn.toolUses) {
|
|
159
|
+
events.push({
|
|
160
|
+
kind: "tool_use",
|
|
161
|
+
toolUseId: tu.id,
|
|
162
|
+
toolName: tu.name,
|
|
163
|
+
input: tu.input,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (pauseAction !== null) {
|
|
167
|
+
// First non-continue Decision wins: surface remaining tool_uses
|
|
168
|
+
// as not-processed so the LLM (on resume) understands they were
|
|
169
|
+
// skipped this turn.
|
|
170
|
+
toolResults.push({
|
|
171
|
+
toolUseId: tu.id,
|
|
172
|
+
content: "Not processed: prior tool_use paused this turn.",
|
|
173
|
+
isError: true,
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const cls = classifyIncomingToolUse(
|
|
179
|
+
{ name: tu.name, input: tu.input },
|
|
180
|
+
plan,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (cls.kind === "out_of_plan") {
|
|
184
|
+
const result = makeOutOfPlanToolResult(tu.id, tu.name);
|
|
185
|
+
toolResults.push(result);
|
|
186
|
+
events.push({ kind: "tool_result", toolUseId: tu.id, payload: result });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (cls.kind === "read") {
|
|
191
|
+
let readResult: unknown;
|
|
192
|
+
try {
|
|
193
|
+
readResult = await options.executor.invokeRead(
|
|
194
|
+
cls.name,
|
|
195
|
+
cls.input,
|
|
196
|
+
state,
|
|
197
|
+
);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
const message =
|
|
200
|
+
err instanceof Error ? err.message : "executor read failed";
|
|
201
|
+
const errResult: ToolResultBlock = {
|
|
202
|
+
toolUseId: tu.id,
|
|
203
|
+
content: `Tool failed: ${message}`,
|
|
204
|
+
isError: true,
|
|
205
|
+
};
|
|
206
|
+
toolResults.push(errResult);
|
|
207
|
+
events.push({
|
|
208
|
+
kind: "tool_result",
|
|
209
|
+
toolUseId: tu.id,
|
|
210
|
+
payload: errResult,
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const result: ToolResultBlock = {
|
|
215
|
+
toolUseId: tu.id,
|
|
216
|
+
content: JSON.stringify({ ok: true, result: readResult }),
|
|
217
|
+
};
|
|
218
|
+
toolResults.push(result);
|
|
219
|
+
events.push({
|
|
220
|
+
kind: "handler_result",
|
|
221
|
+
toolUseId: tu.id,
|
|
222
|
+
result: readResult,
|
|
223
|
+
});
|
|
224
|
+
events.push({ kind: "tool_result", toolUseId: tu.id, payload: result });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// cls.kind === "intent"
|
|
229
|
+
const envelope = buildEnvelopeFromToolUse({
|
|
230
|
+
intentKind: cls.intentKind,
|
|
231
|
+
payload: cls.payload,
|
|
232
|
+
sessionId,
|
|
233
|
+
taint: "UNTRUSTED",
|
|
234
|
+
nonce: deriveNonce({
|
|
235
|
+
sessionId,
|
|
236
|
+
toolUseId: tu.id,
|
|
237
|
+
payload: cls.payload,
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
events.push({ kind: "intent_proposed", envelope });
|
|
241
|
+
|
|
242
|
+
const { decision } = await adjudicateAndAudit(
|
|
243
|
+
envelope as IntentEnvelope<K, P>,
|
|
244
|
+
state,
|
|
245
|
+
options.pack.policy,
|
|
246
|
+
{
|
|
247
|
+
sink: options.auditSink ?? noopAuditSink(),
|
|
248
|
+
ledger: options.ledger,
|
|
249
|
+
context: options.runtimeContext,
|
|
250
|
+
plan: () => ({
|
|
251
|
+
visibleReadTools: plan.visibleReadTools,
|
|
252
|
+
allowedIntents: plan.allowedIntents,
|
|
253
|
+
}),
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
lastDecision = decision;
|
|
257
|
+
events.push({ kind: "decision", decision, envelope });
|
|
258
|
+
traceSink.onTrace({
|
|
259
|
+
phase: "decision_emitted",
|
|
260
|
+
sessionId,
|
|
261
|
+
iteration: iter + 1,
|
|
262
|
+
decisionKind: decision.kind,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const single = await processSingleDecision({
|
|
266
|
+
decision,
|
|
267
|
+
envelope: envelope as IntentEnvelope<K, P>,
|
|
268
|
+
toolUseId: tu.id,
|
|
269
|
+
sessionId,
|
|
270
|
+
state,
|
|
271
|
+
historySnapshot: history,
|
|
272
|
+
});
|
|
273
|
+
events.push(...single.events);
|
|
274
|
+
if (single.toolResult) toolResults.push(single.toolResult);
|
|
275
|
+
if (single.loopAction.kind !== "continue") {
|
|
276
|
+
pauseAction = single.loopAction;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (toolResults.length > 0) {
|
|
281
|
+
history = bridge.appendToolResults(history, toolResults);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (pauseAction !== null) {
|
|
285
|
+
const reason = pauseActionToReason(pauseAction.kind);
|
|
286
|
+
traceSink.onTrace({
|
|
287
|
+
phase: "paused",
|
|
288
|
+
sessionId,
|
|
289
|
+
iteration: iter + 1,
|
|
290
|
+
...(reason !== undefined ? { pauseReason: reason } : {}),
|
|
291
|
+
...(lastDecision !== null
|
|
292
|
+
? { decisionKind: lastDecision.kind }
|
|
293
|
+
: {}),
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
events,
|
|
297
|
+
history,
|
|
298
|
+
outcome: pauseToOutcome(pauseAction, lastDecision),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
traceSink.onTrace({
|
|
304
|
+
phase: "max_iterations_exceeded",
|
|
305
|
+
sessionId,
|
|
306
|
+
iteration: maxIterations,
|
|
307
|
+
...(lastDecision !== null ? { decisionKind: lastDecision.kind } : {}),
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
events,
|
|
311
|
+
history,
|
|
312
|
+
outcome: { kind: "max_iterations_exceeded", lastDecision },
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
type ProcessResult = {
|
|
316
|
+
events: AgentEvent[];
|
|
317
|
+
toolResult: ToolResultBlock | null;
|
|
318
|
+
loopAction: LoopAction;
|
|
319
|
+
};
|
|
320
|
+
async function processSingleDecision(args: {
|
|
321
|
+
decision: Decision;
|
|
322
|
+
envelope: IntentEnvelope<K, P>;
|
|
323
|
+
toolUseId: string;
|
|
324
|
+
sessionId: string;
|
|
325
|
+
state: S;
|
|
326
|
+
historySnapshot: H;
|
|
327
|
+
}): Promise<ProcessResult> {
|
|
328
|
+
const t = await translateDecision({
|
|
329
|
+
decision: args.decision,
|
|
330
|
+
envelope: args.envelope,
|
|
331
|
+
toolUseId: args.toolUseId,
|
|
332
|
+
sessionId: args.sessionId,
|
|
333
|
+
state: args.state,
|
|
334
|
+
executor: options.executor,
|
|
335
|
+
deferStore: options.deferStore,
|
|
336
|
+
confirmationStore: options.confirmationStore,
|
|
337
|
+
historySnapshot: args.historySnapshot,
|
|
338
|
+
rk,
|
|
339
|
+
log: options.log,
|
|
340
|
+
generateToken: () =>
|
|
341
|
+
globalThis.crypto?.randomUUID?.() ??
|
|
342
|
+
`ct-${Math.random().toString(36).slice(2)}-${Date.now()}`,
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
events: [...t.extraEvents],
|
|
346
|
+
toolResult: t.toolResult,
|
|
347
|
+
loopAction: t.loopAction,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
async send(input: SendInput<S, C, H>) {
|
|
354
|
+
const baseHistory = input.history ?? bridge.emptyHistory();
|
|
355
|
+
const initialHistory = bridge.appendUserMessage(
|
|
356
|
+
baseHistory,
|
|
357
|
+
input.userMessage,
|
|
358
|
+
);
|
|
359
|
+
const seedEvents: AgentEvent[] = [
|
|
360
|
+
{ kind: "user_message", text: input.userMessage },
|
|
361
|
+
];
|
|
362
|
+
return runLoop(
|
|
363
|
+
input.sessionId,
|
|
364
|
+
initialHistory,
|
|
365
|
+
input.state,
|
|
366
|
+
input.context,
|
|
367
|
+
seedEvents,
|
|
368
|
+
null,
|
|
369
|
+
);
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
async resume(args: ResumeArgs<S, C, H>) {
|
|
373
|
+
const result = await resumeDeferredIntent({
|
|
374
|
+
sessionId: args.sessionId,
|
|
375
|
+
signal: args.signal,
|
|
376
|
+
redis: options.deferStore,
|
|
377
|
+
rk,
|
|
378
|
+
log: options.log,
|
|
379
|
+
verifyHash: options.verifyParkedHash ?? "warn",
|
|
380
|
+
});
|
|
381
|
+
if (!result.resumed || !result.parked) {
|
|
382
|
+
throw new AdapterError(
|
|
383
|
+
AdapterErrorCode.RESUME_NO_PARKED,
|
|
384
|
+
`No parked envelope for session "${args.sessionId}" and signal "${args.signal}" (reason: ${result.reason ?? "unknown"})`,
|
|
385
|
+
{ sessionId: args.sessionId, signal: args.signal, reason: result.reason },
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const envelope: IntentEnvelope<K, P> = {
|
|
390
|
+
version: 2,
|
|
391
|
+
kind: result.parked.envelope.kind as K,
|
|
392
|
+
payload: result.parked.envelope.payload as P,
|
|
393
|
+
createdAt: new Date().toISOString(),
|
|
394
|
+
nonce: result.parked.envelope.intentHash,
|
|
395
|
+
actor: {
|
|
396
|
+
principal: "system",
|
|
397
|
+
sessionId: result.parked.envelope.actor.sessionId,
|
|
398
|
+
},
|
|
399
|
+
taint: "TRUSTED",
|
|
400
|
+
intentHash: result.parked.envelope.intentHash,
|
|
401
|
+
};
|
|
402
|
+
const resumePlan = options.pack.planner.plan(args.state, args.context);
|
|
403
|
+
const { decision } = await adjudicateAndAudit(
|
|
404
|
+
envelope,
|
|
405
|
+
args.state,
|
|
406
|
+
options.pack.policy,
|
|
407
|
+
{
|
|
408
|
+
sink: options.auditSink ?? noopAuditSink(),
|
|
409
|
+
ledger: options.ledger,
|
|
410
|
+
context: options.runtimeContext,
|
|
411
|
+
plan: () => ({
|
|
412
|
+
visibleReadTools: resumePlan.visibleReadTools,
|
|
413
|
+
allowedIntents: resumePlan.allowedIntents,
|
|
414
|
+
}),
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const seedEvents: AgentEvent[] = [
|
|
419
|
+
{ kind: "intent_proposed", envelope },
|
|
420
|
+
{ kind: "decision", decision, envelope },
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
const fauxToolUseId = `resume-${result.parked.envelope.intentHash.slice(0, 8)}`;
|
|
424
|
+
const seedDecision: SeedDecision<K, P> = {
|
|
425
|
+
decision,
|
|
426
|
+
envelope,
|
|
427
|
+
toolUseId: fauxToolUseId,
|
|
428
|
+
};
|
|
429
|
+
return runLoop(
|
|
430
|
+
args.sessionId,
|
|
431
|
+
args.history ?? bridge.emptyHistory(),
|
|
432
|
+
args.state,
|
|
433
|
+
args.context,
|
|
434
|
+
seedEvents,
|
|
435
|
+
seedDecision,
|
|
436
|
+
);
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
async confirm(args: ConfirmArgs<S, C>) {
|
|
440
|
+
const pending = await options.confirmationStore.take(
|
|
441
|
+
args.confirmationToken,
|
|
442
|
+
);
|
|
443
|
+
if (pending === null) {
|
|
444
|
+
throw new AdapterError(
|
|
445
|
+
AdapterErrorCode.CONFIRMATION_TOKEN_INVALID,
|
|
446
|
+
`Confirmation token "${args.confirmationToken}" is unknown or expired.`,
|
|
447
|
+
{ confirmationToken: args.confirmationToken },
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const verifyMode = options.verifyParkedHash ?? "warn";
|
|
452
|
+
if (verifyMode !== "off") {
|
|
453
|
+
const derived = sha256Canonical({
|
|
454
|
+
version: pending.envelope.version,
|
|
455
|
+
kind: pending.envelope.kind,
|
|
456
|
+
payload: pending.envelope.payload,
|
|
457
|
+
nonce: pending.envelope.nonce,
|
|
458
|
+
actor: pending.envelope.actor,
|
|
459
|
+
taint: pending.envelope.taint,
|
|
460
|
+
});
|
|
461
|
+
if (derived !== pending.envelope.intentHash) {
|
|
462
|
+
options.log?.warn?.(
|
|
463
|
+
{
|
|
464
|
+
sessionId: pending.sessionId,
|
|
465
|
+
stored: pending.envelope.intentHash,
|
|
466
|
+
derived,
|
|
467
|
+
confirmationToken: args.confirmationToken,
|
|
468
|
+
},
|
|
469
|
+
"[adjudicated-agent] confirmation blob tampered — refusing to resume",
|
|
470
|
+
);
|
|
471
|
+
throw new AdapterError(
|
|
472
|
+
AdapterErrorCode.CONFIRMATION_TOKEN_INVALID,
|
|
473
|
+
`Confirmation token "${args.confirmationToken}" failed hash verification (envelope was modified after persistence).`,
|
|
474
|
+
{
|
|
475
|
+
confirmationToken: args.confirmationToken,
|
|
476
|
+
reason: "confirmation_blob_tampered",
|
|
477
|
+
},
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!args.accepted) {
|
|
483
|
+
const declineEvent: AgentEvent = {
|
|
484
|
+
kind: "assistant_text",
|
|
485
|
+
text: "User declined the confirmation. Action skipped.",
|
|
486
|
+
};
|
|
487
|
+
return {
|
|
488
|
+
events: [declineEvent],
|
|
489
|
+
history: pending.assistantHistorySnapshot,
|
|
490
|
+
outcome: {
|
|
491
|
+
kind: "completed" as const,
|
|
492
|
+
assistantText: "User declined the confirmation. Action skipped.",
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const envelope = pending.envelope as IntentEnvelope<K, P>;
|
|
497
|
+
const confirmPlan = options.pack.planner.plan(args.state, args.context);
|
|
498
|
+
const { decision } = await adjudicateAndAudit(
|
|
499
|
+
envelope,
|
|
500
|
+
args.state,
|
|
501
|
+
options.pack.policy,
|
|
502
|
+
{
|
|
503
|
+
sink: options.auditSink ?? noopAuditSink(),
|
|
504
|
+
ledger: options.ledger,
|
|
505
|
+
context: options.runtimeContext,
|
|
506
|
+
plan: () => ({
|
|
507
|
+
visibleReadTools: confirmPlan.visibleReadTools,
|
|
508
|
+
allowedIntents: confirmPlan.allowedIntents,
|
|
509
|
+
}),
|
|
510
|
+
confirmationReceipt: {
|
|
511
|
+
intentHash: envelope.intentHash,
|
|
512
|
+
at: new Date().toISOString(),
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
);
|
|
516
|
+
const seedEvents: AgentEvent[] = [
|
|
517
|
+
{ kind: "intent_proposed", envelope },
|
|
518
|
+
{ kind: "decision", decision, envelope },
|
|
519
|
+
];
|
|
520
|
+
const seedDecision: SeedDecision<K, P> = {
|
|
521
|
+
decision,
|
|
522
|
+
envelope,
|
|
523
|
+
toolUseId: pending.toolUseId,
|
|
524
|
+
};
|
|
525
|
+
return runLoop(
|
|
526
|
+
pending.sessionId,
|
|
527
|
+
pending.assistantHistorySnapshot,
|
|
528
|
+
args.state,
|
|
529
|
+
args.context,
|
|
530
|
+
seedEvents,
|
|
531
|
+
seedDecision,
|
|
532
|
+
);
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
interface SeedDecision<K extends string, P> {
|
|
538
|
+
readonly decision: Decision;
|
|
539
|
+
readonly envelope: IntentEnvelope<K, P>;
|
|
540
|
+
readonly toolUseId: string;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function pauseToOutcome(
|
|
544
|
+
action: LoopAction,
|
|
545
|
+
lastDecision: Decision | null,
|
|
546
|
+
): AgentOutcome {
|
|
547
|
+
switch (action.kind) {
|
|
548
|
+
case "continue":
|
|
549
|
+
return { kind: "max_iterations_exceeded", lastDecision };
|
|
550
|
+
case "pause_for_user_confirmation":
|
|
551
|
+
return {
|
|
552
|
+
kind: "awaiting_confirmation",
|
|
553
|
+
prompt: action.prompt,
|
|
554
|
+
confirmationToken: action.token,
|
|
555
|
+
};
|
|
556
|
+
case "pause_for_defer":
|
|
557
|
+
return {
|
|
558
|
+
kind: "deferred",
|
|
559
|
+
signal: action.signal,
|
|
560
|
+
intentHash: action.intentHash,
|
|
561
|
+
};
|
|
562
|
+
case "complete_for_escalation":
|
|
563
|
+
return {
|
|
564
|
+
kind: "escalated",
|
|
565
|
+
to: action.to,
|
|
566
|
+
reason: action.reason,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|