pi-harness-delegate 0.2.2 → 0.3.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 +35 -12
- package/extensions/activity.ts +212 -16
- package/extensions/command.ts +83 -6
- 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 +617 -206
- package/extensions/notify.ts +50 -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,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 {
|