@prohost/cli 0.2.3 → 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/CHANGELOG.md +106 -0
- package/README.md +33 -1
- package/dist/agent/api.d.ts +53 -5
- package/dist/agent/api.js +51 -11
- package/dist/agent/claude.d.ts +53 -1
- package/dist/agent/claude.js +165 -20
- package/dist/agent/command.d.ts +24 -0
- package/dist/agent/command.js +89 -13
- package/dist/agent/contract.d.ts +52 -0
- package/dist/agent/contract.js +58 -0
- package/dist/agent/daemon.d.ts +2 -0
- package/dist/agent/daemon.js +6 -0
- package/dist/agent/exec.d.ts +77 -4
- package/dist/agent/exec.js +201 -18
- package/dist/agent/heartbeat.d.ts +114 -0
- package/dist/agent/heartbeat.js +165 -0
- package/dist/agent/prompt.d.ts +22 -0
- package/dist/agent/prompt.js +87 -2
- package/dist/agent/run.d.ts +51 -3
- package/dist/agent/run.js +392 -39
- package/dist/index.js +9 -3
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/agent/command.js
CHANGED
|
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
import { agentLogPath, buildSystemdUnit, harnessArguments, installLaunchdDaemon, launchdLabel, uninstallLaunchdDaemon, } from './daemon.js';
|
|
13
13
|
import { PairError, runPair } from './pair.js';
|
|
14
14
|
import { CredentialsMissingError, ensureWorkspace, loadCredentials, prohostHome } from './credentials.js';
|
|
15
|
-
import { DEFAULT_EXEC_TIMEOUT_MS, runAgentHarness } from './run.js';
|
|
15
|
+
import { DEFAULT_EXEC_TIMEOUT_MS, DEFAULT_IDLE_TIMEOUT_MS, runAgentHarness } from './run.js';
|
|
16
16
|
function flagString(flags, name) {
|
|
17
17
|
const value = flags[name];
|
|
18
18
|
return typeof value === 'string' ? value : undefined;
|
|
@@ -28,25 +28,93 @@ function flagString(flags, name) {
|
|
|
28
28
|
function requestedWorkdir(flags) {
|
|
29
29
|
return flagString(flags, 'workdir') ?? flagString(flags, 'cwd');
|
|
30
30
|
}
|
|
31
|
-
/** Returned by {@link
|
|
32
|
-
const
|
|
31
|
+
/** Returned by {@link parseSeconds} for a value that isn't a duration. */
|
|
32
|
+
export const INVALID_DURATION = Symbol('invalid-duration');
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Longest delay a timer can hold, in milliseconds — 2^31-1, just under 24.9
|
|
35
|
+
* days.
|
|
36
|
+
*
|
|
37
|
+
* The bound is `setTimeout`'s, not ours: a delay above this overflows the timer
|
|
38
|
+
* and Node **fires it after 1 ms instead**. So `--idle-timeout 2592000` ("give
|
|
39
|
+
* it thirty days") would not merely be ignored, it would kill the very first
|
|
40
|
+
* run on the spot and report it as stalled — the exact opposite of what was
|
|
41
|
+
* asked for, with nothing to explain it. Rejected at the flag rather than
|
|
42
|
+
* clamped, because anyone typing a number this large means "no limit", and both
|
|
43
|
+
* flags already spell that `0`.
|
|
35
44
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
45
|
+
* Checked against the *converted* value rather than against seconds, because
|
|
46
|
+
* the flags take fractions: `--timeout 2147483.9` is under any whole-second
|
|
47
|
+
* ceiling and still overflows once multiplied out.
|
|
38
48
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
50
|
+
/**
|
|
51
|
+
* Read a duration flag's value. Pure — exported so it can be tested without a
|
|
52
|
+
* process to write errors to.
|
|
53
|
+
*
|
|
54
|
+
* Returns ``undefined`` only when the flag was genuinely absent (the harness
|
|
55
|
+
* then applies its own default) and {@link INVALID_DURATION} for anything that
|
|
56
|
+
* is not a non-negative, finite number of seconds.
|
|
57
|
+
*
|
|
58
|
+
* ``0`` is legal for both flags that use this, and means "no limit of this
|
|
59
|
+
* kind". For `--timeout` it was rejected until long runs existed, because
|
|
60
|
+
* before heartbeats an uncapped run on a laptop that went to sleep was a row
|
|
61
|
+
* that span forever.
|
|
62
|
+
*
|
|
63
|
+
* Takes the raw flag value rather than a string, because "the operator typed
|
|
64
|
+
* this flag and got it wrong" and "the operator did not type this flag" have
|
|
65
|
+
* to be told apart. `--timeout` with nothing after it, and `--timeout -1` —
|
|
66
|
+
* which the argument parser reads as two flags, since a value cannot begin
|
|
67
|
+
* with `-` — both arrive here as `true`. Treated as absent, they would apply
|
|
68
|
+
* the default silently and leave someone believing they had set a limit they
|
|
69
|
+
* had not.
|
|
70
|
+
*/
|
|
71
|
+
export function parseSeconds(raw) {
|
|
41
72
|
if (raw === undefined)
|
|
42
73
|
return undefined;
|
|
74
|
+
// An empty string is the `--timeout ''` case, and `Number('')` is 0 — which
|
|
75
|
+
// would quietly read as "no limit" rather than as the typo it is.
|
|
76
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
77
|
+
return INVALID_DURATION;
|
|
43
78
|
const seconds = Number(raw);
|
|
44
|
-
if (!Number.isFinite(seconds) || seconds
|
|
45
|
-
|
|
46
|
-
return INVALID_TIMEOUT;
|
|
79
|
+
if (!Number.isFinite(seconds) || seconds < 0 || seconds * 1000 > MAX_TIMER_DELAY_MS) {
|
|
80
|
+
return INVALID_DURATION;
|
|
47
81
|
}
|
|
48
82
|
return seconds;
|
|
49
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Read `--timeout`, complaining once for both commands that take it.
|
|
86
|
+
*
|
|
87
|
+
* The wall-clock ceiling is opt-in now (`DEFAULT_EXEC_TIMEOUT_MS` is 0), so an
|
|
88
|
+
* absent flag means "no ceiling" rather than "two hours".
|
|
89
|
+
*/
|
|
90
|
+
function parseTimeoutSeconds(flags) {
|
|
91
|
+
const parsed = parseSeconds(flags.timeout);
|
|
92
|
+
if (parsed === INVALID_DURATION) {
|
|
93
|
+
// Keeps the phrase "--timeout must be a positive number" verbatim: the
|
|
94
|
+
// desktop app's supervisor matches on it to tell a misconfigured child from
|
|
95
|
+
// a recoverable crash (`desktop/src/main/agent/supervisor-policy.ts`), and
|
|
96
|
+
// it reads this CLI's stderr rather than any structured output.
|
|
97
|
+
process.stderr.write('--timeout must be a positive number of seconds under 2147484 (about 24 days), ' +
|
|
98
|
+
'or 0 for no local limit.\n');
|
|
99
|
+
}
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Read `--idle-timeout` — how long a run may produce nothing before it is
|
|
104
|
+
* treated as stalled.
|
|
105
|
+
*
|
|
106
|
+
* Validated in both commands for the same reason `--timeout` is: baked into a
|
|
107
|
+
* plist, a bad value makes every launch exit on this check and KeepAlive
|
|
108
|
+
* restart it forever.
|
|
109
|
+
*/
|
|
110
|
+
function parseIdleTimeoutSeconds(flags) {
|
|
111
|
+
const parsed = parseSeconds(flags['idle-timeout']);
|
|
112
|
+
if (parsed === INVALID_DURATION) {
|
|
113
|
+
process.stderr.write('--idle-timeout must be a positive number of seconds under 2147484 (about 24 days), ' +
|
|
114
|
+
'or 0 to disable the stall watchdog.\n');
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
50
118
|
/**
|
|
51
119
|
* Environment the daemon must be given explicitly.
|
|
52
120
|
*
|
|
@@ -120,7 +188,10 @@ async function installDaemonCommand(flags) {
|
|
|
120
188
|
// plist makes every launch exit on that same validation and KeepAlive restart
|
|
121
189
|
// it forever.
|
|
122
190
|
const timeoutSeconds = parseTimeoutSeconds(flags);
|
|
123
|
-
if (timeoutSeconds ===
|
|
191
|
+
if (timeoutSeconds === INVALID_DURATION)
|
|
192
|
+
return 1;
|
|
193
|
+
const idleTimeoutSeconds = parseIdleTimeoutSeconds(flags);
|
|
194
|
+
if (idleTimeoutSeconds === INVALID_DURATION)
|
|
124
195
|
return 1;
|
|
125
196
|
// Absolute, and created now. A supervisor does not inherit the installing
|
|
126
197
|
// shell's current directory, so a relative `--workdir .` copied verbatim into
|
|
@@ -142,6 +213,7 @@ async function installDaemonCommand(flags) {
|
|
|
142
213
|
exec,
|
|
143
214
|
workdir,
|
|
144
215
|
timeoutSeconds,
|
|
216
|
+
idleTimeoutSeconds,
|
|
145
217
|
machine: flagString(flags, 'machine'),
|
|
146
218
|
url: flagString(flags, 'url'),
|
|
147
219
|
allowUnverified: flags['allow-unverified'] === true,
|
|
@@ -215,7 +287,10 @@ async function runCommand(flags) {
|
|
|
215
287
|
if (!credentials)
|
|
216
288
|
return 1;
|
|
217
289
|
const parsedTimeout = parseTimeoutSeconds(flags);
|
|
218
|
-
if (parsedTimeout ===
|
|
290
|
+
if (parsedTimeout === INVALID_DURATION)
|
|
291
|
+
return 1;
|
|
292
|
+
const parsedIdleTimeout = parseIdleTimeoutSeconds(flags);
|
|
293
|
+
if (parsedIdleTimeout === INVALID_DURATION)
|
|
219
294
|
return 1;
|
|
220
295
|
// First signal stops accepting work and lets the in-flight run finish
|
|
221
296
|
// reporting itself (so the server doesn't wait on a run we abandoned); a
|
|
@@ -239,6 +314,7 @@ async function runCommand(flags) {
|
|
|
239
314
|
machine: flagString(flags, 'machine'),
|
|
240
315
|
cwd: requestedWorkdir(flags),
|
|
241
316
|
timeoutMs: parsedTimeout === undefined ? DEFAULT_EXEC_TIMEOUT_MS : parsedTimeout * 1000,
|
|
317
|
+
idleTimeoutMs: parsedIdleTimeout === undefined ? DEFAULT_IDLE_TIMEOUT_MS : parsedIdleTimeout * 1000,
|
|
242
318
|
dryRun: flags['dry-run'] === true,
|
|
243
319
|
allowUnverified: flags['allow-unverified'] === true,
|
|
244
320
|
// Two spellings for the same opt-out: `--safe-tools` says what you get,
|
package/dist/agent/contract.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Server-side sources of truth:
|
|
14
14
|
* - payload shape — `shared/events/payloads/agent_runs.py`
|
|
15
15
|
* - `POST /v1/agent-runs/{id}/complete` — `api_public/routers/agent_runs.py`
|
|
16
|
+
* - `POST /v1/agent-runs/{id}/heartbeat` — `api_public/routers/agent_runs.py`
|
|
16
17
|
* - `POST /v1/agent-pairing/redeem` — `api_public/routers/agent_pairing.py`
|
|
17
18
|
* - `POST /v1/conversations/{id}/messages` — `api_public/routers/conversations.py`
|
|
18
19
|
*/
|
|
@@ -28,12 +29,55 @@ export declare const RUN_STATUS_SUCCEEDED = "succeeded";
|
|
|
28
29
|
export declare const RUN_STATUS_FAILED = "failed";
|
|
29
30
|
/** Server-side cap on the `error` field of a completion (`MAX_ERROR_CHARS`). */
|
|
30
31
|
export declare const MAX_ERROR_CHARS = 1000;
|
|
32
|
+
/**
|
|
33
|
+
* The completion `error` that means "a person asked for this run to stop".
|
|
34
|
+
*
|
|
35
|
+
* A magic string on purpose, and it must be sent **exactly**: the server
|
|
36
|
+
* matches on it to word the notice it drops in the thread ("cancelled" rather
|
|
37
|
+
* than "failed"). Prefixing it with a reason, or sending
|
|
38
|
+
* `cancelled_by_user: …`, makes a deliberate stop read as a crash.
|
|
39
|
+
*/
|
|
40
|
+
export declare const RUN_ERROR_CANCELLED_BY_USER = "cancelled_by_user";
|
|
41
|
+
/**
|
|
42
|
+
* Cap on the `progress` string sent with a heartbeat.
|
|
43
|
+
*
|
|
44
|
+
* Progress is a hint for the UI — "using Bash", the first line of what the
|
|
45
|
+
* agent is drafting — not a transcript. Bounded here so a brain that emits one
|
|
46
|
+
* enormous block of text can't turn a keep-alive into a payload.
|
|
47
|
+
*/
|
|
48
|
+
export declare const MAX_PROGRESS_CHARS = 200;
|
|
31
49
|
/** `POST` — exchange a one-time pairing code for a scoped credential. */
|
|
32
50
|
export declare const REDEEM_PATH = "/v1/agent-pairing/redeem";
|
|
33
51
|
/** `POST` — send a message into a conversation as the paired agent. */
|
|
34
52
|
export declare function conversationMessagesPath(conversationId: string): string;
|
|
35
53
|
/** `POST` — close out a run. Must be called for every run, success or not. */
|
|
36
54
|
export declare function agentRunCompletePath(runId: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* `POST` — say a run is still alive, and hear back whether it was cancelled.
|
|
57
|
+
*
|
|
58
|
+
* The server holds a lease on every running row and reaps the ones that stop
|
|
59
|
+
* being heard from; a run that legitimately takes four hours has to say so on a
|
|
60
|
+
* cadence or it is reaped mid-work. The response also carries
|
|
61
|
+
* `cancel_requested`, which is the only way a laptop behind NAT learns that
|
|
62
|
+
* somebody pressed stop.
|
|
63
|
+
*/
|
|
64
|
+
export declare function agentRunHeartbeatPath(runId: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Where this run's heartbeats go, in descending order of how much we know.
|
|
67
|
+
*
|
|
68
|
+
* 1. The `heartbeat` block, when the payload carries one. A path the server
|
|
69
|
+
* stated is not a guess, so it is used verbatim.
|
|
70
|
+
* 2. Otherwise, derived from the server-supplied completion path: the two
|
|
71
|
+
* calls are the same resource, so a server that moves its agent-run routes
|
|
72
|
+
* moves both together and a CLI already on somebody's laptop follows
|
|
73
|
+
* without a release.
|
|
74
|
+
* 3. Otherwise the documented path, for a completion path shaped in some way
|
|
75
|
+
* we don't recognise.
|
|
76
|
+
*
|
|
77
|
+
* Steps 2 and 3 are exactly what shipped in 0.3.0 and are not going anywhere:
|
|
78
|
+
* the block is additive, and every server that predates it must keep working.
|
|
79
|
+
*/
|
|
80
|
+
export declare function heartbeatPathFor(run: AgentRunRequest): string;
|
|
37
81
|
/**
|
|
38
82
|
* A file or image hanging off a message.
|
|
39
83
|
*
|
|
@@ -177,6 +221,14 @@ export interface AgentRunRequest {
|
|
|
177
221
|
reply_body_extra?: Record<string, unknown>;
|
|
178
222
|
/** Server-supplied path to complete the run at. */
|
|
179
223
|
completion_path: string;
|
|
224
|
+
/**
|
|
225
|
+
* Server-supplied path to heartbeat the run at.
|
|
226
|
+
*
|
|
227
|
+
* Absent on every server built before the `heartbeat` block was added
|
|
228
|
+
* alongside `completion`, which is most of them — see {@link heartbeatPathFor}
|
|
229
|
+
* for what happens then.
|
|
230
|
+
*/
|
|
231
|
+
heartbeat_path?: string;
|
|
180
232
|
/**
|
|
181
233
|
* Earlier messages on this surface, oldest first. Absent on a server that
|
|
182
234
|
* predates the field — which is the case the harness shipped with, so an
|
package/dist/agent/contract.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Server-side sources of truth:
|
|
14
14
|
* - payload shape — `shared/events/payloads/agent_runs.py`
|
|
15
15
|
* - `POST /v1/agent-runs/{id}/complete` — `api_public/routers/agent_runs.py`
|
|
16
|
+
* - `POST /v1/agent-runs/{id}/heartbeat` — `api_public/routers/agent_runs.py`
|
|
16
17
|
* - `POST /v1/agent-pairing/redeem` — `api_public/routers/agent_pairing.py`
|
|
17
18
|
* - `POST /v1/conversations/{id}/messages` — `api_public/routers/conversations.py`
|
|
18
19
|
*/
|
|
@@ -28,6 +29,23 @@ export const RUN_STATUS_SUCCEEDED = 'succeeded';
|
|
|
28
29
|
export const RUN_STATUS_FAILED = 'failed';
|
|
29
30
|
/** Server-side cap on the `error` field of a completion (`MAX_ERROR_CHARS`). */
|
|
30
31
|
export const MAX_ERROR_CHARS = 1000;
|
|
32
|
+
/**
|
|
33
|
+
* The completion `error` that means "a person asked for this run to stop".
|
|
34
|
+
*
|
|
35
|
+
* A magic string on purpose, and it must be sent **exactly**: the server
|
|
36
|
+
* matches on it to word the notice it drops in the thread ("cancelled" rather
|
|
37
|
+
* than "failed"). Prefixing it with a reason, or sending
|
|
38
|
+
* `cancelled_by_user: …`, makes a deliberate stop read as a crash.
|
|
39
|
+
*/
|
|
40
|
+
export const RUN_ERROR_CANCELLED_BY_USER = 'cancelled_by_user';
|
|
41
|
+
/**
|
|
42
|
+
* Cap on the `progress` string sent with a heartbeat.
|
|
43
|
+
*
|
|
44
|
+
* Progress is a hint for the UI — "using Bash", the first line of what the
|
|
45
|
+
* agent is drafting — not a transcript. Bounded here so a brain that emits one
|
|
46
|
+
* enormous block of text can't turn a keep-alive into a payload.
|
|
47
|
+
*/
|
|
48
|
+
export const MAX_PROGRESS_CHARS = 200;
|
|
31
49
|
/** `POST` — exchange a one-time pairing code for a scoped credential. */
|
|
32
50
|
export const REDEEM_PATH = '/v1/agent-pairing/redeem';
|
|
33
51
|
/** `POST` — send a message into a conversation as the paired agent. */
|
|
@@ -38,6 +56,42 @@ export function conversationMessagesPath(conversationId) {
|
|
|
38
56
|
export function agentRunCompletePath(runId) {
|
|
39
57
|
return `/v1/agent-runs/${encodeURIComponent(runId)}/complete`;
|
|
40
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* `POST` — say a run is still alive, and hear back whether it was cancelled.
|
|
61
|
+
*
|
|
62
|
+
* The server holds a lease on every running row and reaps the ones that stop
|
|
63
|
+
* being heard from; a run that legitimately takes four hours has to say so on a
|
|
64
|
+
* cadence or it is reaped mid-work. The response also carries
|
|
65
|
+
* `cancel_requested`, which is the only way a laptop behind NAT learns that
|
|
66
|
+
* somebody pressed stop.
|
|
67
|
+
*/
|
|
68
|
+
export function agentRunHeartbeatPath(runId) {
|
|
69
|
+
return `/v1/agent-runs/${encodeURIComponent(runId)}/heartbeat`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Where this run's heartbeats go, in descending order of how much we know.
|
|
73
|
+
*
|
|
74
|
+
* 1. The `heartbeat` block, when the payload carries one. A path the server
|
|
75
|
+
* stated is not a guess, so it is used verbatim.
|
|
76
|
+
* 2. Otherwise, derived from the server-supplied completion path: the two
|
|
77
|
+
* calls are the same resource, so a server that moves its agent-run routes
|
|
78
|
+
* moves both together and a CLI already on somebody's laptop follows
|
|
79
|
+
* without a release.
|
|
80
|
+
* 3. Otherwise the documented path, for a completion path shaped in some way
|
|
81
|
+
* we don't recognise.
|
|
82
|
+
*
|
|
83
|
+
* Steps 2 and 3 are exactly what shipped in 0.3.0 and are not going anywhere:
|
|
84
|
+
* the block is additive, and every server that predates it must keep working.
|
|
85
|
+
*/
|
|
86
|
+
export function heartbeatPathFor(run) {
|
|
87
|
+
if (run.heartbeat_path)
|
|
88
|
+
return run.heartbeat_path;
|
|
89
|
+
const completion = run.completion_path;
|
|
90
|
+
const suffix = '/complete';
|
|
91
|
+
return completion.endsWith(suffix)
|
|
92
|
+
? `${completion.slice(0, -suffix.length)}/heartbeat`
|
|
93
|
+
: agentRunHeartbeatPath(run.run_id);
|
|
94
|
+
}
|
|
41
95
|
/**
|
|
42
96
|
* Ceiling on how many context entries are kept from one payload.
|
|
43
97
|
*
|
|
@@ -208,6 +262,7 @@ export function parseRunRequest(data) {
|
|
|
208
262
|
const trigger = obj(data.trigger);
|
|
209
263
|
const replyInstructions = obj(data.reply_instructions);
|
|
210
264
|
const completion = obj(data.completion);
|
|
265
|
+
const heartbeat = obj(data.heartbeat);
|
|
211
266
|
const context = obj(data.context);
|
|
212
267
|
const declaredSurface = str(replyInstructions.surface);
|
|
213
268
|
const conversationId = str(data.conversation_id) ?? str(replyInstructions.conversation_id);
|
|
@@ -250,6 +305,9 @@ export function parseRunRequest(data) {
|
|
|
250
305
|
reply_body_key: str(replyInstructions.body_field) ?? legacyBodyKeyFor(surface),
|
|
251
306
|
reply_body_extra: bodyExtra(replyInstructions.body_extra),
|
|
252
307
|
completion_path: str(completion.path) ?? agentRunCompletePath(runId),
|
|
308
|
+
// No fallback of its own: an absent block is not an error, it is an older
|
|
309
|
+
// server, and `heartbeatPathFor` already knows what to do about that.
|
|
310
|
+
heartbeat_path: str(heartbeat.path),
|
|
253
311
|
// Accepted both at the top level and nested under `context`, because the
|
|
254
312
|
// server can move them without a CLI release either way — and a published
|
|
255
313
|
// CLI cannot be redeployed to follow.
|
package/dist/agent/daemon.d.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface HarnessInvocation {
|
|
|
83
83
|
exec: string;
|
|
84
84
|
workdir: string;
|
|
85
85
|
timeoutSeconds?: number;
|
|
86
|
+
/** Seconds of total silence before a run is treated as stalled. */
|
|
87
|
+
idleTimeoutSeconds?: number;
|
|
86
88
|
machine?: string;
|
|
87
89
|
url?: string;
|
|
88
90
|
allowUnverified?: boolean;
|
package/dist/agent/daemon.js
CHANGED
|
@@ -176,8 +176,14 @@ export function harnessArguments(invocation) {
|
|
|
176
176
|
'--workdir',
|
|
177
177
|
invocation.workdir,
|
|
178
178
|
];
|
|
179
|
+
// Compared against `undefined` rather than tested for truth: `0` is a real
|
|
180
|
+
// setting for both flags — no wall-clock cap, no watchdog — and dropping it
|
|
181
|
+
// would install a daemon quietly running the defaults instead.
|
|
179
182
|
if (invocation.timeoutSeconds !== undefined)
|
|
180
183
|
args.push('--timeout', String(invocation.timeoutSeconds));
|
|
184
|
+
if (invocation.idleTimeoutSeconds !== undefined) {
|
|
185
|
+
args.push('--idle-timeout', String(invocation.idleTimeoutSeconds));
|
|
186
|
+
}
|
|
181
187
|
if (invocation.machine)
|
|
182
188
|
args.push('--machine', invocation.machine);
|
|
183
189
|
if (invocation.url)
|
package/dist/agent/exec.d.ts
CHANGED
|
@@ -17,12 +17,49 @@ export interface ExecOptions {
|
|
|
17
17
|
command: string;
|
|
18
18
|
/** Prompt written to the child's stdin. */
|
|
19
19
|
prompt: string;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Hard cap on wall-clock runtime.
|
|
22
|
+
*
|
|
23
|
+
* ``0`` disables it: the child then runs until it exits on its own, until
|
|
24
|
+
* {@link cancelSignal} fires, or until the server gives up on the run. That
|
|
25
|
+
* is a real configuration (`--timeout 0`), not a degenerate one — some work
|
|
26
|
+
* genuinely takes hours and a local clock is the wrong thing to bound it by.
|
|
27
|
+
*/
|
|
21
28
|
timeoutMs: number;
|
|
29
|
+
/**
|
|
30
|
+
* Kill the run when the child produces **nothing** for this long.
|
|
31
|
+
*
|
|
32
|
+
* The question a wall clock cannot answer: is this run working, or wedged?
|
|
33
|
+
* Bytes on stdout or stderr are the one signal every brain emits — stream
|
|
34
|
+
* events, a progress line, a log — so a window of total silence is the
|
|
35
|
+
* closest thing to evidence that a child has stopped making progress rather
|
|
36
|
+
* than merely taking a while. Every chunk on either stream resets it.
|
|
37
|
+
*
|
|
38
|
+
* ``0`` or unset disables it, at which point only {@link timeoutMs}, the
|
|
39
|
+
* {@link cancelSignal} and the server's lease can end the run.
|
|
40
|
+
*/
|
|
41
|
+
idleTimeoutMs?: number;
|
|
22
42
|
/** Working directory for the child. Defaults to the CLI's cwd. */
|
|
23
43
|
cwd?: string;
|
|
24
44
|
/** Extra environment for the child, merged over ``process.env``. */
|
|
25
45
|
env?: NodeJS.ProcessEnv;
|
|
46
|
+
/**
|
|
47
|
+
* Ends the run early — a person pressed stop, or the server closed the run.
|
|
48
|
+
*
|
|
49
|
+
* Terminated through exactly the same SIGTERM → grace → SIGKILL path a
|
|
50
|
+
* timeout uses, because the problem is identical: the thing being stopped is
|
|
51
|
+
* a process *group* that may be ignoring polite signals.
|
|
52
|
+
*/
|
|
53
|
+
cancelSignal?: AbortSignal;
|
|
54
|
+
/**
|
|
55
|
+
* Each stdout chunk, as it arrives.
|
|
56
|
+
*
|
|
57
|
+
* The captured ``stdout`` is still returned in full; this exists for the
|
|
58
|
+
* things that cannot wait for the process to exit — reading a session id out
|
|
59
|
+
* of the first event, tracking what the agent is doing for the heartbeat, and
|
|
60
|
+
* having *something* to salvage from a run that gets killed.
|
|
61
|
+
*/
|
|
62
|
+
onStdout?: (chunk: string) => void;
|
|
26
63
|
/** Injectable spawn — tests substitute a fake process. */
|
|
27
64
|
spawnImpl?: typeof spawn;
|
|
28
65
|
}
|
|
@@ -31,18 +68,54 @@ export interface ExecResult {
|
|
|
31
68
|
stdout: string;
|
|
32
69
|
stderr: string;
|
|
33
70
|
exitCode: number | null;
|
|
71
|
+
/** The run overran {@link ExecOptions.timeoutMs}. */
|
|
34
72
|
timedOut: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The run went silent for {@link ExecOptions.idleTimeoutMs} and was killed.
|
|
75
|
+
*
|
|
76
|
+
* Separate from {@link timedOut} because the two say different things to
|
|
77
|
+
* whoever reads the failure: one run used its whole budget, the other stopped
|
|
78
|
+
* doing anything at all. They are handled identically everywhere else — both
|
|
79
|
+
* are kills from outside, so both salvage partial output and keep the
|
|
80
|
+
* session they had recorded.
|
|
81
|
+
*/
|
|
82
|
+
idleTimedOut: boolean;
|
|
83
|
+
/** The run was stopped through {@link ExecOptions.cancelSignal}. */
|
|
84
|
+
cancelled: boolean;
|
|
35
85
|
/** Populated when the process could not be started at all. */
|
|
36
86
|
error?: string;
|
|
37
87
|
durationMs: number;
|
|
38
88
|
}
|
|
39
89
|
/** Cap on captured output (in characters) so a runaway agent can't exhaust memory. */
|
|
40
90
|
export declare const MAX_OUTPUT_CHARS: number;
|
|
91
|
+
/** ``error`` on a result the caller stopped deliberately. */
|
|
92
|
+
export declare const CANCELLED_ERROR = "cancelled";
|
|
93
|
+
/**
|
|
94
|
+
* What a timed-out run reports.
|
|
95
|
+
*
|
|
96
|
+
* Ends with the way out, because this string does not stay in `agent.log`: the
|
|
97
|
+
* server echoes a failed run's error into the thread, so the person who was
|
|
98
|
+
* waiting reads it. Told "timed out after 7200000ms" they learn only that
|
|
99
|
+
* something broke; told how to raise the ceiling they can fix it themselves.
|
|
100
|
+
* Kept to one line for the same reason.
|
|
101
|
+
*/
|
|
102
|
+
export declare function timeoutMessage(timeoutMs: number): string;
|
|
103
|
+
/**
|
|
104
|
+
* What a run killed by the inactivity watchdog reports.
|
|
105
|
+
*
|
|
106
|
+
* Same job as {@link timeoutMessage}, and the same constraint: the server
|
|
107
|
+
* echoes a failed run's error into the thread, so this is read by the person
|
|
108
|
+
* who was waiting rather than by whoever tails `agent.log`. It has to say what
|
|
109
|
+
* happened ("nothing came out of it"), that this is an inference rather than a
|
|
110
|
+
* crash the agent reported, and how to change the judgement — in one line,
|
|
111
|
+
* because the notice it lands in has room for one.
|
|
112
|
+
*/
|
|
113
|
+
export declare function idleTimeoutMessage(idleTimeoutMs: number): string;
|
|
41
114
|
/**
|
|
42
115
|
* Execute the agent command with ``prompt`` on stdin and return its stdout.
|
|
43
116
|
*
|
|
44
|
-
* Never throws: a spawn failure, a non-zero exit,
|
|
45
|
-
* as ``ok: false`` with the detail filled in, because the caller
|
|
46
|
-
* *some* outcome to the server for every run.
|
|
117
|
+
* Never throws: a spawn failure, a non-zero exit, a timeout, and a cancellation
|
|
118
|
+
* all come back as ``ok: false`` with the detail filled in, because the caller
|
|
119
|
+
* must report *some* outcome to the server for every run.
|
|
47
120
|
*/
|
|
48
121
|
export declare function execAgent(options: ExecOptions): Promise<ExecResult>;
|