pi-harness-delegate 0.1.0 → 0.2.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,172 +1,216 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
- import type { Harness, BuildArgsOpts, NormalizedPermission, ParseOutcome, ParseState, StreamedResult } from './types.ts';
3
+ import type {
4
+ BuildArgsOpts,
5
+ Harness,
6
+ NormalizedPermission,
7
+ ParseOutcome,
8
+ ParseState,
9
+ StreamedResult,
10
+ } from './types.ts';
4
11
 
5
12
  const execFileAsync = promisify(execFile);
6
13
 
7
14
  function isRecord(v: unknown): v is Record<string, unknown> {
8
- return typeof v === 'object' && v !== null && !Array.isArray(v);
15
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
9
16
  }
10
17
 
11
18
  const SANDBOX_MAP: Record<NormalizedPermission, string> = {
12
- readonly: 'read-only',
13
- edit: 'workspace-write',
14
- danger: 'danger-full-access',
19
+ readonly: 'read-only',
20
+ edit: 'workspace-write',
21
+ danger: 'danger-full-access',
15
22
  };
16
23
 
17
24
  function extractTextFromCodexEvent(o: Record<string, unknown>, state: ParseState): string | undefined {
18
- // Codex JSON variants: try common shapes
19
- // 1. {type:"item.completed", item:{type:"agent_message", text:"..."}}
20
- // 2. {type:"thread.item.completed", item:{type:"agent_message", ...}}
21
- // 3. {type:"event", event:{type:"response.output_text.delta", delta:"..."}}
22
- // 4. Plain {"text":"..."} or {"output":"..."}
23
- if (typeof o.text === 'string' && o.type !== 'tool_use') return o.text;
24
- if (typeof o.output === 'string') return o.output;
25
- if (typeof o.delta === 'string') return o.delta;
26
- if (isRecord(o.event) && typeof o.event.text === 'string') return o.event.text;
27
- if (isRecord(o.event) && typeof (o.event as Record<string, unknown>).delta === 'string') return (o.event as Record<string, unknown>).delta as string;
28
- if (isRecord(o.item) && typeof o.item.text === 'string') return o.item.text;
29
- if (isRecord(o.item) && typeof o.item.output === 'string') return o.item.output;
30
- // agent_message
31
- if (o.type === 'agent_message' && typeof o.text === 'string') return o.text;
32
- return undefined;
25
+ // Codex JSON variants: try common shapes
26
+ // 1. {type:"item.completed", item:{type:"agent_message", text:"..."}}
27
+ // 2. {type:"thread.item.completed", item:{type:"agent_message", ...}}
28
+ // 3. {type:"event", event:{type:"response.output_text.delta", delta:"..."}}
29
+ // 4. Plain {"text":"..."} or {"output":"..."}
30
+ if (typeof o.text === 'string' && o.type !== 'tool_use') return o.text;
31
+ if (typeof o.output === 'string') return o.output;
32
+ if (typeof o.delta === 'string') return o.delta;
33
+ if (isRecord(o.event) && typeof o.event.text === 'string') return o.event.text;
34
+ if (isRecord(o.event) && typeof (o.event as Record<string, unknown>).delta === 'string')
35
+ return (o.event as Record<string, unknown>).delta as string;
36
+ if (isRecord(o.item) && typeof o.item.text === 'string') return o.item.text;
37
+ if (isRecord(o.item) && typeof o.item.output === 'string') return o.item.output;
38
+ // agent_message
39
+ if (o.type === 'agent_message' && typeof o.text === 'string') return o.text;
40
+ return undefined;
33
41
  }
34
42
 
35
43
  export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
36
- let o: unknown;
37
- try {
38
- o = JSON.parse(line);
39
- } catch {
40
- // Non-JSON line: treat as streamed text (codex may emit plain text)
41
- if (line.trim().length > 0) return { streamedText: line + '\n', activities: [] };
42
- return { activities: [] };
43
- }
44
- if (!isRecord(o)) return { activities: [] };
44
+ let o: unknown;
45
+ try {
46
+ o = JSON.parse(line);
47
+ } catch {
48
+ // Non-JSON line: treat as streamed text (codex may emit plain text)
49
+ if (line.trim().length > 0) return { streamedText: line + '\n', activities: [] };
50
+ return { activities: [] };
51
+ }
52
+ if (!isRecord(o)) return { activities: [] };
45
53
 
46
- const activities: ParseOutcome['activities'] = [];
47
- let streamedText: string | undefined;
54
+ const activities: ParseOutcome['activities'] = [];
55
+ let streamedText: string | undefined;
48
56
 
49
- // Tool activity heuristics
50
- // Codex may emit: {type:"item.started", item:{type:"tool_use", name:"...", input:{}}}
51
- // or {type:"tool_use", name:"..."}
52
- const item = isRecord(o.item) ? o.item : null;
53
- const typeStr = typeof o.type === 'string' ? o.type : '';
57
+ // Tool activity heuristics
58
+ // Codex may emit: {type:"item.started", item:{type:"tool_use", name:"...", input:{}}}
59
+ // or {type:"tool_use", name:"..."}
60
+ const item = isRecord(o.item) ? o.item : null;
61
+ const typeStr = typeof o.type === 'string' ? o.type : '';
54
62
 
55
- if (typeStr.includes('tool') || typeStr.includes('item.started') || typeStr.includes('function_call')) {
56
- const toolName = (item && typeof item.name === 'string' ? item.name : typeof o.name === 'string' ? o.name : null) as string | null;
57
- if (toolName) {
58
- if (typeStr.includes('started') || o.type === 'tool_use') {
59
- activities.push({ kind: 'tool_start', name: toolName });
60
- if (item && isRecord(item.input)) activities.push({ kind: 'tool_input', name: toolName, input: item.input as Record<string, unknown> });
61
- else if (isRecord(o.input)) activities.push({ kind: 'tool_input', name: toolName, input: o.input as Record<string, unknown> });
62
- } else if (typeStr.includes('completed') || typeStr.includes('result')) {
63
- activities.push({ kind: 'tool_result', isError: o.is_error === true || o.error === true });
64
- }
65
- }
66
- }
67
- if (o.type === 'tool_result' || typeStr === 'item.completed' && item?.type === 'tool_result') {
68
- activities.push({ kind: 'tool_result', isError: (o as Record<string, unknown>).is_error === true });
69
- }
70
- if (typeStr.includes('thinking') || o.type === 'reasoning' || item?.type === 'reasoning') {
71
- const thinkingText = extractTextFromCodexEvent(o, state);
72
- if (thinkingText) activities.push({ kind: 'thinking', chars: thinkingText.length });
73
- else activities.push({ kind: 'thinking', chars: 10 });
74
- }
63
+ if (typeStr.includes('tool') || typeStr.includes('item.started') || typeStr.includes('function_call')) {
64
+ const toolName = (item && typeof item.name === 'string' ? item.name : typeof o.name === 'string' ? o.name : null) as
65
+ | string
66
+ | null;
67
+ if (toolName) {
68
+ if (typeStr.includes('started') || o.type === 'tool_use') {
69
+ activities.push({ kind: 'tool_start', name: toolName });
70
+ if (item && isRecord(item.input))
71
+ activities.push({
72
+ kind: 'tool_input',
73
+ name: toolName,
74
+ input: item.input as Record<string, unknown>,
75
+ });
76
+ else if (isRecord(o.input))
77
+ activities.push({ kind: 'tool_input', name: toolName, input: o.input as Record<string, unknown> });
78
+ } else if (typeStr.includes('completed') || typeStr.includes('result')) {
79
+ activities.push({ kind: 'tool_result', isError: o.is_error === true || o.error === true });
80
+ }
81
+ }
82
+ }
83
+ if (o.type === 'tool_result' || (typeStr === 'item.completed' && item?.type === 'tool_result')) {
84
+ activities.push({ kind: 'tool_result', isError: (o as Record<string, unknown>).is_error === true });
85
+ }
86
+ if (typeStr.includes('thinking') || o.type === 'reasoning' || item?.type === 'reasoning') {
87
+ const thinkingText = extractTextFromCodexEvent(o, state);
88
+ if (thinkingText) activities.push({ kind: 'thinking', chars: thinkingText.length });
89
+ else activities.push({ kind: 'thinking', chars: 10 });
90
+ }
75
91
 
76
- // Text extraction
77
- const text = extractTextFromCodexEvent(o, state);
78
- if (text && !typeStr.includes('tool') && !typeStr.includes('thinking')) {
79
- // Avoid double-counting tool inputs as text
80
- streamedText = text;
81
- }
92
+ // Text extraction
93
+ const text = extractTextFromCodexEvent(o, state);
94
+ if (text && !typeStr.includes('tool') && !typeStr.includes('thinking')) {
95
+ // Avoid double-counting tool inputs as text
96
+ streamedText = text;
97
+ }
82
98
 
83
- // Result detection: final result often {type:"result", result:"...", total_cost_usd, usage, ...}
84
- // or {type:"thread.completed", ...}
85
- if (o.type === 'result' || o.type === 'thread.completed' || o.type === 'task.completed') {
86
- const usage = isRecord(o.usage) ? o.usage : null;
87
- const result: StreamedResult = {
88
- result: typeof o.result === 'string' ? o.result : typeof o.output === 'string' ? o.output : state.streamedText + (streamedText ?? ''),
89
- isError: o.is_error === true || o.error === true,
90
- numTurns: typeof o.num_turns === 'number' ? o.num_turns : typeof o.turns === 'number' ? o.turns : 0,
91
- totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : typeof o.cost === 'number' ? o.cost : 0,
92
- sessionId: typeof o.session_id === 'string' ? o.session_id : typeof o.thread_id === 'string' ? o.thread_id : typeof o.id === 'string' ? o.id : null,
93
- stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
94
- permissionDenials: Array.isArray(o.permission_denials) ? o.permission_denials : [],
95
- durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
96
- durationApiMs: typeof o.duration_api_ms === 'number' ? o.duration_api_ms : null,
97
- ttftMs: typeof o.ttft_ms === 'number' ? o.ttft_ms : null,
98
- model: typeof o.model === 'string' ? o.model : null,
99
- contextWindow: typeof o.context_window === 'number' ? o.context_window : null,
100
- maxOutputTokens: null,
101
- usage: usage
102
- ? {
103
- inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : typeof (usage as Record<string, unknown>).inputTokens === 'number' ? (usage as Record<string, unknown>).inputTokens as number : 0,
104
- outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : typeof (usage as Record<string, unknown>).outputTokens === 'number' ? (usage as Record<string, unknown>).outputTokens as number : 0,
105
- cacheCreationInputTokens: typeof (usage as Record<string, unknown>).cache_creation_input_tokens === 'number' ? (usage as Record<string, unknown>).cache_creation_input_tokens as number : 0,
106
- cacheReadInputTokens: typeof (usage as Record<string, unknown>).cache_read_input_tokens === 'number' ? (usage as Record<string, unknown>).cache_read_input_tokens as number : 0,
107
- }
108
- : null,
109
- };
110
- return { activities, streamedText, result };
111
- }
99
+ // Result detection: final result often {type:"result", result:"...", total_cost_usd, usage, ...}
100
+ // or {type:"thread.completed", ...}
101
+ if (o.type === 'result' || o.type === 'thread.completed' || o.type === 'task.completed') {
102
+ const usage = isRecord(o.usage) ? o.usage : null;
103
+ const result: StreamedResult = {
104
+ result:
105
+ typeof o.result === 'string'
106
+ ? o.result
107
+ : typeof o.output === 'string'
108
+ ? o.output
109
+ : state.streamedText + (streamedText ?? ''),
110
+ isError: o.is_error === true || o.error === true,
111
+ numTurns: typeof o.num_turns === 'number' ? o.num_turns : typeof o.turns === 'number' ? o.turns : 0,
112
+ totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : typeof o.cost === 'number' ? o.cost : 0,
113
+ sessionId:
114
+ typeof o.session_id === 'string'
115
+ ? o.session_id
116
+ : typeof o.thread_id === 'string'
117
+ ? o.thread_id
118
+ : typeof o.id === 'string'
119
+ ? o.id
120
+ : null,
121
+ stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
122
+ permissionDenials: Array.isArray(o.permission_denials) ? o.permission_denials : [],
123
+ durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
124
+ durationApiMs: typeof o.duration_api_ms === 'number' ? o.duration_api_ms : null,
125
+ ttftMs: typeof o.ttft_ms === 'number' ? o.ttft_ms : null,
126
+ model: typeof o.model === 'string' ? o.model : null,
127
+ contextWindow: typeof o.context_window === 'number' ? o.context_window : null,
128
+ maxOutputTokens: null,
129
+ usage: usage
130
+ ? {
131
+ inputTokens:
132
+ typeof usage.input_tokens === 'number'
133
+ ? usage.input_tokens
134
+ : typeof (usage as Record<string, unknown>).inputTokens === 'number'
135
+ ? ((usage as Record<string, unknown>).inputTokens as number)
136
+ : 0,
137
+ outputTokens:
138
+ typeof usage.output_tokens === 'number'
139
+ ? usage.output_tokens
140
+ : typeof (usage as Record<string, unknown>).outputTokens === 'number'
141
+ ? ((usage as Record<string, unknown>).outputTokens as number)
142
+ : 0,
143
+ cacheCreationInputTokens:
144
+ typeof (usage as Record<string, unknown>).cache_creation_input_tokens === 'number'
145
+ ? ((usage as Record<string, unknown>).cache_creation_input_tokens as number)
146
+ : 0,
147
+ cacheReadInputTokens:
148
+ typeof (usage as Record<string, unknown>).cache_read_input_tokens === 'number'
149
+ ? ((usage as Record<string, unknown>).cache_read_input_tokens as number)
150
+ : 0,
151
+ }
152
+ : null,
153
+ };
154
+ return { activities, streamedText, result };
155
+ }
112
156
 
113
- return { activities, streamedText };
157
+ return { activities, streamedText };
114
158
  }
115
159
 
116
160
  export const codexHarness: Harness = {
117
- name: 'codex',
118
- displayName: 'Muse',
119
- binary: 'codex',
120
- async detect() {
121
- try {
122
- const { stdout } = await execFileAsync('codex', ['--version'], { timeout: 5000 });
123
- return { ok: true, version: stdout.trim() };
124
- } catch {
125
- return { ok: false, hint: 'Install Muse: https://github.com/openai/codex' };
126
- }
127
- },
128
- buildArgs(opts: BuildArgsOpts): string[] {
129
- // codex exec --json <prompt> --sandbox <level> --ask-for-approval <level>
130
- const sandbox = opts.nativePermission ?? SANDBOX_MAP[opts.permission] ?? 'workspace-write';
131
- const args = ['exec', '--json', opts.prompt, '--sandbox', sandbox];
132
- if (opts.permission === 'danger' || sandbox === 'danger-full-access') args.push('--ask-for-approval', 'never');
133
- else if (opts.permission === 'readonly') args.push('--ask-for-approval', 'never');
134
- else args.push('--ask-for-approval', 'on-request');
135
- if (opts.model) args.push('--model', opts.model);
136
- if (opts.resumeSessionId) args.push('--thread-id', opts.resumeSessionId);
137
- // maxBudgetUsd not natively supported; pass as env hint via --config if needed
138
- for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
139
- return args;
140
- },
141
- parseLine(line: string, state: ParseState): ParseOutcome {
142
- return parseCodexLine(line, state);
143
- },
144
- extractResult(state: ParseState): StreamedResult | null {
145
- if (state.result) return state.result;
146
- // Fallback: if no explicit result, synthesize from streamed text if any
147
- if (state.streamedText.trim().length > 0) {
148
- return {
149
- result: state.streamedText,
150
- isError: false,
151
- numTurns: 1,
152
- totalCostUsd: 0,
153
- sessionId: null,
154
- stopReason: null,
155
- permissionDenials: [],
156
- durationMs: null,
157
- durationApiMs: null,
158
- ttftMs: null,
159
- model: null,
160
- contextWindow: null,
161
- maxOutputTokens: null,
162
- usage: null,
163
- };
164
- }
165
- return null;
166
- },
167
- permissionMap: {
168
- readonly: ['read-only'],
169
- edit: ['workspace-write'],
170
- danger: ['danger-full-access'],
171
- },
161
+ name: 'codex',
162
+ displayName: 'Muse',
163
+ binary: 'codex',
164
+ async detect() {
165
+ try {
166
+ const { stdout } = await execFileAsync('codex', ['--version'], { timeout: 5000 });
167
+ return { ok: true, version: stdout.trim() };
168
+ } catch {
169
+ return { ok: false, hint: 'Install Muse: https://github.com/openai/codex' };
170
+ }
171
+ },
172
+ buildArgs(opts: BuildArgsOpts): string[] {
173
+ // codex exec --json <prompt> --sandbox <level> --ask-for-approval <level>
174
+ const sandbox = opts.nativePermission ?? SANDBOX_MAP[opts.permission] ?? 'workspace-write';
175
+ const args = ['exec', '--json', opts.prompt, '--sandbox', sandbox];
176
+ if (opts.permission === 'danger' || sandbox === 'danger-full-access') args.push('--ask-for-approval', 'never');
177
+ else if (opts.permission === 'readonly') args.push('--ask-for-approval', 'never');
178
+ else args.push('--ask-for-approval', 'on-request');
179
+ if (opts.model) args.push('--model', opts.model);
180
+ if (opts.resumeSessionId) args.push('--thread-id', opts.resumeSessionId);
181
+ // maxBudgetUsd not natively supported; pass as env hint via --config if needed
182
+ for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
183
+ return args;
184
+ },
185
+ parseLine(line: string, state: ParseState): ParseOutcome {
186
+ return parseCodexLine(line, state);
187
+ },
188
+ extractResult(state: ParseState): StreamedResult | null {
189
+ if (state.result) return state.result;
190
+ // Fallback: if no explicit result, synthesize from streamed text if any
191
+ if (state.streamedText.trim().length > 0) {
192
+ return {
193
+ result: state.streamedText,
194
+ isError: false,
195
+ numTurns: 1,
196
+ totalCostUsd: 0,
197
+ sessionId: null,
198
+ stopReason: null,
199
+ permissionDenials: [],
200
+ durationMs: null,
201
+ durationApiMs: null,
202
+ ttftMs: null,
203
+ model: null,
204
+ contextWindow: null,
205
+ maxOutputTokens: null,
206
+ usage: null,
207
+ };
208
+ }
209
+ return null;
210
+ },
211
+ permissionMap: {
212
+ readonly: ['read-only'],
213
+ edit: ['workspace-write'],
214
+ danger: ['danger-full-access'],
215
+ },
172
216
  };