@clinebot/agents 0.0.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/README.md +145 -0
- package/dist/agent-input.d.ts +2 -0
- package/dist/agent.d.ts +56 -0
- package/dist/extensions.d.ts +21 -0
- package/dist/hooks/engine.d.ts +42 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/lifecycle.d.ts +5 -0
- package/dist/hooks/node.d.ts +2 -0
- package/dist/hooks/subprocess-runner.d.ts +16 -0
- package/dist/hooks/subprocess.d.ts +268 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +49 -0
- package/dist/index.node.d.ts +5 -0
- package/dist/index.node.js +49 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/policies.d.ts +14 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/types.d.ts +35 -0
- package/dist/message-builder.d.ts +31 -0
- package/dist/prompts/cline.d.ts +1 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/runtime/agent-runtime-bus.d.ts +13 -0
- package/dist/runtime/conversation-store.d.ts +16 -0
- package/dist/runtime/lifecycle-orchestrator.d.ts +28 -0
- package/dist/runtime/tool-orchestrator.d.ts +39 -0
- package/dist/runtime/turn-processor.d.ts +21 -0
- package/dist/teams/index.d.ts +3 -0
- package/dist/teams/multi-agent.d.ts +566 -0
- package/dist/teams/spawn-agent-tool.d.ts +85 -0
- package/dist/teams/team-tools.d.ts +51 -0
- package/dist/tools/ask-question.d.ts +12 -0
- package/dist/tools/create.d.ts +59 -0
- package/dist/tools/execution.d.ts +61 -0
- package/dist/tools/formatting.d.ts +20 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/registry.d.ts +26 -0
- package/dist/tools/validation.d.ts +27 -0
- package/dist/types.d.ts +826 -0
- package/package.json +54 -0
- package/src/agent-input.ts +116 -0
- package/src/agent.test.ts +931 -0
- package/src/agent.ts +1050 -0
- package/src/example.test.ts +564 -0
- package/src/extensions.ts +337 -0
- package/src/hooks/engine.test.ts +163 -0
- package/src/hooks/engine.ts +537 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/lifecycle.ts +239 -0
- package/src/hooks/node.ts +18 -0
- package/src/hooks/subprocess-runner.ts +140 -0
- package/src/hooks/subprocess.test.ts +180 -0
- package/src/hooks/subprocess.ts +620 -0
- package/src/index.browser.ts +1 -0
- package/src/index.node.ts +21 -0
- package/src/index.ts +133 -0
- package/src/mcp/index.ts +17 -0
- package/src/mcp/policies.test.ts +51 -0
- package/src/mcp/policies.ts +53 -0
- package/src/mcp/tools.test.ts +76 -0
- package/src/mcp/tools.ts +60 -0
- package/src/mcp/types.ts +41 -0
- package/src/message-builder.test.ts +175 -0
- package/src/message-builder.ts +429 -0
- package/src/prompts/cline.ts +49 -0
- package/src/prompts/index.ts +1 -0
- package/src/runtime/agent-runtime-bus.ts +53 -0
- package/src/runtime/conversation-store.ts +61 -0
- package/src/runtime/lifecycle-orchestrator.ts +90 -0
- package/src/runtime/tool-orchestrator.ts +177 -0
- package/src/runtime/turn-processor.ts +250 -0
- package/src/streaming.test.ts +197 -0
- package/src/streaming.ts +307 -0
- package/src/teams/index.ts +63 -0
- package/src/teams/multi-agent.lifecycle.test.ts +48 -0
- package/src/teams/multi-agent.ts +1866 -0
- package/src/teams/spawn-agent-tool.test.ts +172 -0
- package/src/teams/spawn-agent-tool.ts +223 -0
- package/src/teams/team-tools.test.ts +448 -0
- package/src/teams/team-tools.ts +929 -0
- package/src/tools/ask-question.ts +78 -0
- package/src/tools/create.ts +104 -0
- package/src/tools/execution.ts +311 -0
- package/src/tools/formatting.ts +73 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/registry.ts +52 -0
- package/src/tools/tools.test.ts +292 -0
- package/src/tools/validation.ts +73 -0
- package/src/types.ts +966 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentExtension,
|
|
3
|
+
AgentExtensionApi,
|
|
4
|
+
AgentExtensionCapability,
|
|
5
|
+
AgentExtensionHookStage,
|
|
6
|
+
AgentExtensionRegistry,
|
|
7
|
+
PluginManifest,
|
|
8
|
+
Tool,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
export interface ContributionRegistryOptions {
|
|
12
|
+
extensions?: AgentExtension[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface NormalizedExtension {
|
|
16
|
+
extension: AgentExtension;
|
|
17
|
+
order: number;
|
|
18
|
+
manifest: {
|
|
19
|
+
capabilities: Set<AgentExtensionCapability>;
|
|
20
|
+
hookStages: Set<AgentExtensionHookStage>;
|
|
21
|
+
raw: PluginManifest;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ALLOWED_CAPABILITIES = new Set<AgentExtensionCapability>([
|
|
26
|
+
"hooks",
|
|
27
|
+
"tools",
|
|
28
|
+
"commands",
|
|
29
|
+
"shortcuts",
|
|
30
|
+
"flags",
|
|
31
|
+
"message_renderers",
|
|
32
|
+
"providers",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const ALLOWED_HOOK_STAGES = new Set<AgentExtensionHookStage>([
|
|
36
|
+
"input",
|
|
37
|
+
"runtime_event",
|
|
38
|
+
"session_start",
|
|
39
|
+
"run_start",
|
|
40
|
+
"iteration_start",
|
|
41
|
+
"turn_start",
|
|
42
|
+
"before_agent_start",
|
|
43
|
+
"tool_call_before",
|
|
44
|
+
"tool_call_after",
|
|
45
|
+
"turn_end",
|
|
46
|
+
"iteration_end",
|
|
47
|
+
"run_end",
|
|
48
|
+
"session_shutdown",
|
|
49
|
+
"error",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const STAGE_TO_HANDLER: Record<
|
|
53
|
+
AgentExtensionHookStage,
|
|
54
|
+
keyof Pick<
|
|
55
|
+
AgentExtension,
|
|
56
|
+
| "onInput"
|
|
57
|
+
| "onRuntimeEvent"
|
|
58
|
+
| "onSessionStart"
|
|
59
|
+
| "onRunStart"
|
|
60
|
+
| "onIterationStart"
|
|
61
|
+
| "onTurnStart"
|
|
62
|
+
| "onBeforeAgentStart"
|
|
63
|
+
| "onToolCall"
|
|
64
|
+
| "onToolResult"
|
|
65
|
+
| "onAgentEnd"
|
|
66
|
+
| "onIterationEnd"
|
|
67
|
+
| "onRunEnd"
|
|
68
|
+
| "onSessionShutdown"
|
|
69
|
+
| "onError"
|
|
70
|
+
>
|
|
71
|
+
> = {
|
|
72
|
+
input: "onInput",
|
|
73
|
+
runtime_event: "onRuntimeEvent",
|
|
74
|
+
session_start: "onSessionStart",
|
|
75
|
+
run_start: "onRunStart",
|
|
76
|
+
iteration_start: "onIterationStart",
|
|
77
|
+
turn_start: "onTurnStart",
|
|
78
|
+
before_agent_start: "onBeforeAgentStart",
|
|
79
|
+
tool_call_before: "onToolCall",
|
|
80
|
+
tool_call_after: "onToolResult",
|
|
81
|
+
turn_end: "onAgentEnd",
|
|
82
|
+
iteration_end: "onIterationEnd",
|
|
83
|
+
run_end: "onRunEnd",
|
|
84
|
+
session_shutdown: "onSessionShutdown",
|
|
85
|
+
error: "onError",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function asExtensionName(extension: AgentExtension, order: number): string {
|
|
89
|
+
return extension.name || `extension_${String(order).padStart(4, "0")}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasHookHandlers(extension: AgentExtension): boolean {
|
|
93
|
+
return (
|
|
94
|
+
typeof extension.onInput === "function" ||
|
|
95
|
+
typeof extension.onRuntimeEvent === "function" ||
|
|
96
|
+
typeof extension.onSessionStart === "function" ||
|
|
97
|
+
typeof extension.onRunStart === "function" ||
|
|
98
|
+
typeof extension.onIterationStart === "function" ||
|
|
99
|
+
typeof extension.onTurnStart === "function" ||
|
|
100
|
+
typeof extension.onBeforeAgentStart === "function" ||
|
|
101
|
+
typeof extension.onToolCall === "function" ||
|
|
102
|
+
typeof extension.onToolResult === "function" ||
|
|
103
|
+
typeof extension.onAgentEnd === "function" ||
|
|
104
|
+
typeof extension.onIterationEnd === "function" ||
|
|
105
|
+
typeof extension.onRunEnd === "function" ||
|
|
106
|
+
typeof extension.onSessionShutdown === "function" ||
|
|
107
|
+
typeof extension.onError === "function"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeManifest(
|
|
112
|
+
extension: AgentExtension,
|
|
113
|
+
order: number,
|
|
114
|
+
): NormalizedExtension["manifest"] {
|
|
115
|
+
const extensionName = asExtensionName(extension, order);
|
|
116
|
+
const manifest = extension.manifest;
|
|
117
|
+
if (!manifest || typeof manifest !== "object") {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Invalid manifest for extension "${extensionName}": manifest is required`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
!Array.isArray(manifest.capabilities) ||
|
|
125
|
+
manifest.capabilities.length === 0
|
|
126
|
+
) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Invalid manifest for extension "${extensionName}": capabilities must be a non-empty array`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const capabilities = new Set<AgentExtensionCapability>();
|
|
133
|
+
for (const capability of manifest.capabilities) {
|
|
134
|
+
if (!ALLOWED_CAPABILITIES.has(capability)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Invalid manifest for extension "${extensionName}": unsupported capability "${String(capability)}"`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
capabilities.add(capability);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rawStages = manifest.hookStages ?? [];
|
|
143
|
+
if (!Array.isArray(rawStages)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Invalid manifest for extension "${extensionName}": hookStages must be an array when provided`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const hookStages = new Set<AgentExtensionHookStage>();
|
|
149
|
+
for (const stage of rawStages) {
|
|
150
|
+
if (!ALLOWED_HOOK_STAGES.has(stage)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Invalid manifest for extension "${extensionName}": unsupported hook stage "${String(stage)}"`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
hookStages.add(stage);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const hookCapabilityEnabled = capabilities.has("hooks");
|
|
159
|
+
const extensionDefinesHooks = hasHookHandlers(extension);
|
|
160
|
+
if (extensionDefinesHooks && !hookCapabilityEnabled) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Invalid manifest for extension "${extensionName}": hook handlers require the "hooks" capability`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (hookCapabilityEnabled && hookStages.size === 0) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Invalid manifest for extension "${extensionName}": hooks capability requires at least one hook stage`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const stage of hookStages) {
|
|
172
|
+
const handler = STAGE_TO_HANDLER[stage];
|
|
173
|
+
if (typeof extension[handler] !== "function") {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Invalid manifest for extension "${extensionName}": stage "${stage}" is declared but handler "${handler}" is missing`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const [stage, handler] of Object.entries(STAGE_TO_HANDLER) as Array<
|
|
181
|
+
[
|
|
182
|
+
AgentExtensionHookStage,
|
|
183
|
+
(typeof STAGE_TO_HANDLER)[AgentExtensionHookStage],
|
|
184
|
+
]
|
|
185
|
+
>) {
|
|
186
|
+
if (typeof extension[handler] === "function" && !hookStages.has(stage)) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Invalid manifest for extension "${extensionName}": handler "${handler}" must declare stage "${stage}"`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
capabilities,
|
|
195
|
+
hookStages,
|
|
196
|
+
raw: manifest,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export class ContributionRegistry {
|
|
201
|
+
private readonly extensions: AgentExtension[];
|
|
202
|
+
private readonly registry: AgentExtensionRegistry = {
|
|
203
|
+
tools: [],
|
|
204
|
+
commands: [],
|
|
205
|
+
shortcuts: [],
|
|
206
|
+
flags: [],
|
|
207
|
+
messageRenderers: [],
|
|
208
|
+
providers: [],
|
|
209
|
+
};
|
|
210
|
+
private normalized: NormalizedExtension[] = [];
|
|
211
|
+
private phase: "resolve" | "validate" | "setup" | "activate" | "run" =
|
|
212
|
+
"resolve";
|
|
213
|
+
|
|
214
|
+
constructor(options: ContributionRegistryOptions = {}) {
|
|
215
|
+
this.extensions = options.extensions ?? [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
resolve(): void {
|
|
219
|
+
if (this.phase !== "resolve") {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this.normalized = this.extensions.map((extension, order) => ({
|
|
223
|
+
extension,
|
|
224
|
+
order,
|
|
225
|
+
manifest: {
|
|
226
|
+
capabilities: new Set(),
|
|
227
|
+
hookStages: new Set(),
|
|
228
|
+
raw: extension.manifest,
|
|
229
|
+
},
|
|
230
|
+
}));
|
|
231
|
+
this.phase = "validate";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
validate(): void {
|
|
235
|
+
if (this.phase === "resolve") {
|
|
236
|
+
this.resolve();
|
|
237
|
+
}
|
|
238
|
+
if (this.phase !== "validate") {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.normalized = this.normalized.map((entry) => ({
|
|
242
|
+
...entry,
|
|
243
|
+
manifest: normalizeManifest(entry.extension, entry.order),
|
|
244
|
+
}));
|
|
245
|
+
this.phase = "setup";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async setup(): Promise<void> {
|
|
249
|
+
if (this.phase === "resolve") {
|
|
250
|
+
this.resolve();
|
|
251
|
+
}
|
|
252
|
+
if (this.phase === "validate") {
|
|
253
|
+
this.validate();
|
|
254
|
+
}
|
|
255
|
+
if (this.phase !== "setup") {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const api: AgentExtensionApi = {
|
|
260
|
+
registerTool: (tool) => this.registry.tools.push(tool),
|
|
261
|
+
registerCommand: (command) => this.registry.commands.push(command),
|
|
262
|
+
registerShortcut: (shortcut) => this.registry.shortcuts.push(shortcut),
|
|
263
|
+
registerFlag: (flag) => this.registry.flags.push(flag),
|
|
264
|
+
registerMessageRenderer: (renderer) =>
|
|
265
|
+
this.registry.messageRenderers.push(renderer),
|
|
266
|
+
registerProvider: (provider) => this.registry.providers.push(provider),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
for (const { extension } of this.normalized) {
|
|
270
|
+
await extension.setup?.(api);
|
|
271
|
+
}
|
|
272
|
+
this.phase = "activate";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
activate(): void {
|
|
276
|
+
if (this.phase === "resolve") {
|
|
277
|
+
this.resolve();
|
|
278
|
+
}
|
|
279
|
+
if (this.phase === "validate") {
|
|
280
|
+
this.validate();
|
|
281
|
+
}
|
|
282
|
+
if (this.phase === "setup") {
|
|
283
|
+
throw new Error(
|
|
284
|
+
"Contribution registry setup must complete before activation",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (this.phase !== "activate") {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
this.phase = "run";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async initialize(): Promise<void> {
|
|
294
|
+
this.resolve();
|
|
295
|
+
this.validate();
|
|
296
|
+
await this.setup();
|
|
297
|
+
this.activate();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
isActivated(): boolean {
|
|
301
|
+
return this.phase === "run";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getRegistrySnapshot(): AgentExtensionRegistry {
|
|
305
|
+
return {
|
|
306
|
+
tools: [...this.registry.tools],
|
|
307
|
+
commands: [...this.registry.commands],
|
|
308
|
+
shortcuts: [...this.registry.shortcuts],
|
|
309
|
+
flags: [...this.registry.flags],
|
|
310
|
+
messageRenderers: [...this.registry.messageRenderers],
|
|
311
|
+
providers: [...this.registry.providers],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
getRegisteredTools(): Tool[] {
|
|
316
|
+
return [...this.registry.tools];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
getValidatedExtensions(): AgentExtension[] {
|
|
320
|
+
if (this.phase === "resolve") {
|
|
321
|
+
this.resolve();
|
|
322
|
+
}
|
|
323
|
+
if (this.phase === "validate") {
|
|
324
|
+
this.validate();
|
|
325
|
+
}
|
|
326
|
+
return this.normalized
|
|
327
|
+
.slice()
|
|
328
|
+
.sort((a, b) => a.order - b.order)
|
|
329
|
+
.map(({ extension }) => extension);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function createContributionRegistry(
|
|
334
|
+
options: ContributionRegistryOptions = {},
|
|
335
|
+
): ContributionRegistry {
|
|
336
|
+
return new ContributionRegistry(options);
|
|
337
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { HookEngine } from "./engine.js";
|
|
3
|
+
|
|
4
|
+
describe("HookEngine", () => {
|
|
5
|
+
it("executes handlers in deterministic priority/name order and merges control", async () => {
|
|
6
|
+
const calls: string[] = [];
|
|
7
|
+
const engine = new HookEngine();
|
|
8
|
+
|
|
9
|
+
engine.register({
|
|
10
|
+
name: "z-low",
|
|
11
|
+
stage: "tool_call_before",
|
|
12
|
+
priority: 1,
|
|
13
|
+
handle: () => {
|
|
14
|
+
calls.push("z-low");
|
|
15
|
+
return { context: "from-z" };
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
engine.register({
|
|
19
|
+
name: "a-high",
|
|
20
|
+
stage: "tool_call_before",
|
|
21
|
+
priority: 10,
|
|
22
|
+
handle: () => {
|
|
23
|
+
calls.push("a-high");
|
|
24
|
+
return {
|
|
25
|
+
cancel: true,
|
|
26
|
+
context: "from-a",
|
|
27
|
+
overrideInput: { safe: true },
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await engine.dispatch({
|
|
33
|
+
stage: "tool_call_before",
|
|
34
|
+
runId: "run-1",
|
|
35
|
+
agentId: "agent-1",
|
|
36
|
+
conversationId: "conv-1",
|
|
37
|
+
parentAgentId: null,
|
|
38
|
+
iteration: 1,
|
|
39
|
+
payload: {},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(calls).toEqual(["a-high", "z-low"]);
|
|
43
|
+
expect(result.control).toEqual({
|
|
44
|
+
cancel: true,
|
|
45
|
+
context: "from-a\nfrom-z",
|
|
46
|
+
overrideInput: { safe: true },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("retries failures using policy and succeeds", async () => {
|
|
51
|
+
const handle = vi
|
|
52
|
+
.fn<() => { context: string }>()
|
|
53
|
+
.mockImplementationOnce(() => {
|
|
54
|
+
throw new Error("boom");
|
|
55
|
+
})
|
|
56
|
+
.mockImplementation(() => ({ context: "ok" }));
|
|
57
|
+
const engine = new HookEngine({
|
|
58
|
+
policies: {
|
|
59
|
+
handlers: {
|
|
60
|
+
retryable: { retries: 1, retryDelayMs: 1 },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
engine.register({
|
|
65
|
+
name: "retryable",
|
|
66
|
+
stage: "run_start",
|
|
67
|
+
handle,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await engine.dispatch({
|
|
71
|
+
stage: "run_start",
|
|
72
|
+
runId: "run-1",
|
|
73
|
+
agentId: "agent-1",
|
|
74
|
+
conversationId: "conv-1",
|
|
75
|
+
parentAgentId: null,
|
|
76
|
+
payload: {},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(handle).toHaveBeenCalledTimes(2);
|
|
80
|
+
expect(result.results[0]?.status).toBe("ok");
|
|
81
|
+
expect(result.results[0]?.attempts).toBe(2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("throws for fail_closed blocking handler failures", async () => {
|
|
85
|
+
const engine = new HookEngine({
|
|
86
|
+
policies: {
|
|
87
|
+
handlers: {
|
|
88
|
+
strict: { failureMode: "fail_closed" },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
engine.register({
|
|
93
|
+
name: "strict",
|
|
94
|
+
stage: "run_start",
|
|
95
|
+
handle: () => {
|
|
96
|
+
throw new Error("deny");
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await expect(
|
|
101
|
+
engine.dispatch({
|
|
102
|
+
stage: "run_start",
|
|
103
|
+
runId: "run-1",
|
|
104
|
+
agentId: "agent-1",
|
|
105
|
+
conversationId: "conv-1",
|
|
106
|
+
parentAgentId: null,
|
|
107
|
+
payload: {},
|
|
108
|
+
}),
|
|
109
|
+
).rejects.toThrow("deny");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("queues async events and drains on shutdown", async () => {
|
|
113
|
+
const calls: string[] = [];
|
|
114
|
+
const dropped = vi.fn();
|
|
115
|
+
const engine = new HookEngine({
|
|
116
|
+
onDroppedEvent: dropped,
|
|
117
|
+
policies: {
|
|
118
|
+
stages: {
|
|
119
|
+
run_end: { mode: "async", maxConcurrency: 1, queueLimit: 1 },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
engine.register({
|
|
124
|
+
name: "async-run-end",
|
|
125
|
+
stage: "run_end",
|
|
126
|
+
handle: async (event) => {
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
128
|
+
calls.push(event.eventId);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await engine.dispatch({
|
|
133
|
+
stage: "run_end",
|
|
134
|
+
runId: "run-1",
|
|
135
|
+
agentId: "agent-1",
|
|
136
|
+
conversationId: "conv-1",
|
|
137
|
+
parentAgentId: null,
|
|
138
|
+
payload: { value: 1 },
|
|
139
|
+
});
|
|
140
|
+
await engine.dispatch({
|
|
141
|
+
stage: "run_end",
|
|
142
|
+
runId: "run-1",
|
|
143
|
+
agentId: "agent-1",
|
|
144
|
+
conversationId: "conv-1",
|
|
145
|
+
parentAgentId: null,
|
|
146
|
+
payload: { value: 2 },
|
|
147
|
+
});
|
|
148
|
+
const droppedResult = await engine.dispatch({
|
|
149
|
+
stage: "run_end",
|
|
150
|
+
runId: "run-1",
|
|
151
|
+
agentId: "agent-1",
|
|
152
|
+
conversationId: "conv-1",
|
|
153
|
+
parentAgentId: null,
|
|
154
|
+
payload: { value: 3 },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(droppedResult.dropped).toBe(true);
|
|
158
|
+
expect(dropped).toHaveBeenCalledTimes(1);
|
|
159
|
+
|
|
160
|
+
await engine.shutdown(500);
|
|
161
|
+
expect(calls).toHaveLength(2);
|
|
162
|
+
});
|
|
163
|
+
});
|