@vzlentin/harness 0.0.0-stage → 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/README.md +62 -2
- package/dist/answer.d.ts +29 -0
- package/dist/answer.js +171 -0
- package/dist/chat.d.ts +48 -0
- package/dist/chat.js +208 -0
- package/dist/children.d.ts +211 -0
- package/dist/children.js +390 -0
- package/dist/chrome.d.ts +18 -0
- package/dist/chrome.js +73 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +245 -0
- package/dist/client.d.ts +49 -0
- package/dist/client.js +162 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +115 -0
- package/dist/durable-entry.d.ts +13 -0
- package/dist/durable-entry.js +33 -0
- package/dist/entries.d.ts +9 -0
- package/dist/entries.js +50 -0
- package/dist/herdr.d.ts +36 -0
- package/dist/herdr.js +106 -0
- package/dist/host.d.ts +18 -0
- package/dist/host.js +731 -0
- package/dist/picker.d.ts +22 -0
- package/dist/picker.js +70 -0
- package/dist/projects.d.ts +23 -0
- package/dist/projects.js +93 -0
- package/dist/protocol.d.ts +232 -0
- package/dist/protocol.js +54 -0
- package/dist/tasks.d.ts +31 -0
- package/dist/tasks.js +78 -0
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +18 -0
- package/dist/tool.d.ts +137 -0
- package/dist/tool.js +209 -0
- package/dist/trust.d.ts +5 -0
- package/dist/trust.js +15 -0
- package/dist/view-state.d.ts +16 -0
- package/dist/view-state.js +39 -0
- package/dist/viewer.d.ts +20 -0
- package/dist/viewer.js +307 -0
- package/package.json +48 -5
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/chord/context";
|
|
2
|
+
import type * as Ai from "@earendil-works/pi-ai";
|
|
3
|
+
import type { Static } from "@earendil-works/pi-ai";
|
|
4
|
+
import type * as Durable from "@earendil-works/pi-durable";
|
|
5
|
+
import type { Extension, ToolExecutionApi } from "@earendil-works/pi-durable";
|
|
6
|
+
import { type Replies, type Request } from "./protocol.ts";
|
|
7
|
+
export declare const TOOL_DESCRIPTION = "Spawn and manage subagents: separate agents with their own context, for second opinions, independent investigations and delegated work.\n- spawn: start a child with `prompts` and `files`. It uses your provider, model and thinking level unless you give others. Give it a short `name`. It prints `Spawned <name> (<handle>)`. The handle is the short hex ID in parentheses, not the name.\n- status, wait, send, rename, stop: act on the child `handle`. `wait` waits until the child answers its last message (up to `timeout` seconds, default 1800) and gives the answer. `stop` also stops the children the child spawned. `send` steers a busy child, or queues `message` with `followUp`.\n- list: your children.\nCalls in one message run one after another, in message order, so a `wait` after a `send` gives the answer to that message.\nChildren persist after a turn. Stop each child when you no longer need it.";
|
|
8
|
+
/** The parameters and the output schema of the tool, built with the `Type` of the caller's pi-ai. */
|
|
9
|
+
export declare function toolSchemas(Type: typeof Ai.Type): {
|
|
10
|
+
parameters: Ai.Type.TObject<{
|
|
11
|
+
action: Ai.Type.TUnion<[Ai.Type.TLiteral<"spawn">, Ai.Type.TLiteral<"send">, Ai.Type.TLiteral<"wait">, Ai.Type.TLiteral<"status">, Ai.Type.TLiteral<"rename">, Ai.Type.TLiteral<"list">, Ai.Type.TLiteral<"stop">]>;
|
|
12
|
+
name: Ai.Type.TOptional<Ai.Type.TString>;
|
|
13
|
+
provider: Ai.Type.TOptional<Ai.Type.TString>;
|
|
14
|
+
model: Ai.Type.TOptional<Ai.Type.TString>;
|
|
15
|
+
thinking: Ai.Type.TOptional<Ai.Type.TString>;
|
|
16
|
+
cwd: Ai.Type.TOptional<Ai.Type.TString>;
|
|
17
|
+
tools: Ai.Type.TOptional<Ai.Type.TArray<Ai.Type.TString>>;
|
|
18
|
+
prompts: Ai.Type.TOptional<Ai.Type.TArray<Ai.Type.TString>>;
|
|
19
|
+
files: Ai.Type.TOptional<Ai.Type.TArray<Ai.Type.TString>>;
|
|
20
|
+
handle: Ai.Type.TOptional<Ai.Type.TString>;
|
|
21
|
+
message: Ai.Type.TOptional<Ai.Type.TString>;
|
|
22
|
+
followUp: Ai.Type.TOptional<Ai.Type.TBoolean>;
|
|
23
|
+
timeout: Ai.Type.TOptional<Ai.Type.TInteger>;
|
|
24
|
+
key: Ai.Type.TOptional<Ai.Type.TString>;
|
|
25
|
+
}>;
|
|
26
|
+
output: Ai.Type.TUnion<[Ai.Type.TObject<{
|
|
27
|
+
action: Ai.Type.TLiteral<"spawn">;
|
|
28
|
+
value: Ai.Type.TObject<{
|
|
29
|
+
handle: Ai.Type.TString;
|
|
30
|
+
name: Ai.Type.TOptional<Ai.Type.TString>;
|
|
31
|
+
state: Ai.Type.TUnion<[Ai.Type.TLiteral<"busy">, Ai.Type.TLiteral<"idle">, Ai.Type.TLiteral<"error">]>;
|
|
32
|
+
provider: Ai.Type.TString;
|
|
33
|
+
model: Ai.Type.TString;
|
|
34
|
+
thinking: Ai.Type.TString;
|
|
35
|
+
cwd: Ai.Type.TString;
|
|
36
|
+
}>;
|
|
37
|
+
}>, Ai.Type.TObject<{
|
|
38
|
+
action: Ai.Type.TLiteral<"send">;
|
|
39
|
+
value: Ai.Type.TObject<{
|
|
40
|
+
verb: Ai.Type.TString;
|
|
41
|
+
child: Ai.Type.TObject<{
|
|
42
|
+
handle: Ai.Type.TString;
|
|
43
|
+
name: Ai.Type.TOptional<Ai.Type.TString>;
|
|
44
|
+
state: Ai.Type.TUnion<[Ai.Type.TLiteral<"busy">, Ai.Type.TLiteral<"idle">, Ai.Type.TLiteral<"error">]>;
|
|
45
|
+
provider: Ai.Type.TString;
|
|
46
|
+
model: Ai.Type.TString;
|
|
47
|
+
thinking: Ai.Type.TString;
|
|
48
|
+
cwd: Ai.Type.TString;
|
|
49
|
+
}>;
|
|
50
|
+
}>;
|
|
51
|
+
}>, Ai.Type.TObject<{
|
|
52
|
+
action: Ai.Type.TLiteral<"wait">;
|
|
53
|
+
value: Ai.Type.TObject<{
|
|
54
|
+
answer: Ai.Type.TString;
|
|
55
|
+
}>;
|
|
56
|
+
}>, Ai.Type.TObject<{
|
|
57
|
+
action: Ai.Type.TLiteral<"status">;
|
|
58
|
+
value: Ai.Type.TObject<{
|
|
59
|
+
handle: Ai.Type.TString;
|
|
60
|
+
name: Ai.Type.TOptional<Ai.Type.TString>;
|
|
61
|
+
state: Ai.Type.TUnion<[Ai.Type.TLiteral<"busy">, Ai.Type.TLiteral<"idle">, Ai.Type.TLiteral<"error">]>;
|
|
62
|
+
provider: Ai.Type.TString;
|
|
63
|
+
model: Ai.Type.TString;
|
|
64
|
+
thinking: Ai.Type.TString;
|
|
65
|
+
cwd: Ai.Type.TString;
|
|
66
|
+
}>;
|
|
67
|
+
}>, Ai.Type.TObject<{
|
|
68
|
+
action: Ai.Type.TLiteral<"rename">;
|
|
69
|
+
value: Ai.Type.TObject<{
|
|
70
|
+
handle: Ai.Type.TString;
|
|
71
|
+
name: Ai.Type.TOptional<Ai.Type.TString>;
|
|
72
|
+
state: Ai.Type.TUnion<[Ai.Type.TLiteral<"busy">, Ai.Type.TLiteral<"idle">, Ai.Type.TLiteral<"error">]>;
|
|
73
|
+
provider: Ai.Type.TString;
|
|
74
|
+
model: Ai.Type.TString;
|
|
75
|
+
thinking: Ai.Type.TString;
|
|
76
|
+
cwd: Ai.Type.TString;
|
|
77
|
+
}>;
|
|
78
|
+
}>, Ai.Type.TObject<{
|
|
79
|
+
action: Ai.Type.TLiteral<"list">;
|
|
80
|
+
value: Ai.Type.TArray<Ai.Type.TObject<{
|
|
81
|
+
handle: Ai.Type.TString;
|
|
82
|
+
name: Ai.Type.TOptional<Ai.Type.TString>;
|
|
83
|
+
state: Ai.Type.TUnion<[Ai.Type.TLiteral<"busy">, Ai.Type.TLiteral<"idle">, Ai.Type.TLiteral<"error">]>;
|
|
84
|
+
provider: Ai.Type.TString;
|
|
85
|
+
model: Ai.Type.TString;
|
|
86
|
+
thinking: Ai.Type.TString;
|
|
87
|
+
cwd: Ai.Type.TString;
|
|
88
|
+
}>>;
|
|
89
|
+
}>, Ai.Type.TObject<{
|
|
90
|
+
action: Ai.Type.TLiteral<"stop">;
|
|
91
|
+
value: Ai.Type.TObject<{
|
|
92
|
+
handle: Ai.Type.TString;
|
|
93
|
+
name: Ai.Type.TOptional<Ai.Type.TString>;
|
|
94
|
+
}>;
|
|
95
|
+
}>]>;
|
|
96
|
+
};
|
|
97
|
+
type Schemas = ReturnType<typeof toolSchemas>;
|
|
98
|
+
export type ToolArgs = Static<Schemas["parameters"]>;
|
|
99
|
+
/** The result of a call: the text for the model, the same as the CLI output, and the host's reply for programs. */
|
|
100
|
+
export type ToolResult = {
|
|
101
|
+
readonly text: string;
|
|
102
|
+
readonly data: Static<Schemas["output"]>;
|
|
103
|
+
};
|
|
104
|
+
/** Sends one request to the host and gives its reply. */
|
|
105
|
+
export type Send = <T extends Request["type"]>(request: Extract<Request, {
|
|
106
|
+
type: T;
|
|
107
|
+
}>, signal: AbortSignal) => Promise<Replies[T]>;
|
|
108
|
+
/** Sends through the host's socket, and starts the host when none runs. */
|
|
109
|
+
export declare const sendToHost: Send;
|
|
110
|
+
/** What a tool call gets from its adapter. */
|
|
111
|
+
export type ToolScope = {
|
|
112
|
+
/** The caller that owns the children. */
|
|
113
|
+
readonly caller: string;
|
|
114
|
+
/** The identity of the call. Without `key`, it is the key of a spawn or send. */
|
|
115
|
+
readonly callKey: string;
|
|
116
|
+
readonly provider: string | undefined;
|
|
117
|
+
readonly model: string | undefined;
|
|
118
|
+
readonly thinking: string;
|
|
119
|
+
/** Relative `cwd` and `files` paths resolve against it. A caller without one cannot spawn. */
|
|
120
|
+
readonly cwd: string | undefined;
|
|
121
|
+
readonly send: Send;
|
|
122
|
+
readonly signal: AbortSignal | undefined;
|
|
123
|
+
};
|
|
124
|
+
/** Runs one call of the tool. */
|
|
125
|
+
export declare function runTool(args: ToolArgs, scope: ToolScope): Promise<ToolResult>;
|
|
126
|
+
/** How a host reaches the harness host. */
|
|
127
|
+
export type DurableScope = {
|
|
128
|
+
/** The caller that owns the children of the calling conversation. */
|
|
129
|
+
caller(api: ToolExecutionApi, context: Context): Promise<string>;
|
|
130
|
+
readonly send: Send;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* The pi-durable extension with the `subagent` tool. A call that runs again after a restart has the same task, so its
|
|
134
|
+
* spawn or send has the same request ID and does not repeat.
|
|
135
|
+
*/
|
|
136
|
+
export declare function subagentTool(durable: typeof Durable, ai: typeof Ai, scope: DurableScope): Extension;
|
|
137
|
+
export {};
|
package/dist/tool.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { request } from "./client.js";
|
|
4
|
+
import { childLine, isValidRunName, runDisplayName } from "./protocol.js";
|
|
5
|
+
const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
6
|
+
/** The signal of a call that its adapter cannot abort. */
|
|
7
|
+
const NO_ABORT = new AbortController().signal;
|
|
8
|
+
export const TOOL_DESCRIPTION = `Spawn and manage subagents: separate agents with their own context, for second opinions, independent investigations and delegated work.
|
|
9
|
+
- spawn: start a child with \`prompts\` and \`files\`. It uses your provider, model and thinking level unless you give others. Give it a short \`name\`. It prints \`Spawned <name> (<handle>)\`. The handle is the short hex ID in parentheses, not the name.
|
|
10
|
+
- status, wait, send, rename, stop: act on the child \`handle\`. \`wait\` waits until the child answers its last message (up to \`timeout\` seconds, default 1800) and gives the answer. \`stop\` also stops the children the child spawned. \`send\` steers a busy child, or queues \`message\` with \`followUp\`.
|
|
11
|
+
- list: your children.
|
|
12
|
+
Calls in one message run one after another, in message order, so a \`wait\` after a \`send\` gives the answer to that message.
|
|
13
|
+
Children persist after a turn. Stop each child when you no longer need it.`;
|
|
14
|
+
/** The parameters and the output schema of the tool, built with the `Type` of the caller's pi-ai. */
|
|
15
|
+
export function toolSchemas(Type) {
|
|
16
|
+
const text = (description) => Type.Optional(Type.String({ description }));
|
|
17
|
+
const texts = (description) => Type.Optional(Type.Array(Type.String(), { description }));
|
|
18
|
+
const parameters = Type.Object({
|
|
19
|
+
action: Type.Union([
|
|
20
|
+
Type.Literal("spawn"),
|
|
21
|
+
Type.Literal("send"),
|
|
22
|
+
Type.Literal("wait"),
|
|
23
|
+
Type.Literal("status"),
|
|
24
|
+
Type.Literal("rename"),
|
|
25
|
+
Type.Literal("list"),
|
|
26
|
+
Type.Literal("stop"),
|
|
27
|
+
], { description: "What to do" }),
|
|
28
|
+
name: text("spawn, rename: the child's name, 1 to 64 characters"),
|
|
29
|
+
provider: text("spawn: the provider; default yours"),
|
|
30
|
+
model: text("spawn: the model; default yours"),
|
|
31
|
+
thinking: text("spawn: off, minimal, low, medium, high, xhigh or max; default yours"),
|
|
32
|
+
cwd: text("spawn: the child's working directory; default yours"),
|
|
33
|
+
tools: texts("spawn: the only tools the child gets; default read, grep, find, ls, write, edit, bash and the package tools"),
|
|
34
|
+
prompts: texts("spawn: the task, in parts"),
|
|
35
|
+
files: texts("spawn: files whose text goes before the task"),
|
|
36
|
+
handle: text("send, wait, status, rename, stop: the hex ID that spawn prints in parentheses, not the name"),
|
|
37
|
+
message: text("send: the message"),
|
|
38
|
+
followUp: Type.Optional(Type.Boolean({ description: "send: queue after the current answer, not a steer" })),
|
|
39
|
+
timeout: Type.Optional(Type.Integer({ minimum: 1, description: "wait: seconds; default 1800" })),
|
|
40
|
+
key: text("spawn, send: a key that makes a repeated call do nothing; default the call"),
|
|
41
|
+
});
|
|
42
|
+
const child = Type.Object({
|
|
43
|
+
handle: Type.String(),
|
|
44
|
+
name: Type.Optional(Type.String()),
|
|
45
|
+
state: Type.Union([Type.Literal("busy"), Type.Literal("idle"), Type.Literal("error")]),
|
|
46
|
+
provider: Type.String(),
|
|
47
|
+
model: Type.String(),
|
|
48
|
+
thinking: Type.String(),
|
|
49
|
+
cwd: Type.String(),
|
|
50
|
+
});
|
|
51
|
+
const result = (action, value) => Type.Object({ action: Type.Literal(action), value });
|
|
52
|
+
const output = Type.Union([
|
|
53
|
+
result("spawn", child),
|
|
54
|
+
result("send", Type.Object({ verb: Type.String(), child })),
|
|
55
|
+
result("wait", Type.Object({ answer: Type.String() })),
|
|
56
|
+
result("status", child),
|
|
57
|
+
result("rename", child),
|
|
58
|
+
result("list", Type.Array(child)),
|
|
59
|
+
result("stop", Type.Object({ handle: Type.String(), name: Type.Optional(Type.String()) })),
|
|
60
|
+
]);
|
|
61
|
+
return { parameters, output };
|
|
62
|
+
}
|
|
63
|
+
/** Sends through the host's socket, and starts the host when none runs. */
|
|
64
|
+
export const sendToHost = (message, signal) => request(message, { start: true, signal });
|
|
65
|
+
/** Runs one call of the tool. */
|
|
66
|
+
export async function runTool(args, scope) {
|
|
67
|
+
const { action } = args;
|
|
68
|
+
const { send, caller } = scope;
|
|
69
|
+
const signal = scope.signal ?? NO_ABORT;
|
|
70
|
+
const requestId = `${caller}:${args.key ?? scope.callKey}`;
|
|
71
|
+
const handle = () => checkHandle(required(args.handle, "handle", action));
|
|
72
|
+
switch (action) {
|
|
73
|
+
case "spawn": {
|
|
74
|
+
const child = await send(spawnRequest(args, scope, requestId), signal);
|
|
75
|
+
return { text: `Spawned ${runDisplayName(child)}\nState: busy\n`, data: { action, value: child } };
|
|
76
|
+
}
|
|
77
|
+
case "send": {
|
|
78
|
+
const message = required(args.message?.trim(), "message", action);
|
|
79
|
+
const followUp = args.followUp === true;
|
|
80
|
+
const sent = await send({ type: "send", requestId, caller, handle: handle(), message, followUp }, signal);
|
|
81
|
+
return { text: `${sent.verb} ${runDisplayName(sent.child)}\n`, data: { action, value: sent } };
|
|
82
|
+
}
|
|
83
|
+
case "wait": {
|
|
84
|
+
const target = handle();
|
|
85
|
+
const waited = await send({ type: "wait", handle: target, timeout: args.timeout ?? 1800 }, signal);
|
|
86
|
+
return { text: `${target} finished\n\n${waited.answer}\n`, data: { action, value: waited } };
|
|
87
|
+
}
|
|
88
|
+
case "status": {
|
|
89
|
+
const child = await send({ type: "status", handle: handle() }, signal);
|
|
90
|
+
const text = `${runDisplayName(child)}: ${child.state} (${child.provider}/${child.model}, ${child.thinking})\n`;
|
|
91
|
+
return { text, data: { action, value: child } };
|
|
92
|
+
}
|
|
93
|
+
case "rename": {
|
|
94
|
+
const name = normalizeRunName(required(args.name, "name", action));
|
|
95
|
+
const child = await send({ type: "rename", caller, handle: handle(), name }, signal);
|
|
96
|
+
return { text: `Renamed ${runDisplayName(child)}\n`, data: { action, value: child } };
|
|
97
|
+
}
|
|
98
|
+
case "list": {
|
|
99
|
+
const children = await send({ type: "list", caller }, signal);
|
|
100
|
+
const text = children.length === 0 ? "No subagents\n" : children.map((child) => `${childLine(child)}\n`).join("");
|
|
101
|
+
return { text, data: { action, value: [...children] } };
|
|
102
|
+
}
|
|
103
|
+
case "stop": {
|
|
104
|
+
const stopped = await send({ type: "stop", caller, handle: handle() }, signal);
|
|
105
|
+
return { text: `Stopped ${runDisplayName(stopped)}\n`, data: { action, value: stopped } };
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
throw new Error(`Unknown action: ${String(action)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** The spawn request of `args`, with the defaults of `scope`. */
|
|
112
|
+
function spawnRequest(args, scope, requestId) {
|
|
113
|
+
const provider = args.provider ?? scope.provider;
|
|
114
|
+
const model = args.model ?? scope.model;
|
|
115
|
+
const thinking = args.thinking ?? scope.thinking;
|
|
116
|
+
if (!provider)
|
|
117
|
+
throw new Error("No provider specified and the caller has no model");
|
|
118
|
+
if (!model)
|
|
119
|
+
throw new Error("No model specified and the caller has no model");
|
|
120
|
+
if (!THINKING_LEVELS.has(thinking))
|
|
121
|
+
throw new Error(`Invalid thinking level: ${thinking}`);
|
|
122
|
+
const base = scope.cwd;
|
|
123
|
+
if (base === undefined)
|
|
124
|
+
throw new Error("spawn requires a caller with a working directory");
|
|
125
|
+
const name = args.name === undefined ? undefined : normalizeRunName(args.name);
|
|
126
|
+
const cwd = resolve(base, args.cwd ?? ".");
|
|
127
|
+
if (!existsSync(cwd))
|
|
128
|
+
throw new Error(`Working directory not found: ${cwd}`);
|
|
129
|
+
const tools = args.tools?.map((tool) => tool.trim()).filter((tool) => tool);
|
|
130
|
+
if (tools !== undefined && tools.length === 0)
|
|
131
|
+
throw new Error("tools requires at least one tool name");
|
|
132
|
+
const files = (args.files ?? []).map((file) => resolve(base, file));
|
|
133
|
+
for (const file of files)
|
|
134
|
+
if (!existsSync(file))
|
|
135
|
+
throw new Error(`File not found: ${file}`);
|
|
136
|
+
const prompts = args.prompts ?? [];
|
|
137
|
+
if (prompts.length === 0 && files.length === 0)
|
|
138
|
+
throw new Error("spawn requires at least one prompt or file");
|
|
139
|
+
// The first message: the text of each file, as Pi gives a file argument to the model, then `Task:` and the prompts.
|
|
140
|
+
let prompt = files.map(fileText).join("");
|
|
141
|
+
if (prompts.length > 0)
|
|
142
|
+
prompt += `Task:\n${prompts.join("\n\n")}`;
|
|
143
|
+
return {
|
|
144
|
+
type: "spawn",
|
|
145
|
+
requestId,
|
|
146
|
+
caller: scope.caller,
|
|
147
|
+
...(name === undefined ? {} : { name }),
|
|
148
|
+
provider,
|
|
149
|
+
model,
|
|
150
|
+
...(args.thinking === undefined ? {} : { thinking: args.thinking }),
|
|
151
|
+
callerThinking: scope.thinking,
|
|
152
|
+
cwd,
|
|
153
|
+
...(tools === undefined ? {} : { tools }),
|
|
154
|
+
prompt,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/** The text of `file` in a `<file>` block. An empty file gives nothing. */
|
|
158
|
+
function fileText(file) {
|
|
159
|
+
if (statSync(file).size === 0)
|
|
160
|
+
return "";
|
|
161
|
+
return `<file name="${file}">\n${readFileSync(file, "utf8").replace(/^\uFEFF/, "")}\n</file>\n`;
|
|
162
|
+
}
|
|
163
|
+
function required(value, argument, action) {
|
|
164
|
+
if (value === undefined || value === "")
|
|
165
|
+
throw new Error(`${action} requires ${argument}`);
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
/** `value` without surrounding spaces. It must be a valid run name. */
|
|
169
|
+
function normalizeRunName(value) {
|
|
170
|
+
const name = value.trim();
|
|
171
|
+
if (!isValidRunName(name))
|
|
172
|
+
throw new Error("Subagent name must be a single line of 1 to 64 characters");
|
|
173
|
+
return name;
|
|
174
|
+
}
|
|
175
|
+
function checkHandle(handle) {
|
|
176
|
+
if (!/^[a-z0-9]+$/.test(handle))
|
|
177
|
+
throw new Error(`Invalid subagent handle: ${handle}`);
|
|
178
|
+
return handle;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The pi-durable extension with the `subagent` tool. A call that runs again after a restart has the same task, so its
|
|
182
|
+
* spawn or send has the same request ID and does not repeat.
|
|
183
|
+
*/
|
|
184
|
+
export function subagentTool(durable, ai, scope) {
|
|
185
|
+
const { parameters, output } = toolSchemas(ai.Type);
|
|
186
|
+
const tool = durable.defineTool({
|
|
187
|
+
name: "subagent",
|
|
188
|
+
description: TOOL_DESCRIPTION,
|
|
189
|
+
parameters,
|
|
190
|
+
structuredOutputSchema: output,
|
|
191
|
+
replay: "safe",
|
|
192
|
+
executionMode: "sequential",
|
|
193
|
+
async execute(args, api, context) {
|
|
194
|
+
const agent = await api.agent(context);
|
|
195
|
+
const { text, data } = await runTool(args, {
|
|
196
|
+
caller: await scope.caller(api, context),
|
|
197
|
+
callKey: String(api.taskId),
|
|
198
|
+
provider: agent.model?.provider,
|
|
199
|
+
model: agent.model?.modelId,
|
|
200
|
+
thinking: agent.thinkingLevel,
|
|
201
|
+
cwd: agent.cwd,
|
|
202
|
+
send: scope.send,
|
|
203
|
+
signal: context.abortSignal,
|
|
204
|
+
});
|
|
205
|
+
return { output: [{ type: "text", text }], structuredOutput: data };
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return durable.defineExtension({ name: "pi-subagent", tools: [tool] });
|
|
209
|
+
}
|
package/dist/trust.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether Pi trusts the project in `cwd` with no one to ask: a directory without project resources is trusted, then a
|
|
3
|
+
* decision saved with `/trust` counts, then the `defaultProjectTrust` setting, where `ask` means not trusted.
|
|
4
|
+
*/
|
|
5
|
+
export declare function savedTrust(cwd: string): boolean;
|
package/dist/trust.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Pi's project trust, apart from the rest of the context, so that a Pi extension can read it without pi-durable. */
|
|
2
|
+
import { getAgentDir, hasTrustRequiringProjectResources, ProjectTrustStore, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
/**
|
|
4
|
+
* Whether Pi trusts the project in `cwd` with no one to ask: a directory without project resources is trusted, then a
|
|
5
|
+
* decision saved with `/trust` counts, then the `defaultProjectTrust` setting, where `ask` means not trusted.
|
|
6
|
+
*/
|
|
7
|
+
export function savedTrust(cwd) {
|
|
8
|
+
if (!hasTrustRequiringProjectResources(cwd))
|
|
9
|
+
return true;
|
|
10
|
+
const agentDir = getAgentDir();
|
|
11
|
+
const decision = new ProjectTrustStore(agentDir).get(cwd);
|
|
12
|
+
if (decision !== null)
|
|
13
|
+
return decision;
|
|
14
|
+
return SettingsManager.create(cwd, agentDir, { projectTrusted: false }).getDefaultProjectTrust() === "always";
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the viewer reads from the documents of a durable conversation view: its live state, its inbox and the texts of
|
|
3
|
+
* its user messages.
|
|
4
|
+
*/
|
|
5
|
+
import type { UserMessage } from "@earendil-works/pi-ai";
|
|
6
|
+
import { type ConversationView, type InboxState, type LiveState } from "@earendil-works/pi-durable";
|
|
7
|
+
export declare function liveOf(view: ConversationView): LiveState;
|
|
8
|
+
export declare function inboxOf(view: ConversationView): InboxState;
|
|
9
|
+
export declare function userText(content: UserMessage["content"]): string;
|
|
10
|
+
/** The queued steers and follow-ups, in inbox order. */
|
|
11
|
+
export declare function queued(inbox: InboxState): {
|
|
12
|
+
mode: "steer" | "followUp";
|
|
13
|
+
text: string;
|
|
14
|
+
}[];
|
|
15
|
+
/** What the conversation does now, as Pi's working indicator says it; empty when it does nothing. */
|
|
16
|
+
export declare function statusText(live: LiveState): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { InboxDoc, LiveDoc } from "@earendil-works/pi-durable";
|
|
2
|
+
export function liveOf(view) {
|
|
3
|
+
return view.docs[LiveDoc.definition.kind] ?? {};
|
|
4
|
+
}
|
|
5
|
+
export function inboxOf(view) {
|
|
6
|
+
return view.docs[InboxDoc.definition.kind] ?? { items: [] };
|
|
7
|
+
}
|
|
8
|
+
export function userText(content) {
|
|
9
|
+
if (typeof content === "string")
|
|
10
|
+
return content;
|
|
11
|
+
return content
|
|
12
|
+
.filter((block) => block.type === "text")
|
|
13
|
+
.map((block) => block.text)
|
|
14
|
+
.join("");
|
|
15
|
+
}
|
|
16
|
+
/** The queued steers and follow-ups, in inbox order. */
|
|
17
|
+
export function queued(inbox) {
|
|
18
|
+
return inbox.items.flatMap((item) => item.mode === "write" ? [] : [{ mode: item.mode, text: userText(item.content) }]);
|
|
19
|
+
}
|
|
20
|
+
/** What the conversation does now, as Pi's working indicator says it; empty when it does nothing. */
|
|
21
|
+
export function statusText(live) {
|
|
22
|
+
const generation = live.generation;
|
|
23
|
+
const compaction = live.compactions?.[0];
|
|
24
|
+
const runningTool = live.tools?.find((slot) => slot.status === "running");
|
|
25
|
+
if (generation?.retry !== undefined)
|
|
26
|
+
return `Retrying (attempt ${generation.attempt + 1}): ${generation.retry.error}`;
|
|
27
|
+
if (generation?.deferred !== undefined)
|
|
28
|
+
return "Waiting for deferred response...";
|
|
29
|
+
if (compaction !== undefined) {
|
|
30
|
+
return compaction.retry
|
|
31
|
+
? `Retrying ${compaction.reason} compaction (attempt ${compaction.attempt + 1})...`
|
|
32
|
+
: `Compacting (${compaction.reason})...`;
|
|
33
|
+
}
|
|
34
|
+
if (runningTool !== undefined)
|
|
35
|
+
return `Running ${runningTool.name}... (esc to interrupt)`;
|
|
36
|
+
if (live.run !== undefined)
|
|
37
|
+
return "Working... (esc to interrupt)";
|
|
38
|
+
return "";
|
|
39
|
+
}
|
package/dist/viewer.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Terminal } from "@earendil-works/pi-tui";
|
|
2
|
+
declare module "@earendil-works/pi-tui" {
|
|
3
|
+
interface Keybindings {
|
|
4
|
+
"app.scroll.lineUp": true;
|
|
5
|
+
"app.scroll.lineDown": true;
|
|
6
|
+
"app.scroll.pageUp": true;
|
|
7
|
+
"app.scroll.pageDown": true;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export type AttachOptions = {
|
|
11
|
+
readonly terminal: Terminal;
|
|
12
|
+
readonly env: NodeJS.ProcessEnv;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Shows the conversation of the child `handle`, or of the project agent for `agent`, in `terminal` and sends
|
|
16
|
+
* what the user types to it. It starts the host when none runs, and rejects before the terminal starts when the handle
|
|
17
|
+
* is unknown. It resolves with the exit code: 0 after Ctrl+C or when the child is stopped, 1 when the host closes the
|
|
18
|
+
* connection. The child keeps running.
|
|
19
|
+
*/
|
|
20
|
+
export declare function attach(handle: string, options: AttachOptions): Promise<number>;
|