pi-harness-delegate 0.1.0 → 0.1.1

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.
@@ -4,155 +4,161 @@ import { join } from 'node:path';
4
4
  import { DEFAULT_TIMEOUT_MS } from './harnesses/types.ts';
5
5
 
6
6
  export interface HarnessConfig {
7
- model?: string;
8
- timeoutMs?: number;
9
- allowDangerous?: boolean;
10
- maxBudgetUsd?: number;
7
+ model?: string;
8
+ timeoutMs?: number;
9
+ allowDangerous?: boolean;
10
+ maxBudgetUsd?: number;
11
11
  }
12
12
 
13
13
  export interface DelegateConfig {
14
- model?: string;
15
- timeoutMs: number;
16
- defaultMode: string;
17
- defaultHarness: string;
18
- allowDangerous: boolean;
19
- inspectThinking: boolean;
20
- maxBudgetUsd?: number;
21
- autoDelegateHints: boolean;
22
- modelAliases: Record<string, string>;
23
- maxConcurrent: number | { global?: number; perHarness?: Record<string, number> };
24
- maxTranscripts: number;
25
- harnesses: Record<string, HarnessConfig>;
14
+ model?: string;
15
+ timeoutMs: number;
16
+ defaultMode: string;
17
+ defaultHarness: string;
18
+ allowDangerous: boolean;
19
+ inspectThinking: boolean;
20
+ maxBudgetUsd?: number;
21
+ autoDelegateHints: boolean;
22
+ modelAliases: Record<string, string>;
23
+ maxConcurrent: number | { global?: number; perHarness?: Record<string, number> };
24
+ maxTranscripts: number;
25
+ harnesses: Record<string, HarnessConfig>;
26
26
  }
27
27
 
28
28
  export function agentDir(): string {
29
- return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
29
+ return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
30
30
  }
31
31
 
32
32
  export function outputsDir(harness?: string): string {
33
- const base = join(agentDir(), 'delegate', 'outputs');
34
- return harness ? join(base, harness) : base;
33
+ const base = join(agentDir(), 'delegate', 'outputs');
34
+ return harness ? join(base, harness) : base;
35
35
  }
36
36
 
37
37
  export function legacyOutputsDir(): string {
38
- return join(agentDir(), 'claude-delegate', 'outputs');
38
+ return join(agentDir(), 'claude-delegate', 'outputs');
39
39
  }
40
40
 
41
41
  export function loadConfig(): DelegateConfig {
42
- const cfg: DelegateConfig = {
43
- timeoutMs: DEFAULT_TIMEOUT_MS,
44
- defaultMode: 'general',
45
- defaultHarness: 'claude',
46
- allowDangerous: false,
47
- inspectThinking: false,
48
- autoDelegateHints: false,
49
- modelAliases: { economy: 'haiku', balanced: 'sonnet', max: 'opus' },
50
- maxConcurrent: 1,
51
- maxTranscripts: 100,
52
- harnesses: {},
53
- };
54
- try {
55
- const file = join(agentDir(), 'settings.json');
56
- if (!existsSync(file)) return cfg;
57
- const settings = JSON.parse(readFileSync(file, 'utf8')) as {
58
- delegate?: Partial<DelegateConfig & { harnesses: Record<string, HarnessConfig> }>;
59
- claudeDelegate?: Partial<DelegateConfig & { model?: string }>;
60
- };
61
- // Legacy claudeDelegate -> delegate.harnesses.claude migration
62
- if (settings.claudeDelegate && !settings.delegate) {
63
- const c = settings.claudeDelegate as Partial<DelegateConfig>;
64
- if (typeof c.model === 'string') cfg.harnesses['claude'] = { ...(cfg.harnesses['claude'] ?? {}), model: c.model };
65
- if (typeof c.timeoutMs === 'number' && c.timeoutMs > 0) cfg.timeoutMs = c.timeoutMs;
66
- if (typeof c.defaultMode === 'string') cfg.defaultMode = c.defaultMode;
67
- if (typeof c.allowDangerous === 'boolean') cfg.allowDangerous = c.allowDangerous;
68
- if (typeof c.inspectThinking === 'boolean') cfg.inspectThinking = c.inspectThinking;
69
- if (typeof (c as DelegateConfig).maxBudgetUsd === 'number' && (c as DelegateConfig).maxBudgetUsd! > 0) cfg.maxBudgetUsd = (c as DelegateConfig).maxBudgetUsd;
70
- if (typeof c.autoDelegateHints === 'boolean') cfg.autoDelegateHints = c.autoDelegateHints;
71
- if ((c as DelegateConfig).modelAliases && typeof (c as DelegateConfig).modelAliases === 'object') {
72
- for (const [k, v] of Object.entries((c as DelegateConfig).modelAliases!)) {
73
- if (typeof v === 'string' && v) cfg.modelAliases[k] = v;
74
- }
75
- }
76
- if (typeof c.maxConcurrent === 'number' && c.maxConcurrent >= 0) cfg.maxConcurrent = c.maxConcurrent;
77
- else if (c.maxConcurrent && typeof c.maxConcurrent === 'object') {
78
- const obj = c.maxConcurrent as { global?: unknown; perHarness?: unknown };
79
- const out: { global?: number; perHarness?: Record<string, number> } = {};
80
- if (typeof obj.global === 'number' && obj.global >= 0) out.global = obj.global;
81
- if (obj.perHarness && typeof obj.perHarness === 'object') {
82
- const ph: Record<string, number> = {};
83
- for (const [k, v] of Object.entries(obj.perHarness as Record<string, unknown>)) {
84
- if (typeof v === 'number' && v >= 0) ph[k] = v;
85
- }
86
- if (Object.keys(ph).length > 0) out.perHarness = ph;
87
- }
88
- if (out.global !== undefined || out.perHarness) cfg.maxConcurrent = out;
89
- }
90
- if (typeof c.maxTranscripts === 'number' && c.maxTranscripts >= 0) cfg.maxTranscripts = c.maxTranscripts;
91
- if (c.harnesses && typeof c.harnesses === 'object') {
92
- for (const [k, v] of Object.entries(c.harnesses)) {
93
- if (v && typeof v === 'object') cfg.harnesses[k] = { ...(cfg.harnesses[k] ?? {}), ...(v as HarnessConfig) };
94
- }
95
- }
96
- // also map harnesses.claude if any
97
- return cfg;
98
- }
99
- const d = settings.delegate ?? {};
100
- if (typeof d.defaultHarness === 'string' && d.defaultHarness) cfg.defaultHarness = d.defaultHarness;
101
- if (typeof d.defaultMode === 'string') cfg.defaultMode = d.defaultMode;
102
- if (typeof d.model === 'string') cfg.model = d.model;
103
- if (typeof d.timeoutMs === 'number' && d.timeoutMs > 0) cfg.timeoutMs = d.timeoutMs;
104
- if (typeof d.allowDangerous === 'boolean') cfg.allowDangerous = d.allowDangerous;
105
- if (typeof d.inspectThinking === 'boolean') cfg.inspectThinking = d.inspectThinking;
106
- if (typeof d.maxBudgetUsd === 'number' && d.maxBudgetUsd > 0) cfg.maxBudgetUsd = d.maxBudgetUsd;
107
- if (typeof d.autoDelegateHints === 'boolean') cfg.autoDelegateHints = d.autoDelegateHints;
108
- if (d.modelAliases && typeof d.modelAliases === 'object') {
109
- for (const [k, v] of Object.entries(d.modelAliases)) {
110
- if (typeof v === 'string' && v) cfg.modelAliases[k] = v;
111
- }
112
- }
113
- if (typeof d.maxConcurrent === 'number' && d.maxConcurrent >= 0) cfg.maxConcurrent = d.maxConcurrent;
114
- else if (d.maxConcurrent && typeof d.maxConcurrent === 'object') {
115
- const obj = d.maxConcurrent as { global?: unknown; perHarness?: unknown };
116
- const out: { global?: number; perHarness?: Record<string, number> } = {};
117
- if (typeof obj.global === 'number' && obj.global >= 0) out.global = obj.global;
118
- if (obj.perHarness && typeof obj.perHarness === 'object') {
119
- const ph: Record<string, number> = {};
120
- for (const [k, v] of Object.entries(obj.perHarness as Record<string, unknown>)) {
121
- if (typeof v === 'number' && v >= 0) ph[k] = v;
122
- }
123
- if (Object.keys(ph).length > 0) out.perHarness = ph;
124
- }
125
- if (out.global !== undefined || out.perHarness) cfg.maxConcurrent = out;
126
- }
127
- if (typeof d.maxTranscripts === 'number' && d.maxTranscripts >= 0) cfg.maxTranscripts = d.maxTranscripts;
128
- if (d.harnesses && typeof d.harnesses === 'object') {
129
- for (const [k, v] of Object.entries(d.harnesses)) {
130
- if (v && typeof v === 'object') cfg.harnesses[k] = { ...(v as HarnessConfig) };
131
- }
132
- }
133
- // also support legacy claudeDelegate merged when delegate also present (delegate wins)
134
- if (settings.claudeDelegate) {
135
- const c = settings.claudeDelegate as Partial<DelegateConfig>;
136
- if (typeof c.model === 'string' && !cfg.harnesses['claude']?.model) {
137
- cfg.harnesses['claude'] = { ...(cfg.harnesses['claude'] ?? {}), model: c.model };
138
- }
139
- }
140
- } catch {
141
- // invalid settings — fall back to defaults
142
- }
143
- return cfg;
42
+ const cfg: DelegateConfig = {
43
+ timeoutMs: DEFAULT_TIMEOUT_MS,
44
+ defaultMode: 'general',
45
+ defaultHarness: 'claude',
46
+ allowDangerous: false,
47
+ inspectThinking: false,
48
+ autoDelegateHints: false,
49
+ modelAliases: { economy: 'haiku', balanced: 'sonnet', max: 'opus' },
50
+ maxConcurrent: 1,
51
+ maxTranscripts: 100,
52
+ harnesses: {},
53
+ };
54
+ try {
55
+ const file = join(agentDir(), 'settings.json');
56
+ if (!existsSync(file)) return cfg;
57
+ const settings = JSON.parse(readFileSync(file, 'utf8')) as {
58
+ delegate?: Partial<DelegateConfig & { harnesses: Record<string, HarnessConfig> }>;
59
+ claudeDelegate?: Partial<DelegateConfig & { model?: string }>;
60
+ };
61
+ // Legacy claudeDelegate -> delegate.harnesses.claude migration
62
+ if (settings.claudeDelegate && !settings.delegate) {
63
+ const c = settings.claudeDelegate as Partial<DelegateConfig>;
64
+ if (typeof c.model === 'string') cfg.harnesses['claude'] = { ...(cfg.harnesses['claude'] ?? {}), model: c.model };
65
+ if (typeof c.timeoutMs === 'number' && c.timeoutMs > 0) cfg.timeoutMs = c.timeoutMs;
66
+ if (typeof c.defaultMode === 'string') cfg.defaultMode = c.defaultMode;
67
+ if (typeof c.allowDangerous === 'boolean') cfg.allowDangerous = c.allowDangerous;
68
+ if (typeof c.inspectThinking === 'boolean') cfg.inspectThinking = c.inspectThinking;
69
+ if (typeof (c as DelegateConfig).maxBudgetUsd === 'number' && (c as DelegateConfig).maxBudgetUsd! > 0)
70
+ cfg.maxBudgetUsd = (c as DelegateConfig).maxBudgetUsd;
71
+ if (typeof c.autoDelegateHints === 'boolean') cfg.autoDelegateHints = c.autoDelegateHints;
72
+ if ((c as DelegateConfig).modelAliases && typeof (c as DelegateConfig).modelAliases === 'object') {
73
+ for (const [k, v] of Object.entries((c as DelegateConfig).modelAliases!)) {
74
+ if (typeof v === 'string' && v) cfg.modelAliases[k] = v;
75
+ }
76
+ }
77
+ if (typeof c.maxConcurrent === 'number' && c.maxConcurrent >= 0) cfg.maxConcurrent = c.maxConcurrent;
78
+ else if (c.maxConcurrent && typeof c.maxConcurrent === 'object') {
79
+ const obj = c.maxConcurrent as { global?: unknown; perHarness?: unknown };
80
+ const out: { global?: number; perHarness?: Record<string, number> } = {};
81
+ if (typeof obj.global === 'number' && obj.global >= 0) out.global = obj.global;
82
+ if (obj.perHarness && typeof obj.perHarness === 'object') {
83
+ const ph: Record<string, number> = {};
84
+ for (const [k, v] of Object.entries(obj.perHarness as Record<string, unknown>)) {
85
+ if (typeof v === 'number' && v >= 0) ph[k] = v;
86
+ }
87
+ if (Object.keys(ph).length > 0) out.perHarness = ph;
88
+ }
89
+ if (out.global !== undefined || out.perHarness) cfg.maxConcurrent = out;
90
+ }
91
+ if (typeof c.maxTranscripts === 'number' && c.maxTranscripts >= 0) cfg.maxTranscripts = c.maxTranscripts;
92
+ if (c.harnesses && typeof c.harnesses === 'object') {
93
+ for (const [k, v] of Object.entries(c.harnesses)) {
94
+ if (v && typeof v === 'object') cfg.harnesses[k] = { ...(cfg.harnesses[k] ?? {}), ...(v as HarnessConfig) };
95
+ }
96
+ }
97
+ // also map harnesses.claude if any
98
+ return cfg;
99
+ }
100
+ const d = settings.delegate ?? {};
101
+ if (typeof d.defaultHarness === 'string' && d.defaultHarness) cfg.defaultHarness = d.defaultHarness;
102
+ if (typeof d.defaultMode === 'string') cfg.defaultMode = d.defaultMode;
103
+ if (typeof d.model === 'string') cfg.model = d.model;
104
+ if (typeof d.timeoutMs === 'number' && d.timeoutMs > 0) cfg.timeoutMs = d.timeoutMs;
105
+ if (typeof d.allowDangerous === 'boolean') cfg.allowDangerous = d.allowDangerous;
106
+ if (typeof d.inspectThinking === 'boolean') cfg.inspectThinking = d.inspectThinking;
107
+ if (typeof d.maxBudgetUsd === 'number' && d.maxBudgetUsd > 0) cfg.maxBudgetUsd = d.maxBudgetUsd;
108
+ if (typeof d.autoDelegateHints === 'boolean') cfg.autoDelegateHints = d.autoDelegateHints;
109
+ if (d.modelAliases && typeof d.modelAliases === 'object') {
110
+ for (const [k, v] of Object.entries(d.modelAliases)) {
111
+ if (typeof v === 'string' && v) cfg.modelAliases[k] = v;
112
+ }
113
+ }
114
+ if (typeof d.maxConcurrent === 'number' && d.maxConcurrent >= 0) cfg.maxConcurrent = d.maxConcurrent;
115
+ else if (d.maxConcurrent && typeof d.maxConcurrent === 'object') {
116
+ const obj = d.maxConcurrent as { global?: unknown; perHarness?: unknown };
117
+ const out: { global?: number; perHarness?: Record<string, number> } = {};
118
+ if (typeof obj.global === 'number' && obj.global >= 0) out.global = obj.global;
119
+ if (obj.perHarness && typeof obj.perHarness === 'object') {
120
+ const ph: Record<string, number> = {};
121
+ for (const [k, v] of Object.entries(obj.perHarness as Record<string, unknown>)) {
122
+ if (typeof v === 'number' && v >= 0) ph[k] = v;
123
+ }
124
+ if (Object.keys(ph).length > 0) out.perHarness = ph;
125
+ }
126
+ if (out.global !== undefined || out.perHarness) cfg.maxConcurrent = out;
127
+ }
128
+ if (typeof d.maxTranscripts === 'number' && d.maxTranscripts >= 0) cfg.maxTranscripts = d.maxTranscripts;
129
+ if (d.harnesses && typeof d.harnesses === 'object') {
130
+ for (const [k, v] of Object.entries(d.harnesses)) {
131
+ if (v && typeof v === 'object') cfg.harnesses[k] = { ...(v as HarnessConfig) };
132
+ }
133
+ }
134
+ // also support legacy claudeDelegate merged when delegate also present (delegate wins)
135
+ if (settings.claudeDelegate) {
136
+ const c = settings.claudeDelegate as Partial<DelegateConfig>;
137
+ if (typeof c.model === 'string' && !cfg.harnesses['claude']?.model) {
138
+ cfg.harnesses['claude'] = { ...(cfg.harnesses['claude'] ?? {}), model: c.model };
139
+ }
140
+ }
141
+ } catch {
142
+ // invalid settings — fall back to defaults
143
+ }
144
+ return cfg;
144
145
  }
145
146
 
146
- export function resolveModelForHarness(cfg: DelegateConfig, harness: string, model?: string, templateModel?: string): string | undefined {
147
- const resolve = (m?: string) => (m ? (cfg.modelAliases[m] ?? m) : undefined);
148
- return resolve(model) ?? resolve(templateModel) ?? resolve(cfg.harnesses[harness]?.model) ?? resolve(cfg.model);
147
+ export function resolveModelForHarness(
148
+ cfg: DelegateConfig,
149
+ harness: string,
150
+ model?: string,
151
+ templateModel?: string,
152
+ ): string | undefined {
153
+ const resolve = (m?: string) => (m ? (cfg.modelAliases[m] ?? m) : undefined);
154
+ return resolve(model) ?? resolve(templateModel) ?? resolve(cfg.harnesses[harness]?.model) ?? resolve(cfg.model);
149
155
  }
150
156
 
151
157
  export function getMaxConcurrent(cfg: DelegateConfig, harness?: string): number {
152
- if (typeof cfg.maxConcurrent === 'number') return cfg.maxConcurrent;
153
- // if object shape {global, perHarness}
154
- const mc = cfg.maxConcurrent as unknown as { global?: number; perHarness?: Record<string, number> };
155
- if (harness && mc.perHarness && typeof mc.perHarness[harness] === 'number') return mc.perHarness[harness]!;
156
- if (typeof mc.global === 'number') return mc.global;
157
- return 1;
158
+ if (typeof cfg.maxConcurrent === 'number') return cfg.maxConcurrent;
159
+ // if object shape {global, perHarness}
160
+ const mc = cfg.maxConcurrent as unknown as { global?: number; perHarness?: Record<string, number> };
161
+ if (harness && mc.perHarness && typeof mc.perHarness[harness] === 'number') return mc.perHarness[harness]!;
162
+ if (typeof mc.global === 'number') return mc.global;
163
+ return 1;
158
164
  }
@@ -1,137 +1,144 @@
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 PERMISSION_MAP: Record<NormalizedPermission, string> = {
12
- readonly: 'read-only',
13
- edit: 'workspace',
14
- danger: 'danger',
19
+ readonly: 'read-only',
20
+ edit: 'workspace',
21
+ danger: 'danger',
15
22
  };
16
23
 
17
24
  function extractAmpText(o: Record<string, unknown>): string | undefined {
18
- if (typeof o.text === 'string') return o.text;
19
- if (typeof o.output === 'string') return o.output;
20
- if (typeof o.delta === 'string') return o.delta;
21
- if (typeof o.content === 'string') return o.content;
22
- return undefined;
25
+ if (typeof o.text === 'string') return o.text;
26
+ if (typeof o.output === 'string') return o.output;
27
+ if (typeof o.delta === 'string') return o.delta;
28
+ if (typeof o.content === 'string') return o.content;
29
+ return undefined;
23
30
  }
24
31
 
25
32
  export function parseAmpLine(line: string, state: ParseState): ParseOutcome {
26
- let o: unknown;
27
- try {
28
- o = JSON.parse(line);
29
- } catch {
30
- if (line.trim().length > 0) return { streamedText: line + '\n', activities: [] };
31
- return { activities: [] };
32
- }
33
- if (!isRecord(o)) return { activities: [] };
34
- const activities: ParseOutcome['activities'] = [];
35
- let streamedText: string | undefined;
36
- const typeStr = typeof o.type === 'string' ? o.type : '';
33
+ let o: unknown;
34
+ try {
35
+ o = JSON.parse(line);
36
+ } catch {
37
+ if (line.trim().length > 0) return { streamedText: line + '\n', activities: [] };
38
+ return { activities: [] };
39
+ }
40
+ if (!isRecord(o)) return { activities: [] };
41
+ const activities: ParseOutcome['activities'] = [];
42
+ let streamedText: string | undefined;
43
+ const typeStr = typeof o.type === 'string' ? o.type : '';
37
44
 
38
- if (typeStr.includes('tool') || o.type === 'tool_use') {
39
- const name = typeof o.name === 'string' ? o.name : 'tool';
40
- if (typeStr.includes('start') || o.type === 'tool_use') {
41
- activities.push({ kind: 'tool_start', name });
42
- if (isRecord(o.input)) activities.push({ kind: 'tool_input', name, input: o.input as Record<string, unknown> });
43
- } else {
44
- activities.push({ kind: 'tool_result', isError: o.is_error === true });
45
- }
46
- }
47
- if (typeStr.includes('thinking')) activities.push({ kind: 'thinking', chars: 10 });
45
+ if (typeStr.includes('tool') || o.type === 'tool_use') {
46
+ const name = typeof o.name === 'string' ? o.name : 'tool';
47
+ if (typeStr.includes('start') || o.type === 'tool_use') {
48
+ activities.push({ kind: 'tool_start', name });
49
+ if (isRecord(o.input)) activities.push({ kind: 'tool_input', name, input: o.input as Record<string, unknown> });
50
+ } else {
51
+ activities.push({ kind: 'tool_result', isError: o.is_error === true });
52
+ }
53
+ }
54
+ if (typeStr.includes('thinking')) activities.push({ kind: 'thinking', chars: 10 });
48
55
 
49
- const text = extractAmpText(o);
50
- if (text && !typeStr.includes('tool')) streamedText = text;
56
+ const text = extractAmpText(o);
57
+ if (text && !typeStr.includes('tool')) streamedText = text;
51
58
 
52
- if (o.type === 'result' || o.type === 'done' || o.type === 'completed') {
53
- const usage = isRecord(o.usage) ? o.usage : null;
54
- const result: StreamedResult = {
55
- result: typeof o.result === 'string' ? o.result : state.streamedText + (streamedText ?? ''),
56
- isError: o.is_error === true,
57
- numTurns: typeof o.num_turns === 'number' ? o.num_turns : 0,
58
- totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : 0,
59
- sessionId: typeof o.session_id === 'string' ? o.session_id : null,
60
- stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
61
- permissionDenials: [],
62
- durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
63
- durationApiMs: null,
64
- ttftMs: null,
65
- model: typeof o.model === 'string' ? o.model : null,
66
- contextWindow: null,
67
- maxOutputTokens: null,
68
- usage: usage
69
- ? {
70
- inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : 0,
71
- outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : 0,
72
- cacheCreationInputTokens: 0,
73
- cacheReadInputTokens: 0,
74
- }
75
- : null,
76
- };
77
- return { activities, streamedText, result };
78
- }
79
- return { activities, streamedText };
59
+ if (o.type === 'result' || o.type === 'done' || o.type === 'completed') {
60
+ const usage = isRecord(o.usage) ? o.usage : null;
61
+ const result: StreamedResult = {
62
+ result: typeof o.result === 'string' ? o.result : state.streamedText + (streamedText ?? ''),
63
+ isError: o.is_error === true,
64
+ numTurns: typeof o.num_turns === 'number' ? o.num_turns : 0,
65
+ totalCostUsd: typeof o.total_cost_usd === 'number' ? o.total_cost_usd : 0,
66
+ sessionId: typeof o.session_id === 'string' ? o.session_id : null,
67
+ stopReason: typeof o.stop_reason === 'string' ? o.stop_reason : null,
68
+ permissionDenials: [],
69
+ durationMs: typeof o.duration_ms === 'number' ? o.duration_ms : null,
70
+ durationApiMs: null,
71
+ ttftMs: null,
72
+ model: typeof o.model === 'string' ? o.model : null,
73
+ contextWindow: null,
74
+ maxOutputTokens: null,
75
+ usage: usage
76
+ ? {
77
+ inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : 0,
78
+ outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : 0,
79
+ cacheCreationInputTokens: 0,
80
+ cacheReadInputTokens: 0,
81
+ }
82
+ : null,
83
+ };
84
+ return { activities, streamedText, result };
85
+ }
86
+ return { activities, streamedText };
80
87
  }
81
88
 
82
89
  export const ampHarness: Harness = {
83
- name: 'amp',
84
- displayName: 'Amp',
85
- binary: 'amp',
86
- async detect() {
87
- try {
88
- const { stdout } = await execFileAsync('amp', ['--version'], { timeout: 5000 });
89
- return { ok: true, version: stdout.trim() };
90
- } catch {
91
- try {
92
- const { stdout } = await execFileAsync('omp', ['--version'], { timeout: 5000 });
93
- return { ok: true, version: stdout.trim() };
94
- } catch {
95
- return { ok: false, hint: 'Install Amp: https://ampcode.com' };
96
- }
97
- }
98
- },
99
- buildArgs(opts: BuildArgsOpts): string[] {
100
- const perm = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'workspace';
101
- const args = ['--output', 'jsonl', opts.prompt, '--permission', perm];
102
- if (opts.model) args.push('--model', opts.model);
103
- if (opts.resumeSessionId) args.push('--resume', opts.resumeSessionId);
104
- for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
105
- return args;
106
- },
107
- parseLine(line: string, state: ParseState): ParseOutcome {
108
- return parseAmpLine(line, state);
109
- },
110
- extractResult(state: ParseState): StreamedResult | null {
111
- if (state.result) return state.result;
112
- if (state.streamedText.trim().length > 0) {
113
- return {
114
- result: state.streamedText,
115
- isError: false,
116
- numTurns: 1,
117
- totalCostUsd: 0,
118
- sessionId: null,
119
- stopReason: null,
120
- permissionDenials: [],
121
- durationMs: null,
122
- durationApiMs: null,
123
- ttftMs: null,
124
- model: null,
125
- contextWindow: null,
126
- maxOutputTokens: null,
127
- usage: null,
128
- };
129
- }
130
- return null;
131
- },
132
- permissionMap: {
133
- readonly: ['read-only'],
134
- edit: ['workspace'],
135
- danger: ['danger'],
136
- },
90
+ name: 'amp',
91
+ displayName: 'Amp',
92
+ binary: 'amp',
93
+ async detect() {
94
+ try {
95
+ const { stdout } = await execFileAsync('amp', ['--version'], { timeout: 5000 });
96
+ return { ok: true, version: stdout.trim() };
97
+ } catch {
98
+ try {
99
+ const { stdout } = await execFileAsync('omp', ['--version'], { timeout: 5000 });
100
+ return { ok: true, version: stdout.trim() };
101
+ } catch {
102
+ return { ok: false, hint: 'Install Amp: https://ampcode.com' };
103
+ }
104
+ }
105
+ },
106
+ buildArgs(opts: BuildArgsOpts): string[] {
107
+ const perm = opts.nativePermission ?? PERMISSION_MAP[opts.permission] ?? 'workspace';
108
+ const args = ['--output', 'jsonl', opts.prompt, '--permission', perm];
109
+ if (opts.model) args.push('--model', opts.model);
110
+ if (opts.resumeSessionId) args.push('--resume', opts.resumeSessionId);
111
+ for (const dir of opts.addDirs ?? []) args.push('--add-dir', dir);
112
+ return args;
113
+ },
114
+ parseLine(line: string, state: ParseState): ParseOutcome {
115
+ return parseAmpLine(line, state);
116
+ },
117
+ extractResult(state: ParseState): StreamedResult | null {
118
+ if (state.result) return state.result;
119
+ if (state.streamedText.trim().length > 0) {
120
+ return {
121
+ result: state.streamedText,
122
+ isError: false,
123
+ numTurns: 1,
124
+ totalCostUsd: 0,
125
+ sessionId: null,
126
+ stopReason: null,
127
+ permissionDenials: [],
128
+ durationMs: null,
129
+ durationApiMs: null,
130
+ ttftMs: null,
131
+ model: null,
132
+ contextWindow: null,
133
+ maxOutputTokens: null,
134
+ usage: null,
135
+ };
136
+ }
137
+ return null;
138
+ },
139
+ permissionMap: {
140
+ readonly: ['read-only'],
141
+ edit: ['workspace'],
142
+ danger: ['danger'],
143
+ },
137
144
  };