pi-herdr-subagents 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/.github/workflows/publish.yml +55 -0
- package/.pi/settings.json +8 -0
- package/.pi/skills/run-integration-tests/SKILL.md +28 -0
- package/LICENSE +21 -0
- package/README.md +483 -0
- package/RELEASING.md +103 -0
- package/agents/planner.md +546 -0
- package/agents/reviewer.md +150 -0
- package/agents/scout.md +104 -0
- package/agents/visual-tester.md +197 -0
- package/agents/worker.md +103 -0
- package/config.json.example +5 -0
- package/package.json +34 -0
- package/pi-extension/subagents/activity.ts +511 -0
- package/pi-extension/subagents/completion.ts +114 -0
- package/pi-extension/subagents/herdr.ts +200 -0
- package/pi-extension/subagents/index.ts +2182 -0
- package/pi-extension/subagents/plan-skill.md +203 -0
- package/pi-extension/subagents/plugin/.claude-plugin/plugin.json +5 -0
- package/pi-extension/subagents/plugin/hooks/hooks.json +15 -0
- package/pi-extension/subagents/plugin/hooks/on-stop.sh +68 -0
- package/pi-extension/subagents/session.ts +180 -0
- package/pi-extension/subagents/status.ts +513 -0
- package/pi-extension/subagents/subagent-done.ts +324 -0
- package/pi-extension/subagents/terminal.ts +106 -0
- package/test/integration/agents/test-echo.md +13 -0
- package/test/integration/agents/test-ping.md +11 -0
- package/test/integration/harness.ts +319 -0
- package/test/integration/mux-surface.test.ts +225 -0
- package/test/integration/subagent-lifecycle.test.ts +329 -0
- package/test/system-prompt-mode.test.ts +163 -0
- package/test/test.ts +2190 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension loaded into sub-agents.
|
|
3
|
+
* - Shows agent identity + available tools as a styled widget above the editor (toggle with Ctrl+J)
|
|
4
|
+
* - Provides a `subagent_done` tool for autonomous agents to self-terminate
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { createSubagentActivityRecorder } from "./activity.ts";
|
|
11
|
+
|
|
12
|
+
export function shouldMarkUserTookOver(agentStarted: boolean): boolean {
|
|
13
|
+
return agentStarted;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function shouldAutoExitOnAgentEnd(
|
|
17
|
+
_userTookOver: boolean,
|
|
18
|
+
messages: any[] | undefined,
|
|
19
|
+
): boolean {
|
|
20
|
+
// Manual input should not strand an auto-exit subagent. If the latest agent
|
|
21
|
+
// turn completed normally, close the session. Escape/abort still leaves it
|
|
22
|
+
// open for inspection or another prompt.
|
|
23
|
+
//
|
|
24
|
+
// stopReason: "error" (e.g. exhausted retries on a provider overload) also
|
|
25
|
+
// returns true — we want to shut down so the parent is woken up — but we
|
|
26
|
+
// pair this with findLatestAssistantError() so the parent learns it was an
|
|
27
|
+
// error, not a clean completion.
|
|
28
|
+
if (messages) {
|
|
29
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
30
|
+
const msg = messages[i];
|
|
31
|
+
if (msg?.role === "assistant") {
|
|
32
|
+
return msg.stopReason !== "aborted";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SubagentErrorInfo {
|
|
41
|
+
errorMessage: string;
|
|
42
|
+
stopReason: "error";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* If the last assistant message in the turn ended with `stopReason: "error"`
|
|
47
|
+
* (typically auto-retry exhausted on an overload / rate limit / server error),
|
|
48
|
+
* return its error info so the parent orchestrator can surface a clear
|
|
49
|
+
* failure instead of silently treating the run as completed.
|
|
50
|
+
*
|
|
51
|
+
* Returns `null` when the latest assistant turn completed normally or was
|
|
52
|
+
* aborted by the user (handled separately by shouldAutoExitOnAgentEnd).
|
|
53
|
+
*/
|
|
54
|
+
export function findLatestAssistantError(
|
|
55
|
+
messages: any[] | undefined,
|
|
56
|
+
): SubagentErrorInfo | null {
|
|
57
|
+
if (!messages) return null;
|
|
58
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
59
|
+
const msg = messages[i];
|
|
60
|
+
if (msg?.role !== "assistant") continue;
|
|
61
|
+
if (msg.stopReason !== "error") return null;
|
|
62
|
+
const raw = typeof msg.errorMessage === "string" ? msg.errorMessage.trim() : "";
|
|
63
|
+
return {
|
|
64
|
+
errorMessage: raw || "Subagent agent loop ended with stopReason=error (no errorMessage field).",
|
|
65
|
+
stopReason: "error",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseDeniedTools(rawValue: string | undefined): string[] {
|
|
72
|
+
return (rawValue ?? "")
|
|
73
|
+
.split(",")
|
|
74
|
+
.map((value) => value.trim())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default function (pi: ExtensionAPI) {
|
|
79
|
+
let toolNames: string[] = [];
|
|
80
|
+
let denied: string[] = [];
|
|
81
|
+
let expanded = false;
|
|
82
|
+
|
|
83
|
+
// Read subagent identity from env vars (set by parent orchestrator)
|
|
84
|
+
const subagentName = process.env.PI_SUBAGENT_NAME ?? "";
|
|
85
|
+
const subagentAgent = process.env.PI_SUBAGENT_AGENT ?? "";
|
|
86
|
+
const deniedToolsValue = process.env.PI_DENY_TOOLS;
|
|
87
|
+
const autoExit = process.env.PI_SUBAGENT_AUTO_EXIT === "1";
|
|
88
|
+
const recorder = createSubagentActivityRecorder({
|
|
89
|
+
runningChildId: process.env.PI_SUBAGENT_ID,
|
|
90
|
+
activityFile: process.env.PI_SUBAGENT_ACTIVITY_FILE,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function renderWidget(ctx: { ui: { setWidget: Function } }, _theme: any) {
|
|
94
|
+
ctx.ui.setWidget(
|
|
95
|
+
"subagent-tools",
|
|
96
|
+
(_tui: any, theme: any) => {
|
|
97
|
+
const box = new Box(1, 0, (text: string) => theme.bg("toolSuccessBg", text));
|
|
98
|
+
|
|
99
|
+
const label = subagentAgent || subagentName;
|
|
100
|
+
const agentTag = label ? theme.bold(theme.fg("accent", `[${label}]`)) : "";
|
|
101
|
+
|
|
102
|
+
if (expanded) {
|
|
103
|
+
// Expanded: full tool list + denied
|
|
104
|
+
const countInfo = theme.fg("dim", ` — ${toolNames.length} available`);
|
|
105
|
+
const hint = theme.fg("muted", " (Ctrl+J to collapse)");
|
|
106
|
+
|
|
107
|
+
const toolList = toolNames
|
|
108
|
+
.map((name: string) => theme.fg("dim", name))
|
|
109
|
+
.join(theme.fg("muted", ", "));
|
|
110
|
+
|
|
111
|
+
let deniedLine = "";
|
|
112
|
+
if (denied.length > 0) {
|
|
113
|
+
const deniedList = denied
|
|
114
|
+
.map((name: string) => theme.fg("error", name))
|
|
115
|
+
.join(theme.fg("muted", ", "));
|
|
116
|
+
deniedLine = "\n" + theme.fg("muted", "denied: ") + deniedList;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const content = new Text(
|
|
120
|
+
`${agentTag}${countInfo}${hint}\n${toolList}${deniedLine}`,
|
|
121
|
+
0,
|
|
122
|
+
0,
|
|
123
|
+
);
|
|
124
|
+
box.addChild(content);
|
|
125
|
+
} else {
|
|
126
|
+
// Collapsed: one-line summary
|
|
127
|
+
const countInfo = theme.fg("dim", ` — ${toolNames.length} tools`);
|
|
128
|
+
const deniedInfo =
|
|
129
|
+
denied.length > 0
|
|
130
|
+
? theme.fg("dim", " · ") + theme.fg("error", `${denied.length} denied`)
|
|
131
|
+
: "";
|
|
132
|
+
const hint = theme.fg("muted", " (Ctrl+J to expand)");
|
|
133
|
+
|
|
134
|
+
const content = new Text(`${agentTag}${countInfo}${deniedInfo}${hint}`, 0, 0);
|
|
135
|
+
box.addChild(content);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return box;
|
|
139
|
+
},
|
|
140
|
+
{ placement: "aboveEditor" },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let userTookOver = false;
|
|
145
|
+
let agentStarted = false;
|
|
146
|
+
|
|
147
|
+
// Show widget + status bar on session start
|
|
148
|
+
pi.on("session_start", (_event, ctx) => {
|
|
149
|
+
recorder.sessionStart();
|
|
150
|
+
const tools = pi.getAllTools();
|
|
151
|
+
toolNames = tools.map((t) => t.name).sort();
|
|
152
|
+
denied = parseDeniedTools(deniedToolsValue);
|
|
153
|
+
|
|
154
|
+
renderWidget(ctx, null);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
pi.on("input", () => {
|
|
158
|
+
recorder.input();
|
|
159
|
+
// Ignore the initial task message that starts an autonomous subagent.
|
|
160
|
+
// Only inputs after the first agent run has started count as user takeover.
|
|
161
|
+
if (!shouldMarkUserTookOver(agentStarted)) return;
|
|
162
|
+
userTookOver = true;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
pi.on("before_agent_start", () => {
|
|
166
|
+
recorder.beforeAgentStart();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
pi.on("agent_start", () => {
|
|
170
|
+
agentStarted = true;
|
|
171
|
+
recorder.agentStart();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
pi.on("agent_end", (event, ctx) => {
|
|
175
|
+
const messages = (event as any).messages as any[] | undefined;
|
|
176
|
+
const shouldExit = autoExit && shouldAutoExitOnAgentEnd(userTookOver, messages);
|
|
177
|
+
|
|
178
|
+
if (shouldExit) {
|
|
179
|
+
// Surface stopReason: "error" turns (auto-retry exhausted, provider
|
|
180
|
+
// overload, etc.) to the parent via the .exit sidecar so the watcher
|
|
181
|
+
// can report a clear failure with the underlying error message.
|
|
182
|
+
// Without this the parent would only see exit code 0 and a stale
|
|
183
|
+
// assistant message, mistaking the crash for a successful completion.
|
|
184
|
+
const errorInfo = findLatestAssistantError(messages);
|
|
185
|
+
const sessionFile = process.env.PI_SUBAGENT_SESSION;
|
|
186
|
+
if (errorInfo && sessionFile) {
|
|
187
|
+
try {
|
|
188
|
+
writeFileSync(
|
|
189
|
+
`${sessionFile}.exit`,
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
type: "error",
|
|
192
|
+
errorMessage: errorInfo.errorMessage,
|
|
193
|
+
stopReason: errorInfo.stopReason,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
} catch {
|
|
197
|
+
// Best effort — even without the sidecar, watcher's session-file
|
|
198
|
+
// fallback can still recover the errorMessage.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
recorder.agentEndDone();
|
|
203
|
+
ctx.shutdown();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
recorder.agentEndWaiting();
|
|
208
|
+
if (autoExit) {
|
|
209
|
+
// Reset any recorded manual input marker. Auto-exit is decided by whether
|
|
210
|
+
// the latest agent turn completed normally, not by who initiated it.
|
|
211
|
+
userTookOver = false;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
pi.on("turn_start", (event) => {
|
|
216
|
+
recorder.turnStart((event as any).turnIndex);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
pi.on("turn_end", (event) => {
|
|
220
|
+
recorder.turnEnd((event as any).turnIndex);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
pi.on("before_provider_request", () => {
|
|
224
|
+
recorder.beforeProviderRequest();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
pi.on("after_provider_response", () => {
|
|
228
|
+
recorder.afterProviderResponse();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
pi.on("message_update", (event) => {
|
|
232
|
+
recorder.messageUpdate((event as any).assistantMessageEvent?.type);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
pi.on("tool_execution_start", (event) => {
|
|
236
|
+
recorder.toolExecutionStart((event as any).toolCallId, (event as any).toolName);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
pi.on("tool_call", (event) => {
|
|
240
|
+
recorder.toolCall((event as any).toolCallId, (event as any).toolName);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
pi.on("tool_execution_update", (event) => {
|
|
244
|
+
recorder.toolExecutionUpdate((event as any).toolCallId, (event as any).toolName);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
pi.on("tool_result", (event) => {
|
|
248
|
+
recorder.toolResult((event as any).toolCallId, (event as any).toolName);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
pi.on("tool_execution_end", (event) => {
|
|
252
|
+
recorder.toolExecutionEnd((event as any).toolCallId, (event as any).toolName);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
pi.on("session_shutdown", (event) => {
|
|
256
|
+
recorder.sessionShutdown((event as any).reason);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Toggle expand/collapse with Ctrl+J
|
|
260
|
+
pi.registerShortcut("ctrl+j", {
|
|
261
|
+
description: "Toggle subagent tools widget",
|
|
262
|
+
handler: (ctx) => {
|
|
263
|
+
expanded = !expanded;
|
|
264
|
+
renderWidget(ctx, null);
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
pi.registerTool({
|
|
269
|
+
name: "caller_ping",
|
|
270
|
+
label: "Caller Ping",
|
|
271
|
+
description:
|
|
272
|
+
"Send a help request to the parent agent and exit this session. " +
|
|
273
|
+
"The parent will be notified with your message and can resume this session with a response. " +
|
|
274
|
+
"Use when you're stuck, need clarification, or need the parent to take action.",
|
|
275
|
+
parameters: Type.Object({
|
|
276
|
+
message: Type.String({ description: "What you need help with" }),
|
|
277
|
+
}),
|
|
278
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
279
|
+
const sessionFile = process.env.PI_SUBAGENT_SESSION;
|
|
280
|
+
if (!sessionFile) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"caller_ping is only available in subagent contexts. " +
|
|
283
|
+
"PI_SUBAGENT_SESSION environment variable is not set.",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
recorder.callerPing();
|
|
288
|
+
const exitData = {
|
|
289
|
+
type: "ping" as const,
|
|
290
|
+
name: process.env.PI_SUBAGENT_NAME ?? "subagent",
|
|
291
|
+
message: params.message,
|
|
292
|
+
};
|
|
293
|
+
writeFileSync(`${sessionFile}.exit`, JSON.stringify(exitData));
|
|
294
|
+
|
|
295
|
+
ctx.shutdown();
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text: "Ping sent. Session will exit and parent will be notified." }],
|
|
298
|
+
details: {},
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
pi.registerTool({
|
|
304
|
+
name: "subagent_done",
|
|
305
|
+
label: "Subagent Done",
|
|
306
|
+
description:
|
|
307
|
+
"Call this tool when you have completed your task. " +
|
|
308
|
+
"It will close this session and return your results to the main session. " +
|
|
309
|
+
"Your LAST assistant message before calling this becomes the summary returned to the caller.",
|
|
310
|
+
parameters: Type.Object({}),
|
|
311
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
312
|
+
const sessionFile = process.env.PI_SUBAGENT_SESSION;
|
|
313
|
+
recorder.subagentDone();
|
|
314
|
+
if (sessionFile) {
|
|
315
|
+
writeFileSync(`${sessionFile}.exit`, JSON.stringify({ type: "done" }));
|
|
316
|
+
}
|
|
317
|
+
ctx.shutdown();
|
|
318
|
+
return {
|
|
319
|
+
content: [{ type: "text", text: "Shutting down subagent session." }],
|
|
320
|
+
details: {},
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
closeHerdrSurface,
|
|
6
|
+
createHerdrSurface,
|
|
7
|
+
createHerdrSurfaceSplit,
|
|
8
|
+
isHerdrAvailable,
|
|
9
|
+
readHerdrScreen,
|
|
10
|
+
readHerdrScreenAsync,
|
|
11
|
+
renameHerdrTab,
|
|
12
|
+
renameHerdrWorkspace,
|
|
13
|
+
sendHerdrCommand,
|
|
14
|
+
sendHerdrEscape,
|
|
15
|
+
} from "./herdr.ts";
|
|
16
|
+
|
|
17
|
+
export type PaneId = string;
|
|
18
|
+
export type SplitDirection = "right" | "down";
|
|
19
|
+
|
|
20
|
+
const SETUP_HINT = "Start pi inside herdr (`herdr`, then run `pi`).";
|
|
21
|
+
|
|
22
|
+
export function isTerminalAvailable(): boolean {
|
|
23
|
+
return isHerdrAvailable();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function terminalSetupHint(): string {
|
|
27
|
+
return SETUP_HINT;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertTerminalAvailable(): void {
|
|
31
|
+
if (!isTerminalAvailable()) throw new Error(`herdr is not available. ${SETUP_HINT}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function shellQuote(value: string): string {
|
|
35
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Create a new herdr tab and return its root pane ID. */
|
|
39
|
+
export function createSubagentPane(name: string): PaneId {
|
|
40
|
+
assertTerminalAvailable();
|
|
41
|
+
return createHerdrSurface(name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Split the current herdr pane and return the child pane ID. */
|
|
45
|
+
export function splitCurrentPane(name: string, direction: SplitDirection): PaneId {
|
|
46
|
+
assertTerminalAvailable();
|
|
47
|
+
return createHerdrSurfaceSplit(name, direction);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function renameCurrentTab(title: string): void {
|
|
51
|
+
assertTerminalAvailable();
|
|
52
|
+
renameHerdrTab(title);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function renameCurrentWorkspace(title: string): void {
|
|
56
|
+
assertTerminalAvailable();
|
|
57
|
+
renameHerdrWorkspace(title);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function runInPane(paneId: PaneId, command: string): void {
|
|
61
|
+
assertTerminalAvailable();
|
|
62
|
+
sendHerdrCommand(paneId, command);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function interruptPane(paneId: PaneId): void {
|
|
66
|
+
assertTerminalAvailable();
|
|
67
|
+
sendHerdrEscape(paneId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function runScriptInPane(
|
|
71
|
+
paneId: PaneId,
|
|
72
|
+
command: string,
|
|
73
|
+
options?: { scriptPath?: string; scriptPreamble?: string },
|
|
74
|
+
): string {
|
|
75
|
+
const scriptPath =
|
|
76
|
+
options?.scriptPath ??
|
|
77
|
+
join(
|
|
78
|
+
tmpdir(),
|
|
79
|
+
"pi-herdr-subagent-scripts",
|
|
80
|
+
`cmd-${Date.now()}-${Math.random().toString(16).slice(2, 8)}.sh`,
|
|
81
|
+
);
|
|
82
|
+
mkdirSync(dirname(scriptPath), { recursive: true });
|
|
83
|
+
|
|
84
|
+
const scriptLines = ["#!/bin/bash"];
|
|
85
|
+
if (options?.scriptPreamble) scriptLines.push(options.scriptPreamble.trimEnd());
|
|
86
|
+
scriptLines.push(command);
|
|
87
|
+
writeFileSync(scriptPath, `${scriptLines.join("\n")}\n`, { mode: 0o755 });
|
|
88
|
+
|
|
89
|
+
runInPane(paneId, `bash ${shellQuote(scriptPath)}`);
|
|
90
|
+
return scriptPath;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readPane(paneId: PaneId, lines = 50): string {
|
|
94
|
+
assertTerminalAvailable();
|
|
95
|
+
return readHerdrScreen(paneId, lines);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function readPaneAsync(paneId: PaneId, lines = 50): Promise<string> {
|
|
99
|
+
assertTerminalAvailable();
|
|
100
|
+
return readHerdrScreenAsync(paneId, lines);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function closePane(paneId: PaneId): void {
|
|
104
|
+
assertTerminalAvailable();
|
|
105
|
+
closeHerdrSurface(paneId);
|
|
106
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-echo
|
|
3
|
+
description: Integration test agent — completes simple file-writing tasks
|
|
4
|
+
model: openrouter/free
|
|
5
|
+
tools: read, bash, write, edit
|
|
6
|
+
spawning: false
|
|
7
|
+
auto-exit: true
|
|
8
|
+
disable-model-invocation: true
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a test agent. Complete the task given to you immediately. Be direct and concise.
|
|
12
|
+
When asked to write content to a file, do it right away using the bash tool.
|
|
13
|
+
Do not ask questions. Do not explain. Just execute the task.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-ping
|
|
3
|
+
description: Integration test agent — calls caller_ping instead of completing task
|
|
4
|
+
model: openrouter/free
|
|
5
|
+
tools: read, bash
|
|
6
|
+
spawning: false
|
|
7
|
+
disable-model-invocation: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
You are a test agent. When given ANY task, you must call the caller_ping tool with the message set to "PING: " followed by the task text you received.
|
|
11
|
+
Do NOT complete the task yourself. Do NOT use any other tools. ONLY call caller_ping.
|