pi-harness-delegate 0.2.2 → 0.4.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 +47 -13
- package/extensions/activity.ts +223 -16
- package/extensions/command.ts +83 -6
- package/extensions/concurrency.ts +106 -0
- package/extensions/config.ts +1 -1
- package/extensions/harnesses/amp.ts +122 -99
- package/extensions/harnesses/claude.ts +8 -3
- package/extensions/harnesses/codex.ts +92 -45
- package/extensions/harnesses/opencode.ts +86 -74
- package/extensions/harnesses/types.ts +6 -4
- package/extensions/index.ts +797 -233
- package/extensions/notify.ts +50 -0
- package/extensions/progress-multi.ts +178 -0
- package/extensions/progress.ts +1 -1
- package/extensions/run-registry.ts +90 -0
- package/extensions/templates.ts +3 -0
- package/extensions/usage.ts +8 -4
- package/package.json +1 -1
|
@@ -9,6 +9,12 @@ import type {
|
|
|
9
9
|
StreamedResult,
|
|
10
10
|
} from './types.ts';
|
|
11
11
|
|
|
12
|
+
// Schema verified against codex-cli 0.149.1 — see tests/fixtures/codex.jsonl.
|
|
13
|
+
// `codex exec` in this version dropped `--ask-for-approval` entirely (exec is inherently
|
|
14
|
+
// non-interactive; sandbox alone governs what's allowed) and resume is a subcommand
|
|
15
|
+
// (`exec resume <id> <prompt>`), not a `--thread-id` flag — both confirmed via `codex exec
|
|
16
|
+
// --help` / `codex exec resume --help`, not just the JSONL capture.
|
|
17
|
+
|
|
12
18
|
const execFileAsync = promisify(execFile);
|
|
13
19
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
14
20
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
@@ -32,6 +38,18 @@ function extractTextFromCodexEvent(o: Record<string, unknown>, _state: ParseStat
|
|
|
32
38
|
if (typeof o.error === 'string') return o.error;
|
|
33
39
|
return undefined;
|
|
34
40
|
}
|
|
41
|
+
|
|
42
|
+
interface CodexHarnessState {
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
turnCount?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function harnessState(state: ParseState): CodexHarnessState {
|
|
48
|
+
const s = (state._harness ?? {}) as CodexHarnessState;
|
|
49
|
+
state._harness = s as unknown as Record<string, unknown>;
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
36
54
|
let o: unknown;
|
|
37
55
|
try {
|
|
@@ -45,15 +63,16 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
45
63
|
let streamedText: string | undefined;
|
|
46
64
|
const typeStr = typeof o.type === 'string' ? o.type : '';
|
|
47
65
|
const item = isRecord(o.item) ? o.item : null;
|
|
66
|
+
const hs = harnessState(state);
|
|
48
67
|
// latch thread_id from thread.started
|
|
49
68
|
if (typeStr === 'thread.started' && typeof o.thread_id === 'string') {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
hs.sessionId = o.thread_id;
|
|
70
|
+
return { activities, streamedText };
|
|
71
|
+
}
|
|
72
|
+
if (typeStr === 'turn.started') {
|
|
73
|
+
hs.turnCount = (hs.turnCount ?? 0) + 1;
|
|
54
74
|
return { activities, streamedText };
|
|
55
75
|
}
|
|
56
|
-
if (typeStr === 'turn.started') return { activities, streamedText };
|
|
57
76
|
if (typeStr === 'error' && typeof o.message === 'string') {
|
|
58
77
|
streamedText = o.message;
|
|
59
78
|
}
|
|
@@ -67,14 +86,12 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
67
86
|
: typeof o.message === 'string'
|
|
68
87
|
? o.message
|
|
69
88
|
: state.streamedText + (streamedText ?? '');
|
|
70
|
-
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
71
|
-
?.sessionId as string | undefined;
|
|
72
89
|
const result: StreamedResult = {
|
|
73
90
|
result: msg,
|
|
74
91
|
isError: true,
|
|
75
|
-
numTurns:
|
|
76
|
-
totalCostUsd:
|
|
77
|
-
sessionId: typeof o.thread_id === 'string' ? o.thread_id : (
|
|
92
|
+
numTurns: null,
|
|
93
|
+
totalCostUsd: null,
|
|
94
|
+
sessionId: typeof o.thread_id === 'string' ? o.thread_id : (hs.sessionId ?? null),
|
|
78
95
|
stopReason: 'error',
|
|
79
96
|
permissionDenials: [],
|
|
80
97
|
durationMs: null,
|
|
@@ -87,7 +104,31 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
87
104
|
};
|
|
88
105
|
return { activities, streamedText, result };
|
|
89
106
|
}
|
|
90
|
-
|
|
107
|
+
// real tool schema: item.started/item.completed carry item.id (correlating id) and item.type
|
|
108
|
+
// (no separate "name" field — command_execution is the only item type observed emitting a
|
|
109
|
+
// shell command; other item types like file_change/mcp_tool_call may exist but weren't
|
|
110
|
+
// captured, so the generic name/input guesses below stay as a fallback for those).
|
|
111
|
+
if (
|
|
112
|
+
item &&
|
|
113
|
+
(typeStr === 'item.started' || typeStr === 'item.completed') &&
|
|
114
|
+
item.type !== 'agent_message' &&
|
|
115
|
+
item.type !== 'reasoning'
|
|
116
|
+
) {
|
|
117
|
+
const id = typeof item.id === 'string' ? item.id : undefined;
|
|
118
|
+
const name = typeof item.name === 'string' ? item.name : typeof item.type === 'string' ? item.type : 'tool';
|
|
119
|
+
if (typeStr === 'item.started') {
|
|
120
|
+
const input =
|
|
121
|
+
typeof item.command === 'string' ? { command: item.command } : isRecord(item.input) ? item.input : {};
|
|
122
|
+
activities.push({ kind: 'tool_start', name });
|
|
123
|
+
activities.push({ kind: 'tool_input', name, input: input as Record<string, unknown>, id });
|
|
124
|
+
} else {
|
|
125
|
+
const isError =
|
|
126
|
+
(typeof item.exit_code === 'number' && item.exit_code !== 0) ||
|
|
127
|
+
item.status === 'failed' ||
|
|
128
|
+
item.is_error === true;
|
|
129
|
+
activities.push({ kind: 'tool_result', isError, id });
|
|
130
|
+
}
|
|
131
|
+
} else if (typeStr.includes('tool') || typeStr.includes('function_call')) {
|
|
91
132
|
const toolName = (item && typeof item.name === 'string' ? item.name : typeof o.name === 'string' ? o.name : null) as
|
|
92
133
|
| string
|
|
93
134
|
| null;
|
|
@@ -102,7 +143,7 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
102
143
|
activities.push({ kind: 'tool_result', isError: o.is_error === true || o.error === true });
|
|
103
144
|
}
|
|
104
145
|
}
|
|
105
|
-
if (o.type === 'tool_result'
|
|
146
|
+
if (o.type === 'tool_result')
|
|
106
147
|
activities.push({ kind: 'tool_result', isError: (o as Record<string, unknown>).is_error === true });
|
|
107
148
|
if (typeStr.includes('thinking') || o.type === 'reasoning' || item?.type === 'reasoning') {
|
|
108
149
|
const thinkingText = extractTextFromCodexEvent(o, state);
|
|
@@ -118,10 +159,15 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
118
159
|
typeStr !== 'turn.failed'
|
|
119
160
|
)
|
|
120
161
|
streamedText = text;
|
|
121
|
-
|
|
162
|
+
// real final-usage event is turn.completed (thread.completed/task.completed/result kept as
|
|
163
|
+
// fallback in case another codex-cli version emits them instead).
|
|
164
|
+
if (
|
|
165
|
+
o.type === 'result' ||
|
|
166
|
+
o.type === 'thread.completed' ||
|
|
167
|
+
o.type === 'task.completed' ||
|
|
168
|
+
typeStr === 'turn.completed'
|
|
169
|
+
) {
|
|
122
170
|
const usage = isRecord(o.usage) ? o.usage : null;
|
|
123
|
-
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
124
|
-
?.sessionId as string | undefined;
|
|
125
171
|
const result: StreamedResult = {
|
|
126
172
|
result:
|
|
127
173
|
typeof o.result === 'string'
|
|
@@ -130,8 +176,16 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
130
176
|
? o.output
|
|
131
177
|
: state.streamedText + (streamedText ?? ''),
|
|
132
178
|
isError: o.is_error === true || o.error === true,
|
|
133
|
-
numTurns:
|
|
134
|
-
|
|
179
|
+
numTurns:
|
|
180
|
+
typeof o.num_turns === 'number'
|
|
181
|
+
? o.num_turns
|
|
182
|
+
: typeof o.turns === 'number'
|
|
183
|
+
? o.turns
|
|
184
|
+
: hs.turnCount
|
|
185
|
+
? hs.turnCount
|
|
186
|
+
: null,
|
|
187
|
+
totalCostUsd:
|
|
188
|
+
typeof o.total_cost_usd === 'number' ? o.total_cost_usd : typeof o.cost === 'number' ? o.cost : null,
|
|
135
189
|
sessionId:
|
|
136
190
|
typeof o.session_id === 'string'
|
|
137
191
|
? o.session_id
|
|
@@ -139,7 +193,7 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
139
193
|
? o.thread_id
|
|
140
194
|
: typeof o.id === 'string'
|
|
141
195
|
? o.id
|
|
142
|
-
: (
|
|
196
|
+
: (hs.sessionId ?? null),
|
|
143
197
|
stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
|
|
144
198
|
permissionDenials: Array.isArray(o.permission_denials) ? o.permission_denials : [],
|
|
145
199
|
durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
|
|
@@ -150,26 +204,22 @@ export function parseCodexLine(line: string, state: ParseState): ParseOutcome {
|
|
|
150
204
|
maxOutputTokens: null,
|
|
151
205
|
usage: usage
|
|
152
206
|
? {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
? ((usage as Record<string, unknown>).inputTokens as number)
|
|
158
|
-
: 0,
|
|
159
|
-
outputTokens:
|
|
160
|
-
typeof usage.output_tokens === 'number'
|
|
161
|
-
? usage.output_tokens
|
|
162
|
-
: typeof (usage as Record<string, unknown>).outputTokens === 'number'
|
|
163
|
-
? ((usage as Record<string, unknown>).outputTokens as number)
|
|
164
|
-
: 0,
|
|
207
|
+
// real fields: input_tokens, cached_input_tokens, cache_write_input_tokens, output_tokens
|
|
208
|
+
// (no total_cost_usd anywhere in this schema — ChatGPT-plan auth doesn't report $ cost).
|
|
209
|
+
inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : 0,
|
|
210
|
+
outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : 0,
|
|
165
211
|
cacheCreationInputTokens:
|
|
166
|
-
typeof
|
|
167
|
-
?
|
|
168
|
-
:
|
|
212
|
+
typeof usage.cache_write_input_tokens === 'number'
|
|
213
|
+
? usage.cache_write_input_tokens
|
|
214
|
+
: typeof (usage as Record<string, unknown>).cache_creation_input_tokens === 'number'
|
|
215
|
+
? ((usage as Record<string, unknown>).cache_creation_input_tokens as number)
|
|
216
|
+
: 0,
|
|
169
217
|
cacheReadInputTokens:
|
|
170
|
-
typeof
|
|
171
|
-
?
|
|
172
|
-
:
|
|
218
|
+
typeof usage.cached_input_tokens === 'number'
|
|
219
|
+
? usage.cached_input_tokens
|
|
220
|
+
: typeof (usage as Record<string, unknown>).cache_read_input_tokens === 'number'
|
|
221
|
+
? ((usage as Record<string, unknown>).cache_read_input_tokens as number)
|
|
222
|
+
: 0,
|
|
173
223
|
}
|
|
174
224
|
: null,
|
|
175
225
|
};
|
|
@@ -191,12 +241,10 @@ export const codexHarness: Harness = {
|
|
|
191
241
|
},
|
|
192
242
|
buildArgs(opts: BuildArgsOpts): string[] {
|
|
193
243
|
const sandbox = opts.nativePermission ?? SANDBOX_MAP[opts.permission] ?? 'workspace-write';
|
|
194
|
-
const args =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
else args.push('--ask-for-approval', 'on-request');
|
|
244
|
+
const args = opts.resumeSessionId
|
|
245
|
+
? ['exec', 'resume', opts.resumeSessionId, opts.prompt, '--json']
|
|
246
|
+
: ['exec', '--json', opts.prompt, '--sandbox', sandbox];
|
|
198
247
|
if (opts.model) args.push('--model', opts.model);
|
|
199
|
-
if (opts.resumeSessionId) args.push('--thread-id', opts.resumeSessionId);
|
|
200
248
|
for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
|
|
201
249
|
return args;
|
|
202
250
|
},
|
|
@@ -206,13 +254,12 @@ export const codexHarness: Harness = {
|
|
|
206
254
|
extractResult(state: ParseState): StreamedResult | null {
|
|
207
255
|
if (state.result) return state.result;
|
|
208
256
|
if (state.streamedText.trim().length > 0) {
|
|
209
|
-
const latched = (
|
|
210
|
-
?.sessionId as string | undefined;
|
|
257
|
+
const latched = (state._harness as CodexHarnessState | undefined)?.sessionId;
|
|
211
258
|
return {
|
|
212
259
|
result: state.streamedText,
|
|
213
260
|
isError: false,
|
|
214
|
-
numTurns:
|
|
215
|
-
totalCostUsd:
|
|
261
|
+
numTurns: null,
|
|
262
|
+
totalCostUsd: null,
|
|
216
263
|
sessionId: latched ?? null,
|
|
217
264
|
stopReason: null,
|
|
218
265
|
permissionDenials: [],
|
|
@@ -9,14 +9,19 @@ import type {
|
|
|
9
9
|
StreamedResult,
|
|
10
10
|
} from './types.ts';
|
|
11
11
|
|
|
12
|
+
// Schema verified against opencode 1.18.16 — see tests/fixtures/opencode.jsonl.
|
|
13
|
+
// `opencode run` in this version has no `--permission` or `--add-dir` flag at all (confirmed via
|
|
14
|
+
// `opencode run --help`) — permission tiers map onto built-in agents instead (`opencode agent
|
|
15
|
+
// list`: "plan" is the read-only-oriented primary agent, "build" is the full-access one).
|
|
16
|
+
|
|
12
17
|
const execFileAsync = promisify(execFile);
|
|
13
18
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
14
19
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
20
|
}
|
|
16
|
-
const
|
|
17
|
-
readonly: '
|
|
18
|
-
edit: '
|
|
19
|
-
danger: '
|
|
21
|
+
const AGENT_MAP: Record<NormalizedPermission, string> = {
|
|
22
|
+
readonly: 'plan',
|
|
23
|
+
edit: 'build',
|
|
24
|
+
danger: 'build',
|
|
20
25
|
};
|
|
21
26
|
function extractOpencodeText(o: Record<string, unknown>): string | undefined {
|
|
22
27
|
if (typeof o.text === 'string') return o.text;
|
|
@@ -28,6 +33,23 @@ function extractOpencodeText(o: Record<string, unknown>): string | undefined {
|
|
|
28
33
|
if (isRecord(o.message) && typeof o.message.text === 'string') return o.message.text;
|
|
29
34
|
return undefined;
|
|
30
35
|
}
|
|
36
|
+
|
|
37
|
+
interface OpencodeHarnessState {
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
costAccum?: number;
|
|
40
|
+
inputAccum?: number;
|
|
41
|
+
outputAccum?: number;
|
|
42
|
+
cacheReadAccum?: number;
|
|
43
|
+
cacheWriteAccum?: number;
|
|
44
|
+
stepCount?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function harnessState(state: ParseState): OpencodeHarnessState {
|
|
48
|
+
const s = (state._harness ?? {}) as OpencodeHarnessState;
|
|
49
|
+
state._harness = s as unknown as Record<string, unknown>;
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome {
|
|
32
54
|
let o: unknown;
|
|
33
55
|
try {
|
|
@@ -40,31 +62,33 @@ export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome
|
|
|
40
62
|
const activities: ParseOutcome['activities'] = [];
|
|
41
63
|
let streamedText: string | undefined;
|
|
42
64
|
const typeStr = typeof o.type === 'string' ? o.type : '';
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
65
|
+
const hs = harnessState(state);
|
|
66
|
+
// latch sessionID — real events carry it top-level (not just nested under part)
|
|
67
|
+
if (typeof o.sessionID === 'string' && !hs.sessionId) hs.sessionId = o.sessionID;
|
|
68
|
+
if (isRecord(o.part) && typeof (o.part as Record<string, unknown>).sessionID === 'string' && !hs.sessionId) {
|
|
69
|
+
hs.sessionId = (o.part as Record<string, unknown>).sessionID as string;
|
|
49
70
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
// real tool schema: one combined `tool_use` event per call (already resolved by the time it's
|
|
72
|
+
// emitted — no separate start/pending line observed), correlated by part.callID.
|
|
73
|
+
if (typeStr === 'tool_use' && isRecord(o.part)) {
|
|
74
|
+
const part = o.part as Record<string, unknown>;
|
|
75
|
+
const name = typeof part.tool === 'string' ? part.tool : 'tool';
|
|
76
|
+
const id = typeof part.callID === 'string' ? part.callID : undefined;
|
|
77
|
+
const toolState = isRecord(part.state) ? (part.state as Record<string, unknown>) : null;
|
|
78
|
+
const input = toolState && isRecord(toolState.input) ? (toolState.input as Record<string, unknown>) : {};
|
|
79
|
+
const isError = Boolean(toolState?.error) || toolState?.status === 'error';
|
|
80
|
+
activities.push({ kind: 'tool_start', name });
|
|
81
|
+
activities.push({ kind: 'tool_input', name, input, id });
|
|
82
|
+
activities.push({ kind: 'tool_result', isError, id });
|
|
83
|
+
} else if (typeStr.includes('tool') || o.type === 'tool_result') {
|
|
84
|
+
// fallback for older/other event shapes that don't match the combined tool_use schema above
|
|
61
85
|
const name =
|
|
62
86
|
typeof o.name === 'string'
|
|
63
87
|
? o.name
|
|
64
88
|
: typeof (o as Record<string, unknown>).tool === 'string'
|
|
65
89
|
? ((o as Record<string, unknown>).tool as string)
|
|
66
90
|
: 'tool';
|
|
67
|
-
if (typeStr.includes('start')
|
|
91
|
+
if (typeStr.includes('start')) {
|
|
68
92
|
activities.push({ kind: 'tool_start', name });
|
|
69
93
|
if (isRecord(o.input)) activities.push({ kind: 'tool_input', name, input: o.input as Record<string, unknown> });
|
|
70
94
|
} else if (typeStr.includes('result') || typeStr.includes('completed') || o.type === 'tool_result')
|
|
@@ -72,54 +96,34 @@ export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome
|
|
|
72
96
|
}
|
|
73
97
|
if (typeStr.includes('thinking') || o.type === 'reasoning') activities.push({ kind: 'thinking', chars: 10 });
|
|
74
98
|
const directText = extractOpencodeText(o);
|
|
75
|
-
if (directText && !typeStr.includes('tool') && !typeStr.includes('thinking'))
|
|
99
|
+
if (directText && typeStr !== 'tool_use' && !typeStr.includes('tool') && !typeStr.includes('thinking'))
|
|
100
|
+
streamedText = directText;
|
|
76
101
|
if (o.type === 'text' && isRecord(o.part) && typeof o.part.text === 'string') streamedText = o.part.text;
|
|
77
102
|
if (o.type === 'result' || o.type === 'completed' || o.type === 'done' || o.type === 'step_finish') {
|
|
78
103
|
const part = isRecord(o.part) ? (o.part as Record<string, unknown>) : null;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
: 0
|
|
93
|
-
: 0;
|
|
94
|
-
const tokensOutput = isRecord(usage)
|
|
95
|
-
? typeof usage.output === 'number'
|
|
96
|
-
? usage.output
|
|
97
|
-
: typeof usage.output_tokens === 'number'
|
|
98
|
-
? usage.output_tokens
|
|
99
|
-
: 0
|
|
100
|
-
: 0;
|
|
101
|
-
const cost =
|
|
102
|
-
typeof o.total_cost_usd === 'number'
|
|
103
|
-
? o.total_cost_usd
|
|
104
|
-
: part && typeof (part as Record<string, unknown>).cost === 'number'
|
|
105
|
-
? ((part as Record<string, unknown>).cost as number)
|
|
106
|
-
: typeof o.cost === 'number'
|
|
107
|
-
? (o.cost as number)
|
|
108
|
-
: 0;
|
|
109
|
-
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
110
|
-
?.sessionId as string | undefined;
|
|
104
|
+
// step_finish reports the cost/tokens of that one step, not a running total — accumulate
|
|
105
|
+
// across every step_finish seen so far so the final result reports a genuine session total.
|
|
106
|
+
if (typeStr === 'step_finish' && part) {
|
|
107
|
+
const tokens = isRecord(part.tokens) ? (part.tokens as Record<string, unknown>) : null;
|
|
108
|
+
const cache = tokens && isRecord(tokens.cache) ? (tokens.cache as Record<string, unknown>) : null;
|
|
109
|
+
hs.costAccum = (hs.costAccum ?? 0) + (typeof part.cost === 'number' ? part.cost : 0);
|
|
110
|
+
hs.inputAccum = (hs.inputAccum ?? 0) + (tokens && typeof tokens.input === 'number' ? tokens.input : 0);
|
|
111
|
+
hs.outputAccum = (hs.outputAccum ?? 0) + (tokens && typeof tokens.output === 'number' ? tokens.output : 0);
|
|
112
|
+
hs.cacheReadAccum = (hs.cacheReadAccum ?? 0) + (cache && typeof cache.read === 'number' ? cache.read : 0);
|
|
113
|
+
hs.cacheWriteAccum = (hs.cacheWriteAccum ?? 0) + (cache && typeof cache.write === 'number' ? cache.write : 0);
|
|
114
|
+
hs.stepCount = (hs.stepCount ?? 0) + 1;
|
|
115
|
+
}
|
|
116
|
+
const measured = (hs.stepCount ?? 0) > 0;
|
|
111
117
|
const sessionId =
|
|
112
118
|
typeof o.session_id === 'string'
|
|
113
119
|
? o.session_id
|
|
114
120
|
: typeof o.sessionID === 'string'
|
|
115
121
|
? o.sessionID
|
|
116
|
-
: typeof (
|
|
117
|
-
? ((
|
|
118
|
-
:
|
|
119
|
-
?
|
|
120
|
-
:
|
|
121
|
-
? o.id
|
|
122
|
-
: (latched ?? null);
|
|
122
|
+
: part && typeof (part as Record<string, unknown>).sessionID === 'string'
|
|
123
|
+
? ((part as Record<string, unknown>).sessionID as string)
|
|
124
|
+
: typeof o.id === 'string'
|
|
125
|
+
? o.id
|
|
126
|
+
: (hs.sessionId ?? null);
|
|
123
127
|
const result: StreamedResult = {
|
|
124
128
|
result:
|
|
125
129
|
typeof o.result === 'string'
|
|
@@ -128,8 +132,12 @@ export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome
|
|
|
128
132
|
? o.output
|
|
129
133
|
: state.streamedText + (streamedText ?? ''),
|
|
130
134
|
isError: o.is_error === true,
|
|
131
|
-
numTurns: typeof o.num_turns === 'number' ? o.num_turns :
|
|
132
|
-
totalCostUsd:
|
|
135
|
+
numTurns: typeof o.num_turns === 'number' ? o.num_turns : measured ? (hs.stepCount as number) : null,
|
|
136
|
+
totalCostUsd: measured
|
|
137
|
+
? (hs.costAccum as number)
|
|
138
|
+
: typeof o.total_cost_usd === 'number'
|
|
139
|
+
? o.total_cost_usd
|
|
140
|
+
: null,
|
|
133
141
|
sessionId,
|
|
134
142
|
stopReason:
|
|
135
143
|
typeof o.stop_reason === 'string'
|
|
@@ -144,8 +152,13 @@ export function parseOpencodeLine(line: string, state: ParseState): ParseOutcome
|
|
|
144
152
|
model: typeof o.model === 'string' ? o.model : null,
|
|
145
153
|
contextWindow: null,
|
|
146
154
|
maxOutputTokens: null,
|
|
147
|
-
usage:
|
|
148
|
-
? {
|
|
155
|
+
usage: measured
|
|
156
|
+
? {
|
|
157
|
+
inputTokens: hs.inputAccum ?? 0,
|
|
158
|
+
outputTokens: hs.outputAccum ?? 0,
|
|
159
|
+
cacheCreationInputTokens: hs.cacheWriteAccum ?? 0,
|
|
160
|
+
cacheReadInputTokens: hs.cacheReadAccum ?? 0,
|
|
161
|
+
}
|
|
149
162
|
: null,
|
|
150
163
|
};
|
|
151
164
|
return { activities, streamedText, result };
|
|
@@ -165,11 +178,11 @@ export const opencodeHarness: Harness = {
|
|
|
165
178
|
}
|
|
166
179
|
},
|
|
167
180
|
buildArgs(opts: BuildArgsOpts): string[] {
|
|
168
|
-
const
|
|
169
|
-
const args = ['run', '--format', 'json',
|
|
181
|
+
const agent = opts.nativePermission ?? AGENT_MAP[opts.permission] ?? 'build';
|
|
182
|
+
const args = ['run', opts.prompt, '--format', 'json', '--agent', agent];
|
|
183
|
+
if (opts.permission === 'danger' && !opts.nativePermission) args.push('--auto');
|
|
170
184
|
if (opts.model) args.push('--model', opts.model);
|
|
171
185
|
if (opts.resumeSessionId) args.push('--session', opts.resumeSessionId);
|
|
172
|
-
for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
|
|
173
186
|
return args;
|
|
174
187
|
},
|
|
175
188
|
parseLine(line: string, state: ParseState): ParseOutcome {
|
|
@@ -178,13 +191,12 @@ export const opencodeHarness: Harness = {
|
|
|
178
191
|
extractResult(state: ParseState): StreamedResult | null {
|
|
179
192
|
if (state.result) return state.result;
|
|
180
193
|
if (state.streamedText.trim().length > 0) {
|
|
181
|
-
const latched = (
|
|
182
|
-
?.sessionId as string | undefined;
|
|
194
|
+
const latched = (state._harness as OpencodeHarnessState | undefined)?.sessionId;
|
|
183
195
|
return {
|
|
184
196
|
result: state.streamedText,
|
|
185
197
|
isError: false,
|
|
186
|
-
numTurns:
|
|
187
|
-
totalCostUsd:
|
|
198
|
+
numTurns: null,
|
|
199
|
+
totalCostUsd: null,
|
|
188
200
|
sessionId: latched ?? null,
|
|
189
201
|
stopReason: null,
|
|
190
202
|
permissionDenials: [],
|
|
@@ -199,5 +211,5 @@ export const opencodeHarness: Harness = {
|
|
|
199
211
|
}
|
|
200
212
|
return null;
|
|
201
213
|
},
|
|
202
|
-
permissionMap: { readonly: ['
|
|
214
|
+
permissionMap: { readonly: ['plan'], edit: ['build'], danger: ['build', '--auto'] },
|
|
203
215
|
};
|
|
@@ -12,8 +12,10 @@ export interface StreamedUsage {
|
|
|
12
12
|
export interface StreamedResult {
|
|
13
13
|
result: string;
|
|
14
14
|
isError: boolean;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/** null when the harness's payload doesn't report a turn count — distinct from a measured 0. */
|
|
16
|
+
numTurns: number | null;
|
|
17
|
+
/** null when the harness's payload doesn't report cost — distinct from a measured $0. */
|
|
18
|
+
totalCostUsd: number | null;
|
|
17
19
|
sessionId: string | null;
|
|
18
20
|
stopReason: string | null;
|
|
19
21
|
permissionDenials: unknown[];
|
|
@@ -28,8 +30,8 @@ export interface StreamedResult {
|
|
|
28
30
|
|
|
29
31
|
export type ActivityEvent =
|
|
30
32
|
| { kind: 'tool_start'; name: string }
|
|
31
|
-
| { kind: 'tool_input'; name: string; input: Record<string, unknown
|
|
32
|
-
| { kind: 'tool_result'; isError: boolean }
|
|
33
|
+
| { kind: 'tool_input'; name: string; input: Record<string, unknown>; id?: string }
|
|
34
|
+
| { kind: 'tool_result'; isError: boolean; id?: string }
|
|
33
35
|
| { kind: 'thinking'; chars: number };
|
|
34
36
|
|
|
35
37
|
export interface ParseState {
|