@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/LICENSE +21 -0
  3. package/README.md +52 -0
  4. package/dist/derived.d.ts +5 -3
  5. package/dist/derived.d.ts.map +1 -1
  6. package/dist/derived.js +11 -7
  7. package/dist/derived.js.map +1 -1
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/acp/index.d.ts +1 -1
  13. package/dist/providers/acp/index.d.ts.map +1 -1
  14. package/dist/providers/acp/index.js +5 -97
  15. package/dist/providers/acp/index.js.map +1 -1
  16. package/dist/providers/acp/session.d.ts +8 -1
  17. package/dist/providers/acp/session.d.ts.map +1 -1
  18. package/dist/providers/acp/session.js +94 -0
  19. package/dist/providers/acp/session.js.map +1 -1
  20. package/dist/providers/claude/attach.d.ts +8 -0
  21. package/dist/providers/claude/attach.d.ts.map +1 -0
  22. package/dist/providers/claude/attach.js +113 -0
  23. package/dist/providers/claude/attach.js.map +1 -0
  24. package/dist/providers/claude/goal-capability.d.ts +15 -0
  25. package/dist/providers/claude/goal-capability.d.ts.map +1 -0
  26. package/dist/providers/claude/goal-capability.js +20 -0
  27. package/dist/providers/claude/goal-capability.js.map +1 -0
  28. package/dist/providers/claude/index.d.ts.map +1 -1
  29. package/dist/providers/claude/index.js +8 -4
  30. package/dist/providers/claude/index.js.map +1 -1
  31. package/dist/providers/claude/session.d.ts +11 -9
  32. package/dist/providers/claude/session.d.ts.map +1 -1
  33. package/dist/providers/claude/session.js +29 -14
  34. package/dist/providers/claude/session.js.map +1 -1
  35. package/dist/providers/codex/attach.d.ts +9 -0
  36. package/dist/providers/codex/attach.d.ts.map +1 -0
  37. package/dist/providers/codex/attach.js +93 -0
  38. package/dist/providers/codex/attach.js.map +1 -0
  39. package/dist/providers/codex/goal-capability.d.ts +13 -0
  40. package/dist/providers/codex/goal-capability.d.ts.map +1 -0
  41. package/dist/providers/codex/goal-capability.js +18 -0
  42. package/dist/providers/codex/goal-capability.js.map +1 -0
  43. package/dist/providers/codex/index.d.ts +1 -0
  44. package/dist/providers/codex/index.d.ts.map +1 -1
  45. package/dist/providers/codex/index.js +9 -6
  46. package/dist/providers/codex/index.js.map +1 -1
  47. package/dist/providers/codex/session.d.ts +11 -7
  48. package/dist/providers/codex/session.d.ts.map +1 -1
  49. package/dist/providers/codex/session.js +24 -12
  50. package/dist/providers/codex/session.js.map +1 -1
  51. package/dist/providers/codex/transcript-normalize.d.ts +28 -0
  52. package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
  53. package/dist/providers/codex/transcript-normalize.js +191 -0
  54. package/dist/providers/codex/transcript-normalize.js.map +1 -0
  55. package/dist/providers/cursor/index.d.ts.map +1 -1
  56. package/dist/providers/cursor/index.js +2 -2
  57. package/dist/providers/cursor/index.js.map +1 -1
  58. package/dist/providers/openclaw/index.d.ts.map +1 -1
  59. package/dist/providers/openclaw/index.js +2 -2
  60. package/dist/providers/openclaw/index.js.map +1 -1
  61. package/dist/providers/opencode/index.d.ts.map +1 -1
  62. package/dist/providers/opencode/index.js +3 -5
  63. package/dist/providers/opencode/index.js.map +1 -1
  64. package/dist/providers/pi/index.d.ts.map +1 -1
  65. package/dist/providers/pi/index.js +3 -5
  66. package/dist/providers/pi/index.js.map +1 -1
  67. package/dist/providers/process/index.d.ts.map +1 -1
  68. package/dist/providers/process/index.js +2 -2
  69. package/dist/providers/process/index.js.map +1 -1
  70. package/dist/registry.d.ts +0 -1
  71. package/dist/registry.d.ts.map +1 -1
  72. package/dist/registry.js +0 -4
  73. package/dist/registry.js.map +1 -1
  74. package/dist/sessions/index.d.ts +3 -0
  75. package/dist/sessions/index.d.ts.map +1 -0
  76. package/dist/sessions/index.js +2 -0
  77. package/dist/sessions/index.js.map +1 -0
  78. package/dist/sessions/record.d.ts +43 -0
  79. package/dist/sessions/record.d.ts.map +1 -0
  80. package/dist/sessions/record.js +85 -0
  81. package/dist/sessions/record.js.map +1 -0
  82. package/dist/types.d.ts +119 -0
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js.map +1 -1
  85. package/dist/utils/uuid.d.ts +7 -1
  86. package/dist/utils/uuid.d.ts.map +1 -1
  87. package/dist/utils/uuid.js +21 -1
  88. package/dist/utils/uuid.js.map +1 -1
  89. package/package.json +64 -7
  90. package/src/derived.ts +311 -0
  91. package/src/goals/controller.ts +442 -0
  92. package/src/goals/index.ts +21 -0
  93. package/src/goals/normalize.ts +173 -0
  94. package/src/goals/sentinel.ts +90 -0
  95. package/src/index.ts +270 -0
  96. package/src/providers/_shared/http-agent.ts +304 -0
  97. package/src/providers/acp/index.ts +103 -0
  98. package/src/providers/acp/parse.ts +131 -0
  99. package/src/providers/acp/session.ts +744 -0
  100. package/src/providers/claude/attach.ts +147 -0
  101. package/src/providers/claude/codec.ts +43 -0
  102. package/src/providers/claude/execute.ts +300 -0
  103. package/src/providers/claude/goal-capability.ts +21 -0
  104. package/src/providers/claude/index.ts +72 -0
  105. package/src/providers/claude/mcp.ts +82 -0
  106. package/src/providers/claude/parse.ts +824 -0
  107. package/src/providers/claude/session.ts +1192 -0
  108. package/src/providers/claude/transcript.ts +555 -0
  109. package/src/providers/codex/attach.ts +123 -0
  110. package/src/providers/codex/codec.ts +50 -0
  111. package/src/providers/codex/execute.ts +337 -0
  112. package/src/providers/codex/goal-capability.ts +19 -0
  113. package/src/providers/codex/index.ts +57 -0
  114. package/src/providers/codex/modes.ts +159 -0
  115. package/src/providers/codex/parse.ts +691 -0
  116. package/src/providers/codex/plan-mode.ts +49 -0
  117. package/src/providers/codex/session.ts +1287 -0
  118. package/src/providers/codex/transcript-normalize.ts +197 -0
  119. package/src/providers/codex/transcript.ts +487 -0
  120. package/src/providers/codex/usage-scanner.ts +178 -0
  121. package/src/providers/copilot/index.ts +19 -0
  122. package/src/providers/cursor/codec.ts +44 -0
  123. package/src/providers/cursor/execute.ts +271 -0
  124. package/src/providers/cursor/index.ts +25 -0
  125. package/src/providers/cursor/parse.ts +288 -0
  126. package/src/providers/gemini/index.ts +21 -0
  127. package/src/providers/openclaw/codec.ts +40 -0
  128. package/src/providers/openclaw/execute.ts +19 -0
  129. package/src/providers/openclaw/index.ts +29 -0
  130. package/src/providers/opencode/codec.ts +50 -0
  131. package/src/providers/opencode/event-parse.ts +141 -0
  132. package/src/providers/opencode/execute.ts +251 -0
  133. package/src/providers/opencode/http-session.ts +427 -0
  134. package/src/providers/opencode/index.ts +30 -0
  135. package/src/providers/opencode/parse.ts +203 -0
  136. package/src/providers/opencode/server.ts +0 -0
  137. package/src/providers/pi/codec.ts +44 -0
  138. package/src/providers/pi/execute.ts +297 -0
  139. package/src/providers/pi/index.ts +30 -0
  140. package/src/providers/pi/parse.ts +231 -0
  141. package/src/providers/pi/session.ts +381 -0
  142. package/src/providers/process/execute.ts +148 -0
  143. package/src/providers/process/index.ts +52 -0
  144. package/src/registry.ts +40 -0
  145. package/src/sessions/index.ts +8 -0
  146. package/src/sessions/record.ts +108 -0
  147. package/src/types.ts +1638 -0
  148. package/src/utils/ask-user-question.ts +57 -0
  149. package/src/utils/auth.ts +661 -0
  150. package/src/utils/binary.ts +179 -0
  151. package/src/utils/endpoint.ts +172 -0
  152. package/src/utils/env.ts +63 -0
  153. package/src/utils/execute-all.ts +68 -0
  154. package/src/utils/exit-plan-mode.ts +40 -0
  155. package/src/utils/instructions.ts +427 -0
  156. package/src/utils/process.ts +223 -0
  157. package/src/utils/runtime-config.ts +100 -0
  158. package/src/utils/runtime-homes.ts +49 -0
  159. package/src/utils/skill-commands.ts +493 -0
  160. package/src/utils/skills.ts +500 -0
  161. package/src/utils/template.ts +16 -0
  162. package/src/utils/tool-names.ts +51 -0
  163. package/src/utils/uuid.ts +21 -0
  164. package/src/utils/workspace.ts +156 -0
@@ -0,0 +1,203 @@
1
+ import type { BaseStreamEventFields, StreamEvent } from "../../types.js";
2
+
3
+ const PROVIDER_TYPE = "opencode";
4
+
5
+ function stubBase(event: Record<string, unknown>, sessionId: string | null = null): BaseStreamEventFields {
6
+ return {
7
+ timestamp: new Date().toISOString(),
8
+ providerType: PROVIDER_TYPE,
9
+ sessionId,
10
+ messageId: null,
11
+ eventId: null,
12
+ turnId: null,
13
+ parentToolCallId: null,
14
+ raw: event,
15
+ };
16
+ }
17
+
18
+ export interface OpenCodeParsedResult {
19
+ sessionId: string | null;
20
+ model: string | null;
21
+ usage: { inputTokens: number; outputTokens: number; cachedInputTokens: number } | null;
22
+ costUsd: number | null;
23
+ summary: string | null;
24
+ isError: boolean;
25
+ errorMessage: string | null;
26
+ }
27
+
28
+ function parseJson(line: string): Record<string, unknown> | null {
29
+ try {
30
+ const parsed = JSON.parse(line);
31
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
32
+ return parsed as Record<string, unknown>;
33
+ }
34
+ } catch {
35
+ // Skip malformed lines
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function asString(value: unknown, fallback: string): string {
41
+ return typeof value === "string" ? value : fallback;
42
+ }
43
+
44
+ function asNumber(value: unknown, fallback: number): number {
45
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
46
+ }
47
+
48
+ function parseObject(value: unknown): Record<string, unknown> {
49
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
50
+ return value as Record<string, unknown>;
51
+ }
52
+ return {};
53
+ }
54
+
55
+ export function parseOpenCodeJsonl(stdout: string): OpenCodeParsedResult {
56
+ let sessionId: string | null = null;
57
+ const messages: string[] = [];
58
+ const errors: string[] = [];
59
+ const usage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 };
60
+ let costUsd = 0;
61
+ let hasUsage = false;
62
+
63
+ for (const rawLine of stdout.split(/\r?\n/)) {
64
+ const line = rawLine.trim();
65
+ if (!line) continue;
66
+
67
+ const event = parseJson(line);
68
+ if (!event) continue;
69
+
70
+ const currentSessionId = asString(event["sessionID"], "").trim();
71
+ if (currentSessionId) sessionId = currentSessionId;
72
+
73
+ const type = asString(event["type"], "");
74
+
75
+ if (type === "text") {
76
+ const part = parseObject(event["part"]);
77
+ const text = asString(part["text"], "").trim();
78
+ if (text) messages.push(text);
79
+ continue;
80
+ }
81
+
82
+ if (type === "step_finish") {
83
+ const part = parseObject(event["part"]);
84
+ const tokens = parseObject(part["tokens"]);
85
+ const cache = parseObject(tokens["cache"]);
86
+ usage.inputTokens += asNumber(tokens["input"], 0);
87
+ usage.cachedInputTokens += asNumber(cache["read"], 0);
88
+ usage.outputTokens += asNumber(tokens["output"], 0) + asNumber(tokens["reasoning"], 0);
89
+ costUsd += asNumber(part["cost"], 0);
90
+ hasUsage = true;
91
+ continue;
92
+ }
93
+
94
+ if (type === "tool_use") {
95
+ const part = parseObject(event["part"]);
96
+ const state = parseObject(part["state"]);
97
+ if (asString(state["status"], "") === "error") {
98
+ const text = asString(state["error"], "").trim();
99
+ if (text) errors.push(text);
100
+ }
101
+ continue;
102
+ }
103
+
104
+ if (type === "error") {
105
+ const text = asString(event["message"] as string ?? event["error"] as string, "").trim();
106
+ if (text) errors.push(text);
107
+ continue;
108
+ }
109
+ }
110
+
111
+ return {
112
+ sessionId,
113
+ model: null,
114
+ usage: hasUsage ? usage : null,
115
+ costUsd: costUsd > 0 ? costUsd : null,
116
+ summary: messages.join("\n\n").trim() || null,
117
+ isError: errors.length > 0,
118
+ errorMessage: errors.length > 0 ? errors.join("\n") : null,
119
+ };
120
+ }
121
+
122
+ export function parseOpenCodeStreamLine(line: string): StreamEvent | null {
123
+ const event = parseJson(line);
124
+ if (!event) return null;
125
+
126
+ const type = asString(event["type"], "");
127
+ const sessionId = asString(event["sessionID"], "").trim() || null;
128
+ const base = stubBase(event, sessionId);
129
+
130
+ if (type === "text") {
131
+ const part = parseObject(event["part"]);
132
+ const text = asString(part["text"], "");
133
+ if (text) return { type: "assistant", text, ...base };
134
+ }
135
+
136
+ if (type === "tool_use") {
137
+ const part = parseObject(event["part"]);
138
+ const name = asString(part["name"], "");
139
+ const state = parseObject(part["state"]);
140
+ const status = asString(state["status"], "");
141
+ if (status === "error" || status === "completed") {
142
+ return {
143
+ type: "tool_result",
144
+ toolCallId: asString(part["id"], "") || asString(part["tool_use_id"], "") || null,
145
+ toolName: name || null,
146
+ content: status === "error" ? asString(state["error"], "") : asString(state["result"], ""),
147
+ isError: status === "error",
148
+ exitCode: null,
149
+ ...base,
150
+ };
151
+ }
152
+ return {
153
+ type: "tool_call",
154
+ toolCallId: asString(part["id"], "") || asString(part["tool_use_id"], "") || null,
155
+ name,
156
+ input: part["input"],
157
+ ...base,
158
+ };
159
+ }
160
+
161
+ if (type === "step_finish") {
162
+ return {
163
+ type: "result",
164
+ text: "",
165
+ costUsd: null,
166
+ isError: false,
167
+ stopReason: null,
168
+ terminalReason: null,
169
+ numTurns: null,
170
+ durationMs: null,
171
+ ...base,
172
+ };
173
+ }
174
+
175
+ if (type === "error") {
176
+ return {
177
+ type: "result",
178
+ text: asString(event["message"], ""),
179
+ costUsd: null,
180
+ isError: true,
181
+ stopReason: null,
182
+ terminalReason: null,
183
+ numTurns: null,
184
+ durationMs: null,
185
+ ...base,
186
+ };
187
+ }
188
+
189
+ // Forward-compat: surface unknown wire events rather than dropping them.
190
+ return { type: "unknown", subtype: type, ...base };
191
+ }
192
+
193
+ const OPENCODE_UNKNOWN_SESSION_RE = /unknown\s+session|session\b.*\bnot\s+found|no session/i;
194
+
195
+ export function isOpenCodeUnknownSessionError(stdout: string, stderr: string): boolean {
196
+ return OPENCODE_UNKNOWN_SESSION_RE.test(stdout) || OPENCODE_UNKNOWN_SESSION_RE.test(stderr);
197
+ }
198
+
199
+ const OPENCODE_AUTH_RE = /api[_ ]?key\s+(?:required|missing|not\s+set|invalid)|unauthorized|authentication.*required/i;
200
+
201
+ export function isOpenCodeAuthRequired(stdout: string, stderr: string): boolean {
202
+ return OPENCODE_AUTH_RE.test(stdout) || OPENCODE_AUTH_RE.test(stderr);
203
+ }
Binary file
@@ -0,0 +1,44 @@
1
+ import type { SessionCodec } from "../../types.js";
2
+
3
+ function readNonEmptyString(value: unknown): string | null {
4
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
5
+ }
6
+
7
+ function parseAsObject(raw: unknown): Record<string, unknown> | null {
8
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
9
+ return raw as Record<string, unknown>;
10
+ }
11
+
12
+ /**
13
+ * Pi uses file-path-based sessions (e.g. ~/.pi/paperclips/2024-01-01T00-00-00-agentId.jsonl).
14
+ * The sessionId stored here is the file path to the session file.
15
+ */
16
+ export const piSessionCodec: SessionCodec = {
17
+ deserialize(raw: unknown): Record<string, unknown> | null {
18
+ const obj = parseAsObject(raw);
19
+ if (!obj) return null;
20
+ const sessionId = readNonEmptyString(obj["sessionId"]) ?? readNonEmptyString(obj["session_id"]);
21
+ if (!sessionId) return null;
22
+ const cwd = readNonEmptyString(obj["cwd"]);
23
+ return {
24
+ sessionId,
25
+ ...(cwd ? { cwd } : {}),
26
+ };
27
+ },
28
+
29
+ serialize(params: Record<string, unknown> | null): Record<string, unknown> | null {
30
+ if (!params) return null;
31
+ const sessionId = readNonEmptyString(params["sessionId"]) ?? readNonEmptyString(params["session_id"]);
32
+ if (!sessionId) return null;
33
+ const cwd = readNonEmptyString(params["cwd"]);
34
+ return {
35
+ sessionId,
36
+ ...(cwd ? { cwd } : {}),
37
+ };
38
+ },
39
+
40
+ getDisplayId(params: Record<string, unknown> | null): string | null {
41
+ if (!params) return null;
42
+ return readNonEmptyString(params["sessionId"]) ?? readNonEmptyString(params["session_id"]);
43
+ },
44
+ };
@@ -0,0 +1,297 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import type { ExecutionContext, ExecutionResult } from "../../types.js";
5
+ import { findBinary } from "../../utils/binary.js";
6
+ import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
7
+ import { runChildProcess, deriveErrorCode } from "../../utils/process.js";
8
+ import { injectHomeSkills } from "../../utils/skills.js";
9
+ import { resolveInstructions } from "../../utils/instructions.js";
10
+ import { prepareWorkspace } from "../../utils/workspace.js";
11
+ import type { PreparedWorkspace } from "../../utils/workspace.js";
12
+ import { uuidv7 } from "../../utils/uuid.js";
13
+ import { parsePiJsonl, parsePiStreamLine, isPiUnknownSessionError } from "./parse.js";
14
+
15
+ const PI_SESSIONS_DIR = path.join(os.homedir(), ".pi", "sessions");
16
+
17
+ function parseModelProvider(model: string | null): string | null {
18
+ if (!model) return null;
19
+ const trimmed = model.trim();
20
+ if (!trimmed.includes("/")) return null;
21
+ return trimmed.slice(0, trimmed.indexOf("/")).trim() || null;
22
+ }
23
+
24
+ function parseModelId(model: string | null): string | null {
25
+ if (!model) return null;
26
+ const trimmed = model.trim();
27
+ if (!trimmed.includes("/")) return trimmed || null;
28
+ return trimmed.slice(trimmed.indexOf("/") + 1).trim() || null;
29
+ }
30
+
31
+ function buildSessionPath(runId: string): string {
32
+ const safeTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
33
+ return path.join(PI_SESSIONS_DIR, `${safeTimestamp}-${runId}.jsonl`);
34
+ }
35
+
36
+ export async function executePiProvider(ctx: ExecutionContext): Promise<ExecutionResult> {
37
+ const runId = ctx.runId ?? uuidv7();
38
+ let cwd = ctx.cwd ?? process.cwd();
39
+ const model = ctx.model ?? ctx.config?.model ?? "";
40
+ const config = ctx.config ?? {};
41
+ const startedAt = new Date().toISOString();
42
+
43
+ // 1. Resolve binary
44
+ ctx.onLifecycle?.({ phase: "preparing", step: "binary" });
45
+ let resolvedBinary;
46
+ try {
47
+ resolvedBinary = await findBinary("pi", config.command);
48
+ } catch (err) {
49
+ const errorMessage = err instanceof Error ? err.message : "Binary not found";
50
+ ctx.onLifecycle?.({ phase: "error", message: errorMessage });
51
+ return {
52
+ runId,
53
+ exitCode: null,
54
+ signal: null,
55
+ status: "failed" as const,
56
+ startedAt,
57
+ completedAt: new Date().toISOString(),
58
+ durationMs: Date.now() - new Date(startedAt).getTime(),
59
+ errorMessage,
60
+ errorCode: "binary_not_found",
61
+ costUsd: null,
62
+ model: null,
63
+ summary: null,
64
+ sessionParams: null,
65
+ sessionDisplayId: null,
66
+ clearSession: false,
67
+ billingType: null,
68
+ };
69
+ }
70
+
71
+ // 2. Workspace isolation
72
+ let workspace: PreparedWorkspace | undefined;
73
+ if (config.workspace) {
74
+ ctx.onLifecycle?.({ phase: "preparing", step: "workspace" });
75
+ workspace = await prepareWorkspace(cwd, config.workspace);
76
+ cwd = workspace.cwd;
77
+ }
78
+
79
+ // 3. Build env
80
+ const env = buildEnv(ctx.env);
81
+ ensurePathInEnv(env);
82
+
83
+ // 3. Resolve instructions — Pi uses native --append-system-prompt flag,
84
+ // but we still validate the file exists early so failures are clear.
85
+ ctx.onLifecycle?.({ phase: "preparing", step: "instructions" });
86
+ if (config.instructionsFile) await resolveInstructions(config.instructionsFile);
87
+
88
+ // 4. Inject skills into ~/.pi/agent/skills/
89
+ ctx.onLifecycle?.({ phase: "preparing", step: "skills" });
90
+ let piSkillsDir: string | null = null;
91
+ if (config.skillDirs && config.skillDirs.length > 0) {
92
+ try {
93
+ piSkillsDir = await injectHomeSkills(config.skillDirs, "pi");
94
+ } catch {
95
+ // Non-fatal
96
+ }
97
+ }
98
+
99
+ // 4. Ensure sessions directory exists
100
+ await fs.mkdir(PI_SESSIONS_DIR, { recursive: true });
101
+
102
+ // 5. Determine session resume
103
+ const sessionParams = ctx.sessionParams ?? null;
104
+ const existingSessionId = (() => {
105
+ if (!sessionParams) return null;
106
+ const id =
107
+ (sessionParams["sessionId"] as string | undefined) ??
108
+ (sessionParams["session_id"] as string | undefined);
109
+ if (!id || typeof id !== "string") return null;
110
+ const sessionCwd = sessionParams["cwd"] as string | undefined;
111
+ if (sessionCwd && path.resolve(sessionCwd) !== path.resolve(cwd)) return null;
112
+ return id;
113
+ })();
114
+ const canResume = existingSessionId !== null;
115
+ const sessionPath = canResume ? existingSessionId! : buildSessionPath(runId);
116
+
117
+ // Create session file if new
118
+ if (!canResume) {
119
+ try {
120
+ await fs.writeFile(sessionPath, "", { flag: "wx" });
121
+ } catch (err) {
122
+ if ((err as NodeJS.ErrnoException).code !== "EEXIST") {
123
+ // Non-fatal, Pi may handle it
124
+ }
125
+ }
126
+ }
127
+
128
+ // 6. Parse model into provider/modelId
129
+ const provider = parseModelProvider(model || null);
130
+ const modelId = parseModelId(model || null);
131
+
132
+ // 7. Build args
133
+ const buildArgs = (sessionFile: string): string[] => {
134
+ const args = [...resolvedBinary.prefixArgs, "--mode", "rpc"];
135
+ if (provider) args.push("--provider", provider);
136
+ if (modelId) args.push("--model", modelId);
137
+ if (config.thinking) args.push("--thinking", config.thinking);
138
+ args.push("--tools", "read,bash,edit,write,grep,find,ls");
139
+ args.push("--session", sessionFile);
140
+ if (piSkillsDir) args.push("--skill", piSkillsDir);
141
+ if (config.instructionsFile) args.push("--append-system-prompt", config.instructionsFile);
142
+ if (config.extraArgs) args.push(...config.extraArgs);
143
+ return args;
144
+ };
145
+
146
+ // 8. Build RPC stdin
147
+ const rpcStdin = JSON.stringify({ type: "prompt", message: ctx.prompt }) + "\n";
148
+
149
+ // 9. Run attempt
150
+ const runAttempt = async (sessionFile: string) => {
151
+ const args = buildArgs(sessionFile);
152
+ let stdoutBuffer = "";
153
+
154
+ ctx.onLifecycle?.({ phase: "spawning" });
155
+ const proc = await runChildProcess({
156
+ runId,
157
+ command: resolvedBinary.bin,
158
+ args,
159
+ cwd,
160
+ env,
161
+ stdin: rpcStdin,
162
+ timeoutSec: config.timeoutSec,
163
+ graceSec: config.graceSec,
164
+ onStart: (pid) => {
165
+ ctx.onLifecycle?.({ phase: "running", pid });
166
+ ctx.onStart?.(pid);
167
+ },
168
+ signal: ctx.signal,
169
+ onOutput: async (stream, chunk) => {
170
+ if (stream === "stderr") {
171
+ if (ctx.onOutput) {
172
+ try { await ctx.onOutput(stream, chunk); } catch { /* swallow */ }
173
+ }
174
+ return;
175
+ }
176
+
177
+ // Buffer stdout by lines for JSONL parsing
178
+ stdoutBuffer += chunk;
179
+ const lines = stdoutBuffer.split("\n");
180
+ stdoutBuffer = lines.pop() || "";
181
+
182
+ for (const line of lines) {
183
+ if (!line) continue;
184
+ if (ctx.onOutput) {
185
+ try { await ctx.onOutput(stream, line + "\n"); } catch { /* swallow */ }
186
+ }
187
+ if (ctx.onEvent) {
188
+ const event = parsePiStreamLine(line);
189
+ if (event) {
190
+ try { await ctx.onEvent(event); } catch { /* swallow */ }
191
+ }
192
+ }
193
+ }
194
+ },
195
+ });
196
+
197
+ // Flush remaining buffer
198
+ if (stdoutBuffer) {
199
+ if (ctx.onOutput) {
200
+ try { await ctx.onOutput("stdout", stdoutBuffer); } catch { /* swallow */ }
201
+ }
202
+ if (ctx.onEvent) {
203
+ const event = parsePiStreamLine(stdoutBuffer);
204
+ if (event) {
205
+ try { await ctx.onEvent(event); } catch { /* swallow */ }
206
+ }
207
+ }
208
+ }
209
+
210
+ return proc;
211
+ };
212
+
213
+ // 10. Initial attempt
214
+ let proc = await runAttempt(sessionPath);
215
+ let clearSession = false;
216
+ let finalSessionPath = sessionPath;
217
+
218
+ // 11. Check for unknown session — retry once with new session
219
+ if (
220
+ canResume &&
221
+ !proc.timedOut &&
222
+ (proc.exitCode ?? 0) !== 0 &&
223
+ isPiUnknownSessionError(proc.stdout, proc.stderr)
224
+ ) {
225
+ finalSessionPath = buildSessionPath(runId + "-retry");
226
+ try {
227
+ await fs.writeFile(finalSessionPath, "", { flag: "wx" });
228
+ } catch {
229
+ // Non-fatal
230
+ }
231
+ proc = await runAttempt(finalSessionPath);
232
+ clearSession = true;
233
+ }
234
+
235
+ // 12. Parse result
236
+ const parsed = parsePiJsonl(proc.stdout);
237
+ const processErrorCode = deriveErrorCode(proc);
238
+
239
+ let errorCode = processErrorCode;
240
+ if (!errorCode && parsed.errorMessage) {
241
+ errorCode = "execution_error";
242
+ }
243
+
244
+ const errorMessage = (() => {
245
+ if (proc.timedOut) return `Timed out after ${config.timeoutSec ?? 0}s`;
246
+ if (parsed.errorMessage) return parsed.errorMessage;
247
+ if ((proc.exitCode ?? 0) !== 0 && !parsed.summary) {
248
+ const stderrLine = proc.stderr.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
249
+ return stderrLine ?? `Pi exited with code ${proc.exitCode ?? -1}`;
250
+ }
251
+ return null;
252
+ })();
253
+
254
+ // Always return the current session path so callers can continue.
255
+ // clearSession tells callers the OLD session was invalid, but we still
256
+ // provide the NEW session handle from the retry.
257
+ const resultSessionParams = { sessionId: finalSessionPath, cwd };
258
+
259
+ const completedAt = new Date().toISOString();
260
+ const resolvedModel = model || null;
261
+ const status = proc.aborted ? "aborted" as const
262
+ : proc.timedOut ? "timeout" as const
263
+ : (errorCode || errorMessage) ? "failed" as const
264
+ : "completed" as const;
265
+
266
+ if (status === "completed") {
267
+ ctx.onLifecycle?.({ phase: "completed" });
268
+ } else if (status === "aborted") {
269
+ ctx.onLifecycle?.({ phase: "cancelled" });
270
+ } else {
271
+ ctx.onLifecycle?.({ phase: "error", message: errorMessage ?? "Unknown error" });
272
+ }
273
+
274
+ return {
275
+ runId,
276
+ exitCode: proc.exitCode,
277
+ signal: proc.signal ?? null,
278
+ status,
279
+ startedAt,
280
+ completedAt,
281
+ durationMs: new Date(completedAt).getTime() - new Date(startedAt).getTime(),
282
+ errorMessage,
283
+ errorCode,
284
+ usage: parsed.usage && resolvedModel
285
+ ? { [resolvedModel]: { inputTokens: parsed.usage.inputTokens, outputTokens: parsed.usage.outputTokens, cachedInputTokens: parsed.usage.cachedInputTokens } }
286
+ : undefined,
287
+ costUsd: parsed.usage?.costUsd ?? null,
288
+ model: resolvedModel,
289
+ summary: parsed.summary,
290
+ sessionParams: resultSessionParams,
291
+ sessionDisplayId: finalSessionPath,
292
+ clearSession,
293
+ billingType: "api",
294
+ raw: null,
295
+ workspace,
296
+ };
297
+ }
@@ -0,0 +1,30 @@
1
+ import type { ProviderModule, SessionContext, AgentSession } from "../../types.js";
2
+ import { piSessionCodec } from "./codec.js";
3
+ import { resolveAuthForProvider } from "../../utils/auth.js";
4
+ import { EMULATED_GOAL_CAPABILITY } from "../../goals/index.js";
5
+
6
+ export const piProvider: ProviderModule = {
7
+ type: "pi",
8
+ capabilities: {
9
+ sessions: true,
10
+ modelDiscovery: false,
11
+ quotaProbing: false,
12
+ mcp: false,
13
+ skills: true,
14
+ instructions: true,
15
+ workspace: true,
16
+ planMode: false,
17
+ concurrentSend: false,
18
+ cancelQueuedMessage: false,
19
+ stopTask: false,
20
+ modes: false,
21
+ goals: EMULATED_GOAL_CAPABILITY,
22
+ },
23
+ // One-shot `pi --mode rpc` for execute(); a persistent `pi --mode rpc` process
24
+ // across turns for createSession(). Both load lazily on first use.
25
+ execute: async (ctx) => (await import("./execute.js")).executePiProvider(ctx),
26
+ createSession: async (ctx: SessionContext): Promise<AgentSession> =>
27
+ (await import("./session.js")).createPiSession(ctx),
28
+ resolveAuth: (ctx) => resolveAuthForProvider("pi", ctx),
29
+ sessionCodec: piSessionCodec,
30
+ };