dsh-agy-cli 0.0.0-stage → 0.12.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.
Files changed (91) hide show
  1. package/LICENSE +31 -0
  2. package/README.en.md +48 -0
  3. package/README.md +67 -2
  4. package/agents/dsh-agy-image-view.md +17 -0
  5. package/agents/dsh-agy-mcp-executor.md +32 -0
  6. package/agents/dsh-agy-read-only.md +20 -0
  7. package/agents/dsh-agy-tool-free.md +16 -0
  8. package/agents/dsh-agy-workspace-write.md +23 -0
  9. package/bin/dsh-agy-cli.js +9 -0
  10. package/bin/win32-x64/agy-launcher.exe +0 -0
  11. package/cordis.patch.yml +16 -0
  12. package/dist/client.js +4259 -0
  13. package/lib/agent-installer.d.ts +66 -0
  14. package/lib/agent-installer.js +147 -0
  15. package/lib/agent-presets.d.ts +42 -0
  16. package/lib/agent-presets.js +130 -0
  17. package/lib/agents-cli.d.ts +3 -0
  18. package/lib/agents-cli.js +110 -0
  19. package/lib/agy/diagnostics.d.ts +29 -0
  20. package/lib/agy/diagnostics.js +109 -0
  21. package/lib/agy/errors.d.ts +20 -0
  22. package/lib/agy/errors.js +77 -0
  23. package/lib/agy/experimental-transport.d.ts +79 -0
  24. package/lib/agy/experimental-transport.js +612 -0
  25. package/lib/agy/keepalive.d.ts +140 -0
  26. package/lib/agy/keepalive.js +445 -0
  27. package/lib/agy/limiter.d.ts +32 -0
  28. package/lib/agy/limiter.js +111 -0
  29. package/lib/agy/log.d.ts +214 -0
  30. package/lib/agy/log.js +503 -0
  31. package/lib/agy/models.d.ts +49 -0
  32. package/lib/agy/models.js +203 -0
  33. package/lib/agy/overrides.d.ts +17 -0
  34. package/lib/agy/overrides.js +73 -0
  35. package/lib/agy/parser.d.ts +102 -0
  36. package/lib/agy/parser.js +492 -0
  37. package/lib/agy/persistent-transport.d.ts +101 -0
  38. package/lib/agy/persistent-transport.js +753 -0
  39. package/lib/agy/process.d.ts +144 -0
  40. package/lib/agy/process.js +452 -0
  41. package/lib/agy/redact.d.ts +3 -0
  42. package/lib/agy/redact.js +24 -0
  43. package/lib/agy/stream-protocol.d.ts +50 -0
  44. package/lib/agy/stream-protocol.js +156 -0
  45. package/lib/agy/windows-launcher.d.ts +21 -0
  46. package/lib/agy/windows-launcher.js +76 -0
  47. package/lib/diagnostics.d.ts +78 -0
  48. package/lib/diagnostics.js +162 -0
  49. package/lib/doctor.d.ts +138 -0
  50. package/lib/doctor.js +940 -0
  51. package/lib/dsh/context.d.ts +133 -0
  52. package/lib/dsh/context.js +352 -0
  53. package/lib/dsh-home.d.ts +6 -0
  54. package/lib/dsh-home.js +11 -0
  55. package/lib/index.d.ts +54 -0
  56. package/lib/index.js +267 -0
  57. package/lib/mcp/agy-registration.d.ts +70 -0
  58. package/lib/mcp/agy-registration.js +245 -0
  59. package/lib/mcp/bridge.d.ts +73 -0
  60. package/lib/mcp/bridge.js +295 -0
  61. package/lib/mcp/endpoint.d.ts +41 -0
  62. package/lib/mcp/endpoint.js +142 -0
  63. package/lib/mcp/protocol.d.ts +107 -0
  64. package/lib/mcp/protocol.js +197 -0
  65. package/lib/mcp/tool-projection.d.ts +64 -0
  66. package/lib/mcp/tool-projection.js +168 -0
  67. package/lib/provider/agy.d.ts +153 -0
  68. package/lib/provider/agy.js +2159 -0
  69. package/lib/provider/config.d.ts +226 -0
  70. package/lib/provider/config.js +294 -0
  71. package/lib/provider/error-codes.d.ts +22 -0
  72. package/lib/provider/error-codes.js +29 -0
  73. package/lib/provider/image-bridge.d.ts +60 -0
  74. package/lib/provider/image-bridge.js +290 -0
  75. package/lib/provider/mock.d.ts +15 -0
  76. package/lib/provider/mock.js +78 -0
  77. package/lib/provider/prompt-budget.d.ts +45 -0
  78. package/lib/provider/prompt-budget.js +596 -0
  79. package/lib/provider/serialize.d.ts +15 -0
  80. package/lib/provider/serialize.js +82 -0
  81. package/lib/provider/tool-protocol.d.ts +80 -0
  82. package/lib/provider/tool-protocol.js +848 -0
  83. package/lib/provider/tool-schema-file.d.ts +11 -0
  84. package/lib/provider/tool-schema-file.js +27 -0
  85. package/lib/session/file-store.d.ts +56 -0
  86. package/lib/session/file-store.js +186 -0
  87. package/lib/session/store.d.ts +49 -0
  88. package/lib/session/store.js +62 -0
  89. package/lib/web/router.d.ts +19 -0
  90. package/lib/web/router.js +994 -0
  91. package/package.json +115 -4
@@ -0,0 +1,144 @@
1
+ export type ProcessTermination = 'completed' | 'non-zero' | 'signaled' | 'aborted' | 'timeout' | 'idle-timeout' | 'output-limit';
2
+ /**
3
+ * How `runProcess` retires the stdin pipe after writing the request frame.
4
+ *
5
+ * `'close'` ends stdin immediately (the historical behaviour). AGY's print mode
6
+ * reads that EOF as "there is no next turn", shuts its store manager down and
7
+ * cancels the turn still being generated — measured as
8
+ * `Print mode: stream input closed after 1 turn(s)` followed by
9
+ * `CORTEX_STEP_STATUS_CANCELED` on every step, which surfaces to the user as
10
+ * text that appears and is then rolled back.
11
+ *
12
+ * `'hold'` keeps the pipe open and closes it only when the run is over (a
13
+ * `result` line arrived, or the process exited). Nothing in AGY's stream-json
14
+ * contract requires a follow-up frame, so holding costs nothing but the
15
+ * lifetime of one pipe — and `idleTimeoutMs`/`timeoutMs` still kill a run that
16
+ * goes silent, so a turn can never hang forever on an unclosed pipe.
17
+ */
18
+ export type AgyStdinMode = 'close' | 'hold';
19
+ /** Maps a termination to the code the retry classifier sees, so a silence kill
20
+ * and a wall-clock kill are distinguishable in telemetry without changing the
21
+ * DSH-facing code (`TIMEOUT` either way). */
22
+ export declare const AGY_REASONING_EFFORTS: readonly ["low", "medium", "high"];
23
+ export type AgyReasoningEffort = typeof AGY_REASONING_EFFORTS[number];
24
+ export declare const AGY_EXECUTION_MODES: readonly ["plan", "accept-edits"];
25
+ export type AgyExecutionMode = typeof AGY_EXECUTION_MODES[number];
26
+ export declare function isAgyReasoningEffort(value: unknown): value is AgyReasoningEffort;
27
+ export declare function isAgyExecutionMode(value: unknown): value is AgyExecutionMode;
28
+ export declare class AgyProcessError extends Error {
29
+ readonly code: 'ABORTED' | 'SPAWN_FAILED' | 'INPUT_FAILED' | 'INPUT_TOO_LARGE' | 'OUTPUT_HANDLER_FAILED' | 'OUTPUT_LIMIT';
30
+ readonly diagnostic?: AgyProcessDiagnostic | undefined;
31
+ constructor(message: string, code: 'ABORTED' | 'SPAWN_FAILED' | 'INPUT_FAILED' | 'INPUT_TOO_LARGE' | 'OUTPUT_HANDLER_FAILED' | 'OUTPUT_LIMIT', options?: ErrorOptions, diagnostic?: AgyProcessDiagnostic | undefined);
32
+ }
33
+ export type AgyProcessDiagnosticStage = 'prepare' | 'spawn' | 'stdin' | 'stdout-handler';
34
+ /**
35
+ * Safe process-failure metadata; never contains the prompt, the response, or paths.
36
+ *
37
+ * `stderrSummary` is the one deliberate exception on the stderr side. When AGY
38
+ * dies before the first stdin write, the EOF is only a symptom: the cause is a
39
+ * line AGY already printed to stderr (an eligibility failure, a rejected model,
40
+ * a broken upstream connection). Without that line every such failure reads as
41
+ * the same uninformative `(Error:EOF stage=stdin)`. It is therefore carried as a
42
+ * single redacted, capped line, and only on the stdin stage.
43
+ */
44
+ export interface AgyProcessDiagnostic {
45
+ readonly stage: AgyProcessDiagnosticStage;
46
+ readonly errorName?: string;
47
+ readonly errorCode?: string;
48
+ readonly lineNumber?: number;
49
+ readonly lineLength?: number;
50
+ readonly lineHash?: string;
51
+ readonly stdoutLineCount?: number;
52
+ readonly stdoutBytes?: number;
53
+ readonly stderrBytes?: number;
54
+ readonly stderrSummary?: string;
55
+ }
56
+ /**
57
+ * Pick the most informative stderr line for a failed launch: AGY writes its
58
+ * fatal reason last, after progress chatter, so the last non-empty line wins.
59
+ */
60
+ export declare function stderrSummaryOf(stderr: string): string | undefined;
61
+ /** Find process diagnostics through the LlmError -> cause chain. */
62
+ export declare function agyProcessDiagnosticOf(error: unknown): AgyProcessDiagnostic | undefined;
63
+ export interface ProcessRequest {
64
+ executable: string;
65
+ args: readonly string[];
66
+ /** Optional stdin payload written once before the stream is closed. */
67
+ stdin?: string;
68
+ /**
69
+ * Whether to end stdin right after writing the payload (default) or keep it
70
+ * open until the run finishes. See `AgyStdinMode`.
71
+ */
72
+ stdinMode?: AgyStdinMode;
73
+ cwd?: string;
74
+ env?: NodeJS.ProcessEnv;
75
+ /** Hard wall-clock ceiling for the whole run. Omitted means unbounded. */
76
+ timeoutMs?: number;
77
+ /**
78
+ * Inactivity budget: the run is killed when no stdout/stderr line arrives for
79
+ * this long. Re-armed on every delivered line, so a turn that keeps producing
80
+ * output can run past `timeoutMs` without being cut off. Omitted means only
81
+ * the wall-clock ceiling applies.
82
+ */
83
+ idleTimeoutMs?: number;
84
+ /** Maximum captured stdout bytes; omitted means unlimited. */
85
+ maxStdoutBytes?: number;
86
+ /** Maximum captured stderr bytes; omitted means unlimited. */
87
+ maxStderrBytes?: number;
88
+ signal?: AbortSignal;
89
+ /** Called synchronously for each complete stdout line as it arrives. */
90
+ onStdoutLine?: (line: string) => void;
91
+ /** Use the Windows no-console bridge for a console-subsystem executable. */
92
+ windowsNoConsole?: boolean;
93
+ }
94
+ export interface ProcessResult {
95
+ exitCode: number | null;
96
+ signal: NodeJS.Signals | null;
97
+ termination: ProcessTermination;
98
+ stdoutLines: readonly string[];
99
+ stderr: string;
100
+ durationMs: number;
101
+ /** Epoch ms of the last stdout/stderr chunk; used to report an idle kill. */
102
+ lastOutputAtMs?: number;
103
+ }
104
+ export interface AgyRequest extends Omit<ProcessRequest, 'executable' | 'args'> {
105
+ executable?: string;
106
+ prompt: string;
107
+ agent?: string;
108
+ model?: string;
109
+ conversation?: string;
110
+ reasoningEffort?: AgyReasoningEffort;
111
+ /** Explicit AGY directories made available to the selected Agent. */
112
+ addDirs?: readonly string[];
113
+ /** Bounded AGY execution mode selected by an Agent preset. */
114
+ mode?: AgyExecutionMode;
115
+ /** Disable slash commands for bundled non-interactive presets. */
116
+ disableSlashCommands?: boolean;
117
+ /** Absolute temporary JSON Schema path passed to AGY's final-result validator. */
118
+ jsonSchemaPath?: string;
119
+ /** Maximum UTF-8 bytes allowed for one encoded stream-json user frame. */
120
+ maxInputFrameBytes?: number;
121
+ /**
122
+ * How long to keep the request pipe open. AGY's print mode treats an
123
+ * immediate EOF as "no next turn" and cancels the turn it is still
124
+ * generating, so the adapter holds the pipe by default; see `AgyStdinMode`.
125
+ */
126
+ stdinMode?: AgyStdinMode;
127
+ }
128
+ export declare function defaultAgyCommand(): 'agy.exe' | 'agy';
129
+ /**
130
+ * Resolve an explicit AGY path, AGY_PATH/AGY_EXECUTABLE, or an executable on
131
+ * PATH. Returning the bare command in the final fallback lets spawn produce
132
+ * the platform-native diagnostic if PATH changes between discovery and run.
133
+ */
134
+ export declare function resolveAgyExecutable(explicit?: string): string;
135
+ /** Build AGY's stdin stream-json argv without invoking a shell. */
136
+ export declare function buildAgyArgs(request: Pick<AgyRequest, 'prompt' | 'agent' | 'model' | 'conversation' | 'reasoningEffort' | 'addDirs' | 'mode' | 'disableSlashCommands' | 'jsonSchemaPath'>): string[];
137
+ /**
138
+ * Run a process with piped output and deterministic lifecycle handling.
139
+ * stdout is delivered line-by-line while the process is running; the result
140
+ * also retains the lines for tests and adapters that do not need live output.
141
+ */
142
+ export declare function runProcess(request: ProcessRequest): Promise<ProcessResult>;
143
+ export declare function runAgyProcess(request: AgyRequest): Promise<ProcessResult>;
144
+ //# sourceMappingURL=process.d.ts.map
@@ -0,0 +1,452 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync } from 'node:fs';
4
+ import { delimiter, join, resolve } from 'node:path';
5
+ import { redactText } from './redact.js';
6
+ import { AgyStreamProtocolError, encodeAgyUserMessage } from './stream-protocol.js';
7
+ import { buildWindowsNoConsoleLaunch } from './windows-launcher.js';
8
+ /** Maps a termination to the code the retry classifier sees, so a silence kill
9
+ * and a wall-clock kill are distinguishable in telemetry without changing the
10
+ * DSH-facing code (`TIMEOUT` either way). */
11
+ export const AGY_REASONING_EFFORTS = ['low', 'medium', 'high'];
12
+ export const AGY_EXECUTION_MODES = ['plan', 'accept-edits'];
13
+ export function isAgyReasoningEffort(value) {
14
+ return typeof value === 'string'
15
+ && AGY_REASONING_EFFORTS.includes(value);
16
+ }
17
+ export function isAgyExecutionMode(value) {
18
+ return typeof value === 'string'
19
+ && AGY_EXECUTION_MODES.includes(value);
20
+ }
21
+ export class AgyProcessError extends Error {
22
+ code;
23
+ diagnostic;
24
+ constructor(message, code, options, diagnostic) {
25
+ super(message, options);
26
+ this.code = code;
27
+ this.diagnostic = diagnostic;
28
+ this.name = 'AgyProcessError';
29
+ }
30
+ }
31
+ /** Longest stderr excerpt carried on a stdin failure. */
32
+ const STDERR_SUMMARY_MAX_LENGTH = 200;
33
+ /**
34
+ * Pick the most informative stderr line for a failed launch: AGY writes its
35
+ * fatal reason last, after progress chatter, so the last non-empty line wins.
36
+ */
37
+ export function stderrSummaryOf(stderr) {
38
+ const lines = stderr.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
39
+ const chosen = lines.at(-1);
40
+ if (chosen === undefined)
41
+ return undefined;
42
+ return redactText(chosen, STDERR_SUMMARY_MAX_LENGTH);
43
+ }
44
+ function errorNameOf(error) {
45
+ return error instanceof Error && error.name.length > 0 ? error.name.slice(0, 128) : undefined;
46
+ }
47
+ function errorCodeOf(error) {
48
+ if (typeof error !== 'object' || error === null)
49
+ return undefined;
50
+ const code = error.code;
51
+ return typeof code === 'string' && code.length > 0 ? code.slice(0, 128) : undefined;
52
+ }
53
+ function integerFieldOf(error, key) {
54
+ if (typeof error !== 'object' || error === null)
55
+ return undefined;
56
+ const value = error[key];
57
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
58
+ }
59
+ function shortLineHash(line) {
60
+ return createHash('sha256').update(line, 'utf8').digest('hex').slice(0, 16);
61
+ }
62
+ function processDiagnosticFor(error, stage, details = {}) {
63
+ const line = details.line;
64
+ const errorName = errorNameOf(error);
65
+ const errorCode = errorCodeOf(error);
66
+ const lineNumber = integerFieldOf(error, 'lineNumber');
67
+ return {
68
+ stage,
69
+ ...(errorName === undefined ? {} : { errorName }),
70
+ ...(errorCode === undefined ? {} : { errorCode }),
71
+ ...(lineNumber === undefined ? {} : { lineNumber }),
72
+ ...(line === undefined ? {} : {
73
+ lineLength: line.length,
74
+ lineHash: shortLineHash(line),
75
+ }),
76
+ ...(details.stdoutLineCount === undefined ? {} : { stdoutLineCount: details.stdoutLineCount }),
77
+ ...(details.stdoutBytes === undefined ? {} : { stdoutBytes: details.stdoutBytes }),
78
+ ...(details.stderrBytes === undefined ? {} : { stderrBytes: details.stderrBytes }),
79
+ ...(details.stderrSummary === undefined ? {} : { stderrSummary: details.stderrSummary }),
80
+ };
81
+ }
82
+ function diagnosticLabel(diagnostic) {
83
+ if (diagnostic === undefined)
84
+ return '';
85
+ const cause = [diagnostic.errorName, diagnostic.errorCode].filter(Boolean).join(':');
86
+ const line = diagnostic.lineNumber === undefined ? '' : ` line=${diagnostic.lineNumber}`;
87
+ const length = diagnostic.lineLength === undefined ? '' : ` length=${diagnostic.lineLength}`;
88
+ const stage = `stage=${diagnostic.stage}`;
89
+ const stderr = diagnostic.stderrSummary === undefined ? '' : ` stderr=${JSON.stringify(diagnostic.stderrSummary)}`;
90
+ return ` (${[cause || undefined, stage, line.trim(), length.trim(), stderr.trim()].filter(Boolean).join(' ')})`;
91
+ }
92
+ /** Find process diagnostics through the LlmError -> cause chain. */
93
+ export function agyProcessDiagnosticOf(error) {
94
+ let current = error;
95
+ for (let depth = 0; depth < 4 && current !== undefined; depth += 1) {
96
+ if (current instanceof AgyProcessError && current.diagnostic !== undefined)
97
+ return current.diagnostic;
98
+ if (!(current instanceof Error))
99
+ return undefined;
100
+ current = current.cause;
101
+ }
102
+ return undefined;
103
+ }
104
+ export function defaultAgyCommand() {
105
+ return process.platform === 'win32' ? 'agy.exe' : 'agy';
106
+ }
107
+ function terminateProcessTree(child) {
108
+ const pid = child.pid;
109
+ if (pid === undefined) {
110
+ child.kill();
111
+ return;
112
+ }
113
+ if (process.platform === 'win32') {
114
+ const killer = spawn('taskkill', ['/pid', String(pid), '/t', '/f'], {
115
+ shell: false,
116
+ windowsHide: true,
117
+ stdio: 'ignore',
118
+ });
119
+ killer.once('error', () => child.kill());
120
+ return;
121
+ }
122
+ try {
123
+ process.kill(-pid, 'SIGKILL');
124
+ }
125
+ catch {
126
+ child.kill('SIGKILL');
127
+ }
128
+ }
129
+ /**
130
+ * Resolve an explicit AGY path, AGY_PATH/AGY_EXECUTABLE, or an executable on
131
+ * PATH. Returning the bare command in the final fallback lets spawn produce
132
+ * the platform-native diagnostic if PATH changes between discovery and run.
133
+ */
134
+ export function resolveAgyExecutable(explicit) {
135
+ const configured = explicit?.trim()
136
+ || process.env.AGY_PATH?.trim()
137
+ || process.env.AGY_EXECUTABLE?.trim();
138
+ if (configured !== undefined && configured.length > 0)
139
+ return resolve(configured);
140
+ const command = defaultAgyCommand();
141
+ const pathEntries = (process.env.PATH ?? '').split(delimiter).filter(Boolean);
142
+ const suffixes = process.platform === 'win32'
143
+ ? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
144
+ : [''];
145
+ for (const directory of pathEntries) {
146
+ const direct = join(directory, command);
147
+ if (existsSync(direct))
148
+ return direct;
149
+ if (process.platform === 'win32' && !command.includes('.')) {
150
+ for (const suffix of suffixes) {
151
+ const candidate = join(directory, `${command}${suffix.toLowerCase()}`);
152
+ if (existsSync(candidate))
153
+ return candidate;
154
+ }
155
+ }
156
+ }
157
+ return command;
158
+ }
159
+ /** Build AGY's stdin stream-json argv without invoking a shell. */
160
+ export function buildAgyArgs(request) {
161
+ if (request.reasoningEffort !== undefined && !isAgyReasoningEffort(request.reasoningEffort)) {
162
+ throw new TypeError('AGY reasoning effort must be one of: low, medium, high');
163
+ }
164
+ if (request.mode !== undefined && !isAgyExecutionMode(request.mode)) {
165
+ throw new TypeError('AGY execution mode must be one of: plan, accept-edits');
166
+ }
167
+ if (request.addDirs?.some(directory => typeof directory !== 'string' || directory.trim().length === 0)) {
168
+ throw new TypeError('AGY add-dir entries must be non-empty paths');
169
+ }
170
+ if (request.jsonSchemaPath !== undefined && request.jsonSchemaPath.trim().length === 0) {
171
+ throw new TypeError('AGY json-schema path must be non-empty');
172
+ }
173
+ return [
174
+ '-p', '',
175
+ ...(request.agent === undefined ? [] : ['--agent', request.agent]),
176
+ ...(request.model === undefined ? [] : ['--model', request.model]),
177
+ ...(request.conversation === undefined ? [] : ['--conversation', request.conversation]),
178
+ ...(request.reasoningEffort === undefined ? [] : ['--effort', request.reasoningEffort]),
179
+ ...(request.addDirs?.flatMap(directory => ['--add-dir', directory]) ?? []),
180
+ ...(request.mode === undefined ? [] : ['--mode', request.mode]),
181
+ ...(request.disableSlashCommands === true ? ['--disable-slash-commands'] : []),
182
+ ...(request.jsonSchemaPath === undefined ? [] : ['--json-schema', request.jsonSchemaPath]),
183
+ '--input-format', 'stream-json',
184
+ '--output-format', 'stream-json',
185
+ ];
186
+ }
187
+ function splitLines(buffer, final) {
188
+ const parts = buffer.split(/\r?\n/);
189
+ const remainder = parts.pop() ?? '';
190
+ if (final && remainder.length > 0) {
191
+ parts.push(remainder.replace(/\r$/, ''));
192
+ return { lines: parts, remainder: '' };
193
+ }
194
+ return { lines: parts, remainder };
195
+ }
196
+ function byteLength(value) {
197
+ return Buffer.byteLength(value, 'utf8');
198
+ }
199
+ /**
200
+ * Run a process with piped output and deterministic lifecycle handling.
201
+ * stdout is delivered line-by-line while the process is running; the result
202
+ * also retains the lines for tests and adapters that do not need live output.
203
+ */
204
+ export function runProcess(request) {
205
+ if (request.signal?.aborted) {
206
+ return Promise.reject(new AgyProcessError('AGY process aborted before start', 'ABORTED'));
207
+ }
208
+ const startedAt = Date.now();
209
+ let launch;
210
+ try {
211
+ launch = request.windowsNoConsole === true
212
+ ? buildWindowsNoConsoleLaunch(request.executable, request.args)
213
+ : { executable: request.executable, args: request.args };
214
+ if (process.platform === 'win32' && request.windowsNoConsole === true && !existsSync(launch.executable)) {
215
+ throw new Error('Bundled Windows AGY launcher is missing');
216
+ }
217
+ }
218
+ catch (error) {
219
+ const cause = error instanceof Error ? error : new Error(String(error));
220
+ const diagnostic = processDiagnosticFor(cause, 'prepare');
221
+ return Promise.reject(new AgyProcessError(`Unable to prepare AGY launcher: ${cause.message}${diagnosticLabel(diagnostic)}`, 'SPAWN_FAILED', { cause }, diagnostic));
222
+ }
223
+ let child;
224
+ try {
225
+ child = spawn(launch.executable, [...launch.args], {
226
+ cwd: request.cwd,
227
+ env: request.env === undefined ? process.env : { ...process.env, ...request.env },
228
+ shell: false,
229
+ detached: process.platform !== 'win32',
230
+ windowsHide: true,
231
+ stdio: ['pipe', 'pipe', 'pipe'],
232
+ });
233
+ }
234
+ catch (error) {
235
+ const cause = error instanceof Error ? error : new Error(String(error));
236
+ const code = cause.code ?? 'unknown';
237
+ const diagnostic = processDiagnosticFor(cause, 'spawn');
238
+ return Promise.reject(new AgyProcessError(`Unable to start AGY executable (${code})${diagnosticLabel(diagnostic)}`, 'SPAWN_FAILED', { cause }, diagnostic));
239
+ }
240
+ return new Promise((resolveResult, rejectResult) => {
241
+ let stdoutBuffer = '';
242
+ let stderr = '';
243
+ let stdoutBytes = 0;
244
+ let stderrBytes = 0;
245
+ const stdoutLines = [];
246
+ let termination;
247
+ let outputHandlerError;
248
+ let outputHandlerDiagnostic;
249
+ let inputError;
250
+ let settled = false;
251
+ let lastOutputAtMs = startedAt;
252
+ let turnClosed = false;
253
+ const deliver = (line) => {
254
+ stdoutLines.push(line);
255
+ if (!turnClosed && resultSeen(line)) {
256
+ turnClosed = true;
257
+ endStdin();
258
+ }
259
+ if (request.onStdoutLine === undefined || outputHandlerError !== undefined)
260
+ return;
261
+ try {
262
+ request.onStdoutLine(line);
263
+ }
264
+ catch (error) {
265
+ outputHandlerError = error;
266
+ outputHandlerDiagnostic = processDiagnosticFor(error, 'stdout-handler', {
267
+ line,
268
+ stdoutLineCount: stdoutLines.length,
269
+ stdoutBytes,
270
+ stderrBytes,
271
+ });
272
+ termination = 'aborted';
273
+ terminateProcessTree(child);
274
+ }
275
+ };
276
+ const flushStdout = (final) => {
277
+ const split = splitLines(stdoutBuffer, final);
278
+ stdoutBuffer = split.remainder;
279
+ for (const line of split.lines)
280
+ deliver(line);
281
+ };
282
+ const kill = (reason) => {
283
+ if (termination === undefined)
284
+ termination = reason;
285
+ if (!child.killed)
286
+ terminateProcessTree(child);
287
+ };
288
+ // `hold` keeps the request pipe open until the turn is over. Ending it
289
+ // early is read by AGY as "no next turn" and cancels the running one.
290
+ const stdinMode = request.stdinMode ?? 'close';
291
+ let stdinClosed = false;
292
+ const endStdin = () => {
293
+ if (stdinClosed || request.stdin === undefined)
294
+ return;
295
+ stdinClosed = true;
296
+ child.stdin.end();
297
+ };
298
+ const resultSeen = (line) => {
299
+ try {
300
+ const parsed = JSON.parse(line);
301
+ return typeof parsed === 'object'
302
+ && parsed !== null
303
+ && parsed.event === 'result';
304
+ }
305
+ catch {
306
+ return false;
307
+ }
308
+ };
309
+ const abortListener = () => kill('aborted');
310
+ request.signal?.addEventListener('abort', abortListener, { once: true });
311
+ const timeout = request.timeoutMs !== undefined && request.timeoutMs > 0
312
+ ? setTimeout(() => kill('timeout'), request.timeoutMs)
313
+ : undefined;
314
+ // Idle watchdog: re-armed on every stdout/stderr chunk, so long reasoning
315
+ // or a long tool step is not mistaken for a hung process. A turn that keeps
316
+ // producing output outlives `timeoutMs`; only real silence trips it.
317
+ const idleBudgetMs = request.idleTimeoutMs !== undefined && request.idleTimeoutMs > 0
318
+ ? request.idleTimeoutMs
319
+ : undefined;
320
+ let idle;
321
+ const armIdle = () => {
322
+ if (idleBudgetMs === undefined || settled)
323
+ return;
324
+ if (idle !== undefined)
325
+ clearTimeout(idle);
326
+ idle = setTimeout(() => kill('idle-timeout'), idleBudgetMs);
327
+ };
328
+ armIdle();
329
+ // AGY's own headless default is an unbounded turn, and its `--print-timeout`
330
+ // is passed separately; a fixed wall clock would cut off a healthy turn, so
331
+ // it is opt-in via `timeoutMs` and never implied by the idle budget.
332
+ child.stdout.setEncoding('utf8');
333
+ child.stderr.setEncoding('utf8');
334
+ child.stdout.on('data', (chunk) => {
335
+ armIdle();
336
+ lastOutputAtMs = Date.now();
337
+ stdoutBytes += byteLength(chunk);
338
+ if (request.maxStdoutBytes !== undefined && stdoutBytes > request.maxStdoutBytes) {
339
+ termination = 'output-limit';
340
+ terminateProcessTree(child);
341
+ return;
342
+ }
343
+ stdoutBuffer += chunk;
344
+ flushStdout(false);
345
+ });
346
+ child.stdout.on('end', () => {
347
+ if (termination !== 'output-limit')
348
+ flushStdout(true);
349
+ // A final `result` line is the contract's "turn is over" signal; the
350
+ // process is expected to exit on its own, but a held pipe gives it a
351
+ // clean shutdown cue rather than an EOF it may read as a new turn.
352
+ if (stdinMode === 'hold')
353
+ endStdin();
354
+ });
355
+ child.stderr.on('data', (chunk) => {
356
+ armIdle();
357
+ lastOutputAtMs = Date.now();
358
+ stderrBytes += byteLength(chunk);
359
+ if (request.maxStderrBytes !== undefined && stderrBytes > request.maxStderrBytes) {
360
+ termination = 'output-limit';
361
+ terminateProcessTree(child);
362
+ return;
363
+ }
364
+ stderr += chunk;
365
+ });
366
+ if (request.stdin !== undefined) {
367
+ child.stdin.once('error', error => {
368
+ if (settled || inputError !== undefined)
369
+ return;
370
+ inputError = error;
371
+ kill('aborted');
372
+ });
373
+ }
374
+ child.once('error', (error) => {
375
+ if (settled)
376
+ return;
377
+ settled = true;
378
+ if (timeout !== undefined)
379
+ clearTimeout(timeout);
380
+ if (idle !== undefined)
381
+ clearTimeout(idle);
382
+ request.signal?.removeEventListener('abort', abortListener);
383
+ rejectResult(new AgyProcessError(`Unable to start AGY executable (${error.code ?? 'unknown'})${diagnosticLabel(processDiagnosticFor(error, 'spawn'))}`, 'SPAWN_FAILED', { cause: error }, processDiagnosticFor(error, 'spawn')));
384
+ });
385
+ child.once('close', (exitCode, signal) => {
386
+ if (settled)
387
+ return;
388
+ settled = true;
389
+ endStdin();
390
+ if (timeout !== undefined)
391
+ clearTimeout(timeout);
392
+ if (idle !== undefined)
393
+ clearTimeout(idle);
394
+ request.signal?.removeEventListener('abort', abortListener);
395
+ if (termination !== 'output-limit')
396
+ flushStdout(true);
397
+ if (outputHandlerError !== undefined) {
398
+ rejectResult(new AgyProcessError(`AGY stdout handler failed${diagnosticLabel(outputHandlerDiagnostic)}`, 'OUTPUT_HANDLER_FAILED', { cause: outputHandlerError }, outputHandlerDiagnostic));
399
+ return;
400
+ }
401
+ if (inputError !== undefined) {
402
+ const stderrSummary = stderrSummaryOf(stderr);
403
+ const diagnostic = processDiagnosticFor(inputError, 'stdin', {
404
+ stdoutLineCount: stdoutLines.length,
405
+ stdoutBytes,
406
+ stderrBytes,
407
+ ...(stderrSummary === undefined ? {} : { stderrSummary }),
408
+ });
409
+ rejectResult(new AgyProcessError(`Unable to write AGY stream-json input${diagnosticLabel(diagnostic)}`, 'INPUT_FAILED', { cause: inputError instanceof Error ? inputError : new Error(String(inputError)) }, diagnostic));
410
+ return;
411
+ }
412
+ const finalTermination = termination
413
+ ?? (exitCode === 0 ? 'completed' : signal === null ? 'non-zero' : 'signaled');
414
+ resolveResult({
415
+ exitCode,
416
+ signal,
417
+ termination: finalTermination,
418
+ stdoutLines,
419
+ stderr,
420
+ durationMs: Date.now() - startedAt,
421
+ lastOutputAtMs,
422
+ });
423
+ });
424
+ if (request.stdin !== undefined) {
425
+ child.stdin.write(request.stdin);
426
+ // `hold` leaves the pipe open until `deliver` sees AGY's closing `result`
427
+ // line (or the process exits, which closes it anyway). Ending it here is
428
+ // what made AGY cancel a turn it was still generating.
429
+ if (stdinMode === 'close')
430
+ endStdin();
431
+ }
432
+ });
433
+ }
434
+ export function runAgyProcess(request) {
435
+ const executable = resolveAgyExecutable(request.executable);
436
+ let stdin;
437
+ try {
438
+ stdin = encodeAgyUserMessage(request.prompt, request.maxInputFrameBytes);
439
+ }
440
+ catch (error) {
441
+ const tooLarge = error instanceof AgyStreamProtocolError && error.code === 'FRAME_TOO_LARGE';
442
+ return Promise.reject(new AgyProcessError(error instanceof Error ? error.message : 'Unable to encode AGY stream-json input', tooLarge ? 'INPUT_TOO_LARGE' : 'INPUT_FAILED', { cause: error instanceof Error ? error : new Error(String(error)) }));
443
+ }
444
+ return runProcess({
445
+ ...request,
446
+ executable,
447
+ args: buildAgyArgs(request),
448
+ stdin,
449
+ windowsNoConsole: true,
450
+ });
451
+ }
452
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1,3 @@
1
+ /** Redact common credentials, environment assignments, and user-home prefixes. */
2
+ export declare function redactText(value: string, maxLength?: number): string;
3
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1,24 @@
1
+ // Key part allows the surrounding quotes JSON produces: `"token": "v"` must
2
+ // match even though the separator sits one character after the sensitive word.
3
+ const SENSITIVE_ASSIGNMENT = /["']?(authorization|bearer|access[_-]?token|refresh[_-]?token|token|api[_-]?key|password|secret)["']?\s*[:=]\s*(?:bearer\s+)?(?:"[^"]*"|'[^']*'|[^\s,;}]+)/gi;
4
+ const SENSITIVE_QUERY = /([?&](?:access[_-]?token|refresh[_-]?token|api[_-]?key|token|password|secret)=)[^&#\s]+/gi;
5
+ // Bare token shapes that carry no key at all: the sk- keys this workspace's
6
+ // own commit discipline greps for, and credentials in a URL userinfo section.
7
+ const BARE_SK_TOKEN = /\bsk-[A-Za-z0-9]{20,}\b/g;
8
+ const URL_USERINFO = /[a-z][a-z0-9+.-]*:\/\/[^/?#\s]+:[^/?#\s]+@/gi;
9
+ const WINDOWS_USER_PATH = /[A-Za-z]:\\Users\\[^\s"'`,;)]*/g;
10
+ const UNIX_USER_PATH = /\/(?:Users|home)\/[^\s"'`,;)]*/g;
11
+ const ENV_ASSIGNMENT = /\b[A-Z][A-Z0-9_]{2,}=(?:"[^"]*"|'[^']*'|[^\s,;]+)/g;
12
+ /** Redact common credentials, environment assignments, and user-home prefixes. */
13
+ export function redactText(value, maxLength = 1_000) {
14
+ return value
15
+ .replace(SENSITIVE_ASSIGNMENT, '$1=[REDACTED]')
16
+ .replace(SENSITIVE_QUERY, '$1[REDACTED]')
17
+ .replace(BARE_SK_TOKEN, '[REDACTED]')
18
+ .replace(URL_USERINFO, '[REDACTED]@')
19
+ .replace(ENV_ASSIGNMENT, '[ENV]=[REDACTED]')
20
+ .replace(WINDOWS_USER_PATH, '<user-path>')
21
+ .replace(UNIX_USER_PATH, '<user-path>')
22
+ .slice(0, maxLength);
23
+ }
24
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Small, transport-neutral helpers for AGY's stdin stream-json mode.
3
+ *
4
+ * Real AGY 1.1.15 stream-json input is event-driven. Factual envelope sampled
5
+ * in V8-M1 (see findings.md 2026-08-20, .tmp/v8-m1/three-turn.log):
6
+ * input: {"event":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}} per NDJSON line
7
+ * output: {"event":"init","conversation_id":"...","init":{...}} then
8
+ * {"event":"step_update",...} then {"event":"result",...} per turn.
9
+ * The generic encodeAgyStreamInput is retained for forward compatibility, but
10
+ * callers should use the typed encodeAgyUserMessage helper.
11
+ */
12
+ export type AgyProtocolValue = Record<string, unknown> | readonly unknown[] | string | number | boolean | null;
13
+ export declare class AgyStreamProtocolError extends Error {
14
+ readonly code: 'INVALID_INPUT' | 'FRAME_TOO_LARGE' | 'INVALID_OUTPUT';
15
+ constructor(message: string, code: 'INVALID_INPUT' | 'FRAME_TOO_LARGE' | 'INVALID_OUTPUT', options?: ErrorOptions);
16
+ }
17
+ /** Typed input for AGY 1.1.15 stream-json: one user turn per NDJSON line. */
18
+ export interface AgyStreamUserMessage {
19
+ readonly event: "user";
20
+ readonly message: {
21
+ readonly role: "user";
22
+ readonly content: readonly {
23
+ readonly type: "text";
24
+ readonly text: string;
25
+ }[];
26
+ };
27
+ }
28
+ /** Encode one stdin NDJSON message without invoking a shell. */
29
+ export declare function encodeAgyStreamInput(message: unknown, maxFrameBytes?: number): string;
30
+ /** Return the exact UTF-8 size of one encoded AGY user frame. */
31
+ export declare function getAgyUserMessageByteLength(text: string): number;
32
+ /** Encode one AGY stream-json user turn with the factual V8-M1 envelope. */
33
+ export declare function encodeAgyUserMessage(text: string, maxFrameBytes?: number): string;
34
+ /**
35
+ * Replace all free-form strings and identifiers while retaining enough
36
+ * structure to replay parser behavior. Stable protocol enum fields remain.
37
+ */
38
+ export declare function sanitizeAgyProtocolValue(value: unknown, key?: string, depth?: number): unknown;
39
+ export interface AgyProtocolRecordSummary {
40
+ readonly topLevelKeys: readonly string[];
41
+ readonly event: string | null;
42
+ readonly shapePaths: readonly string[];
43
+ readonly valueKinds: readonly string[];
44
+ readonly stringLengths: Readonly<Record<string, number>>;
45
+ }
46
+ /** Summarize one decoded output record without returning its payload. */
47
+ export declare function summarizeAgyProtocolValue(value: unknown): AgyProtocolRecordSummary;
48
+ /** Convert one decoded output record into a safe fixture value. */
49
+ export declare function sanitizeAgyProtocolRecord(value: unknown): unknown;
50
+ //# sourceMappingURL=stream-protocol.d.ts.map