@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,824 @@
1
+ import type {
2
+ AuthRequiredReason,
3
+ BaseStreamEventFields,
4
+ ModelUsage,
5
+ RateLimitInfo,
6
+ StreamEvent,
7
+ } from "../../types.js";
8
+ import { normalizeClaudeGoalAttachment, type NormalizedGoalFields } from "../../goals/normalize.js";
9
+
10
+ const PROVIDER_TYPE = "claude";
11
+
12
+ /** Shell command users run outside an interactive Claude session to
13
+ * re-authenticate. Surfaced on `auth_required` events. The in-CLI slash
14
+ * command is `/login`; this is the equivalent for hosts that are spawning
15
+ * `claude` as a subprocess and need to prompt the user to log in
16
+ * externally. */
17
+ export const CLAUDE_LOGIN_COMMAND = "claude auth login";
18
+
19
+ export interface ClaudeParsedResult {
20
+ sessionId: string | null;
21
+ model: string | null;
22
+ /**
23
+ * Per-model usage from Claude's `modelUsage` result payload (rich) or
24
+ * synthesized from the final `result.usage` when `modelUsage` is absent.
25
+ */
26
+ modelUsage: Record<string, ModelUsage> | null;
27
+ costUsd: number | null;
28
+ summary: string | null;
29
+ isError: boolean;
30
+ errorCode: string | null;
31
+ /** Populated when the final result event carries these fields. */
32
+ stopReason: string | null;
33
+ terminalReason: string | null;
34
+ numTurns: number | null;
35
+ durationApiMs: number | null;
36
+ permissionDenials: unknown[] | null;
37
+ rateLimits: RateLimitInfo[];
38
+ /** The final `result` event verbatim, or null if none was seen. */
39
+ finalEvent: Record<string, unknown> | null;
40
+ }
41
+
42
+ function parseJson(line: string): Record<string, unknown> | null {
43
+ try {
44
+ const parsed = JSON.parse(line);
45
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
46
+ return parsed as Record<string, unknown>;
47
+ }
48
+ } catch {
49
+ // Skip malformed lines
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function asString(value: unknown, fallback: string): string {
55
+ return typeof value === "string" ? value : fallback;
56
+ }
57
+
58
+ function asNullableString(value: unknown): string | null {
59
+ return typeof value === "string" && value.length > 0 ? value : null;
60
+ }
61
+
62
+ function asStringArray(value: unknown): string[] | undefined {
63
+ if (!Array.isArray(value)) return undefined;
64
+ return value.filter((item): item is string => typeof item === "string");
65
+ }
66
+
67
+ function asNumber(value: unknown, fallback: number): number {
68
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
69
+ }
70
+
71
+ function asNullableNumber(value: unknown): number | null {
72
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
73
+ }
74
+
75
+ function parseObject(value: unknown): Record<string, unknown> {
76
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
77
+ return value as Record<string, unknown>;
78
+ }
79
+ return {};
80
+ }
81
+
82
+ function baseFieldsFromEvent(
83
+ event: Record<string, unknown>,
84
+ messageId: string | null,
85
+ ): BaseStreamEventFields {
86
+ return {
87
+ timestamp: new Date().toISOString(),
88
+ providerType: PROVIDER_TYPE,
89
+ sessionId: asNullableString(event["session_id"]),
90
+ messageId,
91
+ eventId: asNullableString(event["uuid"]),
92
+ turnId: null, // Claude doesn't model turns explicitly
93
+ parentToolCallId: asNullableString(event["parent_tool_use_id"]),
94
+ raw: event,
95
+ };
96
+ }
97
+
98
+ /** Build a normalized `goal_status` event from Claude goal fields + envelope. */
99
+ function goalEventFromFields(
100
+ fields: NormalizedGoalFields,
101
+ event: Record<string, unknown>,
102
+ ): StreamEvent {
103
+ const ev: Extract<StreamEvent, { type: "goal_status" }> = {
104
+ type: "goal_status",
105
+ objective: fields.objective,
106
+ status: fields.status,
107
+ met: fields.met,
108
+ enforced: fields.enforced,
109
+ source: fields.source,
110
+ ...baseFieldsFromEvent(event, null),
111
+ };
112
+ if (fields.blockedReason !== undefined) ev.blockedReason = fields.blockedReason;
113
+ return ev;
114
+ }
115
+
116
+ function rateLimitFromEvent(event: Record<string, unknown>): RateLimitInfo | null {
117
+ const info = parseObject(event["rate_limit_info"]);
118
+ if (Object.keys(info).length === 0) return null;
119
+ const resetsAt = info["resetsAt"];
120
+ const overageResetsAt = info["overageResetsAt"];
121
+ const resetEpoch =
122
+ typeof resetsAt === "number" && Number.isFinite(resetsAt) ? resetsAt :
123
+ typeof overageResetsAt === "number" && Number.isFinite(overageResetsAt) ? overageResetsAt :
124
+ null;
125
+ return {
126
+ status: asString(info["status"], "unknown"),
127
+ limitType: asNullableString(info["rateLimitType"]),
128
+ resetAt: resetEpoch !== null ? new Date(resetEpoch * 1000).toISOString() : null,
129
+ overageStatus: asNullableString(info["overageStatus"]),
130
+ isUsingOverage: typeof info["isUsingOverage"] === "boolean" ? info["isUsingOverage"] : null,
131
+ };
132
+ }
133
+
134
+ function modelUsageFromResult(
135
+ result: Record<string, unknown>,
136
+ fallbackModel: string | null,
137
+ ): Record<string, ModelUsage> | null {
138
+ const perModel = parseObject(result["modelUsage"]);
139
+ const modelKeys = Object.keys(perModel);
140
+ if (modelKeys.length > 0) {
141
+ const out: Record<string, ModelUsage> = {};
142
+ for (const key of modelKeys) {
143
+ const entry = parseObject(perModel[key]);
144
+ const usage: ModelUsage = {
145
+ inputTokens: asNumber(entry["inputTokens"], 0),
146
+ outputTokens: asNumber(entry["outputTokens"], 0),
147
+ };
148
+ const cacheRead = asNumber(entry["cacheReadInputTokens"], 0);
149
+ if (cacheRead > 0) usage.cachedInputTokens = cacheRead;
150
+ const cacheCreation = asNumber(entry["cacheCreationInputTokens"], 0);
151
+ if (cacheCreation > 0) usage.cacheCreationInputTokens = cacheCreation;
152
+ const cost = asNullableNumber(entry["costUSD"]);
153
+ if (cost !== null) usage.costUsd = cost;
154
+ const webSearch = asNumber(entry["webSearchRequests"], 0);
155
+ if (webSearch > 0) usage.webSearchRequests = webSearch;
156
+ const ctxWindow = asNullableNumber(entry["contextWindow"]);
157
+ if (ctxWindow !== null) usage.contextWindow = ctxWindow;
158
+ const maxOut = asNullableNumber(entry["maxOutputTokens"]);
159
+ if (maxOut !== null) usage.maxOutputTokens = maxOut;
160
+ out[key] = usage;
161
+ }
162
+ return out;
163
+ }
164
+
165
+ // Fallback: synthesize single-model usage from `result.usage` if present.
166
+ const usageObj = parseObject(result["usage"]);
167
+ if (Object.keys(usageObj).length === 0) return null;
168
+ const model = asNullableString(result["model"]) ?? fallbackModel;
169
+ if (!model) return null;
170
+ const usage: ModelUsage = {
171
+ inputTokens: asNumber(usageObj["input_tokens"], 0),
172
+ outputTokens: asNumber(usageObj["output_tokens"], 0),
173
+ };
174
+ const cacheRead = asNumber(usageObj["cache_read_input_tokens"], 0);
175
+ if (cacheRead > 0) usage.cachedInputTokens = cacheRead;
176
+ const cacheCreation = asNumber(usageObj["cache_creation_input_tokens"], 0);
177
+ if (cacheCreation > 0) usage.cacheCreationInputTokens = cacheCreation;
178
+ return { [model]: usage };
179
+ }
180
+
181
+ export function parseClaudeStreamJson(stdout: string): ClaudeParsedResult {
182
+ let sessionId: string | null = null;
183
+ let model: string | null = null;
184
+ let finalResult: Record<string, unknown> | null = null;
185
+ const assistantTexts: string[] = [];
186
+ const rateLimits: RateLimitInfo[] = [];
187
+
188
+ for (const rawLine of stdout.split(/\r?\n/)) {
189
+ const line = rawLine.trim();
190
+ if (!line) continue;
191
+ const event = parseJson(line);
192
+ if (!event) continue;
193
+
194
+ const type = asString(event["type"], "");
195
+
196
+ if (type === "system" && asString(event["subtype"], "") === "init") {
197
+ sessionId = asNullableString(event["session_id"]) ?? sessionId;
198
+ model = asNullableString(event["model"]) ?? model;
199
+ continue;
200
+ }
201
+
202
+ if (type === "rate_limit_event") {
203
+ const info = rateLimitFromEvent(event);
204
+ if (info) rateLimits.push(info);
205
+ continue;
206
+ }
207
+
208
+ if (type === "assistant") {
209
+ sessionId = asNullableString(event["session_id"]) ?? sessionId;
210
+ const message = parseObject(event["message"]);
211
+ const content = Array.isArray(message["content"]) ? message["content"] : [];
212
+ for (const entry of content) {
213
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
214
+ const block = entry as Record<string, unknown>;
215
+ if (asString(block["type"], "") === "text") {
216
+ const text = asString(block["text"], "");
217
+ if (text) assistantTexts.push(text);
218
+ }
219
+ }
220
+ continue;
221
+ }
222
+
223
+ if (type === "result") {
224
+ finalResult = event;
225
+ sessionId = asNullableString(event["session_id"]) ?? sessionId;
226
+ }
227
+ }
228
+
229
+ if (!finalResult) {
230
+ return {
231
+ sessionId,
232
+ model,
233
+ costUsd: null,
234
+ modelUsage: null,
235
+ summary: assistantTexts.join("\n\n").trim() || null,
236
+ isError: false,
237
+ errorCode: null,
238
+ stopReason: null,
239
+ terminalReason: null,
240
+ numTurns: null,
241
+ durationApiMs: null,
242
+ permissionDenials: null,
243
+ rateLimits,
244
+ finalEvent: null,
245
+ };
246
+ }
247
+
248
+ const modelUsage = modelUsageFromResult(finalResult, model);
249
+ const costRaw = finalResult["total_cost_usd"];
250
+ const costUsd = typeof costRaw === "number" && Number.isFinite(costRaw) ? costRaw : null;
251
+ const summary = asString(finalResult["result"], assistantTexts.join("\n\n")).trim() || null;
252
+ const isError = finalResult["is_error"] === true;
253
+
254
+ let errorCode: string | null = null;
255
+ if (isClaudeMaxTurns(stdout)) {
256
+ errorCode = "max_turns";
257
+ } else if (classifyClaudeAuthFromResult(finalResult)) {
258
+ // Structured signal beats regex — set the canonical errorCode so
259
+ // execute.ts doesn't need a separate regex pass for the same fact.
260
+ errorCode = "auth_required";
261
+ }
262
+
263
+ const denials = finalResult["permission_denials"];
264
+ const permissionDenials = Array.isArray(denials) && denials.length > 0 ? denials : null;
265
+
266
+ return {
267
+ sessionId,
268
+ model,
269
+ costUsd,
270
+ modelUsage,
271
+ summary,
272
+ isError,
273
+ errorCode,
274
+ stopReason: asNullableString(finalResult["stop_reason"]),
275
+ terminalReason: asNullableString(finalResult["terminal_reason"]),
276
+ numTurns: asNullableNumber(finalResult["num_turns"]),
277
+ durationApiMs: asNullableNumber(finalResult["duration_api_ms"]),
278
+ permissionDenials,
279
+ rateLimits,
280
+ finalEvent: finalResult,
281
+ };
282
+ }
283
+
284
+ export function toStreamEvents(stdout: string): StreamEvent[] {
285
+ const events: StreamEvent[] = [];
286
+ const partial: PartialStreamContext = { messageId: null };
287
+ for (const rawLine of stdout.split(/\r?\n/)) {
288
+ const line = rawLine.trim();
289
+ if (!line) continue;
290
+ events.push(...parseStreamLine(line, partial));
291
+ }
292
+ return events;
293
+ }
294
+
295
+ /**
296
+ * Cross-line context for `--include-partial-messages` streams. The owning
297
+ * message id arrives only on the `message_start` stream_event, so the caller
298
+ * owns one of these per stream and threads it through every `parseStreamLine`
299
+ * call — deltas are then stamped with the `messageId` of the message they
300
+ * belong to, matching the eventual consolidated `assistant` event so hosts can
301
+ * reconcile optimistic delta text against the durable event.
302
+ */
303
+ export interface PartialStreamContext {
304
+ messageId: string | null;
305
+ }
306
+
307
+ /**
308
+ * Map a `stream_event` wrapper line into delta events.
309
+ *
310
+ * Emitted (when carrying text):
311
+ * - `content_block_delta` + `text_delta` → `assistant_delta`
312
+ * - `content_block_delta` + `thinking_delta` → `thinking_delta` (best-effort —
313
+ * on recent Claude versions these deltas are the only place thinking prose
314
+ * appears, since the consolidated thinking block is withheld)
315
+ *
316
+ * Known scaffolding (`message_start`/`message_stop` — which also maintain the
317
+ * cross-line message id — `content_block_start`/`stop`, `message_delta`,
318
+ * `signature_delta`, `ping`) is consumed silently rather than spamming one
319
+ * `unknown` event per token. Genuinely unrecognized subtypes still surface as
320
+ * `unknown` for forward-compat.
321
+ */
322
+ function parsePartialStreamEvent(
323
+ wrapper: Record<string, unknown>,
324
+ partial: PartialStreamContext | undefined,
325
+ ): StreamEvent[] {
326
+ const ev =
327
+ typeof wrapper["event"] === "object" && wrapper["event"] !== null && !Array.isArray(wrapper["event"])
328
+ ? (wrapper["event"] as Record<string, unknown>)
329
+ : null;
330
+ if (!ev) return [];
331
+ const evType = asString(ev["type"], "");
332
+
333
+ if (evType === "message_start") {
334
+ if (partial) {
335
+ const msg =
336
+ typeof ev["message"] === "object" && ev["message"] !== null
337
+ ? (ev["message"] as Record<string, unknown>)
338
+ : null;
339
+ partial.messageId = msg && typeof msg["id"] === "string" ? msg["id"] : null;
340
+ }
341
+ return [];
342
+ }
343
+ if (evType === "message_stop") {
344
+ if (partial) partial.messageId = null;
345
+ return [];
346
+ }
347
+
348
+ if (evType === "content_block_delta") {
349
+ const delta =
350
+ typeof ev["delta"] === "object" && ev["delta"] !== null
351
+ ? (ev["delta"] as Record<string, unknown>)
352
+ : null;
353
+ const blockIndex = typeof ev["index"] === "number" ? ev["index"] : 0;
354
+ const base = baseFieldsFromEvent(wrapper, partial?.messageId ?? null);
355
+
356
+ if (delta?.["type"] === "text_delta" && typeof delta["text"] === "string" && delta["text"]) {
357
+ return [{ type: "assistant_delta", text: delta["text"], blockIndex, ...base }];
358
+ }
359
+ if (
360
+ delta?.["type"] === "thinking_delta" &&
361
+ typeof delta["thinking"] === "string" &&
362
+ delta["thinking"]
363
+ ) {
364
+ return [{ type: "thinking_delta", text: delta["thinking"], blockIndex, ...base }];
365
+ }
366
+ // signature_delta, empty deltas, input_json_delta, … — scaffolding.
367
+ return [];
368
+ }
369
+
370
+ if (
371
+ evType === "content_block_start" ||
372
+ evType === "content_block_stop" ||
373
+ evType === "message_delta" ||
374
+ evType === "ping"
375
+ ) {
376
+ return [];
377
+ }
378
+
379
+ // Unrecognized stream_event subtype — forward-compat escape hatch.
380
+ return [{
381
+ type: "unknown",
382
+ subtype: `stream_event:${evType || "unknown"}`,
383
+ ...baseFieldsFromEvent(wrapper, partial?.messageId ?? null),
384
+ }];
385
+ }
386
+
387
+ export function parseStreamLine(line: string, partial?: PartialStreamContext): StreamEvent[] {
388
+ const event = parseJson(line);
389
+ if (!event) return [];
390
+
391
+ const type = asString(event["type"], "");
392
+
393
+ // `--include-partial-messages` wraps raw API streaming events in
394
+ // `stream_event` lines. Only present when the caller opted in via
395
+ // `config.includePartialMessages` — with the flag off this branch never
396
+ // fires and parsing is bit-identical to previous behavior.
397
+ if (type === "stream_event") {
398
+ return parsePartialStreamEvent(event, partial);
399
+ }
400
+
401
+ if (type === "system" && asString(event["subtype"], "") === "init") {
402
+ return [{
403
+ type: "system",
404
+ subtype: "init",
405
+ model: asNullableString(event["model"]),
406
+ cwd: asNullableString(event["cwd"]),
407
+ tools: Array.isArray(event["tools"]) ? event["tools"] as string[] : null,
408
+ permissionMode: asNullableString(event["permissionMode"]),
409
+ slashCommands: asStringArray(event["slash_commands"]),
410
+ skills: asStringArray(event["skills"]),
411
+ ...baseFieldsFromEvent(event, null),
412
+ }];
413
+ }
414
+
415
+ if (type === "rate_limit_event") {
416
+ const info = rateLimitFromEvent(event);
417
+ if (!info) return [];
418
+ return [{
419
+ type: "rate_limit",
420
+ status: info.status,
421
+ limitType: info.limitType,
422
+ resetAt: info.resetAt,
423
+ overageStatus: info.overageStatus,
424
+ isUsingOverage: info.isUsingOverage,
425
+ ...baseFieldsFromEvent(event, null),
426
+ }];
427
+ }
428
+
429
+ if (type === "permission-mode") {
430
+ const mode = asNullableString(event["permissionMode"]);
431
+ if (!mode) return [];
432
+ return [{
433
+ type: "permission_mode",
434
+ permissionMode: mode,
435
+ ...baseFieldsFromEvent(event, null),
436
+ }];
437
+ }
438
+
439
+ // `/goal` writes a `goal_status` attachment recording the sentinel's view of
440
+ // the goal (`{type:"goal_status", met, sentinel, condition}`). It appears as a
441
+ // top-level `attachment` line in both the live stream and the on-disk
442
+ // transcript. Non-goal attachments fall through to the forward-compat sink.
443
+ if (type === "attachment") {
444
+ const fields = normalizeClaudeGoalAttachment(parseObject(event["attachment"]));
445
+ if (fields) return [goalEventFromFields(fields, event)];
446
+ }
447
+
448
+ if (type === "assistant") {
449
+ const message = parseObject(event["message"]);
450
+ // Claude emits a "synthetic" assistant message (model === "<synthetic>")
451
+ // with `error: "authentication_failed"` immediately before the failing
452
+ // `result` event when auth is broken. Its text duplicates the result
453
+ // text (e.g. "Invalid API key · Fix external API key") and confuses
454
+ // consumers that render assistant messages — drop it entirely. The
455
+ // auth_required signal is emitted from the result branch below where
456
+ // we also have `api_error_status` available.
457
+ if (
458
+ asString(event["error"], "") === "authentication_failed" &&
459
+ asString(message["model"], "") === "<synthetic>"
460
+ ) {
461
+ return [];
462
+ }
463
+ const messageId = asNullableString(message["id"]);
464
+ const base = baseFieldsFromEvent(event, messageId);
465
+ const out: StreamEvent[] = [];
466
+ const content = Array.isArray(message["content"]) ? message["content"] : [];
467
+ for (const entry of content) {
468
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
469
+ const block = entry as Record<string, unknown>;
470
+ const blockType = asString(block["type"], "");
471
+ if (blockType === "text") {
472
+ out.push({ type: "assistant", text: asString(block["text"], ""), ...base });
473
+ } else if (blockType === "thinking") {
474
+ out.push({ type: "thinking", text: asString(block["thinking"], ""), ...base });
475
+ } else if (blockType === "tool_use") {
476
+ out.push({
477
+ type: "tool_call",
478
+ toolCallId: asNullableString(block["id"]),
479
+ name: asString(block["name"], ""),
480
+ input: block["input"],
481
+ ...base,
482
+ });
483
+ } else if (blockType === "tool_result") {
484
+ out.push({
485
+ type: "tool_result",
486
+ toolCallId: asNullableString(block["tool_use_id"]),
487
+ // Claude's wire `tool_result` block carries only `tool_use_id`, not
488
+ // the tool name. The session/exec tool-name tracker fills this by
489
+ // correlating with the earlier `tool_call`.
490
+ toolName: null,
491
+ content: asString(block["content"], ""),
492
+ isError: block["is_error"] === true,
493
+ exitCode: null,
494
+ ...base,
495
+ });
496
+ }
497
+ }
498
+ return out;
499
+ }
500
+
501
+ if (type === "user") {
502
+ // Claude encodes tool results as a `user` event carrying tool_result blocks.
503
+ const message = parseObject(event["message"]);
504
+ const messageId = asNullableString(message["id"]);
505
+ const base = baseFieldsFromEvent(event, messageId);
506
+ const out: StreamEvent[] = [];
507
+ const content = Array.isArray(message["content"]) ? message["content"] : [];
508
+ for (const entry of content) {
509
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
510
+ const block = entry as Record<string, unknown>;
511
+ if (asString(block["type"], "") === "tool_result") {
512
+ out.push({
513
+ type: "tool_result",
514
+ toolCallId: asNullableString(block["tool_use_id"]),
515
+ // Claude's wire `tool_result` block carries only `tool_use_id`, not
516
+ // the tool name. The session/exec tool-name tracker fills this by
517
+ // correlating with the earlier `tool_call`.
518
+ toolName: null,
519
+ content: asString(block["content"], ""),
520
+ isError: block["is_error"] === true,
521
+ exitCode: null,
522
+ ...base,
523
+ });
524
+ }
525
+ }
526
+ return out;
527
+ }
528
+
529
+ if (type === "result") {
530
+ const out: StreamEvent[] = [];
531
+ const auth = classifyClaudeAuthFromResult(event);
532
+ if (auth) {
533
+ out.push({
534
+ type: "auth_required",
535
+ httpStatus: auth.httpStatus,
536
+ reason: auth.reason,
537
+ loginCommand: CLAUDE_LOGIN_COMMAND,
538
+ message: auth.message,
539
+ ...baseFieldsFromEvent(event, null),
540
+ });
541
+ }
542
+ out.push({
543
+ type: "result",
544
+ text: asString(event["result"], ""),
545
+ costUsd: asNullableNumber(event["total_cost_usd"]),
546
+ isError: event["is_error"] === true,
547
+ stopReason: asNullableString(event["stop_reason"]),
548
+ terminalReason: asNullableString(event["terminal_reason"]),
549
+ numTurns: asNullableNumber(event["num_turns"]),
550
+ durationMs: asNullableNumber(event["duration_ms"]),
551
+ ...baseFieldsFromEvent(event, null),
552
+ });
553
+ return out;
554
+ }
555
+
556
+ // Forward-compat: surface any unrecognized event type with full base fields
557
+ // + raw. Lets consumers see new CLI event types without a library bump.
558
+ return [{
559
+ type: "unknown",
560
+ subtype: type,
561
+ ...baseFieldsFromEvent(event, null),
562
+ }];
563
+ }
564
+
565
+ /**
566
+ * Loose text-match fallback for `isClaudeAuthRequired` — used by execute.ts
567
+ * before the structured `api_error_status` check was added, and kept for
568
+ * forward-compat when Claude introduces new auth phrasings before we model
569
+ * them. Prefer `classifyClaudeAuthFromResult` for new code; it works off
570
+ * structured wire fields plus the documented user-facing strings.
571
+ *
572
+ * Source of strings: https://code.claude.com/docs/en/errors
573
+ */
574
+ const CLAUDE_AUTH_REQUIRED_RE =
575
+ /(?:not\s+logged\s+in|please\s+log\s+in|please\s+run\s+`?(?:claude\s+(?:auth\s+)?login|\/login)`?|login\s+required|requires\s+login|unauthorized|authentication\s+required|authentication_failed|authentication_error|oauth\s+token\s+(?:has\s+expired|revoked)|does\s+not\s+meet\s+scope\s+requirement|invalid\s+api\s+key|invalid\s+bearer\s+token|disabled\s+organization|routines\s+are\s+disabled)/i;
576
+ const CLAUDE_UNKNOWN_SESSION_RE = /no conversation found with session id|unknown session|session .* not found/i;
577
+
578
+ export function isClaudeUnknownSessionError(stdout: string, stderr: string): boolean {
579
+ return CLAUDE_UNKNOWN_SESSION_RE.test(stdout) || CLAUDE_UNKNOWN_SESSION_RE.test(stderr);
580
+ }
581
+
582
+ export function isClaudeAuthRequired(stdout: string, stderr: string): boolean {
583
+ return CLAUDE_AUTH_REQUIRED_RE.test(stdout) || CLAUDE_AUTH_REQUIRED_RE.test(stderr);
584
+ }
585
+
586
+ /**
587
+ * Map Claude's user-facing auth error text to a stable `AuthRequiredReason`.
588
+ * Strings sourced from https://code.claude.com/docs/en/errors. Case
589
+ * insensitive; lenient substring match — the documented phrasings are
590
+ * stable, but treat unrecognized text as `"unknown"` rather than throwing
591
+ * so consumers still get an event with a usable httpStatus.
592
+ */
593
+ function authReasonFromText(text: string): AuthRequiredReason {
594
+ const t = text.toLowerCase();
595
+ // Order matters: "OAuth token has expired" must beat the generic
596
+ // `disabled` substring below.
597
+ if (t.includes("oauth token has expired") || t.includes("token has expired")) return "expired";
598
+ if (t.includes("oauth token revoked") || t.includes("token revoked")) return "revoked";
599
+ if (t.includes("not logged in")) return "missing";
600
+ if (t.includes("does not meet scope requirement") || t.includes("scope requirement")) return "scope";
601
+ if (t.includes("disabled organization") || t.includes("organization has been disabled")) return "disabled_org";
602
+ if (t.includes("routines are disabled")) return "routines_disabled";
603
+ if (t.includes("invalid api key")) return "invalid";
604
+ // Bearer token / Bedrock security token fall under "invalid"
605
+ if (t.includes("invalid bearer token") || t.includes("security token") || t.includes("failed to authenticate")) {
606
+ return "invalid";
607
+ }
608
+ return "unknown";
609
+ }
610
+
611
+ /**
612
+ * Classify a Claude `result` event as an auth failure. Returns null when
613
+ * the event isn't an auth failure (success, max-turns, rate-limit, etc.).
614
+ *
615
+ * Detection priority:
616
+ * 1. `api_error_status` is 401 or 403 — definitive HTTP-level auth failure.
617
+ * 2. `is_error: true` AND the result text matches a documented auth string
618
+ * (covers the CLI's short-circuit "Not logged in" path where
619
+ * `api_error_status` is null because no HTTP call ever happened).
620
+ *
621
+ * Exported so the streaming session path (`session.ts`) can run the same
622
+ * classification — its `handleResult` consumes `result` events directly
623
+ * and never goes through `parseStreamLine`.
624
+ *
625
+ * @internal
626
+ */
627
+ export function classifyClaudeAuthFromResult(event: Record<string, unknown>): {
628
+ httpStatus: number | null;
629
+ reason: AuthRequiredReason;
630
+ message: string | null;
631
+ } | null {
632
+ if (event["is_error"] !== true) return null;
633
+ const text = asString(event["result"], "");
634
+ const apiStatus = event["api_error_status"];
635
+ if (apiStatus === 401 || apiStatus === 403) {
636
+ return { httpStatus: apiStatus, reason: authReasonFromText(text), message: text || null };
637
+ }
638
+ // Short-circuit path: no HTTP round trip happened (api_error_status is
639
+ // null), but the result text still carries the documented auth string.
640
+ const reason = authReasonFromText(text);
641
+ if (reason !== "unknown") {
642
+ return { httpStatus: null, reason, message: text || null };
643
+ }
644
+ return null;
645
+ }
646
+
647
+
648
+ /**
649
+ * Pulls Claude's inner discriminator and payload out of an `unknown`
650
+ * StreamEvent's `raw`. Returns null for non-Claude events, non-unknown
651
+ * events, or events whose `raw` is not an object.
652
+ *
653
+ * This is opt-in ergonomics — consumers who want to dispatch on Claude's
654
+ * `system` subtypes (`away_summary`, `compact_boundary`, `turn_duration`,
655
+ * `api_error`, `bridge_status`, etc.) can use this instead of reaching
656
+ * into `event.raw` directly. `content` is returned as `unknown` to avoid
657
+ * silently coercing non-string or object payloads.
658
+ */
659
+ export function getClaudeUnknownDetails(
660
+ event: StreamEvent,
661
+ ): { subtype: string | null; content: unknown } | null {
662
+ if (event.type !== "unknown") return null;
663
+ if (event.providerType !== "claude") return null;
664
+ const raw = event.raw;
665
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
666
+ const r = raw as Record<string, unknown>;
667
+ return {
668
+ subtype: asNullableString(r["subtype"]),
669
+ content: r["content"],
670
+ };
671
+ }
672
+
673
+ /**
674
+ * Status values across Claude's two task-status fields:
675
+ * `task_notification.status` (`completed` | `failed` | `stopped`) and
676
+ * `task_updated.patch.status` (`pending` | `running` | `completed` | `failed`
677
+ * | `killed` | `paused`). Surfaced per-event as-is.
678
+ */
679
+ export type ClaudeTaskStatus =
680
+ | "pending"
681
+ | "running"
682
+ | "paused"
683
+ | "completed"
684
+ | "failed"
685
+ | "killed"
686
+ | "stopped";
687
+
688
+ /** Token / activity usage carried on `task_progress` and `task_notification`. */
689
+ export interface ClaudeTaskUsage {
690
+ totalTokens: number | null;
691
+ toolUses: number | null;
692
+ durationMs: number | null;
693
+ }
694
+
695
+ /**
696
+ * Typed view of one Claude background-task lifecycle event. Fields not present
697
+ * on a given `phase` are `null` (e.g. `status` is null on `started`/`progress`;
698
+ * `outputFile`/`summary` exist only on `notification`).
699
+ */
700
+ export interface ClaudeTaskDetails {
701
+ phase: "started" | "progress" | "updated" | "notification";
702
+ taskId: string;
703
+ /** Links back to the launching `tool_call` (Task/Bash). Absent on `updated`. */
704
+ toolUseId: string | null;
705
+ /** e.g. "local_bash" | a subagent type — optional on the wire. */
706
+ taskType: string | null;
707
+ /** Subagent type for Task-tool subagents (`started`/`progress`). */
708
+ subagentType: string | null;
709
+ /** Workflow name for workflow tasks (`started`). */
710
+ workflowName: string | null;
711
+ description: string | null;
712
+ /**
713
+ * This event's status, read from wherever the CLI placed it
714
+ * (`task_notification.status` or `task_updated.patch.status`). Null on
715
+ * `started`/`progress`, which carry no status.
716
+ */
717
+ status: ClaudeTaskStatus | null;
718
+ usage: ClaudeTaskUsage | null;
719
+ /** Path to the task's transcript/output file (`notification`). */
720
+ outputFile: string | null;
721
+ /** Final summary text (`notification`). */
722
+ summary: string | null;
723
+ /** Completion timestamp from `task_updated.patch.end_time`, when present. */
724
+ endTime: number | null;
725
+ }
726
+
727
+ const CLAUDE_TASK_PHASES: Record<string, ClaudeTaskDetails["phase"]> = {
728
+ task_started: "started",
729
+ task_progress: "progress",
730
+ task_updated: "updated",
731
+ task_notification: "notification",
732
+ };
733
+
734
+ const CLAUDE_TASK_STATUSES = new Set<string>([
735
+ "pending",
736
+ "running",
737
+ "paused",
738
+ "completed",
739
+ "failed",
740
+ "killed",
741
+ "stopped",
742
+ ]);
743
+
744
+ function asClaudeTaskStatus(value: unknown): ClaudeTaskStatus | null {
745
+ return typeof value === "string" && CLAUDE_TASK_STATUSES.has(value)
746
+ ? (value as ClaudeTaskStatus)
747
+ : null;
748
+ }
749
+
750
+ function taskUsageFromRaw(value: unknown): ClaudeTaskUsage | null {
751
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
752
+ const u = value as Record<string, unknown>;
753
+ return {
754
+ totalTokens: asNullableNumber(u["total_tokens"]),
755
+ toolUses: asNullableNumber(u["tool_uses"]),
756
+ durationMs: asNullableNumber(u["duration_ms"]),
757
+ };
758
+ }
759
+
760
+ /**
761
+ * Decode a Claude background-task lifecycle event into typed fields.
762
+ *
763
+ * Claude emits `task_started` / `task_progress` / `task_updated` /
764
+ * `task_notification` as `type:"system"` on the wire; agentex surfaces those as
765
+ * `type:"unknown"` (the forward-compat escape hatch — only `system`+`init` gets
766
+ * its own typed variant), with the payload preserved on `event.raw`. This
767
+ * accessor reads that payload into named fields, returning `null` for any event
768
+ * that isn't a Claude task lifecycle event. The full payload always remains on
769
+ * `event.raw` as the backstop, so fields agentex doesn't model yet stay
770
+ * reachable.
771
+ *
772
+ * This is a stateless, per-event decode — NOT a reducer. `status` reflects only
773
+ * the event in hand (and `task_updated` is a sparse patch). Collapsing a task's
774
+ * events into one current state over its lifetime is the consumer's job.
775
+ */
776
+ export function getClaudeTaskDetails(event: StreamEvent): ClaudeTaskDetails | null {
777
+ if (event.type !== "unknown") return null;
778
+ if (event.providerType !== PROVIDER_TYPE) return null;
779
+ const raw = event.raw;
780
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
781
+ const r = raw as Record<string, unknown>;
782
+
783
+ const phase = CLAUDE_TASK_PHASES[asString(r["subtype"], "")];
784
+ if (!phase) return null;
785
+
786
+ // `task_updated` is a sparse patch: status/description/end_time live under
787
+ // `patch`, not at the top level.
788
+ const patch = phase === "updated" ? parseObject(r["patch"]) : null;
789
+
790
+ return {
791
+ phase,
792
+ taskId: asString(r["task_id"], ""),
793
+ toolUseId: asNullableString(r["tool_use_id"]),
794
+ taskType: asNullableString(r["task_type"]),
795
+ subagentType: asNullableString(r["subagent_type"]),
796
+ workflowName: asNullableString(r["workflow_name"]),
797
+ description: patch
798
+ ? asNullableString(patch["description"])
799
+ : asNullableString(r["description"]),
800
+ status: patch
801
+ ? asClaudeTaskStatus(patch["status"])
802
+ : asClaudeTaskStatus(r["status"]),
803
+ usage: taskUsageFromRaw(r["usage"]),
804
+ outputFile: asNullableString(r["output_file"]),
805
+ summary: asNullableString(r["summary"]),
806
+ endTime: patch ? asNullableNumber(patch["end_time"]) : null,
807
+ };
808
+ }
809
+
810
+ export function isClaudeMaxTurns(stdout: string): boolean {
811
+ for (const rawLine of stdout.split(/\r?\n/)) {
812
+ const line = rawLine.trim();
813
+ if (!line) continue;
814
+ const event = parseJson(line);
815
+ if (!event) continue;
816
+ if (asString(event["type"], "") === "result") {
817
+ const subtype = asString(event["subtype"], "").toLowerCase();
818
+ if (subtype === "error_max_turns") return true;
819
+ const stopReason = asString(event["stop_reason"], "").toLowerCase();
820
+ if (stopReason === "max_turns") return true;
821
+ }
822
+ }
823
+ return false;
824
+ }