Repository navigation
Expand file tree
/
Copy pathprogress.ts
More file actions
225 lines (209 loc) · 8.89 KB
/
Copy pathprogress.ts
File metadata and controls
225 lines (209 loc) · 8.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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
112
113
114
115
116
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/**
* The agent's public progress and interaction contracts.
*
* `runAgent` reports through one optional callback and asks through one
* optional set of capabilities. Neither reaches into a UI singleton, a store,
* or a session: every payload is copied data, every question is awaited on an
* injected answerer. The caller decides what each event looks like.
*/
import type { SettingsConflict } from '@shared/claude-settings';
// ── What the agent hands back and asks with ─────────────────────────
import type { OutroData } from '@shared/outro';
import type { ResolvedBinding } from './runner/shared/types';
export type { OutroData } from '@shared/outro';
/** A single question rendered by the WizardAsk overlay. */
export interface AskQuestion {
/** Key for the response map */
id: string;
prompt: string;
/** text = single-line free input; single/multi = picker */
kind: 'single' | 'multi' | 'text';
/** Required for `single` and `multi`. Ignored for `text`. */
options?: { label: string; value: string; description?: string }[];
/** Defaults to true */
required?: boolean;
/**
* Only meaningful for kind='text'. When true, the wizard-tools `wizard_ask`
* tool stores the user's answer in the session secret vault and returns
* `{ secretRef }` to the agent instead of the plain string — so the value
* never enters the LLM conversation. The TUI masks the input as it is typed
* (see `shouldMaskAnswer`). See `secret-vault.ts`.
*/
sensitive?: boolean;
}
/**
* Copy for a modal shown before an optional step runs, so the user can decline
* it. The program that owns the step supplies the words; the runner and the
* screen only carry them.
*/
export interface TaskNotice {
title: string;
/** Paragraphs, in order. */
body: string[];
/** Optional highlighted list, e.g. what was detected. */
items?: string[];
docsLabel?: string;
docsUrl?: string;
confirmLabel: string;
cancelLabel: string;
prompt: string;
}
/** Map of question id → answer (string for single/text, string[] for multi). */
export type AskAnswers = Record<string, string | string[]>;
/** A pending wizard_ask request held by the store. */
export interface PendingQuestion {
id: string;
questions: AskQuestion[];
/**
* UTC ISO 8601 timestamp of when the ask was created. Published on the
* task stream as `pending_input.asked_at` so the web app can age the
* prompt; stable across pushes for the lifetime of one ask.
*/
askedAt?: string;
/** Skill id of the caller. Set by the wizard from session.skillId. */
source: string;
/**
* When true, the ask overlay renders standalone URLs in prompt text as
* OSC 8 hyperlinks and copies a lone URL to the clipboard. Opt-in per
* program (set from `AgentRunDefinition.richLinks` via the ask bridge); defaults
* to false so existing flows render prompts exactly as before. See
* `LinkText` / `link-helpers`.
*/
richLinks?: boolean;
}
// ── What the agent reports ──────────────────────────────────────────
/**
* One assistant turn's token usage, for the hidden Ctrl+T token/cost HUD.
* `model` is the model that produced *this* turn (e.g. the SDK's
* `message.message.model`) — a subagent can run on a different model than
* the main session, and some programs override to Haiku, so pricing must key
* off the per-turn model rather than a single run-wide assumption. Omit only
* when the caller genuinely has no model context (falls back to Sonnet
* pricing — see `pricePerMtokForModel` in `@shared/token-pricing`).
*/
export interface TokenUsageDelta {
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
cacheCreation5m: number;
cacheCreation1h: number;
model?: string;
}
/** The run spinner as the agent drives it. Each call is one `spinner` event. */
export interface SpinnerHandle {
start(message?: string): void;
stop(message?: string): void;
message(msg?: string): void;
}
/**
* Context the agent attaches to a 401 so the caller can pick the right copy.
*
* `hasSettingsConflict` is true when a Claude Code settings file (project,
* project-local, the user's global config, or managed) actually overrides the
* LLM Gateway auth. `conflicts` carries the exact files and keys so the screen
* can name them. When there is no conflict, the 401 has a different cause (bad
* PAT prefix, missing scope, expired key, region mismatch) and we should not
* advise the user to log out of Claude Code.
*/
export interface AuthErrorDetail {
hasSettingsConflict: boolean;
conflicts?: SettingsConflict[];
/**
* True when the agent SDK authenticated from a stored Claude login
* (`apiKeySource: "/login managed key"`) instead of the wizard's gateway
* token — conflicting Anthropic credentials. Takes priority in the screen.
*/
usingManagedLogin?: boolean;
/** Human-readable places a conflicting Anthropic credential may live. */
credentialPlaces?: string[];
/**
* True when a pre-run refresh already failed on a dead grant. The login is
* gone and re-running is the only fix, so this outranks every other branch —
* none of the usual advice (key type, scopes, region) applies.
*/
sessionExpired?: boolean;
logFilePath: string;
}
/** One task in the run's task list, as the `tasks` event carries it. */
export interface TaskSnapshot {
id?: string;
source?: string;
content: string;
status: string;
activeForm?: string;
}
export type ProgressLogLevel = 'info' | 'warn' | 'error' | 'success' | 'step';
/**
* Everything the agent reports while it runs, in the order it happens.
*
* Payloads are copies. Never a store, a setter, a function or a live
* collection. The callback returns nothing and the agent never branches on it.
*/
export type AgentProgress =
/** The run's resolved sequence, harness and model, once, before it starts. */
| { kind: 'binding'; binding: ResolvedBinding }
/** The run's main work has started. */
| { kind: 'lifecycle'; phase: 'started' }
/** The run finished and the caller may show its outro. */
| { kind: 'lifecycle'; phase: 'completed'; message: string }
/** The run spinner: start, stop or change its message. One per run. */
| {
kind: 'spinner';
action: 'start' | 'stop' | 'message';
message?: string;
}
/** A log line at a level. */
| { kind: 'log'; level: ProgressLogLevel; message: string }
/** A `[STATUS]` line the agent printed. */
| { kind: 'status'; message: string }
/** The full task list, already sorted for display. */
| { kind: 'tasks'; tasks: TaskSnapshot[] }
/** The stage of work derived from the active tool. */
| { kind: 'stage'; stage: string }
/** A PostHog URL the agent created (`setDashboardUrl` / `setNotebookUrl`). */
| { kind: 'url'; which: 'dashboard' | 'notebook'; url: string }
/** One assistant turn's token usage. */
| { kind: 'usage'; delta: TokenUsageDelta }
/** The SDK's authoritative run cost, in USD. */
| { kind: 'finalCost'; usd: number }
/** The gateway returned 401; a failure follows. */
| { kind: 'authError'; detail: AuthErrorDetail }
/** The handoff document the agent published. */
| { kind: 'handoff'; text: string }
/** The run's final outro payload. */
| { kind: 'completion'; outro: OutroData }
/** One short line per agent step, only from a run that collects its transcript. */
| { kind: 'activity'; line: string };
export type ProgressEmitter = (event: AgentProgress) => void;
/**
* The questions the agent may need a person (or a script) to answer. Every
* capability is optional. With none supplied the agent installs no ask bridge,
* so `wizard_ask` returns its existing "not available" error, and an optional
* task notice is declined — the same path a `--ci` run takes today.
* Each request's `signal` aborts when that request times out, the run's
* signal aborts, or another task fails the run. On that abort the caller
* dismisses that request alone, and that dismissal must not throw: abort
* listeners run where the agent cannot catch them, so Node would rethrow the
* error as an uncaught exception.
*/
export interface AgentInteraction {
/**
* Open a question and resolve with the answers. The bridge that calls this
* owns the timeout, the `__cancelled__` sentinel and the analytics.
*
* `onAnswer` reports that the user answered one question of a request that
* has more to come, so the bridge can re-arm its timeout against the user's
* silence rather than against the age of the whole request. A host that
* answers a request in one shot never calls it.
*/
ask?: (
question: PendingQuestion,
context: { signal: AbortSignal; onAnswer?: () => void },
) => Promise<AskAnswers>;
/** Offer an optional step and resolve with whether to keep it. */
taskNotice?: (
notice: TaskNotice,
context: { signal: AbortSignal },
) => Promise<boolean>;
}