@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,239 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentConfig,
|
|
3
|
+
AgentExtension,
|
|
4
|
+
AgentExtensionHookStage,
|
|
5
|
+
AgentHookControl,
|
|
6
|
+
} from "../types.js";
|
|
7
|
+
import type { HookEngine, HookHandler } from "./engine.js";
|
|
8
|
+
|
|
9
|
+
type LifecycleConfig = Pick<AgentConfig, "hooks" | "extensions">;
|
|
10
|
+
|
|
11
|
+
type ExtensionStageHandlerSpec = {
|
|
12
|
+
handler: (
|
|
13
|
+
extension: AgentExtension,
|
|
14
|
+
event: { payload: unknown },
|
|
15
|
+
) => Promise<AgentHookControl | undefined> | AgentHookControl | undefined;
|
|
16
|
+
name: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const EXTENSION_STAGE_HANDLERS: Record<
|
|
20
|
+
AgentExtensionHookStage,
|
|
21
|
+
ExtensionStageHandlerSpec
|
|
22
|
+
> = {
|
|
23
|
+
input: {
|
|
24
|
+
name: "onInput",
|
|
25
|
+
handler: (extension, event) =>
|
|
26
|
+
extension.onInput?.(event.payload as never) as
|
|
27
|
+
| AgentHookControl
|
|
28
|
+
| undefined,
|
|
29
|
+
},
|
|
30
|
+
session_start: {
|
|
31
|
+
name: "onSessionStart",
|
|
32
|
+
handler: (extension, event) =>
|
|
33
|
+
extension.onSessionStart?.(event.payload as never) as
|
|
34
|
+
| AgentHookControl
|
|
35
|
+
| undefined,
|
|
36
|
+
},
|
|
37
|
+
run_start: {
|
|
38
|
+
name: "onRunStart",
|
|
39
|
+
handler: (extension, event) =>
|
|
40
|
+
extension.onRunStart?.(event.payload as never) as
|
|
41
|
+
| AgentHookControl
|
|
42
|
+
| undefined,
|
|
43
|
+
},
|
|
44
|
+
iteration_start: {
|
|
45
|
+
name: "onIterationStart",
|
|
46
|
+
handler: (extension, event) =>
|
|
47
|
+
extension.onIterationStart?.(event.payload as never) as
|
|
48
|
+
| AgentHookControl
|
|
49
|
+
| undefined,
|
|
50
|
+
},
|
|
51
|
+
turn_start: {
|
|
52
|
+
name: "onTurnStart",
|
|
53
|
+
handler: (extension, event) =>
|
|
54
|
+
extension.onTurnStart?.(event.payload as never) as
|
|
55
|
+
| AgentHookControl
|
|
56
|
+
| undefined,
|
|
57
|
+
},
|
|
58
|
+
before_agent_start: {
|
|
59
|
+
name: "onBeforeAgentStart",
|
|
60
|
+
handler: (extension, event) =>
|
|
61
|
+
extension.onBeforeAgentStart?.(event.payload as never) as
|
|
62
|
+
| AgentHookControl
|
|
63
|
+
| undefined,
|
|
64
|
+
},
|
|
65
|
+
tool_call_before: {
|
|
66
|
+
name: "onToolCall",
|
|
67
|
+
handler: (extension, event) =>
|
|
68
|
+
extension.onToolCall?.(event.payload as never) as
|
|
69
|
+
| AgentHookControl
|
|
70
|
+
| undefined,
|
|
71
|
+
},
|
|
72
|
+
tool_call_after: {
|
|
73
|
+
name: "onToolResult",
|
|
74
|
+
handler: (extension, event) =>
|
|
75
|
+
extension.onToolResult?.(event.payload as never) as
|
|
76
|
+
| AgentHookControl
|
|
77
|
+
| undefined,
|
|
78
|
+
},
|
|
79
|
+
turn_end: {
|
|
80
|
+
name: "onAgentEnd",
|
|
81
|
+
handler: (extension, event) =>
|
|
82
|
+
extension.onAgentEnd?.(event.payload as never) as
|
|
83
|
+
| AgentHookControl
|
|
84
|
+
| undefined,
|
|
85
|
+
},
|
|
86
|
+
iteration_end: {
|
|
87
|
+
name: "onIterationEnd",
|
|
88
|
+
handler: async (extension, event) => {
|
|
89
|
+
await extension.onIterationEnd?.(event.payload as never);
|
|
90
|
+
return undefined;
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
run_end: {
|
|
94
|
+
name: "onRunEnd",
|
|
95
|
+
handler: async (extension, event) => {
|
|
96
|
+
await extension.onRunEnd?.(event.payload as never);
|
|
97
|
+
return undefined;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
session_shutdown: {
|
|
101
|
+
name: "onSessionShutdown",
|
|
102
|
+
handler: (extension, event) =>
|
|
103
|
+
extension.onSessionShutdown?.(event.payload as never) as
|
|
104
|
+
| AgentHookControl
|
|
105
|
+
| undefined,
|
|
106
|
+
},
|
|
107
|
+
error: {
|
|
108
|
+
name: "onError",
|
|
109
|
+
handler: async (extension, event) => {
|
|
110
|
+
await extension.onError?.(event.payload as never);
|
|
111
|
+
return undefined;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
runtime_event: {
|
|
115
|
+
name: "onRuntimeEvent",
|
|
116
|
+
handler: async (extension, event) => {
|
|
117
|
+
await extension.onRuntimeEvent?.(event.payload as never);
|
|
118
|
+
return undefined;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export function registerLifecycleHandlers(
|
|
124
|
+
hookEngine: HookEngine,
|
|
125
|
+
config: LifecycleConfig,
|
|
126
|
+
): void {
|
|
127
|
+
const register = (handler: HookHandler): void => {
|
|
128
|
+
hookEngine.register(handler);
|
|
129
|
+
};
|
|
130
|
+
const hooks = config.hooks;
|
|
131
|
+
|
|
132
|
+
if (hooks?.onSessionStart) {
|
|
133
|
+
register({
|
|
134
|
+
name: "hooks.onSessionStart",
|
|
135
|
+
stage: "session_start",
|
|
136
|
+
handle: (event) => hooks.onSessionStart?.(event.payload as never),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (hooks?.onRunStart) {
|
|
140
|
+
register({
|
|
141
|
+
name: "hooks.onRunStart",
|
|
142
|
+
stage: "run_start",
|
|
143
|
+
handle: (event) => hooks.onRunStart?.(event.payload as never),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (hooks?.onRunEnd) {
|
|
147
|
+
register({
|
|
148
|
+
name: "hooks.onRunEnd",
|
|
149
|
+
stage: "run_end",
|
|
150
|
+
handle: async (event) => {
|
|
151
|
+
await hooks.onRunEnd?.(event.payload as never);
|
|
152
|
+
return undefined;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (hooks?.onIterationStart) {
|
|
157
|
+
register({
|
|
158
|
+
name: "hooks.onIterationStart",
|
|
159
|
+
stage: "iteration_start",
|
|
160
|
+
handle: (event) => hooks.onIterationStart?.(event.payload as never),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (hooks?.onIterationEnd) {
|
|
164
|
+
register({
|
|
165
|
+
name: "hooks.onIterationEnd",
|
|
166
|
+
stage: "iteration_end",
|
|
167
|
+
handle: async (event) => {
|
|
168
|
+
await hooks.onIterationEnd?.(event.payload as never);
|
|
169
|
+
return undefined;
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (hooks?.onTurnStart) {
|
|
174
|
+
register({
|
|
175
|
+
name: "hooks.onTurnStart",
|
|
176
|
+
stage: "turn_start",
|
|
177
|
+
handle: (event) => hooks.onTurnStart?.(event.payload as never),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (hooks?.onTurnEnd) {
|
|
181
|
+
register({
|
|
182
|
+
name: "hooks.onTurnEnd",
|
|
183
|
+
stage: "turn_end",
|
|
184
|
+
handle: (event) => hooks.onTurnEnd?.(event.payload as never),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (hooks?.onToolCallStart) {
|
|
188
|
+
register({
|
|
189
|
+
name: "hooks.onToolCallStart",
|
|
190
|
+
stage: "tool_call_before",
|
|
191
|
+
handle: (event) => hooks.onToolCallStart?.(event.payload as never),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (hooks?.onToolCallEnd) {
|
|
195
|
+
register({
|
|
196
|
+
name: "hooks.onToolCallEnd",
|
|
197
|
+
stage: "tool_call_after",
|
|
198
|
+
handle: (event) => hooks.onToolCallEnd?.(event.payload as never),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (hooks?.onSessionShutdown) {
|
|
202
|
+
register({
|
|
203
|
+
name: "hooks.onSessionShutdown",
|
|
204
|
+
stage: "session_shutdown",
|
|
205
|
+
handle: (event) => hooks.onSessionShutdown?.(event.payload as never),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (hooks?.onError) {
|
|
209
|
+
register({
|
|
210
|
+
name: "hooks.onError",
|
|
211
|
+
stage: "error",
|
|
212
|
+
handle: async (event) => {
|
|
213
|
+
await hooks.onError?.(event.payload as never);
|
|
214
|
+
return undefined;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const [index, extension] of (config.extensions ?? []).entries()) {
|
|
220
|
+
if (!extension.manifest.capabilities.includes("hooks")) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const order = String(index).padStart(4, "0");
|
|
224
|
+
const extensionName = extension.name || `extension_${order}`;
|
|
225
|
+
const base = `${order}:${extensionName}`;
|
|
226
|
+
const subscribedStages = new Set(extension.manifest.hookStages ?? []);
|
|
227
|
+
for (const stage of subscribedStages) {
|
|
228
|
+
const stageHandler = EXTENSION_STAGE_HANDLERS[stage];
|
|
229
|
+
if (!stageHandler) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
register({
|
|
233
|
+
name: `${base}.${stageHandler.name}`,
|
|
234
|
+
stage,
|
|
235
|
+
handle: (event) => stageHandler.handler(extension, event),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createSubprocessHooks,
|
|
3
|
+
type HookEventName,
|
|
4
|
+
HookEventNameSchema,
|
|
5
|
+
type HookEventPayload,
|
|
6
|
+
HookEventPayloadSchema,
|
|
7
|
+
parseHookEventPayload,
|
|
8
|
+
type RunHookOptions,
|
|
9
|
+
type RunHookResult,
|
|
10
|
+
runHook,
|
|
11
|
+
type SubprocessHookControl,
|
|
12
|
+
type SubprocessHooksOptions,
|
|
13
|
+
} from "./subprocess.js";
|
|
14
|
+
export {
|
|
15
|
+
type RunSubprocessEventOptions,
|
|
16
|
+
type RunSubprocessEventResult,
|
|
17
|
+
runSubprocessEvent,
|
|
18
|
+
} from "./subprocess-runner.js";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface RunSubprocessEventOptions {
|
|
4
|
+
command: string[];
|
|
5
|
+
cwd?: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
detached?: boolean;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RunSubprocessEventResult {
|
|
12
|
+
exitCode: number | null;
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
parsedJson?: unknown;
|
|
16
|
+
parseError?: string;
|
|
17
|
+
timedOut?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseStdout(stdout: string): {
|
|
21
|
+
parsedJson?: unknown;
|
|
22
|
+
parseError?: string;
|
|
23
|
+
} {
|
|
24
|
+
const trimmed = stdout.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines = trimmed
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((line) => line.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
const prefixed = lines
|
|
34
|
+
.filter((line) => line.startsWith("HOOK_CONTROL\t"))
|
|
35
|
+
.map((line) => line.slice("HOOK_CONTROL\t".length));
|
|
36
|
+
|
|
37
|
+
const candidate =
|
|
38
|
+
prefixed.length > 0 ? prefixed[prefixed.length - 1] : trimmed;
|
|
39
|
+
try {
|
|
40
|
+
return { parsedJson: JSON.parse(candidate) };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
parseError:
|
|
44
|
+
error instanceof Error
|
|
45
|
+
? error.message
|
|
46
|
+
: "Failed to parse subprocess stdout JSON",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatSpawnError(error: unknown, command: string[]): Error {
|
|
52
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
53
|
+
const withCode = err as Error & { code?: string };
|
|
54
|
+
const commandLabel = command.join(" ");
|
|
55
|
+
if (withCode.code === "EACCES") {
|
|
56
|
+
return new Error(
|
|
57
|
+
`Failed to execute hook command "${commandLabel}" (EACCES). Configure hooks with an explicit interpreter/command array (for example: ["bash", "/path/to/script"]) or make the script executable with a valid shebang.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return new Error(
|
|
61
|
+
`Failed to execute hook command "${commandLabel}": ${err.message}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runSubprocessEvent(
|
|
66
|
+
payload: unknown,
|
|
67
|
+
options: RunSubprocessEventOptions,
|
|
68
|
+
): Promise<RunSubprocessEventResult | undefined> {
|
|
69
|
+
const command = options.command;
|
|
70
|
+
if (!Array.isArray(command) || command.length === 0) {
|
|
71
|
+
throw new Error("runSubprocessEvent requires a non-empty command");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const detached = !!options.detached;
|
|
75
|
+
const child = spawn(command[0], command.slice(1), {
|
|
76
|
+
cwd: options.cwd,
|
|
77
|
+
env: options.env,
|
|
78
|
+
stdio: detached ? ["pipe", "ignore", "ignore"] : ["pipe", "pipe", "pipe"],
|
|
79
|
+
detached,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!child.stdin) {
|
|
83
|
+
throw new Error("runSubprocessEvent failed to create stdin pipe");
|
|
84
|
+
}
|
|
85
|
+
child.stdin.write(JSON.stringify(payload));
|
|
86
|
+
child.stdin.end();
|
|
87
|
+
|
|
88
|
+
if (detached) {
|
|
89
|
+
await new Promise<void>((resolve, reject) => {
|
|
90
|
+
child.once("error", (error) => {
|
|
91
|
+
reject(formatSpawnError(error, command));
|
|
92
|
+
});
|
|
93
|
+
child.once("spawn", () => resolve());
|
|
94
|
+
});
|
|
95
|
+
child.unref();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!child.stdout || !child.stderr) {
|
|
100
|
+
throw new Error("runSubprocessEvent failed to create stdout/stderr pipes");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let stdout = "";
|
|
104
|
+
let stderr = "";
|
|
105
|
+
let timedOut = false;
|
|
106
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
107
|
+
|
|
108
|
+
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
109
|
+
stdout += chunk.toString();
|
|
110
|
+
});
|
|
111
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
112
|
+
stderr += chunk.toString();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return await new Promise<RunSubprocessEventResult>((resolve, reject) => {
|
|
116
|
+
child.once("error", (error) => {
|
|
117
|
+
reject(formatSpawnError(error, command));
|
|
118
|
+
});
|
|
119
|
+
if ((options.timeoutMs ?? 0) > 0) {
|
|
120
|
+
timeoutId = setTimeout(() => {
|
|
121
|
+
timedOut = true;
|
|
122
|
+
child.kill("SIGKILL");
|
|
123
|
+
}, options.timeoutMs);
|
|
124
|
+
}
|
|
125
|
+
child.once("close", (exitCode) => {
|
|
126
|
+
if (timeoutId) {
|
|
127
|
+
clearTimeout(timeoutId);
|
|
128
|
+
}
|
|
129
|
+
const { parsedJson, parseError } = parseStdout(stdout);
|
|
130
|
+
resolve({
|
|
131
|
+
exitCode,
|
|
132
|
+
stdout,
|
|
133
|
+
stderr,
|
|
134
|
+
parsedJson,
|
|
135
|
+
parseError,
|
|
136
|
+
timedOut,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { createSubprocessHooks, runHook } from "./subprocess.js";
|
|
6
|
+
|
|
7
|
+
const tmpPaths: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
for (const path of tmpPaths) {
|
|
11
|
+
await rm(path, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
tmpPaths.length = 0;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("hooks", () => {
|
|
17
|
+
it("runHook pipes payload to command and parses JSON stdout", async () => {
|
|
18
|
+
const result = await runHook(
|
|
19
|
+
{
|
|
20
|
+
clineVersion: "",
|
|
21
|
+
hookName: "tool_call",
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
taskId: "conv-1",
|
|
24
|
+
workspaceRoots: [],
|
|
25
|
+
userId: "agent-1",
|
|
26
|
+
agent_id: "agent-1",
|
|
27
|
+
parent_agent_id: null,
|
|
28
|
+
iteration: 1,
|
|
29
|
+
tool_call: {
|
|
30
|
+
id: "call-1",
|
|
31
|
+
name: "read_file",
|
|
32
|
+
input: { path: "README.md" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
command: [
|
|
37
|
+
process.execPath,
|
|
38
|
+
"-e",
|
|
39
|
+
"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const p=JSON.parse(d);process.stdout.write(JSON.stringify({cancel:p.hookName==='tool_call',context:'ok'}));});",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(result?.exitCode).toBe(0);
|
|
45
|
+
expect(result?.parsedJson).toEqual({ cancel: true, context: "ok" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("createSubprocessHooks maps lifecycle payloads and returns hook controls", async () => {
|
|
49
|
+
const dir = await mkdtemp(join(tmpdir(), "agents-hooks-"));
|
|
50
|
+
tmpPaths.push(dir);
|
|
51
|
+
const output = join(dir, "events.log");
|
|
52
|
+
|
|
53
|
+
const script =
|
|
54
|
+
"const fs=require('node:fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const p=JSON.parse(d);fs.appendFileSync(process.argv[1],JSON.stringify(p)+'\\n');if(p.hookName==='tool_call'){process.stdout.write(JSON.stringify({cancel:true,context:'stop-now',overrideInput:{safe:true}}));}});";
|
|
55
|
+
|
|
56
|
+
const hookControl = createSubprocessHooks({
|
|
57
|
+
command: [process.execPath, "-e", script, output],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const control = await hookControl.hooks.onToolCallStart?.({
|
|
61
|
+
agentId: "agent-main",
|
|
62
|
+
conversationId: "conv-main",
|
|
63
|
+
parentAgentId: null,
|
|
64
|
+
iteration: 2,
|
|
65
|
+
call: {
|
|
66
|
+
id: "c-1",
|
|
67
|
+
name: "bash",
|
|
68
|
+
input: { command: "ls" },
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
expect(control).toEqual({
|
|
72
|
+
cancel: true,
|
|
73
|
+
context: "stop-now",
|
|
74
|
+
overrideInput: { safe: true },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await expect(
|
|
78
|
+
hookControl.hooks.onToolCallEnd?.({
|
|
79
|
+
agentId: "agent-main",
|
|
80
|
+
conversationId: "conv-main",
|
|
81
|
+
parentAgentId: null,
|
|
82
|
+
iteration: 2,
|
|
83
|
+
record: {
|
|
84
|
+
id: "c-1",
|
|
85
|
+
name: "bash",
|
|
86
|
+
input: { command: "ls" },
|
|
87
|
+
output: "ok",
|
|
88
|
+
durationMs: 1,
|
|
89
|
+
startedAt: new Date(),
|
|
90
|
+
endedAt: new Date(),
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
).resolves.toBeUndefined();
|
|
94
|
+
await expect(
|
|
95
|
+
hookControl.hooks.onTurnEnd?.({
|
|
96
|
+
agentId: "agent-main",
|
|
97
|
+
conversationId: "conv-main",
|
|
98
|
+
parentAgentId: null,
|
|
99
|
+
iteration: 2,
|
|
100
|
+
turn: {
|
|
101
|
+
text: "done",
|
|
102
|
+
toolCalls: [],
|
|
103
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
104
|
+
truncated: false,
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
).resolves.toBeUndefined();
|
|
108
|
+
await expect(
|
|
109
|
+
hookControl.shutdown({
|
|
110
|
+
agentId: "agent-main",
|
|
111
|
+
conversationId: "conv-main",
|
|
112
|
+
parentAgentId: null,
|
|
113
|
+
reason: "test",
|
|
114
|
+
}),
|
|
115
|
+
).resolves.toBeUndefined();
|
|
116
|
+
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
118
|
+
const lines = (await readFile(output, "utf8"))
|
|
119
|
+
.trim()
|
|
120
|
+
.split("\n")
|
|
121
|
+
.map((line) => JSON.parse(line));
|
|
122
|
+
|
|
123
|
+
expect(lines.some((e) => e.hookName === "tool_call")).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("reports dispatch errors without throwing", async () => {
|
|
127
|
+
const onDispatchError = vi
|
|
128
|
+
.fn<(error: Error) => void>()
|
|
129
|
+
.mockImplementation(() => undefined);
|
|
130
|
+
|
|
131
|
+
const hookControl = createSubprocessHooks({
|
|
132
|
+
command: ["/path/does/not/exist"],
|
|
133
|
+
onDispatchError: (error) => onDispatchError(error),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await expect(
|
|
137
|
+
hookControl.hooks.onToolCallEnd?.({
|
|
138
|
+
agentId: "agent-main",
|
|
139
|
+
conversationId: "conv-main",
|
|
140
|
+
parentAgentId: null,
|
|
141
|
+
iteration: 2,
|
|
142
|
+
record: {
|
|
143
|
+
id: "c-1",
|
|
144
|
+
name: "bash",
|
|
145
|
+
input: { command: "ls" },
|
|
146
|
+
output: "ok",
|
|
147
|
+
durationMs: 1,
|
|
148
|
+
startedAt: new Date(),
|
|
149
|
+
endedAt: new Date(),
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
).resolves.toBeUndefined();
|
|
153
|
+
|
|
154
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
155
|
+
expect(onDispatchError).toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("treats invalid tool_call stdout as dispatch error", async () => {
|
|
159
|
+
const onDispatchError = vi.fn<(error: Error) => void>();
|
|
160
|
+
const hookControl = createSubprocessHooks({
|
|
161
|
+
command: [process.execPath, "-e", "process.stdout.write('not-json')"],
|
|
162
|
+
onDispatchError: (error) => onDispatchError(error),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const result = await hookControl.hooks.onToolCallStart?.({
|
|
166
|
+
agentId: "agent-main",
|
|
167
|
+
conversationId: "conv-main",
|
|
168
|
+
parentAgentId: null,
|
|
169
|
+
iteration: 1,
|
|
170
|
+
call: {
|
|
171
|
+
id: "c-1",
|
|
172
|
+
name: "bash",
|
|
173
|
+
input: { command: "ls" },
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(result).toBeUndefined();
|
|
178
|
+
expect(onDispatchError).toHaveBeenCalledTimes(1);
|
|
179
|
+
});
|
|
180
|
+
});
|