@parall/codex-agent 1.30.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { RuntimeEvent } from "@parall/agent-core";
1
+ import type { RuntimeEvent } from '@parall/agent-core';
2
2
 
3
3
  /**
4
4
  * Translation layer between `codex app-server` JSON-RPC notifications and
@@ -31,39 +31,39 @@ export class EventMapper {
31
31
 
32
32
  switch (method) {
33
33
  // Streaming deltas are intentionally dropped — see the class comment.
34
- case "item/agentMessage/delta":
35
- case "item/reasoning/textDelta":
36
- case "item/reasoning/summaryTextDelta":
34
+ case 'item/agentMessage/delta':
35
+ case 'item/reasoning/textDelta':
36
+ case 'item/reasoning/summaryTextDelta':
37
37
  break;
38
38
 
39
- case "item/started": {
39
+ case 'item/started': {
40
40
  const item = p.item as Record<string, unknown> | undefined;
41
- if (!item || typeof item.type !== "string") break;
42
- events.push(...this.mapItem(item, "started"));
41
+ if (!item || typeof item.type !== 'string') break;
42
+ events.push(...this.mapItem(item, 'started'));
43
43
  break;
44
44
  }
45
45
 
46
- case "item/completed": {
46
+ case 'item/completed': {
47
47
  const item = p.item as Record<string, unknown> | undefined;
48
- if (!item || typeof item.type !== "string") break;
49
- events.push(...this.mapItem(item, "completed"));
48
+ if (!item || typeof item.type !== 'string') break;
49
+ events.push(...this.mapItem(item, 'completed'));
50
50
  break;
51
51
  }
52
52
 
53
- case "turn/completed": {
53
+ case 'turn/completed': {
54
54
  const turn = p.turn as Record<string, unknown> | undefined;
55
55
  const status = turn ? asString(turn.status) : undefined;
56
56
  const error = turn?.error as Record<string, unknown> | undefined;
57
- if (status === "failed") {
58
- const message = asString(error?.message) ?? "Codex turn failed";
59
- events.push({ type: "error", message });
57
+ if (status === 'failed') {
58
+ const message = asString(error?.message) ?? 'Codex turn failed';
59
+ events.push({ type: 'error', message });
60
60
  }
61
61
  break;
62
62
  }
63
63
 
64
- case "error": {
65
- const message = asString(p.message) ?? "Codex error";
66
- events.push({ type: "error", message });
64
+ case 'error': {
65
+ const message = asString(p.message) ?? 'Codex error';
66
+ events.push({ type: 'error', message });
67
67
  break;
68
68
  }
69
69
  }
@@ -71,75 +71,82 @@ export class EventMapper {
71
71
  return events;
72
72
  }
73
73
 
74
- private mapItem(item: Record<string, unknown>, phase: "started" | "completed"): RuntimeEvent[] {
74
+ private mapItem(item: Record<string, unknown>, phase: 'started' | 'completed'): RuntimeEvent[] {
75
75
  const type = asString(item.type);
76
76
  const id = asString(item.id);
77
77
  const now = Date.now();
78
78
 
79
- if (type === "agentMessage") {
80
- if (phase !== "completed") return [];
79
+ if (type === 'agentMessage') {
80
+ if (phase !== 'completed') return [];
81
81
  const text = asString(item.text);
82
82
  if (!text) return [];
83
83
  // Layer 0 symmetric output contract: plain text is never auto-projected;
84
84
  // agents must explicitly call `@parall/cli messages send` / `dm` to reach
85
85
  // the chat. The text is still recorded as a session step for audit.
86
- return [{ type: "text", text, project: false }];
86
+ return [{ type: 'text', text, project: false }];
87
87
  }
88
88
 
89
- if (type === "reasoning") {
90
- if (phase !== "completed") return [];
89
+ if (type === 'reasoning') {
90
+ if (phase !== 'completed') return [];
91
91
  const text = joinReasoningText(item);
92
92
  if (!text) return [];
93
- return [{ type: "thinking", text }];
93
+ return [{ type: 'thinking', text }];
94
94
  }
95
95
 
96
- if (type === "commandExecution") {
96
+ if (type === 'commandExecution') {
97
97
  const command = formatCommand(item.command);
98
98
  const callId = id ?? `shell-${now}`;
99
- if (phase === "started") {
99
+ if (phase === 'started') {
100
100
  this.toolCallStart.set(callId, now);
101
- return [{
102
- type: "tool_call",
103
- callId,
104
- toolName: "shell",
105
- input: { command, cwd: asString(item.cwd) },
106
- startedAt: new Date(now).toISOString(),
107
- }];
101
+ return [
102
+ {
103
+ type: 'tool_call',
104
+ callId,
105
+ toolName: 'shell',
106
+ input: { command, cwd: asString(item.cwd) },
107
+ startedAt: new Date(now).toISOString(),
108
+ },
109
+ ];
108
110
  }
109
111
  // Codex app-server uses camelCase: aggregatedOutput / exitCode / durationMs
110
112
  // (see openai/codex codex-rs/app-server-protocol). Earlier snake_case
111
113
  // probing dropped exit codes and split outputs that arrive as one string.
112
114
  const durationMs = this.resolveDuration(callId, now, item.durationMs);
113
115
  const output = asString(item.aggregatedOutput) ?? joinStreams(item.stdout, item.stderr);
114
- const exitCode = typeof item.exitCode === "number" ? item.exitCode : undefined;
116
+ const exitCode = typeof item.exitCode === 'number' ? item.exitCode : undefined;
115
117
  const status = asString(item.status);
116
- const failed = (exitCode !== undefined && exitCode !== 0)
117
- || status === "failed"
118
- || status === "declined";
119
- return [{
120
- type: "tool_result",
121
- callId,
122
- toolName: "shell",
123
- output,
124
- ...(failed ? { error: output || `shell ${status ?? "exited"} (code=${exitCode ?? "?"})` } : {}),
125
- ...(durationMs !== undefined ? { durationMs } : {}),
126
- }];
118
+ const failed =
119
+ (exitCode !== undefined && exitCode !== 0) || status === 'failed' || status === 'declined';
120
+ return [
121
+ {
122
+ type: 'tool_result',
123
+ callId,
124
+ toolName: 'shell',
125
+ output,
126
+ ...(failed
127
+ ? { error: output || `shell ${status ?? 'exited'} (code=${exitCode ?? '?'})` }
128
+ : {}),
129
+ ...(durationMs !== undefined ? { durationMs } : {}),
130
+ },
131
+ ];
127
132
  }
128
133
 
129
- if (type === "mcpToolCall") {
134
+ if (type === 'mcpToolCall') {
130
135
  const server = asString(item.server);
131
136
  const tool = asString(item.tool);
132
- const toolName = server && tool ? `${server}:${tool}` : tool ?? server ?? "mcp";
137
+ const toolName = server && tool ? `${server}:${tool}` : (tool ?? server ?? 'mcp');
133
138
  const callId = id ?? `mcp-${now}`;
134
- if (phase === "started") {
139
+ if (phase === 'started') {
135
140
  this.toolCallStart.set(callId, now);
136
- return [{
137
- type: "tool_call",
138
- callId,
139
- toolName,
140
- input: item.arguments ?? {},
141
- startedAt: new Date(now).toISOString(),
142
- }];
141
+ return [
142
+ {
143
+ type: 'tool_call',
144
+ callId,
145
+ toolName,
146
+ input: item.arguments ?? {},
147
+ startedAt: new Date(now).toISOString(),
148
+ },
149
+ ];
143
150
  }
144
151
  const durationMs = this.resolveDuration(callId, now, item.durationMs);
145
152
  const result = item.result;
@@ -148,64 +155,73 @@ export class EventMapper {
148
155
  // success path renders an empty-quoted output and the failure path's
149
156
  // `output || "MCP tool call failed"` fallback gets eaten because the
150
157
  // literal `'""'` is truthy.
151
- const output = result == null
152
- ? ""
153
- : typeof result === "string" ? result : JSON.stringify(result);
158
+ const output =
159
+ result == null ? '' : typeof result === 'string' ? result : JSON.stringify(result);
154
160
  const success = item.isSuccess !== false;
155
- return [{
156
- type: "tool_result",
157
- callId,
158
- toolName,
159
- output,
160
- ...(success ? {} : { error: output || "MCP tool call failed" }),
161
- ...(durationMs !== undefined ? { durationMs } : {}),
162
- }];
161
+ return [
162
+ {
163
+ type: 'tool_result',
164
+ callId,
165
+ toolName,
166
+ output,
167
+ ...(success ? {} : { error: output || 'MCP tool call failed' }),
168
+ ...(durationMs !== undefined ? { durationMs } : {}),
169
+ },
170
+ ];
163
171
  }
164
172
 
165
- if (type === "fileChange") {
173
+ if (type === 'fileChange') {
166
174
  const callId = id ?? `patch-${now}`;
167
- if (phase === "started") {
175
+ if (phase === 'started') {
168
176
  this.toolCallStart.set(callId, now);
169
- return [{
170
- type: "tool_call",
171
- callId,
172
- toolName: "patch",
173
- input: { changes: item.changes ?? [] },
174
- startedAt: new Date(now).toISOString(),
175
- }];
177
+ return [
178
+ {
179
+ type: 'tool_call',
180
+ callId,
181
+ toolName: 'patch',
182
+ input: { changes: item.changes ?? [] },
183
+ startedAt: new Date(now).toISOString(),
184
+ },
185
+ ];
176
186
  }
177
187
  const durationMs = this.resolveDuration(callId, now, undefined);
178
- return [{
179
- type: "tool_result",
180
- callId,
181
- toolName: "patch",
182
- output: "",
183
- ...(durationMs !== undefined ? { durationMs } : {}),
184
- }];
188
+ return [
189
+ {
190
+ type: 'tool_result',
191
+ callId,
192
+ toolName: 'patch',
193
+ output: '',
194
+ ...(durationMs !== undefined ? { durationMs } : {}),
195
+ },
196
+ ];
185
197
  }
186
198
 
187
- if (type === "webSearch") {
199
+ if (type === 'webSearch') {
188
200
  const callId = id ?? `search-${now}`;
189
- if (phase === "started") {
201
+ if (phase === 'started') {
190
202
  this.toolCallStart.set(callId, now);
191
- return [{
192
- type: "tool_call",
193
- callId,
194
- toolName: "web_search",
195
- input: { query: asString(item.query) ?? "" },
196
- startedAt: new Date(now).toISOString(),
197
- }];
203
+ return [
204
+ {
205
+ type: 'tool_call',
206
+ callId,
207
+ toolName: 'web_search',
208
+ input: { query: asString(item.query) ?? '' },
209
+ startedAt: new Date(now).toISOString(),
210
+ },
211
+ ];
198
212
  }
199
213
  const durationMs = this.resolveDuration(callId, now, item.durationMs);
200
214
  const results = Array.isArray(item.results) ? item.results : [];
201
- const output = results.length > 0 ? JSON.stringify(results) : "";
202
- return [{
203
- type: "tool_result",
204
- callId,
205
- toolName: "web_search",
206
- output,
207
- ...(durationMs !== undefined ? { durationMs } : {}),
208
- }];
215
+ const output = results.length > 0 ? JSON.stringify(results) : '';
216
+ return [
217
+ {
218
+ type: 'tool_result',
219
+ callId,
220
+ toolName: 'web_search',
221
+ output,
222
+ ...(durationMs !== undefined ? { durationMs } : {}),
223
+ },
224
+ ];
209
225
  }
210
226
 
211
227
  return [];
@@ -217,10 +233,14 @@ export class EventMapper {
217
233
  * tracked start timestamp so long-lived bridges don't accumulate stale
218
234
  * entries when the server routinely supplies durationMs.
219
235
  */
220
- private resolveDuration(callId: string, now: number, serverDurationMs: unknown): number | undefined {
236
+ private resolveDuration(
237
+ callId: string,
238
+ now: number,
239
+ serverDurationMs: unknown,
240
+ ): number | undefined {
221
241
  const start = this.toolCallStart.get(callId);
222
242
  this.toolCallStart.delete(callId);
223
- if (typeof serverDurationMs === "number" && Number.isFinite(serverDurationMs)) {
243
+ if (typeof serverDurationMs === 'number' && Number.isFinite(serverDurationMs)) {
224
244
  return Math.max(0, serverDurationMs);
225
245
  }
226
246
  if (start === undefined) return undefined;
@@ -229,30 +249,36 @@ export class EventMapper {
229
249
  }
230
250
 
231
251
  function asString(value: unknown): string | undefined {
232
- if (typeof value !== "string") return undefined;
252
+ if (typeof value !== 'string') return undefined;
233
253
  const trimmed = value.trim();
234
254
  return trimmed.length > 0 ? trimmed : undefined;
235
255
  }
236
256
 
237
257
  function joinReasoningText(item: Record<string, unknown>): string {
238
- const summary = Array.isArray(item.summary) ? item.summary.filter((x) => typeof x === "string") : [];
239
- const content = Array.isArray(item.content) ? item.content.filter((x) => typeof x === "string") : [];
258
+ const summary = Array.isArray(item.summary)
259
+ ? item.summary.filter((x) => typeof x === 'string')
260
+ : [];
261
+ const content = Array.isArray(item.content)
262
+ ? item.content.filter((x) => typeof x === 'string')
263
+ : [];
240
264
  const parts = [...summary, ...content, asString(item.text)].filter(Boolean) as string[];
241
- return parts.join("\n").trim();
265
+ return parts.join('\n').trim();
242
266
  }
243
267
 
244
268
  function joinStreams(stdout: unknown, stderr: unknown): string {
245
269
  const out = asString(stdout);
246
270
  const err = asString(stderr);
247
271
  if (out && err) return `${out}\n${err}`;
248
- return out ?? err ?? "";
272
+ return out ?? err ?? '';
249
273
  }
250
274
 
251
275
  /** Codex sends `command` either as a plain string or as an argv array (e.g. ["sh", "-c", "ls"]). */
252
276
  function formatCommand(command: unknown): string {
253
- if (typeof command === "string") return command;
277
+ if (typeof command === 'string') return command;
254
278
  if (Array.isArray(command)) {
255
- return command.map((part) => (typeof part === "string" ? part : JSON.stringify(part))).join(" ");
279
+ return command
280
+ .map((part) => (typeof part === 'string' ? part : JSON.stringify(part)))
281
+ .join(' ');
256
282
  }
257
- return "";
283
+ return '';
258
284
  }