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.
- package/README.md +8 -6
- package/extensions/activity.ts +225 -201
- package/extensions/command.ts +61 -61
- package/extensions/config.ts +137 -131
- package/extensions/harnesses/amp.ts +122 -115
- package/extensions/harnesses/claude.ts +136 -111
- package/extensions/harnesses/codex.ts +191 -147
- package/extensions/harnesses/opencode.ts +133 -116
- package/extensions/harnesses/registry.ts +22 -22
- package/extensions/harnesses/types.ts +60 -60
- package/extensions/hint.ts +22 -19
- package/extensions/index.ts +1128 -743
- package/extensions/progress.ts +104 -104
- package/extensions/run-claude.ts +44 -35
- package/extensions/runner.ts +111 -99
- package/extensions/stream-parse.ts +32 -24
- package/extensions/templates.ts +134 -117
- package/extensions/usage.ts +24 -24
- package/package.json +69 -56
|
@@ -1,172 +1,216 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
-
import type {
|
|
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
|
-
|
|
15
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
const SANDBOX_MAP: Record<NormalizedPermission, string> = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
54
|
+
const activities: ParseOutcome['activities'] = [];
|
|
55
|
+
let streamedText: string | undefined;
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
157
|
+
return { activities, streamedText };
|
|
114
158
|
}
|
|
115
159
|
|
|
116
160
|
export const codexHarness: Harness = {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
};
|