pi-famulus 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +7 -7
- package/src/child-mode.ts +214 -0
- package/src/comms/comms.ts +18 -13
- package/src/comms/registry-host.ts +10 -0
- package/src/comms/tools.ts +9 -2
- package/src/comms/types.ts +9 -3
- package/src/config.ts +16 -0
- package/src/index.ts +128 -30
- package/src/manager-client.ts +145 -5
- package/src/pi-command.ts +123 -0
- package/src/prompts.generated.ts +7 -8
- package/src/subagent/hosted-session.ts +268 -0
- package/src/subagent/overrun.ts +11 -0
- package/src/subagent/pi-runtime.ts +4 -0
- package/src/subagent/runner.ts +19 -9
- package/src/subagent/tool.ts +70 -9
- package/src/subagent/types.ts +2 -0
- package/src/tui/attach-command.ts +164 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/attach <child>`: talk to a hosted subagent through the daemon, in the pi TUI.
|
|
3
|
+
* Lines are the child's transcript; Enter sends a prompt (steered while it runs).
|
|
4
|
+
*/
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { ManagerClient } from "../manager-client";
|
|
7
|
+
import type { SubagentRegistry } from "../subagent/registry";
|
|
8
|
+
import { fitLines, loadPiTui } from "./pi-tui-load";
|
|
9
|
+
|
|
10
|
+
export interface AttachCommandDeps {
|
|
11
|
+
getClient: () => ManagerClient | null;
|
|
12
|
+
getRegistry: () => SubagentRegistry | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function registerAttachCommand(pi: ExtensionAPI, deps: AttachCommandDeps): void {
|
|
16
|
+
pi.registerCommand("attach", {
|
|
17
|
+
description: "Talk to a hosted subagent (child id)",
|
|
18
|
+
getArgumentCompletions: () => {
|
|
19
|
+
const children = deps.getRegistry()?.list().flatMap((run) => run.children.map((child) => child.childId)) ?? [];
|
|
20
|
+
return children.map((id) => ({ value: id, label: id }));
|
|
21
|
+
},
|
|
22
|
+
handler: async (args, ctx) => {
|
|
23
|
+
const childId = args.trim();
|
|
24
|
+
if (!childId) {
|
|
25
|
+
ctx.ui.notify("Usage: /attach <child id>", "warning");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const client = deps.getClient();
|
|
29
|
+
if (!client?.isAvailable() || client.protocolLevel() < 6) {
|
|
30
|
+
ctx.ui.notify("Hosted subagents need a connected pi-famulus daemon (protocol 6).", "error");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!ctx.hasUI || ctx.mode !== "tui") {
|
|
34
|
+
ctx.ui.notify(`Attach from a terminal with: pi-famulus attach ${childId}`, "info");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await openAttachView(ctx, client, childId);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface EntryMessage {
|
|
43
|
+
role?: string;
|
|
44
|
+
content?: string | { text?: string }[];
|
|
45
|
+
message?: EntryMessage;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function textOf(message: EntryMessage | undefined): string {
|
|
49
|
+
const content = message?.content;
|
|
50
|
+
if (typeof content === "string") return content;
|
|
51
|
+
if (!Array.isArray(content)) return "";
|
|
52
|
+
return content.map((part) => (typeof part === "string" ? part : part?.text ?? "")).join("");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function linesFromEntries(entries: unknown): string[] {
|
|
56
|
+
if (!Array.isArray(entries)) return [];
|
|
57
|
+
const out: string[] = [];
|
|
58
|
+
for (const entry of entries as EntryMessage[]) {
|
|
59
|
+
const message = entry.message ?? entry;
|
|
60
|
+
const text = textOf(message);
|
|
61
|
+
if (!text) continue;
|
|
62
|
+
if (message.role === "user") out.push(`you: ${text}`);
|
|
63
|
+
else if (message.role === "assistant") out.push(text);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Live transcript for one hosted child. Closing does not respawn a parked child. */
|
|
69
|
+
export async function openAttachView(ctx: ExtensionContext, client: ManagerClient, childId: string): Promise<void> {
|
|
70
|
+
const piTui = loadPiTui();
|
|
71
|
+
if (!piTui) {
|
|
72
|
+
ctx.ui.notify(`pi-famulus attach ${childId}`, "info");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await client.agentWatch(childId).catch((err: unknown) => {
|
|
76
|
+
throw new Error(err instanceof Error ? err.message : String(err));
|
|
77
|
+
});
|
|
78
|
+
const seeded = await client.agentInput(childId, "get_entries", "", "parent").catch(() => null);
|
|
79
|
+
const state = await client.agentInput(childId, "get_state", "", "parent").catch(() => null);
|
|
80
|
+
const lines: string[] = [`attached to ${childId}`, ""];
|
|
81
|
+
lines.push(...linesFromEntries(seeded?.entries));
|
|
82
|
+
let input = "";
|
|
83
|
+
let activity = state?.isStreaming === true ? "running" : "idle";
|
|
84
|
+
let status = activity;
|
|
85
|
+
let requestRender: (() => void) | null = null;
|
|
86
|
+
const unsub = client.onEvent((event) => {
|
|
87
|
+
const hosted = event as {
|
|
88
|
+
event?: string;
|
|
89
|
+
child_id?: string;
|
|
90
|
+
record?: { type?: string; message?: { role?: string; content?: { text?: string }[] } };
|
|
91
|
+
activity?: string;
|
|
92
|
+
residency?: string;
|
|
93
|
+
driver?: string;
|
|
94
|
+
};
|
|
95
|
+
if (hosted.child_id !== childId) return;
|
|
96
|
+
if (hosted.event === "agent_state") {
|
|
97
|
+
activity = hosted.activity ?? activity;
|
|
98
|
+
status = `${hosted.activity ?? "?"} · ${hosted.residency ?? "?"} · ${hosted.driver ?? "parent"}`;
|
|
99
|
+
requestRender?.();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (hosted.event !== "agent_rpc" || !hosted.record) return;
|
|
103
|
+
const record = hosted.record;
|
|
104
|
+
if (record.type === "message_end" && record.message?.role === "assistant") {
|
|
105
|
+
const text = textOf(record.message);
|
|
106
|
+
if (text) lines.push(text);
|
|
107
|
+
} else if (record.type === "agent_start") {
|
|
108
|
+
activity = "running";
|
|
109
|
+
lines.push("…");
|
|
110
|
+
}
|
|
111
|
+
requestRender?.();
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
await ctx.ui.custom<void>((tui, _theme, _kb, done) => {
|
|
115
|
+
requestRender = () => tui.requestRender();
|
|
116
|
+
return {
|
|
117
|
+
render(width: number) {
|
|
118
|
+
const limit = Math.max(1, width);
|
|
119
|
+
const rows = Math.max(4, (tui.terminal?.rows ?? 24) - 6);
|
|
120
|
+
const body = lines.flatMap((line) => fitLines(line, limit)).slice(-rows);
|
|
121
|
+
return [
|
|
122
|
+
`◆ ${childId} ${status}`,
|
|
123
|
+
...body,
|
|
124
|
+
"",
|
|
125
|
+
`› ${input}`,
|
|
126
|
+
"enter send esc close",
|
|
127
|
+
];
|
|
128
|
+
},
|
|
129
|
+
invalidate() {},
|
|
130
|
+
handleInput(data: string) {
|
|
131
|
+
if (data === "\u001b") {
|
|
132
|
+
done();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (data === "\r" || data === "\n") {
|
|
136
|
+
const text = input;
|
|
137
|
+
input = "";
|
|
138
|
+
if (!text.trim()) return;
|
|
139
|
+
lines.push(`you: ${text}`);
|
|
140
|
+
const op = activity === "running" ? "prompt_steer" : "prompt";
|
|
141
|
+
void client.agentInput(childId, op, text, "user").catch((err: unknown) => {
|
|
142
|
+
lines.push(err instanceof Error ? err.message : String(err));
|
|
143
|
+
requestRender?.();
|
|
144
|
+
});
|
|
145
|
+
tui.requestRender();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (data === "\u007f" || data === "\b") {
|
|
149
|
+
input = input.slice(0, -1);
|
|
150
|
+
tui.requestRender();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (data.length === 1 && data >= " ") {
|
|
154
|
+
input += data;
|
|
155
|
+
tui.requestRender();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
} finally {
|
|
161
|
+
unsub();
|
|
162
|
+
await client.agentUnwatch(childId).catch(() => {});
|
|
163
|
+
}
|
|
164
|
+
}
|