@agentex/agent 0.0.24 → 0.0.26
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/CHANGELOG.md +338 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/derived.d.ts +5 -3
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +11 -7
- package/dist/derived.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/acp/index.d.ts +1 -1
- package/dist/providers/acp/index.d.ts.map +1 -1
- package/dist/providers/acp/index.js +5 -97
- package/dist/providers/acp/index.js.map +1 -1
- package/dist/providers/acp/session.d.ts +8 -1
- package/dist/providers/acp/session.d.ts.map +1 -1
- package/dist/providers/acp/session.js +94 -0
- package/dist/providers/acp/session.js.map +1 -1
- package/dist/providers/claude/attach.d.ts +8 -0
- package/dist/providers/claude/attach.d.ts.map +1 -0
- package/dist/providers/claude/attach.js +113 -0
- package/dist/providers/claude/attach.js.map +1 -0
- package/dist/providers/claude/goal-capability.d.ts +15 -0
- package/dist/providers/claude/goal-capability.d.ts.map +1 -0
- package/dist/providers/claude/goal-capability.js +20 -0
- package/dist/providers/claude/goal-capability.js.map +1 -0
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +8 -4
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +11 -9
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +29 -14
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts +9 -0
- package/dist/providers/codex/attach.d.ts.map +1 -0
- package/dist/providers/codex/attach.js +93 -0
- package/dist/providers/codex/attach.js.map +1 -0
- package/dist/providers/codex/goal-capability.d.ts +13 -0
- package/dist/providers/codex/goal-capability.d.ts.map +1 -0
- package/dist/providers/codex/goal-capability.js +18 -0
- package/dist/providers/codex/goal-capability.js.map +1 -0
- package/dist/providers/codex/index.d.ts +1 -0
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +9 -6
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/session.d.ts +11 -7
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +24 -12
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +28 -0
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
- package/dist/providers/codex/transcript-normalize.js +191 -0
- package/dist/providers/codex/transcript-normalize.js.map +1 -0
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +2 -2
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/openclaw/index.d.ts.map +1 -1
- package/dist/providers/openclaw/index.js +2 -2
- package/dist/providers/openclaw/index.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +3 -5
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/pi/index.d.ts.map +1 -1
- package/dist/providers/pi/index.js +3 -5
- package/dist/providers/pi/index.js.map +1 -1
- package/dist/providers/process/index.d.ts.map +1 -1
- package/dist/providers/process/index.js +2 -2
- package/dist/providers/process/index.js.map +1 -1
- package/dist/registry.d.ts +0 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +0 -4
- package/dist/registry.js.map +1 -1
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/record.d.ts +43 -0
- package/dist/sessions/record.d.ts.map +1 -0
- package/dist/sessions/record.js +85 -0
- package/dist/sessions/record.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/uuid.d.ts +7 -1
- package/dist/utils/uuid.d.ts.map +1 -1
- package/dist/utils/uuid.js +21 -1
- package/dist/utils/uuid.js.map +1 -1
- package/package.json +64 -7
- package/src/derived.ts +311 -0
- package/src/goals/controller.ts +442 -0
- package/src/goals/index.ts +21 -0
- package/src/goals/normalize.ts +173 -0
- package/src/goals/sentinel.ts +90 -0
- package/src/index.ts +270 -0
- package/src/providers/_shared/http-agent.ts +304 -0
- package/src/providers/acp/index.ts +103 -0
- package/src/providers/acp/parse.ts +131 -0
- package/src/providers/acp/session.ts +744 -0
- package/src/providers/claude/attach.ts +147 -0
- package/src/providers/claude/codec.ts +43 -0
- package/src/providers/claude/execute.ts +300 -0
- package/src/providers/claude/goal-capability.ts +21 -0
- package/src/providers/claude/index.ts +72 -0
- package/src/providers/claude/mcp.ts +82 -0
- package/src/providers/claude/parse.ts +824 -0
- package/src/providers/claude/session.ts +1192 -0
- package/src/providers/claude/transcript.ts +555 -0
- package/src/providers/codex/attach.ts +123 -0
- package/src/providers/codex/codec.ts +50 -0
- package/src/providers/codex/execute.ts +337 -0
- package/src/providers/codex/goal-capability.ts +19 -0
- package/src/providers/codex/index.ts +57 -0
- package/src/providers/codex/modes.ts +159 -0
- package/src/providers/codex/parse.ts +691 -0
- package/src/providers/codex/plan-mode.ts +49 -0
- package/src/providers/codex/session.ts +1287 -0
- package/src/providers/codex/transcript-normalize.ts +197 -0
- package/src/providers/codex/transcript.ts +487 -0
- package/src/providers/codex/usage-scanner.ts +178 -0
- package/src/providers/copilot/index.ts +19 -0
- package/src/providers/cursor/codec.ts +44 -0
- package/src/providers/cursor/execute.ts +271 -0
- package/src/providers/cursor/index.ts +25 -0
- package/src/providers/cursor/parse.ts +288 -0
- package/src/providers/gemini/index.ts +21 -0
- package/src/providers/openclaw/codec.ts +40 -0
- package/src/providers/openclaw/execute.ts +19 -0
- package/src/providers/openclaw/index.ts +29 -0
- package/src/providers/opencode/codec.ts +50 -0
- package/src/providers/opencode/event-parse.ts +141 -0
- package/src/providers/opencode/execute.ts +251 -0
- package/src/providers/opencode/http-session.ts +427 -0
- package/src/providers/opencode/index.ts +30 -0
- package/src/providers/opencode/parse.ts +203 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/pi/codec.ts +44 -0
- package/src/providers/pi/execute.ts +297 -0
- package/src/providers/pi/index.ts +30 -0
- package/src/providers/pi/parse.ts +231 -0
- package/src/providers/pi/session.ts +381 -0
- package/src/providers/process/execute.ts +148 -0
- package/src/providers/process/index.ts +52 -0
- package/src/registry.ts +40 -0
- package/src/sessions/index.ts +8 -0
- package/src/sessions/record.ts +108 -0
- package/src/types.ts +1638 -0
- package/src/utils/ask-user-question.ts +57 -0
- package/src/utils/auth.ts +661 -0
- package/src/utils/binary.ts +179 -0
- package/src/utils/endpoint.ts +172 -0
- package/src/utils/env.ts +63 -0
- package/src/utils/execute-all.ts +68 -0
- package/src/utils/exit-plan-mode.ts +40 -0
- package/src/utils/instructions.ts +427 -0
- package/src/utils/process.ts +223 -0
- package/src/utils/runtime-config.ts +100 -0
- package/src/utils/runtime-homes.ts +49 -0
- package/src/utils/skill-commands.ts +493 -0
- package/src/utils/skills.ts +500 -0
- package/src/utils/template.ts +16 -0
- package/src/utils/tool-names.ts +51 -0
- package/src/utils/uuid.ts +21 -0
- package/src/utils/workspace.ts +156 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { BaseStreamEventFields, StreamEvent } from "../../types.js";
|
|
2
|
+
|
|
3
|
+
const PROVIDER_TYPE = "pi";
|
|
4
|
+
|
|
5
|
+
function stubBase(event: Record<string, unknown>): BaseStreamEventFields {
|
|
6
|
+
return {
|
|
7
|
+
timestamp: new Date().toISOString(),
|
|
8
|
+
providerType: PROVIDER_TYPE,
|
|
9
|
+
sessionId: null,
|
|
10
|
+
messageId: null,
|
|
11
|
+
eventId: null,
|
|
12
|
+
turnId: null,
|
|
13
|
+
parentToolCallId: null,
|
|
14
|
+
raw: event,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PiParsedResult {
|
|
19
|
+
sessionId: string | null;
|
|
20
|
+
model: string | null;
|
|
21
|
+
usage: { inputTokens: number; outputTokens: number; cachedInputTokens: number; costUsd: number } | null;
|
|
22
|
+
summary: string | null;
|
|
23
|
+
isError: boolean;
|
|
24
|
+
errorMessage: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseJson(line: string): Record<string, unknown> | null {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(line);
|
|
30
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
31
|
+
return parsed as Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Skip malformed lines
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function asString(value: unknown, fallback: string): string {
|
|
40
|
+
return typeof value === "string" ? value : fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function asNumber(value: unknown, fallback: number): number {
|
|
44
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
48
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
49
|
+
return value as Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractTextContent(content: unknown): string {
|
|
53
|
+
if (typeof content === "string") return content;
|
|
54
|
+
if (!Array.isArray(content)) return "";
|
|
55
|
+
return (content as Array<Record<string, unknown>>)
|
|
56
|
+
.filter((c) => c["type"] === "text" && c["text"])
|
|
57
|
+
.map((c) => c["text"] as string)
|
|
58
|
+
.join("");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parsePiJsonl(stdout: string): PiParsedResult {
|
|
62
|
+
const messages: string[] = [];
|
|
63
|
+
const errors: string[] = [];
|
|
64
|
+
const usage = { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, costUsd: 0 };
|
|
65
|
+
let hasUsage = false;
|
|
66
|
+
let finalMessage: string | null = null;
|
|
67
|
+
|
|
68
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
69
|
+
const line = rawLine.trim();
|
|
70
|
+
if (!line) continue;
|
|
71
|
+
|
|
72
|
+
const event = parseJson(line);
|
|
73
|
+
if (!event) continue;
|
|
74
|
+
|
|
75
|
+
const eventType = asString(event["type"], "");
|
|
76
|
+
|
|
77
|
+
// Skip RPC protocol internals
|
|
78
|
+
if (eventType === "response" || eventType === "extension_ui_request" || eventType === "extension_ui_response" || eventType === "extension_error") {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (eventType === "agent_end") {
|
|
83
|
+
const agentMessages = event["messages"] as Array<Record<string, unknown>> | undefined;
|
|
84
|
+
if (agentMessages && agentMessages.length > 0) {
|
|
85
|
+
const lastMessage = agentMessages[agentMessages.length - 1];
|
|
86
|
+
if (lastMessage?.["role"] === "assistant") {
|
|
87
|
+
finalMessage = extractTextContent(lastMessage["content"]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (eventType === "turn_end") {
|
|
94
|
+
const message = asRecord(event["message"]);
|
|
95
|
+
if (message) {
|
|
96
|
+
const text = extractTextContent(message["content"]);
|
|
97
|
+
if (text) {
|
|
98
|
+
finalMessage = text;
|
|
99
|
+
messages.push(text);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const usageObj = asRecord(message["usage"]);
|
|
103
|
+
if (usageObj) {
|
|
104
|
+
usage.inputTokens += asNumber(usageObj["input"], 0);
|
|
105
|
+
usage.outputTokens += asNumber(usageObj["output"], 0);
|
|
106
|
+
usage.cachedInputTokens += asNumber(usageObj["cacheRead"], 0);
|
|
107
|
+
hasUsage = true;
|
|
108
|
+
|
|
109
|
+
const cost = asRecord(usageObj["cost"]);
|
|
110
|
+
if (cost) {
|
|
111
|
+
usage.costUsd += asNumber(cost["total"], 0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (eventType === "message_update") {
|
|
119
|
+
const assistantEvent = asRecord(event["assistantMessageEvent"]);
|
|
120
|
+
if (assistantEvent) {
|
|
121
|
+
const msgType = asString(assistantEvent["type"], "");
|
|
122
|
+
if (msgType === "text_delta") {
|
|
123
|
+
const delta = asString(assistantEvent["delta"], "");
|
|
124
|
+
if (delta) {
|
|
125
|
+
if (messages.length === 0) {
|
|
126
|
+
messages.push(delta);
|
|
127
|
+
} else {
|
|
128
|
+
messages[messages.length - 1] += delta;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (eventType === "usage" || event["usage"]) {
|
|
137
|
+
const usageObj = asRecord(event["usage"]);
|
|
138
|
+
if (usageObj) {
|
|
139
|
+
usage.inputTokens += asNumber(usageObj["inputTokens"] ?? usageObj["input"], 0);
|
|
140
|
+
usage.outputTokens += asNumber(usageObj["outputTokens"] ?? usageObj["output"], 0);
|
|
141
|
+
usage.cachedInputTokens += asNumber(usageObj["cachedInputTokens"] ?? usageObj["cacheRead"], 0);
|
|
142
|
+
hasUsage = true;
|
|
143
|
+
|
|
144
|
+
const cost = asRecord(usageObj["cost"]);
|
|
145
|
+
if (cost) {
|
|
146
|
+
usage.costUsd += asNumber(cost["total"] ?? usageObj["costUsd"], 0);
|
|
147
|
+
} else {
|
|
148
|
+
usage.costUsd += asNumber(usageObj["costUsd"], 0);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (eventType === "error") {
|
|
154
|
+
const text = asString(event["message"] as string ?? event["error"] as string, "").trim();
|
|
155
|
+
if (text) errors.push(text);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const summary = finalMessage ?? (messages.join("\n\n").trim() || null);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
sessionId: null, // Pi uses file-based sessions, not server-returned IDs
|
|
163
|
+
model: null,
|
|
164
|
+
usage: hasUsage ? usage : null,
|
|
165
|
+
summary,
|
|
166
|
+
isError: errors.length > 0,
|
|
167
|
+
errorMessage: errors.length > 0 ? errors.join("\n") : null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function parsePiStreamLine(line: string): StreamEvent | null {
|
|
172
|
+
const event = parseJson(line);
|
|
173
|
+
if (!event) return null;
|
|
174
|
+
|
|
175
|
+
const eventType = asString(event["type"], "");
|
|
176
|
+
const base = stubBase(event);
|
|
177
|
+
|
|
178
|
+
if (eventType === "message_update") {
|
|
179
|
+
const assistantEvent = asRecord(event["assistantMessageEvent"]);
|
|
180
|
+
if (assistantEvent && asString(assistantEvent["type"], "") === "text_delta") {
|
|
181
|
+
const delta = asString(assistantEvent["delta"], "");
|
|
182
|
+
if (delta) return { type: "assistant", text: delta, ...base };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (eventType === "tool_execution_start") {
|
|
187
|
+
return {
|
|
188
|
+
type: "tool_call",
|
|
189
|
+
toolCallId: asString(event["toolCallId"], "") || null,
|
|
190
|
+
name: asString(event["toolName"], ""),
|
|
191
|
+
input: event["args"],
|
|
192
|
+
...base,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (eventType === "tool_execution_end") {
|
|
197
|
+
const result = event["result"];
|
|
198
|
+
return {
|
|
199
|
+
type: "tool_result",
|
|
200
|
+
toolCallId: asString(event["toolCallId"], "") || null,
|
|
201
|
+
toolName: asString(event["toolName"], "") || null,
|
|
202
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
203
|
+
isError: event["isError"] === true,
|
|
204
|
+
exitCode: null,
|
|
205
|
+
...base,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (eventType === "agent_end") {
|
|
210
|
+
return {
|
|
211
|
+
type: "result",
|
|
212
|
+
text: "",
|
|
213
|
+
costUsd: null,
|
|
214
|
+
isError: false,
|
|
215
|
+
stopReason: null,
|
|
216
|
+
terminalReason: null,
|
|
217
|
+
numTurns: null,
|
|
218
|
+
durationMs: null,
|
|
219
|
+
...base,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Forward-compat: surface unknown wire events rather than dropping them.
|
|
224
|
+
return { type: "unknown", subtype: eventType, ...base };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const PI_UNKNOWN_SESSION_RE = /unknown\s+session|session\s+not\s+found|session\s+.*\s+not\s+found|no\s+session/i;
|
|
228
|
+
|
|
229
|
+
export function isPiUnknownSessionError(stdout: string, stderr: string): boolean {
|
|
230
|
+
return PI_UNKNOWN_SESSION_RE.test(stdout) || PI_UNKNOWN_SESSION_RE.test(stderr);
|
|
231
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import type {
|
|
6
|
+
AgentSession,
|
|
7
|
+
CancelResult,
|
|
8
|
+
ClearGoalResult,
|
|
9
|
+
GoalOptions,
|
|
10
|
+
GoalState,
|
|
11
|
+
StopTaskResult,
|
|
12
|
+
SendHandle,
|
|
13
|
+
SendOptions,
|
|
14
|
+
SessionContext,
|
|
15
|
+
SessionState,
|
|
16
|
+
SetGoalResult,
|
|
17
|
+
TurnResult,
|
|
18
|
+
} from "../../types.js";
|
|
19
|
+
import { GoalController, EMULATED_GOAL_CAPABILITY } from "../../goals/index.js";
|
|
20
|
+
import { findBinary } from "../../utils/binary.js";
|
|
21
|
+
import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
|
|
22
|
+
import { uuidv7 } from "../../utils/uuid.js";
|
|
23
|
+
import { resolveInstructions } from "../../utils/instructions.js";
|
|
24
|
+
import { injectHomeSkills } from "../../utils/skills.js";
|
|
25
|
+
import { parsePiStreamLine } from "./parse.js";
|
|
26
|
+
import { piSessionCodec } from "./codec.js";
|
|
27
|
+
|
|
28
|
+
const PI_SESSIONS_DIR = path.join(os.homedir(), ".pi", "sessions");
|
|
29
|
+
|
|
30
|
+
function parseProviderModel(model: string | undefined): { provider: string | null; modelId: string | null } {
|
|
31
|
+
const m = (model ?? "").trim();
|
|
32
|
+
if (!m) return { provider: null, modelId: null };
|
|
33
|
+
const idx = m.indexOf("/");
|
|
34
|
+
if (idx < 0) return { provider: null, modelId: m };
|
|
35
|
+
return { provider: m.slice(0, idx).trim() || null, modelId: m.slice(idx + 1).trim() || null };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildSessionPath(): string {
|
|
39
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
40
|
+
return path.join(PI_SESSIONS_DIR, `${ts}-${uuidv7()}.jsonl`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PendingTurn {
|
|
44
|
+
resolve: (r: TurnResult) => void;
|
|
45
|
+
settled: boolean;
|
|
46
|
+
cleanup: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Create + connect a persistent `pi --mode rpc` session. */
|
|
50
|
+
export async function createPiSession(ctx: SessionContext): Promise<AgentSession> {
|
|
51
|
+
const session = new PiSession(ctx);
|
|
52
|
+
await session.connect();
|
|
53
|
+
return session;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @internal Exported for unit testing — not part of the public API. */
|
|
57
|
+
export class PiSession implements AgentSession {
|
|
58
|
+
private _state: SessionState = "idle";
|
|
59
|
+
private proc: ChildProcess | null = null;
|
|
60
|
+
private _sessionPath = "";
|
|
61
|
+
private _lineBuffer = "";
|
|
62
|
+
|
|
63
|
+
private _turnActive = false;
|
|
64
|
+
private _turnText = "";
|
|
65
|
+
private _pending: PendingTurn | null = null;
|
|
66
|
+
private _inFlight: Promise<TurnResult> | null = null;
|
|
67
|
+
private _draining = false;
|
|
68
|
+
/** Serial dispatch so onEvent fires in order and a turn resolves after them. */
|
|
69
|
+
private _eventChain: Promise<void> = Promise.resolve();
|
|
70
|
+
/** Decided outcome of an interrupted turn, applied when its `agent_end` acks. */
|
|
71
|
+
private _pendingStatus: TurnResult["status"] | null = null;
|
|
72
|
+
private _pendingMessage: string | null = null;
|
|
73
|
+
/** Force-resolve timer if pi never acks an abort with `agent_end`. */
|
|
74
|
+
private _graceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
75
|
+
/** Count of `agent_end`s to swallow (from turns force-resolved before their ack). */
|
|
76
|
+
private _staleEnds = 0;
|
|
77
|
+
|
|
78
|
+
/** Goal engine. pi has no native goal surface — always emulated. */
|
|
79
|
+
private readonly _goals: GoalController;
|
|
80
|
+
|
|
81
|
+
constructor(private readonly ctx: SessionContext) {
|
|
82
|
+
this._goals = new GoalController({
|
|
83
|
+
providerType: "pi",
|
|
84
|
+
capability: EMULATED_GOAL_CAPABILITY,
|
|
85
|
+
getSessionId: () => this.sessionId,
|
|
86
|
+
send: (m) => this.send(m),
|
|
87
|
+
dispatch: (event) => {
|
|
88
|
+
if (!this.ctx.onEvent) return;
|
|
89
|
+
const handler = this.ctx.onEvent;
|
|
90
|
+
this._eventChain = this._eventChain.then(() => handler(event)).catch(() => {});
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get sessionId(): string | null {
|
|
96
|
+
return this._sessionPath || null;
|
|
97
|
+
}
|
|
98
|
+
get state(): SessionState {
|
|
99
|
+
return this._state;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async connect(): Promise<void> {
|
|
103
|
+
const config = this.ctx.config ?? {};
|
|
104
|
+
const resolved = await findBinary("pi", config.command);
|
|
105
|
+
const env = buildEnv(this.ctx.env);
|
|
106
|
+
ensurePathInEnv(env);
|
|
107
|
+
const cwd = this.ctx.cwd ?? process.cwd();
|
|
108
|
+
|
|
109
|
+
if (config.instructionsFile) await resolveInstructions(config.instructionsFile);
|
|
110
|
+
let skillsDir: string | null = null;
|
|
111
|
+
if (config.skillDirs && config.skillDirs.length > 0) {
|
|
112
|
+
try {
|
|
113
|
+
skillsDir = await injectHomeSkills(config.skillDirs, "pi");
|
|
114
|
+
} catch {
|
|
115
|
+
/* non-fatal */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await fs.mkdir(PI_SESSIONS_DIR, { recursive: true });
|
|
119
|
+
|
|
120
|
+
// Resume an existing session file, or start a new one. Pi keys sessions by
|
|
121
|
+
// file path; `--session <file>` resumes when the file exists, creates otherwise.
|
|
122
|
+
const decoded = piSessionCodec.deserialize(this.ctx.sessionParams ?? null);
|
|
123
|
+
const resumeId = typeof decoded?.["sessionId"] === "string" ? (decoded["sessionId"] as string) : null;
|
|
124
|
+
this._sessionPath = resumeId || buildSessionPath();
|
|
125
|
+
|
|
126
|
+
const { provider, modelId } = parseProviderModel(this.ctx.config?.model);
|
|
127
|
+
const args = [...resolved.prefixArgs, "--mode", "rpc"];
|
|
128
|
+
if (provider) args.push("--provider", provider);
|
|
129
|
+
if (modelId) args.push("--model", modelId);
|
|
130
|
+
if (config.thinking) args.push("--thinking", config.thinking);
|
|
131
|
+
args.push("--tools", "read,bash,edit,write,grep,find,ls");
|
|
132
|
+
args.push("--session", this._sessionPath);
|
|
133
|
+
if (skillsDir) args.push("--skill", skillsDir);
|
|
134
|
+
if (config.instructionsFile) args.push("--append-system-prompt", config.instructionsFile);
|
|
135
|
+
if (config.extraArgs) args.push(...config.extraArgs);
|
|
136
|
+
|
|
137
|
+
const proc = spawn(resolved.bin, args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
138
|
+
this.proc = proc;
|
|
139
|
+
|
|
140
|
+
proc.stdout!.setEncoding("utf-8");
|
|
141
|
+
proc.stdout!.on("data", (chunk: string) => this.onStdout(chunk));
|
|
142
|
+
proc.stderr!.setEncoding("utf-8");
|
|
143
|
+
proc.stderr!.on("data", (chunk: string) => {
|
|
144
|
+
if (this.ctx.onOutput) {
|
|
145
|
+
try {
|
|
146
|
+
void this.ctx.onOutput("stderr", chunk);
|
|
147
|
+
} catch {
|
|
148
|
+
/* swallow */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
proc.on("exit", (code, signal) => {
|
|
153
|
+
if (this._state !== "closed") {
|
|
154
|
+
this._state = "closed";
|
|
155
|
+
this.failPending(`pi process exited (code=${code}, signal=${signal})`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
proc.on("error", (err) => {
|
|
159
|
+
if (this._state !== "closed") {
|
|
160
|
+
this._state = "closed";
|
|
161
|
+
this.failPending(err instanceof Error ? err.message : String(err));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (this.ctx.signal) {
|
|
166
|
+
if (this.ctx.signal.aborted) void this.close();
|
|
167
|
+
else this.ctx.signal.addEventListener("abort", () => void this.close(), { once: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
// stdout → events (strict \n framing per pi's RPC contract)
|
|
173
|
+
// -------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
private onStdout(chunk: string): void {
|
|
176
|
+
if (this.ctx.onOutput) {
|
|
177
|
+
try {
|
|
178
|
+
void this.ctx.onOutput("stdout", chunk);
|
|
179
|
+
} catch {
|
|
180
|
+
/* swallow */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this._lineBuffer += chunk;
|
|
184
|
+
const lines = this._lineBuffer.split("\n");
|
|
185
|
+
this._lineBuffer = lines.pop() ?? "";
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const trimmed = line.replace(/\r$/, "").trim();
|
|
188
|
+
if (trimmed) this.handleLine(trimmed);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private handleLine(line: string): void {
|
|
193
|
+
// Command acks (`{"type":"response",...}`) aren't agent events — skip.
|
|
194
|
+
let obj: unknown;
|
|
195
|
+
try {
|
|
196
|
+
obj = JSON.parse(line);
|
|
197
|
+
} catch {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (obj && typeof obj === "object" && (obj as Record<string, unknown>)["type"] === "response") return;
|
|
201
|
+
|
|
202
|
+
const ev = parsePiStreamLine(line);
|
|
203
|
+
if (!ev) return;
|
|
204
|
+
|
|
205
|
+
// `agent_end` (mapped to "result") is a turn terminal. Swallow a stale end
|
|
206
|
+
// from a turn that was already force-resolved (timed out and pi never acked
|
|
207
|
+
// before the grace deadline) so it can't resolve the NEXT turn.
|
|
208
|
+
if (ev.type === "result" && this._staleEnds > 0) {
|
|
209
|
+
this._staleEnds--;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Only accumulate/emit while a turn is active so stragglers from a finished
|
|
214
|
+
// turn don't contaminate the next one's summary or event stream.
|
|
215
|
+
if (!this._turnActive) return;
|
|
216
|
+
|
|
217
|
+
if (ev.type === "assistant") this._turnText += ev.text;
|
|
218
|
+
|
|
219
|
+
// Emit through the serial chain so handlers run in order.
|
|
220
|
+
if (this.ctx.onEvent) {
|
|
221
|
+
const handler = this.ctx.onEvent;
|
|
222
|
+
this._eventChain = this._eventChain.then(() => handler(ev)).catch(() => {});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// agent_end → the turn is complete. If the turn was interrupted, apply the
|
|
226
|
+
// decided status (timeout/aborted); otherwise it completed.
|
|
227
|
+
if (ev.type === "result") {
|
|
228
|
+
const status = this._pendingStatus ?? "completed";
|
|
229
|
+
const message = this._pendingMessage ?? undefined;
|
|
230
|
+
void this._eventChain.then(() => this.resolveTurn(status, message));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private resolveTurn(status: TurnResult["status"], errorMessage?: string): void {
|
|
235
|
+
const pending = this._pending;
|
|
236
|
+
if (!pending || pending.settled) return;
|
|
237
|
+
pending.settled = true;
|
|
238
|
+
pending.cleanup();
|
|
239
|
+
if (this._graceTimer) {
|
|
240
|
+
clearTimeout(this._graceTimer);
|
|
241
|
+
this._graceTimer = null;
|
|
242
|
+
}
|
|
243
|
+
const summary = this._turnText || null;
|
|
244
|
+
// Reset per-turn accumulators here (not only in send()) so anything that
|
|
245
|
+
// arrives between turns can't leak into the next turn's summary.
|
|
246
|
+
this._turnText = "";
|
|
247
|
+
this._turnActive = false;
|
|
248
|
+
this._inFlight = null;
|
|
249
|
+
this._pending = null;
|
|
250
|
+
this._pendingStatus = null;
|
|
251
|
+
this._pendingMessage = null;
|
|
252
|
+
if (this._state !== "closed") this._state = "idle";
|
|
253
|
+
const result: TurnResult = {
|
|
254
|
+
summary,
|
|
255
|
+
costUsd: null,
|
|
256
|
+
status,
|
|
257
|
+
errorCode: status === "completed" ? null : status === "failed" ? "error" : status,
|
|
258
|
+
errorMessage: errorMessage ?? null,
|
|
259
|
+
};
|
|
260
|
+
pending.resolve(result);
|
|
261
|
+
void this._goals.onTurnSettled(result);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private failPending(message: string): void {
|
|
265
|
+
if (this._pending && !this._pending.settled) this.resolveTurn("failed", message);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// -------------------------------------------------------------------------
|
|
269
|
+
// Public API
|
|
270
|
+
// -------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
async send(message: string, options?: SendOptions): Promise<SendHandle> {
|
|
273
|
+
if (this._state === "closed") throw new Error("Session is closed");
|
|
274
|
+
if (this._draining) throw new Error("Session is draining — no new sends accepted");
|
|
275
|
+
if (this._turnActive) {
|
|
276
|
+
throw new Error("Pi session is busy — a turn is already in progress (concurrentSend not supported)");
|
|
277
|
+
}
|
|
278
|
+
if (!this.proc?.stdin) throw new Error("Pi session is not connected");
|
|
279
|
+
|
|
280
|
+
const uuid = uuidv7();
|
|
281
|
+
this._turnActive = true;
|
|
282
|
+
this._turnText = "";
|
|
283
|
+
this._state = "thinking";
|
|
284
|
+
|
|
285
|
+
const timeoutSec = options?.timeoutSec ?? this.ctx.config?.timeoutSec;
|
|
286
|
+
const result = new Promise<TurnResult>((resolve) => {
|
|
287
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
288
|
+
const onAbort = (): void => this.onTurnInterrupt("aborted");
|
|
289
|
+
const cleanup = (): void => {
|
|
290
|
+
if (timer) clearTimeout(timer);
|
|
291
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
292
|
+
};
|
|
293
|
+
this._pending = { resolve, settled: false, cleanup };
|
|
294
|
+
if (timeoutSec && timeoutSec > 0) {
|
|
295
|
+
timer = setTimeout(() => this.onTurnInterrupt("timeout"), timeoutSec * 1000);
|
|
296
|
+
}
|
|
297
|
+
if (options?.signal) {
|
|
298
|
+
if (options.signal.aborted) this.onTurnInterrupt("aborted");
|
|
299
|
+
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
this._inFlight = result;
|
|
303
|
+
|
|
304
|
+
this.proc.stdin.write(JSON.stringify({ id: uuid, type: "prompt", message }) + "\n");
|
|
305
|
+
return { uuid, result };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private onTurnInterrupt(status: "timeout" | "aborted"): void {
|
|
309
|
+
if (!this._pending || this._pending.settled) return;
|
|
310
|
+
// Decide the outcome, send `abort`, but keep the turn open until pi acks with
|
|
311
|
+
// `agent_end` so the aborted turn's stragglers stay inside this turn (and
|
|
312
|
+
// don't bleed into the next). Force-resolve after a grace deadline if pi
|
|
313
|
+
// never acks — and then swallow the eventual stale `agent_end`.
|
|
314
|
+
this._pendingStatus = status;
|
|
315
|
+
this._pendingMessage = status === "timeout" ? "Turn exceeded its timeout" : "Turn aborted";
|
|
316
|
+
void this.interrupt();
|
|
317
|
+
if (this._graceTimer) clearTimeout(this._graceTimer);
|
|
318
|
+
const graceMs = (this.ctx.config?.graceSec ?? 5) * 1000;
|
|
319
|
+
this._graceTimer = setTimeout(() => {
|
|
320
|
+
this._graceTimer = null;
|
|
321
|
+
this._staleEnds++;
|
|
322
|
+
this.resolveTurn(status, status === "timeout" ? "Turn exceeded its timeout" : "Turn aborted");
|
|
323
|
+
}, graceMs);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async cancel(_uuid: string): Promise<CancelResult> {
|
|
327
|
+
return { cancelled: false };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async stopTask(_taskId: string): Promise<StopTaskResult> {
|
|
331
|
+
// pi has no per-task stop control; capabilities.stopTask is false.
|
|
332
|
+
return { stopped: false };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
setGoal(objective: string, options?: GoalOptions): Promise<SetGoalResult> {
|
|
336
|
+
return this._goals.setGoal(objective, options);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
clearGoal(options?: { reason?: "cleared" | "blocked" }): Promise<ClearGoalResult> {
|
|
340
|
+
return this._goals.clearGoal(options);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getGoal(): GoalState | null {
|
|
344
|
+
return this._goals.getGoal();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async interrupt(): Promise<void> {
|
|
348
|
+
if (this.proc?.stdin && !this.proc.stdin.destroyed) {
|
|
349
|
+
try {
|
|
350
|
+
this.proc.stdin.write(JSON.stringify({ type: "abort" }) + "\n");
|
|
351
|
+
} catch {
|
|
352
|
+
/* best effort */
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async drain(): Promise<void> {
|
|
358
|
+
this._draining = true;
|
|
359
|
+
if (this._inFlight) {
|
|
360
|
+
try {
|
|
361
|
+
await this._inFlight;
|
|
362
|
+
} catch {
|
|
363
|
+
/* ignore */
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
await this.close();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async close(): Promise<void> {
|
|
370
|
+
this._state = "closed";
|
|
371
|
+
const proc = this.proc;
|
|
372
|
+
this.proc = null;
|
|
373
|
+
if (proc && !proc.killed) {
|
|
374
|
+
try {
|
|
375
|
+
proc.kill();
|
|
376
|
+
} catch {
|
|
377
|
+
/* ignore */
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|