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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `delegate()` concurrency guard, factored out of index.ts so it's usable — and testable —
|
|
3
|
+
* independent of the TUI.
|
|
4
|
+
*
|
|
5
|
+
* Combines the file-based cross-process registry (`run-registry.ts`) with an in-process counter
|
|
6
|
+
* fallback (registry I/O failures never block a delegation). `acquireSlot()` is the single choke
|
|
7
|
+
* point: `wait: false` preserves the original fail-fast behavior for ad-hoc single-harness runs
|
|
8
|
+
* (throws immediately at capacity); `wait: true` polls until a slot frees, which is what turns a
|
|
9
|
+
* fan-out into a bounded pool without a separate worker-pool abstraction — callers just kick off
|
|
10
|
+
* all the harnesses at once and let `acquireSlot` serialize the ones that don't fit yet.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type DelegateConfig, getMaxConcurrent } from './config.ts';
|
|
14
|
+
import { acquireRun, countActiveRuns, releaseRun } from './run-registry.ts';
|
|
15
|
+
|
|
16
|
+
const activeRuns = new Map<string, number>();
|
|
17
|
+
let globalActiveRuns = 0;
|
|
18
|
+
|
|
19
|
+
/** In-process active-run count (optionally filtered to one harness). Exposed for `/delegate status`. */
|
|
20
|
+
export function inProcessActiveCount(harness?: string): number {
|
|
21
|
+
return harness ? (activeRuns.get(harness) ?? 0) : globalActiveRuns;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Active-run count combining the in-process counter with the cross-process registry (the max of
|
|
25
|
+
* the two — registry I/O failures fall back to the in-process view). */
|
|
26
|
+
export function activeCount(harness?: string): number {
|
|
27
|
+
return Math.max(inProcessActiveCount(harness), countActiveRuns(harness));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Thrown by `acquireSlot({wait: false})` when at capacity. */
|
|
31
|
+
export class ConcurrencyLimitError extends Error {}
|
|
32
|
+
|
|
33
|
+
export interface AcquireSlotOptions {
|
|
34
|
+
harness: string;
|
|
35
|
+
mode: string;
|
|
36
|
+
config: DelegateConfig;
|
|
37
|
+
/** false (default): throw immediately at capacity. true: poll until a slot frees. */
|
|
38
|
+
wait: boolean;
|
|
39
|
+
/** Aborts a `wait: true` poll early. */
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
pollIntervalMs?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function abortError(): Error {
|
|
45
|
+
const err = new Error('aborted');
|
|
46
|
+
err.name = 'AbortError';
|
|
47
|
+
return err;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
if (signal?.aborted) {
|
|
53
|
+
reject(abortError());
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const onAbort = () => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
reject(abortError());
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
signal?.removeEventListener('abort', onAbort);
|
|
62
|
+
resolve();
|
|
63
|
+
}, ms);
|
|
64
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Acquire a concurrency slot for one delegate() run. Resolves with a release function (idempotent,
|
|
70
|
+
* never throws) once a slot is held; the caller must call it exactly once when the run finishes.
|
|
71
|
+
*
|
|
72
|
+
* Checks the global limit before the per-harness limit — same precedence and error text as the
|
|
73
|
+
* original inline guard, so single-run (`wait: false`) callers see unchanged behavior.
|
|
74
|
+
*/
|
|
75
|
+
export async function acquireSlot(opts: AcquireSlotOptions): Promise<() => void> {
|
|
76
|
+
const { harness, mode, config, wait, signal, pollIntervalMs = 200 } = opts;
|
|
77
|
+
for (;;) {
|
|
78
|
+
if (signal?.aborted) throw abortError();
|
|
79
|
+
const maxGlobal = getMaxConcurrent(config);
|
|
80
|
+
const globalCount = activeCount();
|
|
81
|
+
if (maxGlobal > 0 && globalCount >= maxGlobal) {
|
|
82
|
+
if (!wait) throw new ConcurrencyLimitError('another delegate run is already in progress (global limit)');
|
|
83
|
+
await sleep(pollIntervalMs, signal);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const perHarnessLimit = getMaxConcurrent(config, harness);
|
|
87
|
+
const perHarnessCount = activeCount(harness);
|
|
88
|
+
if (perHarnessLimit > 0 && perHarnessCount >= perHarnessLimit) {
|
|
89
|
+
if (!wait) throw new ConcurrencyLimitError(`another ${harness} run is already in progress`);
|
|
90
|
+
await sleep(pollIntervalMs, signal);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
activeRuns.set(harness, perHarnessCount + 1);
|
|
95
|
+
globalActiveRuns++;
|
|
96
|
+
const runHandle = acquireRun(harness, mode);
|
|
97
|
+
let released = false;
|
|
98
|
+
return () => {
|
|
99
|
+
if (released) return;
|
|
100
|
+
released = true;
|
|
101
|
+
activeRuns.set(harness, Math.max(0, (activeRuns.get(harness) ?? 1) - 1));
|
|
102
|
+
globalActiveRuns = Math.max(0, globalActiveRuns - 1);
|
|
103
|
+
releaseRun(runHandle);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
package/extensions/config.ts
CHANGED
|
@@ -47,7 +47,7 @@ export function loadConfig(): DelegateConfig {
|
|
|
47
47
|
inspectThinking: false,
|
|
48
48
|
autoDelegateHints: false,
|
|
49
49
|
modelAliases: { economy: 'haiku', balanced: 'sonnet', max: 'opus' },
|
|
50
|
-
maxConcurrent:
|
|
50
|
+
maxConcurrent: 4,
|
|
51
51
|
maxTranscripts: 100,
|
|
52
52
|
harnesses: {},
|
|
53
53
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
import { accessSync, constants } from 'node:fs';
|
|
3
|
+
import { delimiter, join } from 'node:path';
|
|
2
4
|
import { promisify } from 'node:util';
|
|
3
5
|
import type {
|
|
4
6
|
BuildArgsOpts,
|
|
@@ -9,14 +11,47 @@ import type {
|
|
|
9
11
|
StreamedResult,
|
|
10
12
|
} from './types.ts';
|
|
11
13
|
|
|
14
|
+
// Schema verified against omp/17.2.9 (the only binary of this harness installed on the capture
|
|
15
|
+
// machine — see tests/fixtures/amp.jsonl and AGENTS.md for what "amp" vs "omp" means here).
|
|
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
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `amp` isn't on PATH on machines that only have the `omp` alias binary installed — resolve
|
|
24
|
+
* which one actually exists once at module load, so the spawned binary matches what `detect()`
|
|
25
|
+
* already tolerates. Falls back to 'amp' (the documented default) when neither is found, so
|
|
26
|
+
* error messages still name the expected tool.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveAmpBinary(pathEnv: string | undefined, exists: (p: string) => boolean = pathExists): string {
|
|
29
|
+
const dirs = (pathEnv ?? '').split(delimiter).filter(Boolean);
|
|
30
|
+
for (const dir of dirs) {
|
|
31
|
+
if (exists(join(dir, 'amp'))) return 'amp';
|
|
32
|
+
}
|
|
33
|
+
for (const dir of dirs) {
|
|
34
|
+
if (exists(join(dir, 'omp'))) return 'omp';
|
|
35
|
+
}
|
|
36
|
+
return 'amp';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pathExists(p: string): boolean {
|
|
40
|
+
try {
|
|
41
|
+
accessSync(p, constants.X_OK);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const RESOLVED_BINARY = resolveAmpBinary(process.env.PATH);
|
|
49
|
+
|
|
50
|
+
// approval-mode maps cleanly onto the normalized 3-tier permission model.
|
|
16
51
|
const PERMISSION_MAP: Record<NormalizedPermission, string> = {
|
|
17
|
-
readonly: '
|
|
18
|
-
edit: '
|
|
19
|
-
danger: '
|
|
52
|
+
readonly: 'always-ask',
|
|
53
|
+
edit: 'write',
|
|
54
|
+
danger: 'yolo',
|
|
20
55
|
};
|
|
21
56
|
function extractAmpText(o: Record<string, unknown>): string | undefined {
|
|
22
57
|
if (typeof o.text === 'string') return o.text;
|
|
@@ -26,6 +61,23 @@ function extractAmpText(o: Record<string, unknown>): string | undefined {
|
|
|
26
61
|
if (isRecord(o.part) && typeof o.part.text === 'string') return o.part.text;
|
|
27
62
|
return undefined;
|
|
28
63
|
}
|
|
64
|
+
|
|
65
|
+
interface AmpHarnessState {
|
|
66
|
+
sessionId?: string;
|
|
67
|
+
costAccum?: number;
|
|
68
|
+
inputAccum?: number;
|
|
69
|
+
outputAccum?: number;
|
|
70
|
+
cacheReadAccum?: number;
|
|
71
|
+
cacheWriteAccum?: number;
|
|
72
|
+
turnCount?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function harnessState(state: ParseState): AmpHarnessState {
|
|
76
|
+
const s = (state._harness ?? {}) as AmpHarnessState;
|
|
77
|
+
state._harness = s as unknown as Record<string, unknown>;
|
|
78
|
+
return s;
|
|
79
|
+
}
|
|
80
|
+
|
|
29
81
|
export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
|
|
30
82
|
let o: unknown;
|
|
31
83
|
try {
|
|
@@ -38,33 +90,30 @@ export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
|
|
|
38
90
|
const activities: ParseOutcome['activities'] = [];
|
|
39
91
|
let streamedText: string | undefined;
|
|
40
92
|
const typeStr = typeof o.type === 'string' ? o.type : '';
|
|
93
|
+
const hs = harnessState(state);
|
|
41
94
|
// latch session id
|
|
42
|
-
if (typeStr === 'session' && typeof o.id === 'string')
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
if (typeStr === 'session' && typeof (o as Record<string, unknown>).sessionID === 'string') {
|
|
55
|
-
(state as unknown as Record<string, unknown>)._harness = {
|
|
56
|
-
...(((state as unknown as Record<string, unknown>)._harness as Record<string, unknown>) ?? {}),
|
|
57
|
-
sessionId: (o as Record<string, unknown>).sessionID as string,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
if (typeStr.includes('tool') || o.type === 'tool_use') {
|
|
61
|
-
const name = typeof o.name === 'string' ? o.name : 'tool';
|
|
62
|
-
if (typeStr.includes('start') || o.type === 'tool_use') {
|
|
95
|
+
if (typeStr === 'session' && typeof o.id === 'string') hs.sessionId = o.id;
|
|
96
|
+
if (isRecord(o.part) && typeof (o.part as Record<string, unknown>).sessionID === 'string')
|
|
97
|
+
hs.sessionId = (o.part as Record<string, unknown>).sessionID as string;
|
|
98
|
+
if (typeStr === 'session' && typeof (o as Record<string, unknown>).sessionID === 'string')
|
|
99
|
+
hs.sessionId = (o as Record<string, unknown>).sessionID as string;
|
|
100
|
+
|
|
101
|
+
// real tool schema: top-level tool_execution_start/tool_execution_end, correlated by toolCallId
|
|
102
|
+
if (typeStr === 'tool_execution_start' || typeStr === 'tool_execution_end') {
|
|
103
|
+
const name = typeof o.toolName === 'string' ? o.toolName : 'tool';
|
|
104
|
+
const id = typeof o.toolCallId === 'string' ? o.toolCallId : undefined;
|
|
105
|
+
if (typeStr === 'tool_execution_start') {
|
|
63
106
|
activities.push({ kind: 'tool_start', name });
|
|
64
|
-
|
|
65
|
-
|
|
107
|
+
activities.push({
|
|
108
|
+
kind: 'tool_input',
|
|
109
|
+
name,
|
|
110
|
+
input: isRecord(o.args) ? (o.args as Record<string, unknown>) : {},
|
|
111
|
+
id,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
activities.push({ kind: 'tool_result', isError: o.isError === true, id });
|
|
115
|
+
}
|
|
66
116
|
}
|
|
67
|
-
if (typeStr.includes('thinking')) activities.push({ kind: 'thinking', chars: 10 });
|
|
68
117
|
if (typeStr === 'message_update' && isRecord(o.assistantMessageEvent)) {
|
|
69
118
|
const ev = o.assistantMessageEvent as Record<string, unknown>;
|
|
70
119
|
if (ev.type === 'thinking_delta' && typeof ev.delta === 'string')
|
|
@@ -73,7 +122,11 @@ export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
|
|
|
73
122
|
else if (ev.type === 'thinking_start') activities.push({ kind: 'thinking', chars: 5 });
|
|
74
123
|
}
|
|
75
124
|
if (typeStr === 'turn_end' || typeStr === 'agent_end') {
|
|
76
|
-
const msg = isRecord(o.message)
|
|
125
|
+
const msg = isRecord(o.message)
|
|
126
|
+
? (o.message as Record<string, unknown>)
|
|
127
|
+
: Array.isArray(o.messages) && isRecord(o.messages[o.messages.length - 1])
|
|
128
|
+
? (o.messages[o.messages.length - 1] as Record<string, unknown>)
|
|
129
|
+
: null;
|
|
77
130
|
if (msg && Array.isArray(msg.content)) {
|
|
78
131
|
for (const block of msg.content as unknown[]) {
|
|
79
132
|
if (isRecord(block) && block.type === 'text' && typeof block.text === 'string') streamedText = block.text;
|
|
@@ -81,58 +134,31 @@ export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
|
|
|
81
134
|
activities.push({ kind: 'thinking', chars: (block.thinking as string).length });
|
|
82
135
|
}
|
|
83
136
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
137
|
+
// real usage lives at message.usage on turn_end (per-turn, not cumulative — see accumulation below)
|
|
138
|
+
if (typeStr === 'turn_end' && isRecord(msg?.usage)) {
|
|
139
|
+
const u = msg.usage as Record<string, unknown>;
|
|
140
|
+
const cost =
|
|
141
|
+
isRecord(u.cost) && typeof (u.cost as Record<string, unknown>).total === 'number'
|
|
142
|
+
? ((u.cost as Record<string, unknown>).total as number)
|
|
143
|
+
: 0;
|
|
144
|
+
hs.costAccum = (hs.costAccum ?? 0) + cost;
|
|
145
|
+
hs.inputAccum = (hs.inputAccum ?? 0) + (typeof u.input === 'number' ? u.input : 0);
|
|
146
|
+
hs.outputAccum = (hs.outputAccum ?? 0) + (typeof u.output === 'number' ? u.output : 0);
|
|
147
|
+
hs.cacheReadAccum = (hs.cacheReadAccum ?? 0) + (typeof u.cacheRead === 'number' ? u.cacheRead : 0);
|
|
148
|
+
hs.cacheWriteAccum = (hs.cacheWriteAccum ?? 0) + (typeof u.cacheWrite === 'number' ? u.cacheWrite : 0);
|
|
149
|
+
hs.turnCount = (hs.turnCount ?? 0) + 1;
|
|
92
150
|
}
|
|
93
151
|
}
|
|
94
152
|
const text = extractAmpText(o);
|
|
95
|
-
if (text &&
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
) {
|
|
103
|
-
const usage = isRecord(o.usage)
|
|
104
|
-
? o.usage
|
|
105
|
-
: isRecord(o.message) && isRecord((o.message as Record<string, unknown>).usage)
|
|
106
|
-
? ((o.message as Record<string, unknown>).usage as Record<string, unknown>)
|
|
107
|
-
: null;
|
|
108
|
-
const actualUsage = isRecord(usage)
|
|
109
|
-
? usage
|
|
110
|
-
: isRecord(o.message) && isRecord((o.message as Record<string, unknown>).usage)
|
|
111
|
-
? ((o.message as Record<string, unknown>).usage as Record<string, unknown>)
|
|
153
|
+
if (text && typeStr !== 'tool_execution_start' && typeStr !== 'tool_execution_end' && !streamedText)
|
|
154
|
+
streamedText = text;
|
|
155
|
+
if (typeStr === 'turn_end' || typeStr === 'agent_end') {
|
|
156
|
+
const msg = isRecord(o.message)
|
|
157
|
+
? (o.message as Record<string, unknown>)
|
|
158
|
+
: Array.isArray(o.messages) && isRecord(o.messages[o.messages.length - 1])
|
|
159
|
+
? (o.messages[o.messages.length - 1] as Record<string, unknown>)
|
|
112
160
|
: null;
|
|
113
|
-
const
|
|
114
|
-
const latched = ((state as unknown as Record<string, unknown>)._harness as Record<string, unknown> | undefined)
|
|
115
|
-
?.sessionId as string | undefined;
|
|
116
|
-
const cost =
|
|
117
|
-
isRecord(actualUsage) && typeof (actualUsage as Record<string, unknown>).total === 'number'
|
|
118
|
-
? ((actualUsage as Record<string, unknown>).total as number)
|
|
119
|
-
: typeof o.total_cost_usd === 'number'
|
|
120
|
-
? o.total_cost_usd
|
|
121
|
-
: 0;
|
|
122
|
-
const inputTokens = isRecord(actualUsage)
|
|
123
|
-
? typeof (actualUsage as Record<string, unknown>).input === 'number'
|
|
124
|
-
? ((actualUsage as Record<string, unknown>).input as number)
|
|
125
|
-
: typeof (actualUsage as Record<string, unknown>).input_tokens === 'number'
|
|
126
|
-
? ((actualUsage as Record<string, unknown>).input_tokens as number)
|
|
127
|
-
: 0
|
|
128
|
-
: 0;
|
|
129
|
-
const outputTokens = isRecord(actualUsage)
|
|
130
|
-
? typeof (actualUsage as Record<string, unknown>).output === 'number'
|
|
131
|
-
? ((actualUsage as Record<string, unknown>).output as number)
|
|
132
|
-
: typeof (actualUsage as Record<string, unknown>).output_tokens === 'number'
|
|
133
|
-
? ((actualUsage as Record<string, unknown>).output_tokens as number)
|
|
134
|
-
: 0
|
|
135
|
-
: 0;
|
|
161
|
+
const measured = (hs.turnCount ?? 0) > 0;
|
|
136
162
|
const result: StreamedResult = {
|
|
137
163
|
result:
|
|
138
164
|
typeof o.result === 'string'
|
|
@@ -141,9 +167,10 @@ export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
|
|
|
141
167
|
? state.streamedText + streamedText
|
|
142
168
|
: state.streamedText || (text ?? ''),
|
|
143
169
|
isError: o.is_error === true,
|
|
144
|
-
numTurns:
|
|
145
|
-
totalCostUsd:
|
|
146
|
-
sessionId:
|
|
170
|
+
numTurns: measured ? (hs.turnCount as number) : null,
|
|
171
|
+
totalCostUsd: measured ? (hs.costAccum as number) : null,
|
|
172
|
+
sessionId:
|
|
173
|
+
typeof o.session_id === 'string' ? o.session_id : typeof o.id === 'string' ? o.id : (hs.sessionId ?? null),
|
|
147
174
|
stopReason:
|
|
148
175
|
typeof o.stop_reason === 'string'
|
|
149
176
|
? o.stop_reason
|
|
@@ -165,18 +192,14 @@ export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
|
|
|
165
192
|
model: typeof o.model === 'string' ? o.model : typeof msg?.model === 'string' ? (msg.model as string) : null,
|
|
166
193
|
contextWindow: null,
|
|
167
194
|
maxOutputTokens: null,
|
|
168
|
-
usage:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
? ((actualUsage as Record<string, unknown>).cacheRead as number)
|
|
177
|
-
: 0,
|
|
178
|
-
}
|
|
179
|
-
: null,
|
|
195
|
+
usage: measured
|
|
196
|
+
? {
|
|
197
|
+
inputTokens: hs.inputAccum ?? 0,
|
|
198
|
+
outputTokens: hs.outputAccum ?? 0,
|
|
199
|
+
cacheCreationInputTokens: hs.cacheWriteAccum ?? 0,
|
|
200
|
+
cacheReadInputTokens: hs.cacheReadAccum ?? 0,
|
|
201
|
+
}
|
|
202
|
+
: null,
|
|
180
203
|
};
|
|
181
204
|
if (!result.result) result.result = state.streamedText + (streamedText ?? '');
|
|
182
205
|
return { activities, streamedText, result };
|
|
@@ -186,7 +209,7 @@ export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
|
|
|
186
209
|
export const ampHarness: Harness = {
|
|
187
210
|
name: 'amp',
|
|
188
211
|
displayName: 'Amp',
|
|
189
|
-
binary:
|
|
212
|
+
binary: RESOLVED_BINARY,
|
|
190
213
|
async detect() {
|
|
191
214
|
try {
|
|
192
215
|
const { stdout } = await execFileAsync('amp', ['--version'], { timeout: 5000 });
|
|
@@ -201,11 +224,12 @@ export const ampHarness: Harness = {
|
|
|
201
224
|
}
|
|
202
225
|
},
|
|
203
226
|
buildArgs(opts: BuildArgsOpts): string[] {
|
|
204
|
-
const
|
|
205
|
-
const args = ['
|
|
227
|
+
const approvalMode = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'always-ask';
|
|
228
|
+
const args = ['-p', '--mode', 'json', '--approval-mode', approvalMode];
|
|
206
229
|
if (opts.model) args.push('--model', opts.model);
|
|
207
230
|
if (opts.resumeSessionId) args.push('--resume', opts.resumeSessionId);
|
|
208
231
|
for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
|
|
232
|
+
args.push(opts.prompt);
|
|
209
233
|
return args;
|
|
210
234
|
},
|
|
211
235
|
parseLine(line: string, state: ParseState): ParseOutcome {
|
|
@@ -214,13 +238,12 @@ export const ampHarness: Harness = {
|
|
|
214
238
|
extractResult(state: ParseState): StreamedResult | null {
|
|
215
239
|
if (state.result) return state.result;
|
|
216
240
|
if (state.streamedText.trim().length > 0) {
|
|
217
|
-
const latched = (
|
|
218
|
-
?.sessionId as string | undefined;
|
|
241
|
+
const latched = (state._harness as AmpHarnessState | undefined)?.sessionId;
|
|
219
242
|
return {
|
|
220
243
|
result: state.streamedText,
|
|
221
244
|
isError: false,
|
|
222
|
-
numTurns:
|
|
223
|
-
totalCostUsd:
|
|
245
|
+
numTurns: null,
|
|
246
|
+
totalCostUsd: null,
|
|
224
247
|
sessionId: latched ?? null,
|
|
225
248
|
stopReason: null,
|
|
226
249
|
permissionDenials: [],
|
|
@@ -235,5 +258,5 @@ export const ampHarness: Harness = {
|
|
|
235
258
|
}
|
|
236
259
|
return null;
|
|
237
260
|
},
|
|
238
|
-
permissionMap: { readonly: ['
|
|
261
|
+
permissionMap: { readonly: ['always-ask'], edit: ['write'], danger: ['yolo'] },
|
|
239
262
|
};
|
|
@@ -56,13 +56,18 @@ export function parseClaudeLine(line: string, state: ParseState): ParseOutcome {
|
|
|
56
56
|
kind: 'tool_input',
|
|
57
57
|
name: block.name,
|
|
58
58
|
input: isRecord(block.input) ? block.input : {},
|
|
59
|
+
id: typeof block.id === 'string' ? block.id : undefined,
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
} else if (o.type === 'user' && isRecord(o.message)) {
|
|
63
64
|
for (const block of Array.isArray(o.message.content) ? o.message.content : []) {
|
|
64
65
|
if (isRecord(block) && block.type === 'tool_result') {
|
|
65
|
-
activities.push({
|
|
66
|
+
activities.push({
|
|
67
|
+
kind: 'tool_result',
|
|
68
|
+
isError: block.is_error === true,
|
|
69
|
+
id: typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined,
|
|
70
|
+
});
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
} else if (o.type === 'result') {
|
|
@@ -82,8 +87,8 @@ export function parseClaudeLine(line: string, state: ParseState): ParseOutcome {
|
|
|
82
87
|
const result: StreamedResult = {
|
|
83
88
|
result: typeof o.result === 'string' ? o.result : state.streamedText,
|
|
84
89
|
isError: o.is_error === true,
|
|
85
|
-
numTurns: typeof o.num_turns === 'number' ? o.num_turns :
|
|
86
|
-
totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd :
|
|
90
|
+
numTurns: typeof o.num_turns === 'number' ? o.num_turns : null,
|
|
91
|
+
totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : null,
|
|
87
92
|
sessionId: typeof o.session_id === 'string' ? o.session_id : null,
|
|
88
93
|
stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
|
|
89
94
|
permissionDenials: Array.isArray(o.permission_denials) ? o.permission_denials : [],
|