@frockbot/kernel-contracts 0.1.3 → 0.2.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/package.json +1 -1
- package/src/authoring.ts +64 -0
- package/src/catalog-change.test.ts +56 -0
- package/src/iframe-ui.test.ts +99 -0
- package/src/iframe-ui.ts +400 -0
- package/src/index.ts +3 -0
- package/src/isolate-context-catalog.generated.ts +25 -0
- package/src/isolate.test.ts +154 -43
- package/src/isolate.ts +689 -101
- package/src/loop-events.test.ts +80 -0
- package/src/loop-events.ts +711 -0
- package/src/send-to-user.ts +7 -56
- package/src/session.test.ts +16 -0
- package/src/tool-execution.ts +16 -0
- package/src/types.ts +229 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
// The Agent loop's public event vocabulary.
|
|
2
|
+
//
|
|
3
|
+
// This is the single inventory Packages use to discover loop extension
|
|
4
|
+
// points. In-process first-party listeners receive the richer Cordis call
|
|
5
|
+
// signatures declared beside the services that dispatch them; a Bot isolate
|
|
6
|
+
// receives only the structured-clonable payload DTO named here. In
|
|
7
|
+
// particular, no payload contains a live Agent, Session, Context, AbortSignal,
|
|
8
|
+
// storage handle, credential, or service binding.
|
|
9
|
+
//
|
|
10
|
+
// Ordering is part of the contract: a Bot-isolate host appends its listeners
|
|
11
|
+
// only after the first-party application has mounted. First-party policy
|
|
12
|
+
// therefore observes the original dispatch before Bot-authored policy, and a
|
|
13
|
+
// first-party listener may short-circuit without entering an isolate. A hook
|
|
14
|
+
// is registered only on the mounted Bot's Cordis root and is additionally
|
|
15
|
+
// fenced by botId and Composition generation, so it cannot reach another Bot
|
|
16
|
+
// or an in-flight Turn pinned to another generation.
|
|
17
|
+
import type {
|
|
18
|
+
PromptAssembly,
|
|
19
|
+
PromptAssemblyContext,
|
|
20
|
+
} from "./prompt-assembly.js";
|
|
21
|
+
import type {
|
|
22
|
+
ToolExecutionContext,
|
|
23
|
+
ToolExecutionResult,
|
|
24
|
+
ToolPreparation,
|
|
25
|
+
} from "./tool-execution.js";
|
|
26
|
+
import type {
|
|
27
|
+
LlmMessage,
|
|
28
|
+
LlmStreamEvent,
|
|
29
|
+
NormalizedModelRequest,
|
|
30
|
+
SessionEventEnvelope,
|
|
31
|
+
ToolCall,
|
|
32
|
+
ToolSchema,
|
|
33
|
+
TurnTypeV1,
|
|
34
|
+
} from "./types.js";
|
|
35
|
+
import { decodeNormalizedModelRequestV1 } from "./types.js";
|
|
36
|
+
import type { Session } from "./session.js";
|
|
37
|
+
import { decodeSkillRefsV1, type SkillRefV1 } from "./skills.js";
|
|
38
|
+
|
|
39
|
+
export type LoopEventDispatchModeV1 = "waterfall" | "serial" | "emit";
|
|
40
|
+
|
|
41
|
+
export type LoopAgentStatusV1 = "idle" | "running" | "disposed";
|
|
42
|
+
|
|
43
|
+
export interface LoopAgentSnapshotV1 {
|
|
44
|
+
botId: string;
|
|
45
|
+
agentId: string;
|
|
46
|
+
sessionId: string;
|
|
47
|
+
status: LoopAgentStatusV1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The live in-process projection used only by first-party Cordis listeners. */
|
|
51
|
+
export interface LoopAgentRuntimeV1 {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly botId: string;
|
|
54
|
+
readonly session: Session;
|
|
55
|
+
readonly status: LoopAgentStatusV1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface LoopStepSnapshotV1 extends LoopAgentSnapshotV1 {
|
|
59
|
+
compositionGenerationId: string;
|
|
60
|
+
turn: number;
|
|
61
|
+
step: number;
|
|
62
|
+
turnType: TurnTypeV1;
|
|
63
|
+
subagentRole?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface LoopAgentInputV1 {
|
|
67
|
+
messageId: string;
|
|
68
|
+
text: string;
|
|
69
|
+
skills?: SkillRefV1[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type LoopPreStepDecisionV1 =
|
|
73
|
+
| { kind: "enter"; inputs: LoopAgentInputV1[] }
|
|
74
|
+
| { kind: "reject"; reason: string };
|
|
75
|
+
|
|
76
|
+
export type LoopRequestErrorActionV1 = { kind: "retry" } | { kind: "fail" };
|
|
77
|
+
|
|
78
|
+
export type LoopStepContinuationV1 = { kind: "continue" } | { kind: "stop" };
|
|
79
|
+
|
|
80
|
+
export interface LoopToolExecutionContextV1 {
|
|
81
|
+
botId: string;
|
|
82
|
+
agentId: string;
|
|
83
|
+
sessionId: string;
|
|
84
|
+
compositionGenerationId: string;
|
|
85
|
+
effectId: string;
|
|
86
|
+
toolCall?: ToolCall;
|
|
87
|
+
turnType: TurnTypeV1;
|
|
88
|
+
subagentRole?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function loopToolExecutionContextSnapshotV1(
|
|
92
|
+
context: ToolExecutionContext,
|
|
93
|
+
): LoopToolExecutionContextV1 {
|
|
94
|
+
return structuredClone({
|
|
95
|
+
botId: context.botId,
|
|
96
|
+
agentId: context.agentId,
|
|
97
|
+
sessionId: context.sessionId,
|
|
98
|
+
compositionGenerationId: context.compositionGenerationId,
|
|
99
|
+
effectId: context.effectId,
|
|
100
|
+
...(context.toolCall === undefined ? {} : { toolCall: context.toolCall }),
|
|
101
|
+
turnType: context.turnType,
|
|
102
|
+
...(context.subagentRole === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { subagentRole: context.subagentRole }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The structured-clonable payload carried for each public event. */
|
|
109
|
+
export interface LoopEventPayloadMapV1 {
|
|
110
|
+
"agent/created": { agent: LoopAgentSnapshotV1 };
|
|
111
|
+
"agent/disposed": { agent: LoopAgentSnapshotV1 };
|
|
112
|
+
"agent/status": {
|
|
113
|
+
agent: LoopAgentSnapshotV1;
|
|
114
|
+
status: LoopAgentStatusV1;
|
|
115
|
+
};
|
|
116
|
+
"agent/inbox/inserted": {
|
|
117
|
+
agent: LoopAgentSnapshotV1;
|
|
118
|
+
input: LoopAgentInputV1;
|
|
119
|
+
};
|
|
120
|
+
"agent/inbox/claimed": {
|
|
121
|
+
agent: LoopAgentSnapshotV1;
|
|
122
|
+
inputs: LoopAgentInputV1[];
|
|
123
|
+
turn: number;
|
|
124
|
+
};
|
|
125
|
+
"agent/pre-step": {
|
|
126
|
+
step: LoopStepSnapshotV1;
|
|
127
|
+
inputs: LoopAgentInputV1[];
|
|
128
|
+
decision: LoopPreStepDecisionV1;
|
|
129
|
+
};
|
|
130
|
+
"system-prompt/assemble": {
|
|
131
|
+
context: PromptAssemblyContext;
|
|
132
|
+
assembly: PromptAssembly;
|
|
133
|
+
};
|
|
134
|
+
"agent/message-window": {
|
|
135
|
+
step: LoopStepSnapshotV1;
|
|
136
|
+
messages: LlmMessage[];
|
|
137
|
+
};
|
|
138
|
+
"agent/tool-exposure": {
|
|
139
|
+
step: LoopStepSnapshotV1;
|
|
140
|
+
tools: ToolSchema[];
|
|
141
|
+
};
|
|
142
|
+
"agent/request": {
|
|
143
|
+
step: LoopStepSnapshotV1;
|
|
144
|
+
request: NormalizedModelRequest;
|
|
145
|
+
};
|
|
146
|
+
"agent/request-error": {
|
|
147
|
+
step: LoopStepSnapshotV1;
|
|
148
|
+
error: { name: string; message: string };
|
|
149
|
+
action: LoopRequestErrorActionV1;
|
|
150
|
+
};
|
|
151
|
+
"llm/stream": { request: NormalizedModelRequest };
|
|
152
|
+
"tools/pre-execute": {
|
|
153
|
+
call: ToolCall;
|
|
154
|
+
context: LoopToolExecutionContextV1;
|
|
155
|
+
preparation: ToolPreparation;
|
|
156
|
+
};
|
|
157
|
+
"tools/execute": {
|
|
158
|
+
call: ToolCall;
|
|
159
|
+
context: LoopToolExecutionContextV1;
|
|
160
|
+
};
|
|
161
|
+
"tools/post-execute": {
|
|
162
|
+
call: ToolCall;
|
|
163
|
+
context: LoopToolExecutionContextV1;
|
|
164
|
+
result: ToolExecutionResult;
|
|
165
|
+
};
|
|
166
|
+
"tools/result": { call: ToolCall; result: ToolExecutionResult };
|
|
167
|
+
"agent/step-continuation": {
|
|
168
|
+
step: LoopStepSnapshotV1;
|
|
169
|
+
decision: LoopStepContinuationV1;
|
|
170
|
+
};
|
|
171
|
+
"agent/model-outcome-committed": {
|
|
172
|
+
agent: LoopAgentSnapshotV1;
|
|
173
|
+
requestId: string;
|
|
174
|
+
outcome: "completed" | "not-started";
|
|
175
|
+
};
|
|
176
|
+
"agent/turn-stopping": { agent: LoopAgentSnapshotV1; turn: number };
|
|
177
|
+
"agent/cancel-requested": {
|
|
178
|
+
agent: LoopAgentSnapshotV1;
|
|
179
|
+
reason: "user" | "shutdown";
|
|
180
|
+
};
|
|
181
|
+
"agent/error": {
|
|
182
|
+
agent: LoopAgentSnapshotV1;
|
|
183
|
+
error: { name: string; message: string };
|
|
184
|
+
};
|
|
185
|
+
"session/event": SessionEventEnvelope;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The value a waterfall listener may replace; observations return nothing. */
|
|
189
|
+
export interface LoopEventReturnMapV1 {
|
|
190
|
+
"agent/created": void;
|
|
191
|
+
"agent/disposed": void;
|
|
192
|
+
"agent/status": void;
|
|
193
|
+
"agent/inbox/inserted": void;
|
|
194
|
+
"agent/inbox/claimed": void;
|
|
195
|
+
"agent/pre-step": LoopPreStepDecisionV1;
|
|
196
|
+
"system-prompt/assemble": PromptAssembly;
|
|
197
|
+
"agent/message-window": LlmMessage[];
|
|
198
|
+
"agent/tool-exposure": ToolSchema[];
|
|
199
|
+
"agent/request": NormalizedModelRequest;
|
|
200
|
+
"agent/request-error": LoopRequestErrorActionV1;
|
|
201
|
+
"llm/stream": AsyncIterable<LlmStreamEvent>;
|
|
202
|
+
"tools/pre-execute": ToolPreparation;
|
|
203
|
+
"tools/execute": ToolExecutionResult;
|
|
204
|
+
"tools/post-execute": ToolExecutionResult;
|
|
205
|
+
"tools/result": void;
|
|
206
|
+
"agent/step-continuation": LoopStepContinuationV1;
|
|
207
|
+
"agent/model-outcome-committed": void;
|
|
208
|
+
"agent/turn-stopping": void;
|
|
209
|
+
"agent/cancel-requested": void;
|
|
210
|
+
"agent/error": void;
|
|
211
|
+
"session/event": void;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export type LoopEventNameV1 = keyof LoopEventPayloadMapV1;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Waterfalls safe to bridge into a Bot isolate. Operational wrappers that
|
|
218
|
+
* carry an AbortSignal, an async stream, or an effect body remain first-party:
|
|
219
|
+
* the isolate receives policy DTOs, never control of the durable skeleton.
|
|
220
|
+
*/
|
|
221
|
+
export const BOT_ISOLATE_HOOK_EVENTS_V1 = [
|
|
222
|
+
"agent/pre-step",
|
|
223
|
+
"system-prompt/assemble",
|
|
224
|
+
"agent/message-window",
|
|
225
|
+
"agent/tool-exposure",
|
|
226
|
+
"tools/pre-execute",
|
|
227
|
+
"tools/post-execute",
|
|
228
|
+
"agent/step-continuation",
|
|
229
|
+
] as const satisfies readonly LoopEventNameV1[];
|
|
230
|
+
|
|
231
|
+
export type BotIsolateHookEventNameV1 =
|
|
232
|
+
(typeof BOT_ISOLATE_HOOK_EVENTS_V1)[number];
|
|
233
|
+
|
|
234
|
+
export function isBotIsolateHookEventNameV1(
|
|
235
|
+
value: unknown,
|
|
236
|
+
): value is BotIsolateHookEventNameV1 {
|
|
237
|
+
return BOT_ISOLATE_HOOK_EVENTS_V1.some((event) => event === value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function hookRecord(value: unknown, label: string): Record<string, unknown> {
|
|
241
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
242
|
+
throw new Error(`${label} must be an object`);
|
|
243
|
+
}
|
|
244
|
+
return value as Record<string, unknown>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function hookExactKeys(
|
|
248
|
+
value: Record<string, unknown>,
|
|
249
|
+
required: readonly string[],
|
|
250
|
+
optional: readonly string[],
|
|
251
|
+
label: string,
|
|
252
|
+
): void {
|
|
253
|
+
const allowed = new Set([...required, ...optional]);
|
|
254
|
+
if (
|
|
255
|
+
!required.every((key) => Object.hasOwn(value, key)) ||
|
|
256
|
+
!Object.keys(value).every((key) => allowed.has(key))
|
|
257
|
+
) {
|
|
258
|
+
throw new Error(`${label} has invalid fields`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function hookString(
|
|
263
|
+
value: unknown,
|
|
264
|
+
label: string,
|
|
265
|
+
maximum = 1_000_000,
|
|
266
|
+
allowEmpty = false,
|
|
267
|
+
): string {
|
|
268
|
+
if (
|
|
269
|
+
typeof value !== "string" ||
|
|
270
|
+
(!allowEmpty && value.length === 0) ||
|
|
271
|
+
value.length > maximum
|
|
272
|
+
) {
|
|
273
|
+
throw new Error(`${label} must be a bounded string`);
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function decodeHookCall(value: unknown, label: string): ToolCall {
|
|
279
|
+
const call = hookRecord(value, label);
|
|
280
|
+
hookExactKeys(call, ["id", "name", "input"], [], label);
|
|
281
|
+
// Reuse the normalized request decoder's exact ToolCall and JSON checks.
|
|
282
|
+
const decoded = decodeNormalizedModelRequestV1(
|
|
283
|
+
{
|
|
284
|
+
requestId: "hook-decode",
|
|
285
|
+
provider: "hook-decode",
|
|
286
|
+
model: "hook-decode",
|
|
287
|
+
system: "",
|
|
288
|
+
messages: [{ role: "assistant", content: "", toolCalls: [call] }],
|
|
289
|
+
tools: [],
|
|
290
|
+
},
|
|
291
|
+
label,
|
|
292
|
+
);
|
|
293
|
+
return (decoded.messages[0] as { toolCalls: ToolCall[] }).toolCalls[0]!;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function decodeHookResult(value: unknown, label: string): ToolExecutionResult {
|
|
297
|
+
const result = hookRecord(value, label);
|
|
298
|
+
hookExactKeys(
|
|
299
|
+
result,
|
|
300
|
+
["content", "isError"],
|
|
301
|
+
["endsTurn", "attachments"],
|
|
302
|
+
label,
|
|
303
|
+
);
|
|
304
|
+
if (typeof result.isError !== "boolean") {
|
|
305
|
+
throw new Error(`${label}.isError must be a boolean`);
|
|
306
|
+
}
|
|
307
|
+
if (result.endsTurn !== undefined && typeof result.endsTurn !== "boolean") {
|
|
308
|
+
throw new Error(`${label}.endsTurn must be a boolean`);
|
|
309
|
+
}
|
|
310
|
+
const decoded = decodeNormalizedModelRequestV1(
|
|
311
|
+
{
|
|
312
|
+
requestId: "hook-decode",
|
|
313
|
+
provider: "hook-decode",
|
|
314
|
+
model: "hook-decode",
|
|
315
|
+
system: "",
|
|
316
|
+
messages: [
|
|
317
|
+
{
|
|
318
|
+
role: "tool",
|
|
319
|
+
callId: "hook-decode",
|
|
320
|
+
name: "hook_decode",
|
|
321
|
+
content: result.content,
|
|
322
|
+
isError: result.isError,
|
|
323
|
+
...(result.attachments === undefined
|
|
324
|
+
? {}
|
|
325
|
+
: { attachments: result.attachments }),
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
tools: [],
|
|
329
|
+
},
|
|
330
|
+
label,
|
|
331
|
+
);
|
|
332
|
+
const message = decoded.messages[0] as Extract<LlmMessage, { role: "tool" }>;
|
|
333
|
+
return {
|
|
334
|
+
content: hookString(message.content, `${label}.content`, 1_000_000, true),
|
|
335
|
+
isError: message.isError,
|
|
336
|
+
...(result.endsTurn === undefined
|
|
337
|
+
? {}
|
|
338
|
+
: { endsTurn: result.endsTurn as boolean }),
|
|
339
|
+
...(message.attachments === undefined
|
|
340
|
+
? {}
|
|
341
|
+
: { attachments: message.attachments }),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function decodeHookInputs(value: unknown, label: string): LoopAgentInputV1[] {
|
|
346
|
+
if (!Array.isArray(value) || value.length > 256) {
|
|
347
|
+
throw new Error(`${label} must be a bounded array`);
|
|
348
|
+
}
|
|
349
|
+
return value.map((input, index) => {
|
|
350
|
+
const itemLabel = `${label}[${index}]`;
|
|
351
|
+
const item = hookRecord(input, itemLabel);
|
|
352
|
+
hookExactKeys(item, ["messageId", "text"], ["skills"], itemLabel);
|
|
353
|
+
const skills = item.skills;
|
|
354
|
+
if (skills !== undefined && !Array.isArray(skills)) {
|
|
355
|
+
throw new Error(`${itemLabel}.skills must be an array`);
|
|
356
|
+
}
|
|
357
|
+
const decodedSkills =
|
|
358
|
+
skills === undefined
|
|
359
|
+
? undefined
|
|
360
|
+
: decodeSkillRefsV1(skills, `${itemLabel}.skills`);
|
|
361
|
+
return {
|
|
362
|
+
messageId: hookString(item.messageId, `${itemLabel}.messageId`, 256),
|
|
363
|
+
text: hookString(item.text, `${itemLabel}.text`, 1_000_000, true),
|
|
364
|
+
...(decodedSkills === undefined ? {} : { skills: decodedSkills }),
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function sameHookCall(left: ToolCall, right: ToolCall): boolean {
|
|
370
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Exact, event-specific decoding for an untrusted isolate replacement. */
|
|
374
|
+
export function decodeBotIsolateHookReplacementV1<
|
|
375
|
+
Event extends BotIsolateHookEventNameV1,
|
|
376
|
+
>(
|
|
377
|
+
event: Event,
|
|
378
|
+
input: unknown,
|
|
379
|
+
original: LoopEventReturnMapV1[Event],
|
|
380
|
+
): LoopEventReturnMapV1[Event] {
|
|
381
|
+
const label = `isolate hook ${event} replacement`;
|
|
382
|
+
let decoded: LoopEventReturnMapV1[BotIsolateHookEventNameV1];
|
|
383
|
+
switch (event) {
|
|
384
|
+
case "agent/pre-step": {
|
|
385
|
+
const decision = hookRecord(input, label);
|
|
386
|
+
if (decision.kind === "enter") {
|
|
387
|
+
hookExactKeys(decision, ["kind", "inputs"], [], label);
|
|
388
|
+
decoded = {
|
|
389
|
+
kind: "enter",
|
|
390
|
+
inputs: decodeHookInputs(decision.inputs, `${label}.inputs`),
|
|
391
|
+
};
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
hookExactKeys(decision, ["kind", "reason"], [], label);
|
|
395
|
+
if (decision.kind !== "reject") {
|
|
396
|
+
throw new Error(`${label}.kind is invalid`);
|
|
397
|
+
}
|
|
398
|
+
decoded = {
|
|
399
|
+
kind: "reject",
|
|
400
|
+
reason: hookString(decision.reason, `${label}.reason`, 2_048),
|
|
401
|
+
};
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case "system-prompt/assemble": {
|
|
405
|
+
const assembly = hookRecord(input, label);
|
|
406
|
+
hookExactKeys(assembly, ["text", "sections"], [], label);
|
|
407
|
+
if (!Array.isArray(assembly.sections) || assembly.sections.length > 256) {
|
|
408
|
+
throw new Error(`${label}.sections must be a bounded array`);
|
|
409
|
+
}
|
|
410
|
+
decoded = {
|
|
411
|
+
text: hookString(assembly.text, `${label}.text`, 1_000_000, true),
|
|
412
|
+
sections: assembly.sections.map((section, index) => {
|
|
413
|
+
const sectionLabel = `${label}.sections[${index}]`;
|
|
414
|
+
const item = hookRecord(section, sectionLabel);
|
|
415
|
+
hookExactKeys(item, ["id", "text"], [], sectionLabel);
|
|
416
|
+
return {
|
|
417
|
+
id: hookString(item.id, `${sectionLabel}.id`, 256),
|
|
418
|
+
text: hookString(
|
|
419
|
+
item.text,
|
|
420
|
+
`${sectionLabel}.text`,
|
|
421
|
+
1_000_000,
|
|
422
|
+
true,
|
|
423
|
+
),
|
|
424
|
+
};
|
|
425
|
+
}),
|
|
426
|
+
};
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case "agent/message-window": {
|
|
430
|
+
decoded = decodeNormalizedModelRequestV1(
|
|
431
|
+
{
|
|
432
|
+
requestId: "hook-decode",
|
|
433
|
+
provider: "hook-decode",
|
|
434
|
+
model: "hook-decode",
|
|
435
|
+
system: "",
|
|
436
|
+
messages: input,
|
|
437
|
+
tools: [],
|
|
438
|
+
},
|
|
439
|
+
label,
|
|
440
|
+
).messages;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case "agent/tool-exposure": {
|
|
444
|
+
decoded = decodeNormalizedModelRequestV1(
|
|
445
|
+
{
|
|
446
|
+
requestId: "hook-decode",
|
|
447
|
+
provider: "hook-decode",
|
|
448
|
+
model: "hook-decode",
|
|
449
|
+
system: "",
|
|
450
|
+
messages: [],
|
|
451
|
+
tools: input,
|
|
452
|
+
},
|
|
453
|
+
label,
|
|
454
|
+
).tools;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case "tools/pre-execute": {
|
|
458
|
+
const prior = original as ToolPreparation;
|
|
459
|
+
const preparation = hookRecord(input, label);
|
|
460
|
+
if (preparation.kind === "ready") {
|
|
461
|
+
hookExactKeys(preparation, ["kind", "call", "idempotent"], [], label);
|
|
462
|
+
if (typeof preparation.idempotent !== "boolean") {
|
|
463
|
+
throw new Error(`${label}.idempotent must be a boolean`);
|
|
464
|
+
}
|
|
465
|
+
const ready = {
|
|
466
|
+
kind: "ready" as const,
|
|
467
|
+
call: decodeHookCall(preparation.call, `${label}.call`),
|
|
468
|
+
idempotent: preparation.idempotent,
|
|
469
|
+
};
|
|
470
|
+
if (
|
|
471
|
+
prior.kind === "denied" ||
|
|
472
|
+
!sameHookCall(ready.call, prior.call) ||
|
|
473
|
+
ready.idempotent !== prior.idempotent
|
|
474
|
+
) {
|
|
475
|
+
throw new Error(`${label} cannot lift or alter prior preparation`);
|
|
476
|
+
}
|
|
477
|
+
decoded = ready;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
hookExactKeys(preparation, ["kind", "call", "result"], [], label);
|
|
481
|
+
if (preparation.kind !== "denied") {
|
|
482
|
+
throw new Error(`${label}.kind is invalid`);
|
|
483
|
+
}
|
|
484
|
+
const denied = {
|
|
485
|
+
kind: "denied" as const,
|
|
486
|
+
call: decodeHookCall(preparation.call, `${label}.call`),
|
|
487
|
+
result: decodeHookResult(preparation.result, `${label}.result`),
|
|
488
|
+
};
|
|
489
|
+
if (!sameHookCall(denied.call, prior.call)) {
|
|
490
|
+
throw new Error(`${label} cannot alter the tool call`);
|
|
491
|
+
}
|
|
492
|
+
decoded = denied;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case "tools/post-execute":
|
|
496
|
+
decoded = decodeHookResult(input, label);
|
|
497
|
+
break;
|
|
498
|
+
case "agent/step-continuation": {
|
|
499
|
+
const decision = hookRecord(input, label);
|
|
500
|
+
hookExactKeys(decision, ["kind"], [], label);
|
|
501
|
+
if (decision.kind !== "continue" && decision.kind !== "stop") {
|
|
502
|
+
throw new Error(`${label}.kind is invalid`);
|
|
503
|
+
}
|
|
504
|
+
decoded = { kind: decision.kind };
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return decoded as LoopEventReturnMapV1[Event];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export interface LoopEventDefinitionV1 {
|
|
512
|
+
mode: LoopEventDispatchModeV1;
|
|
513
|
+
payload: string;
|
|
514
|
+
returns: string;
|
|
515
|
+
isolateHook: boolean;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* The complete public loop-event table. `payload` and `returns` name the DTOs
|
|
520
|
+
* above so generated authoring help and architecture docs use one vocabulary.
|
|
521
|
+
*/
|
|
522
|
+
export const LOOP_EVENTS_V1 = {
|
|
523
|
+
"agent/created": {
|
|
524
|
+
mode: "emit",
|
|
525
|
+
payload: "{ agent: LoopAgentSnapshotV1 }",
|
|
526
|
+
returns: "void",
|
|
527
|
+
isolateHook: false,
|
|
528
|
+
},
|
|
529
|
+
"agent/disposed": {
|
|
530
|
+
mode: "emit",
|
|
531
|
+
payload: "{ agent: LoopAgentSnapshotV1 }",
|
|
532
|
+
returns: "void",
|
|
533
|
+
isolateHook: false,
|
|
534
|
+
},
|
|
535
|
+
"agent/status": {
|
|
536
|
+
mode: "emit",
|
|
537
|
+
payload: "{ agent, status }",
|
|
538
|
+
returns: "void",
|
|
539
|
+
isolateHook: false,
|
|
540
|
+
},
|
|
541
|
+
"agent/inbox/inserted": {
|
|
542
|
+
mode: "emit",
|
|
543
|
+
payload: "{ agent, input }",
|
|
544
|
+
returns: "void",
|
|
545
|
+
isolateHook: false,
|
|
546
|
+
},
|
|
547
|
+
"agent/inbox/claimed": {
|
|
548
|
+
mode: "emit",
|
|
549
|
+
payload: "{ agent, inputs, turn }",
|
|
550
|
+
returns: "void",
|
|
551
|
+
isolateHook: false,
|
|
552
|
+
},
|
|
553
|
+
"agent/pre-step": {
|
|
554
|
+
mode: "waterfall",
|
|
555
|
+
payload: "{ step, inputs, decision }",
|
|
556
|
+
returns: "LoopPreStepDecisionV1",
|
|
557
|
+
isolateHook: true,
|
|
558
|
+
},
|
|
559
|
+
"system-prompt/assemble": {
|
|
560
|
+
mode: "waterfall",
|
|
561
|
+
payload: "{ context, assembly }",
|
|
562
|
+
returns: "PromptAssembly",
|
|
563
|
+
isolateHook: true,
|
|
564
|
+
},
|
|
565
|
+
"agent/message-window": {
|
|
566
|
+
mode: "waterfall",
|
|
567
|
+
payload: "{ step, messages }",
|
|
568
|
+
returns: "LlmMessage[]",
|
|
569
|
+
isolateHook: true,
|
|
570
|
+
},
|
|
571
|
+
"agent/tool-exposure": {
|
|
572
|
+
mode: "waterfall",
|
|
573
|
+
payload: "{ step, tools }",
|
|
574
|
+
returns: "ToolSchema[]",
|
|
575
|
+
isolateHook: true,
|
|
576
|
+
},
|
|
577
|
+
"agent/request": {
|
|
578
|
+
mode: "waterfall",
|
|
579
|
+
payload: "{ step, request }",
|
|
580
|
+
returns: "NormalizedModelRequest",
|
|
581
|
+
isolateHook: false,
|
|
582
|
+
},
|
|
583
|
+
"agent/request-error": {
|
|
584
|
+
mode: "waterfall",
|
|
585
|
+
payload: "{ step, error, action }",
|
|
586
|
+
returns: "LoopRequestErrorActionV1",
|
|
587
|
+
isolateHook: false,
|
|
588
|
+
},
|
|
589
|
+
"llm/stream": {
|
|
590
|
+
mode: "waterfall",
|
|
591
|
+
payload: "{ request }",
|
|
592
|
+
returns: "AsyncIterable<LlmStreamEvent>",
|
|
593
|
+
isolateHook: false,
|
|
594
|
+
},
|
|
595
|
+
"tools/pre-execute": {
|
|
596
|
+
mode: "waterfall",
|
|
597
|
+
payload: "{ call, context, preparation }",
|
|
598
|
+
returns: "ToolPreparation",
|
|
599
|
+
isolateHook: true,
|
|
600
|
+
},
|
|
601
|
+
"tools/execute": {
|
|
602
|
+
mode: "waterfall",
|
|
603
|
+
payload: "{ call, context }",
|
|
604
|
+
returns: "ToolExecutionResult",
|
|
605
|
+
isolateHook: false,
|
|
606
|
+
},
|
|
607
|
+
"tools/post-execute": {
|
|
608
|
+
mode: "waterfall",
|
|
609
|
+
payload: "{ call, context, result }",
|
|
610
|
+
returns: "ToolExecutionResult",
|
|
611
|
+
isolateHook: true,
|
|
612
|
+
},
|
|
613
|
+
"tools/result": {
|
|
614
|
+
mode: "emit",
|
|
615
|
+
payload: "{ call, result }",
|
|
616
|
+
returns: "void",
|
|
617
|
+
isolateHook: false,
|
|
618
|
+
},
|
|
619
|
+
"agent/step-continuation": {
|
|
620
|
+
mode: "waterfall",
|
|
621
|
+
payload: "{ step, decision }",
|
|
622
|
+
returns: "LoopStepContinuationV1",
|
|
623
|
+
isolateHook: true,
|
|
624
|
+
},
|
|
625
|
+
"agent/model-outcome-committed": {
|
|
626
|
+
mode: "serial",
|
|
627
|
+
payload: "{ agent, requestId, outcome }",
|
|
628
|
+
returns: "void",
|
|
629
|
+
isolateHook: false,
|
|
630
|
+
},
|
|
631
|
+
"agent/turn-stopping": {
|
|
632
|
+
mode: "serial",
|
|
633
|
+
payload: "{ agent, turn }",
|
|
634
|
+
returns: "void",
|
|
635
|
+
isolateHook: false,
|
|
636
|
+
},
|
|
637
|
+
"agent/cancel-requested": {
|
|
638
|
+
mode: "emit",
|
|
639
|
+
payload: "{ agent, reason }",
|
|
640
|
+
returns: "void",
|
|
641
|
+
isolateHook: false,
|
|
642
|
+
},
|
|
643
|
+
"agent/error": {
|
|
644
|
+
mode: "emit",
|
|
645
|
+
payload: "{ agent, error }",
|
|
646
|
+
returns: "void",
|
|
647
|
+
isolateHook: false,
|
|
648
|
+
},
|
|
649
|
+
"session/event": {
|
|
650
|
+
mode: "emit",
|
|
651
|
+
payload: "SessionEventEnvelope",
|
|
652
|
+
returns: "void",
|
|
653
|
+
isolateHook: false,
|
|
654
|
+
},
|
|
655
|
+
} as const satisfies Record<LoopEventNameV1, LoopEventDefinitionV1>;
|
|
656
|
+
|
|
657
|
+
declare module "cordis" {
|
|
658
|
+
interface Events {
|
|
659
|
+
"agent/created": (agent: LoopAgentRuntimeV1) => void;
|
|
660
|
+
"agent/disposed": (agent: LoopAgentRuntimeV1) => void;
|
|
661
|
+
"agent/status": (
|
|
662
|
+
agent: LoopAgentRuntimeV1,
|
|
663
|
+
status: LoopAgentStatusV1,
|
|
664
|
+
) => void;
|
|
665
|
+
"agent/inbox/inserted": (
|
|
666
|
+
agent: LoopAgentRuntimeV1,
|
|
667
|
+
input: LoopAgentInputV1,
|
|
668
|
+
) => void;
|
|
669
|
+
"agent/inbox/claimed": (
|
|
670
|
+
agent: LoopAgentRuntimeV1,
|
|
671
|
+
inputs: LoopAgentInputV1[],
|
|
672
|
+
turn: number,
|
|
673
|
+
) => void;
|
|
674
|
+
"agent/pre-step": (
|
|
675
|
+
agent: LoopAgentRuntimeV1,
|
|
676
|
+
inputs: LoopAgentInputV1[],
|
|
677
|
+
turn: number,
|
|
678
|
+
step: number,
|
|
679
|
+
next: () => Promise<LoopPreStepDecisionV1>,
|
|
680
|
+
) => Promise<LoopPreStepDecisionV1>;
|
|
681
|
+
"agent/message-window": (
|
|
682
|
+
agent: LoopAgentRuntimeV1,
|
|
683
|
+
messages: LlmMessage[],
|
|
684
|
+
turn: number,
|
|
685
|
+
step: number,
|
|
686
|
+
signal: AbortSignal,
|
|
687
|
+
next: () => Promise<LlmMessage[]>,
|
|
688
|
+
) => Promise<LlmMessage[]>;
|
|
689
|
+
"agent/tool-exposure": (
|
|
690
|
+
agent: LoopAgentRuntimeV1,
|
|
691
|
+
tools: ToolSchema[],
|
|
692
|
+
turn: number,
|
|
693
|
+
step: number,
|
|
694
|
+
signal: AbortSignal,
|
|
695
|
+
next: () => Promise<ToolSchema[]>,
|
|
696
|
+
) => Promise<ToolSchema[]>;
|
|
697
|
+
"agent/step-continuation": (
|
|
698
|
+
agent: LoopAgentRuntimeV1,
|
|
699
|
+
decision: LoopStepContinuationV1,
|
|
700
|
+
turn: number,
|
|
701
|
+
step: number,
|
|
702
|
+
signal: AbortSignal,
|
|
703
|
+
next: () => Promise<LoopStepContinuationV1>,
|
|
704
|
+
) => Promise<LoopStepContinuationV1>;
|
|
705
|
+
"agent/cancel-requested": (
|
|
706
|
+
agent: LoopAgentRuntimeV1,
|
|
707
|
+
reason: "user" | "shutdown",
|
|
708
|
+
) => void;
|
|
709
|
+
"agent/error": (agent: LoopAgentRuntimeV1, error: unknown) => void;
|
|
710
|
+
}
|
|
711
|
+
}
|