@ryan_nookpi/pi-extension-subagent 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.
@@ -0,0 +1,223 @@
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: SDK stream payloads and tool bridge results are dynamic runtime data. */
2
+ import { AbortError, query } from "@anthropic-ai/claude-agent-sdk";
3
+ import type { AgentConfig } from "./agents.js";
4
+ import { mapThinkingToClaudeEffort } from "./claude-args.js";
5
+ import { createClaudeSdkStreamAdapter } from "./claude-sdk-stream-adapter.js";
6
+ import { createSidecarWriter } from "./claude-sidecar-writer.js";
7
+ import { getFinalOutput } from "./runner.js";
8
+ import type { OnUpdateCallback, SingleResult, SubagentDetails } from "./types.js";
9
+ import { mapPiToolsToClaude, validateClaudeRuntimeModel } from "./utils/agent-utils.js";
10
+
11
+ function appendStderrDiagnostic(stderr: string, message: string): string {
12
+ const line = `[runner] ${message}`;
13
+ return stderr ? `${stderr.trimEnd()}\n${line}\n` : `${line}\n`;
14
+ }
15
+
16
+ function extractToolNamesFromPrecedingAssistant(messages: SingleResult["messages"]): string[] {
17
+ for (let index = messages.length - 1; index >= 0; index--) {
18
+ const message = messages[index];
19
+ if (message.role !== "assistant") continue;
20
+ const names: string[] = [];
21
+ for (const part of message.content as any[]) {
22
+ if (part.type === "toolCall" && typeof part.name === "string") names.push(part.name);
23
+ }
24
+ return names;
25
+ }
26
+ return [];
27
+ }
28
+
29
+ function normalizeTaskForSubagentPrompt(task: string): string {
30
+ if (task.startsWith("/")) return ` \\${task}`;
31
+ return task;
32
+ }
33
+
34
+ function resolveClaudeSdkBuiltInTools(requestedTools?: string[]): string[] | undefined {
35
+ if (!requestedTools || requestedTools.length === 0) return undefined;
36
+ return mapPiToolsToClaude(requestedTools);
37
+ }
38
+
39
+ export async function runClaudeAgentViaSdk(
40
+ defaultCwd: string,
41
+ agent: AgentConfig,
42
+ task: string,
43
+ step: number | undefined,
44
+ signal: AbortSignal | undefined,
45
+ onUpdate: OnUpdateCallback | undefined,
46
+ makeDetails: (results: SingleResult[]) => SubagentDetails,
47
+ resumeSessionId?: string,
48
+ sidecarSessionFile?: string,
49
+ ): Promise<SingleResult> {
50
+ try {
51
+ validateClaudeRuntimeModel(agent.model);
52
+ } catch (error: any) {
53
+ return {
54
+ agent: agent.name,
55
+ agentSource: agent.source,
56
+ task,
57
+ exitCode: 1,
58
+ messages: [],
59
+ stderr: error.message,
60
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
61
+ runtime: "claude",
62
+ step,
63
+ };
64
+ }
65
+
66
+ const adapter = createClaudeSdkStreamAdapter();
67
+ const sidecar = sidecarSessionFile ? createSidecarWriter(sidecarSessionFile) : null;
68
+ const abortController = new AbortController();
69
+ let stderrBuf = "";
70
+ let completionMarkerWritten = false;
71
+ let sidecarInitialUserWritten = false;
72
+ let externalAbortRequested = false;
73
+ let sdkFailed = false;
74
+
75
+ let enabledTools: string[] | undefined;
76
+ try {
77
+ enabledTools = resolveClaudeSdkBuiltInTools(agent.tools);
78
+ } catch (error: any) {
79
+ return {
80
+ agent: agent.name,
81
+ agentSource: agent.source,
82
+ task,
83
+ exitCode: 1,
84
+ messages: [],
85
+ stderr: error.message,
86
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
87
+ runtime: "claude",
88
+ step,
89
+ };
90
+ }
91
+
92
+ const model = agent.model?.replace(/^anthropic\//, "");
93
+ const effort = agent.thinking ? mapThinkingToClaudeEffort(agent.thinking) : undefined;
94
+
95
+ const emitUpdate = () => {
96
+ if (!onUpdate) return;
97
+ const state = adapter.getState();
98
+ const text = state.liveText ?? getFinalOutput(state.messages) ?? "(running...)";
99
+ const partial = adapter.toSingleResult(agent.name, agent.source, task, 0, step, stderrBuf);
100
+ partial.liveText = state.liveText;
101
+ partial.liveThinking = state.liveThinking;
102
+ partial.liveToolCalls = state.liveToolCalls;
103
+ partial.thoughtText = state.thoughtText;
104
+ onUpdate({
105
+ content: [{ type: "text", text }],
106
+ details: makeDetails([partial]),
107
+ });
108
+ };
109
+
110
+ const writeCompletionMarkerOnce = (exitCode: number, stopReason?: string) => {
111
+ if (!sidecar || completionMarkerWritten) return;
112
+ completionMarkerWritten = true;
113
+ sidecar.writeDone({ exitCode, stopReason, runtime: "claude" });
114
+ };
115
+
116
+ const forwardAbort = () => {
117
+ externalAbortRequested = true;
118
+ abortController.abort();
119
+ };
120
+
121
+ if (signal?.aborted) {
122
+ forwardAbort();
123
+ writeCompletionMarkerOnce(1, "aborted");
124
+ throw new Error("Subagent was aborted");
125
+ }
126
+ signal?.addEventListener("abort", forwardAbort, { once: true });
127
+
128
+ let sdkQuery: ReturnType<typeof query>;
129
+ try {
130
+ sdkQuery = query({
131
+ prompt: normalizeTaskForSubagentPrompt(task),
132
+ options: {
133
+ abortController,
134
+ cwd: defaultCwd,
135
+ model,
136
+ effort,
137
+ systemPrompt: agent.systemPrompt.trim() || undefined,
138
+ permissionMode: "bypassPermissions",
139
+ allowDangerouslySkipPermissions: true,
140
+ settingSources: [],
141
+ includePartialMessages: true,
142
+ persistSession: true,
143
+ ...(enabledTools ? { tools: enabledTools, allowedTools: enabledTools } : {}),
144
+ resume: resumeSessionId,
145
+ stderr: (data) => {
146
+ stderrBuf += data;
147
+ },
148
+ },
149
+ });
150
+ } catch (error) {
151
+ if (externalAbortRequested) {
152
+ writeCompletionMarkerOnce(1, "aborted");
153
+ throw new Error("Subagent was aborted");
154
+ }
155
+ sdkFailed = true;
156
+ stderrBuf = appendStderrDiagnostic(
157
+ stderrBuf,
158
+ `claude-sdk error: ${error instanceof Error ? error.message : String(error)}`,
159
+ );
160
+ const result = adapter.toSingleResult(agent.name, agent.source, task, 1, step, stderrBuf);
161
+ result.sessionFile = sidecarSessionFile;
162
+ result.claudeProjectDir = defaultCwd;
163
+ return result;
164
+ }
165
+
166
+ try {
167
+ for await (const message of sdkQuery) {
168
+ if (sidecar && !sidecarInitialUserWritten) {
169
+ sidecarInitialUserWritten = true;
170
+ sidecar.writeUserMessage(task);
171
+ }
172
+
173
+ const state = adapter.getState();
174
+ const messageCountBefore = state.messages.length;
175
+ const isResult = adapter.processMessage(message);
176
+ emitUpdate();
177
+
178
+ if (sidecar && state.messages.length > messageCountBefore) {
179
+ for (let index = messageCountBefore; index < state.messages.length; index++) {
180
+ const addedMessage = state.messages[index];
181
+ if (addedMessage.role === "assistant") {
182
+ sidecar.writeAssistantTurn(state);
183
+ } else if (addedMessage.role === "user") {
184
+ const toolNames = extractToolNamesFromPrecedingAssistant(state.messages);
185
+ const textParts = (addedMessage.content as any[])
186
+ .filter((part: any) => part.type === "text" && part.text)
187
+ .map((part: any) => part.text);
188
+ const content = textParts.join("\n") || "(no output)";
189
+ sidecar.writeToolResult(toolNames[0] ?? "tool", content);
190
+ }
191
+ }
192
+ }
193
+
194
+ if (isResult) {
195
+ if (sidecar) sidecar.writeFinalAssistant(state);
196
+ writeCompletionMarkerOnce(state.isError ? 1 : 0, state.stopReason);
197
+ }
198
+ }
199
+ } catch (error) {
200
+ if (!externalAbortRequested && (!(error instanceof AbortError) || !abortController.signal.aborted)) {
201
+ sdkFailed = true;
202
+ stderrBuf = appendStderrDiagnostic(
203
+ stderrBuf,
204
+ `claude-sdk error: ${error instanceof Error ? error.message : String(error)}`,
205
+ );
206
+ }
207
+ } finally {
208
+ sdkQuery.close();
209
+ }
210
+
211
+ if (externalAbortRequested) {
212
+ writeCompletionMarkerOnce(1, "aborted");
213
+ throw new Error("Subagent was aborted");
214
+ }
215
+
216
+ const state = adapter.getState();
217
+ const finalExitCode = sdkFailed || state.isError ? 1 : 0;
218
+ writeCompletionMarkerOnce(finalExitCode, state.stopReason);
219
+ const result = adapter.toSingleResult(agent.name, agent.source, task, finalExitCode, step, stderrBuf);
220
+ result.sessionFile = sidecarSessionFile;
221
+ result.claudeProjectDir = defaultCwd;
222
+ return result;
223
+ }
@@ -0,0 +1,49 @@
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: SDK messages are intentionally forwarded into the shared Claude event parser. */
2
+ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
3
+ import {
4
+ type ClaudeStreamState,
5
+ createStreamState,
6
+ processClaudeEvent,
7
+ stateToSingleResult,
8
+ } from "./claude-stream-parser.js";
9
+ import type { SingleResult } from "./types.js";
10
+
11
+ export interface ClaudeSdkStreamAdapter {
12
+ processMessage(message: SDKMessage): boolean;
13
+ getState(): ClaudeStreamState;
14
+ toSingleResult(
15
+ agent: string,
16
+ agentSource: "user" | "project" | "unknown",
17
+ task: string,
18
+ exitCode: number,
19
+ step: number | undefined,
20
+ stderr: string,
21
+ ): SingleResult;
22
+ }
23
+
24
+ class ClaudeSdkStreamAdapterImpl implements ClaudeSdkStreamAdapter {
25
+ private readonly state = createStreamState();
26
+
27
+ processMessage(message: SDKMessage): boolean {
28
+ return processClaudeEvent(this.state, message as any);
29
+ }
30
+
31
+ getState(): ClaudeStreamState {
32
+ return this.state;
33
+ }
34
+
35
+ toSingleResult(
36
+ agent: string,
37
+ agentSource: "user" | "project" | "unknown",
38
+ task: string,
39
+ exitCode: number,
40
+ step: number | undefined,
41
+ stderr: string,
42
+ ): SingleResult {
43
+ return stateToSingleResult(this.state, agent, agentSource, task, exitCode, step, stderr);
44
+ }
45
+ }
46
+
47
+ export function createClaudeSdkStreamAdapter(): ClaudeSdkStreamAdapter {
48
+ return new ClaudeSdkStreamAdapterImpl();
49
+ }
@@ -0,0 +1,107 @@
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: sidecar JSONL entries use dynamic shapes matching existing session format. */
2
+ import * as fs from "node:fs";
3
+ import type { ClaudeStreamState } from "./claude-stream-parser.js";
4
+
5
+ export interface SidecarWriter {
6
+ writeUserMessage(task: string): void;
7
+ writeAssistantTurn(state: ClaudeStreamState): void;
8
+ writeToolResult(toolName: string, content: string): void;
9
+ writeFinalAssistant(state: ClaudeStreamState): void;
10
+ writeDone(meta: { exitCode: number; stopReason?: string; runtime?: string }): void;
11
+ readonly filePath: string;
12
+ }
13
+
14
+ function makeEnvelope(role: string, content: any, extra?: Record<string, any>): string {
15
+ const entry: any = {
16
+ type: "message",
17
+ timestamp: Date.now(),
18
+ message: {
19
+ role,
20
+ content,
21
+ timestamp: Date.now(),
22
+ ...extra,
23
+ },
24
+ };
25
+ return JSON.stringify(entry);
26
+ }
27
+
28
+ function buildAssistantContent(state: ClaudeStreamState): any[] {
29
+ if (state.messages.length === 0) return [];
30
+
31
+ const lastAssistant = [...state.messages].reverse().find((m) => m.role === "assistant");
32
+ if (!lastAssistant) return [];
33
+
34
+ const parts: any[] = [];
35
+ for (const part of lastAssistant.content as any[]) {
36
+ if (part.type === "text" && part.text) {
37
+ parts.push({ type: "text", text: part.text });
38
+ } else if (part.type === "thinking" && (part as any).thinking) {
39
+ const thinking = (part as any).thinking as string;
40
+ const preview = thinking.length > 200 ? `${thinking.slice(0, 200)}...` : thinking;
41
+ parts.push({ type: "thinking", thinking: preview });
42
+ } else if (part.type === "toolCall") {
43
+ parts.push({
44
+ type: "toolCall",
45
+ name: (part as any).name ?? "tool",
46
+ arguments: (part as any).arguments ?? {},
47
+ });
48
+ }
49
+ }
50
+ return parts;
51
+ }
52
+
53
+ export function createSidecarWriter(filePath: string): SidecarWriter {
54
+ let lastAssistantTurnIndex = -1;
55
+
56
+ function append(line: string): void {
57
+ fs.appendFileSync(filePath, `${line}\n`, "utf-8");
58
+ }
59
+
60
+ return {
61
+ get filePath() {
62
+ return filePath;
63
+ },
64
+
65
+ writeUserMessage(task: string): void {
66
+ append(makeEnvelope("user", [{ type: "text", text: task }]));
67
+ },
68
+
69
+ writeAssistantTurn(state: ClaudeStreamState): void {
70
+ const turnIndex = state.messages.filter((m) => m.role === "assistant").length;
71
+ if (turnIndex <= lastAssistantTurnIndex) return;
72
+
73
+ const content = buildAssistantContent(state);
74
+ if (content.length === 0) return;
75
+
76
+ lastAssistantTurnIndex = turnIndex;
77
+ append(makeEnvelope("assistant", content));
78
+ },
79
+
80
+ writeToolResult(toolName: string, content: string): void {
81
+ append(makeEnvelope("toolResult", [{ type: "text", text: content }], { toolName }));
82
+ },
83
+
84
+ writeFinalAssistant(state: ClaudeStreamState): void {
85
+ const turnIndex = state.messages.filter((m) => m.role === "assistant").length;
86
+ if (turnIndex <= lastAssistantTurnIndex) return;
87
+
88
+ const content = buildAssistantContent(state);
89
+ if (content.length === 0) return;
90
+
91
+ lastAssistantTurnIndex = turnIndex;
92
+ append(makeEnvelope("assistant", content));
93
+ },
94
+
95
+ writeDone(meta: { exitCode: number; stopReason?: string; runtime?: string }): void {
96
+ append(
97
+ JSON.stringify({
98
+ type: "subagent_done",
99
+ timestamp: Date.now(),
100
+ exitCode: meta.exitCode,
101
+ stopReason: meta.stopReason,
102
+ runtime: meta.runtime,
103
+ }),
104
+ );
105
+ },
106
+ };
107
+ }