@relayflows/sdk 2.0.48 → 2.0.50
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/dist/agent-artifacts.d.ts +11 -0
- package/dist/agent-artifacts.d.ts.map +1 -1
- package/dist/agent-artifacts.js +32 -1
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/agent-transcript.d.ts +1 -0
- package/dist/agent-transcript.d.ts.map +1 -1
- package/dist/agent-transcript.js +21 -4
- package/dist/agent-transcript.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +10 -1
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/cloud-run.d.ts +2 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +6 -2
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli-commands.d.ts +4 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +5 -1
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloud-run.d.ts +5 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +11 -0
- package/dist/cloud-run.js.map +1 -1
- package/dist/local-agent-pins.d.ts +7 -0
- package/dist/local-agent-pins.d.ts.map +1 -0
- package/dist/local-agent-pins.js +7 -0
- package/dist/local-agent-pins.js.map +1 -0
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +8 -4
- package/dist/local-agent.js.map +1 -1
- package/dist/observer-link.d.ts +7 -7
- package/dist/observer-link.js +6 -6
- package/dist/worker-cli.d.ts +12 -0
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +23 -4
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-input.d.ts.map +1 -1
- package/dist/worker-input.js +14 -2
- package/dist/worker-input.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +31 -4
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-artifacts.ts +34 -1
- package/src/agent-transcript.ts +21 -4
- package/src/authored-worker-step.ts +10 -1
- package/src/cli/cloud-run.ts +7 -2
- package/src/cli-commands.ts +5 -1
- package/src/cli.ts +26 -8
- package/src/cloud-run.ts +16 -0
- package/src/local-agent-pins.ts +7 -0
- package/src/local-agent.ts +8 -4
- package/src/observer-link.ts +7 -7
- package/src/worker-cli.ts +34 -4
- package/src/worker-input.ts +14 -2
- package/src/worker.ts +30 -4
package/src/cli.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { runCloudLogsFollow, runCloudStatusWatch } from './cli/cloud-live.js';
|
|
|
48
48
|
import { transcriptTailSource } from './transcript-tail.js';
|
|
49
49
|
import { checkTypeScriptFlow } from './cli/check-typescript.js';
|
|
50
50
|
import { runCloudCli } from './cli/cloud-run.js';
|
|
51
|
+
import { isCloudRunBudgetMinutes } from './cloud-run.js';
|
|
51
52
|
import { runCloudSyncCli } from './cli/cloud-sync.js';
|
|
52
53
|
import { parseCloudDeployArgs, runCloudDeployCli, runCloudDeploymentsCli, runCloudUndeployCli, type CloudDeployArgs } from './cli/cloud-deploy.js';
|
|
53
54
|
import { runCloudRollbackCli, runCloudVersionsCli } from './cli/cloud-versions.js';
|
|
@@ -100,7 +101,7 @@ export type ParsedArgs =
|
|
|
100
101
|
| BuildArgs
|
|
101
102
|
| DeployArgs
|
|
102
103
|
| { command: 'serve-webhook'; dataDir: string; port: number; admitted?: readonly string[] }
|
|
103
|
-
| { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean; noConnect: boolean }
|
|
104
|
+
| { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean; noConnect: boolean; budgetMinutes: number | undefined }
|
|
104
105
|
| { command: 'sync'; runId: string; json: boolean; root: string; dryRun: boolean }
|
|
105
106
|
| CloudDeployArgs
|
|
106
107
|
| { command: 'deployments'; json: boolean }
|
|
@@ -147,8 +148,8 @@ const USAGE = [
|
|
|
147
148
|
'flows check [--against-daemon|--no-daemon-check] [--watch] [--json] [--explain-warnings] <flow.ts|flow.yaml|spec.json>',
|
|
148
149
|
'flows serve-webhook --data-dir <dir> --port <p> [--allow <name>[,<name>]]',
|
|
149
150
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--cloud-mirror] [--detach] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] [--reuse-from <run-id>] <flow.yaml|spec.json>',
|
|
150
|
-
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.yaml|spec.json>',
|
|
151
|
-
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.ts> --input <inline-json-or-file>',
|
|
151
|
+
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] [--budget-minutes <n>] <flow.yaml|spec.json>',
|
|
152
|
+
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] [--budget-minutes <n>] <flow.ts> --input <inline-json-or-file>',
|
|
152
153
|
'flows sync [--json] [--dry-run] [--dir <path>] <run-id>',
|
|
153
154
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--cloud-mirror] [--detach] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] <flow.ts> --input <inline-json-or-file>',
|
|
154
155
|
'flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>',
|
|
@@ -253,6 +254,15 @@ export async function runCli(
|
|
|
253
254
|
return 2;
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
if (parsed.command === 'run' && !parsed.json && !parsed.noObserverLink
|
|
258
|
+
&& !(parsed.cloudMirror || cloudMirrorRequested(process.env))) {
|
|
259
|
+
const link = resolveObserverLinkEnv(process.env);
|
|
260
|
+
if (!link.suppressed && link.workspaceKey === undefined) {
|
|
261
|
+
io.stderr('Observer unavailable: no workspace key configured; set RELAYCAST_WORKSPACE_KEY or configure '
|
|
262
|
+
+ '~/.agentworkforce/relay/workspaces.json for an Observer link, or pass --cloud-mirror for the Cloud dashboard');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
256
266
|
if ((parsed.command === 'run' || parsed.command === 'resume') && parsed.detach) {
|
|
257
267
|
const result = await startDetachedRun(args, {
|
|
258
268
|
...parsed, ...(options.selfCommand === undefined ? {} : { selfCommand: options.selfCommand }),
|
|
@@ -536,9 +546,9 @@ async function checkAuthoredFlowComposed(path: string): Promise<{ report: CheckR
|
|
|
536
546
|
|
|
537
547
|
/**
|
|
538
548
|
* Resolve the pending observer-mint into a URL (or nothing), and route any
|
|
539
|
-
* mint warning to stderr under a `[observer]` label.
|
|
540
|
-
*
|
|
541
|
-
*
|
|
549
|
+
* mint warning to stderr under a `[observer]` label. An `undefined` input
|
|
550
|
+
* prints nothing here; any missing-key hint was already emitted while the
|
|
551
|
+
* observer session was configured.
|
|
542
552
|
*/
|
|
543
553
|
async function observerUrlFrom(
|
|
544
554
|
mint: Promise<{ observerUrl?: string; warning?: string }> | undefined,
|
|
@@ -729,6 +739,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
729
739
|
let wait = false;
|
|
730
740
|
let syncCode = false;
|
|
731
741
|
let noConnect = false;
|
|
742
|
+
let budgetMinutes: number | undefined;
|
|
732
743
|
let localAgent = false;
|
|
733
744
|
let detach = false;
|
|
734
745
|
let agentCapacity: number | undefined;
|
|
@@ -754,6 +765,13 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
754
765
|
else noConnect = true;
|
|
755
766
|
continue;
|
|
756
767
|
}
|
|
768
|
+
if (argument === '--budget-minutes') {
|
|
769
|
+
const value = args[++index];
|
|
770
|
+
if (command !== 'run' || budgetMinutes !== undefined || value === undefined || !/^[0-9]+$/.test(value)) return undefined;
|
|
771
|
+
budgetMinutes = Number(value);
|
|
772
|
+
if (!isCloudRunBudgetMinutes(budgetMinutes)) return undefined;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
757
775
|
if (argument === '--detach') {
|
|
758
776
|
if (command === 'check' || detach) return undefined;
|
|
759
777
|
detach = true;
|
|
@@ -865,9 +883,9 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
865
883
|
if (detach || allowHumanInfluenced || sawDataDir || !spawn || localAgent || noObserverLink || cloudMirror
|
|
866
884
|
|| reuseFromRunId !== undefined) return undefined;
|
|
867
885
|
if (sawInput && !isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
868
|
-
return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode, noConnect };
|
|
886
|
+
return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode, noConnect, budgetMinutes };
|
|
869
887
|
}
|
|
870
|
-
if (wait || syncCode || noConnect) return undefined;
|
|
888
|
+
if (wait || syncCode || noConnect || budgetMinutes !== undefined) return undefined;
|
|
871
889
|
if (reuseFromRunId !== undefined && isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
872
890
|
|
|
873
891
|
if (command === 'run' && input !== undefined && !isAuthoredFlowPath(positionals[0]!)) return undefined;
|
package/src/cloud-run.ts
CHANGED
|
@@ -17,9 +17,20 @@ import { cloudRunState, isCloudRunActive, type CloudRunState } from './cloud-run
|
|
|
17
17
|
import { packWorkingTree, prepareCloudSync, uploadCloudCode } from './cloud-sync.js';
|
|
18
18
|
|
|
19
19
|
export type CloudFlowSource = FlowSpec | { path: string };
|
|
20
|
+
export const MIN_CLOUD_RUN_BUDGET_MINUTES = 30;
|
|
21
|
+
export const MAX_CLOUD_RUN_BUDGET_MINUTES = 180;
|
|
22
|
+
|
|
23
|
+
export function isCloudRunBudgetMinutes(value: number): boolean {
|
|
24
|
+
return Number.isSafeInteger(value)
|
|
25
|
+
&& value >= MIN_CLOUD_RUN_BUDGET_MINUTES
|
|
26
|
+
&& value <= MAX_CLOUD_RUN_BUDGET_MINUTES;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
export interface RunInCloudOptions extends CloudConnectionOptions {
|
|
21
30
|
/** Existing Cloud workspace to run in; provisioning/authorization remains server-side. */
|
|
22
31
|
workspaceId?: string;
|
|
32
|
+
/** Hosted run deadline in minutes. Applies to this admission only, not a later resume. */
|
|
33
|
+
runBudgetMinutes?: number;
|
|
23
34
|
/** Exact JSON input. Required at runtime for authored .flow.ts; refused for declarative source. */
|
|
24
35
|
input?: JsonValue;
|
|
25
36
|
/**
|
|
@@ -252,6 +263,10 @@ export async function runInCloud(
|
|
|
252
263
|
): Promise<CloudRunReceipt> {
|
|
253
264
|
// Validate locally first: resolving the connection may refresh a stored
|
|
254
265
|
// login, which posts the refresh token and rotates the credential file.
|
|
266
|
+
if (options.runBudgetMinutes !== undefined && !isCloudRunBudgetMinutes(options.runBudgetMinutes)) {
|
|
267
|
+
throw new CloudFlowError('configuration',
|
|
268
|
+
`runBudgetMinutes must be an integer from ${MIN_CLOUD_RUN_BUDGET_MINUTES} to ${MAX_CLOUD_RUN_BUDGET_MINUTES}.`);
|
|
269
|
+
}
|
|
255
270
|
const envSecrets = snapshotEnvSecrets(options.envSecrets);
|
|
256
271
|
const submission = await prepareCloudSubmission(flow, options);
|
|
257
272
|
const { baseUrl } = await resolveCloudConnection(options);
|
|
@@ -281,6 +296,7 @@ export async function runInCloud(
|
|
|
281
296
|
result = await cloudFetch('/api/v1/workflows/run', options, { method: 'POST', detail: true, body: JSON.stringify({
|
|
282
297
|
...cloudSubmissionBody(submission),
|
|
283
298
|
...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
|
|
299
|
+
...(options.runBudgetMinutes === undefined ? {} : { relayflowV2RunBudgetMinutes: options.runBudgetMinutes }),
|
|
284
300
|
...(synced === undefined ? {} : { runId: synced.runId, s3CodeKey: synced.codeKey }),
|
|
285
301
|
...(envSecrets === undefined ? {} : { envSecrets }),
|
|
286
302
|
}) });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Surface a timed-out local agent pins so a fresh worker at the clean tree
|
|
2
|
+
* cannot take the continuation. The local worker always holds it. A step
|
|
3
|
+
* declares it only when a deadline can continue. */
|
|
4
|
+
export const LOCAL_AGENT_WORKSPACE = 'local-workspace';
|
|
5
|
+
|
|
6
|
+
/** Revision the local worker reports before a deadline stamps the dirty tree. */
|
|
7
|
+
export const LOCAL_AGENT_REVISION = 'local';
|
package/src/local-agent.ts
CHANGED
|
@@ -4,9 +4,10 @@ import type { JournalClient } from './journal-client.js';
|
|
|
4
4
|
import { AgentWorker } from './worker.js';
|
|
5
5
|
import type { KernelRunSpec } from './spec.js';
|
|
6
6
|
import { communicationInstruction } from './communication/spec.js';
|
|
7
|
+
import { LOCAL_AGENT_REVISION, LOCAL_AGENT_WORKSPACE } from './local-agent-pins.js';
|
|
7
8
|
import { DEFAULT_LOCAL_AGENT_CAPACITY } from './worker-slots.js';
|
|
8
9
|
|
|
9
|
-
/** A local worker
|
|
10
|
+
/** A local worker. It holds `local-workspace` at `local` so a deadline can stamp the dirty tree. */
|
|
10
11
|
export async function attachLocalAgent(
|
|
11
12
|
client: JournalClient,
|
|
12
13
|
dataDir?: string,
|
|
@@ -24,7 +25,8 @@ export async function attachLocalAgent(
|
|
|
24
25
|
// This worker has consumed no messages: its read offsets are zero, including
|
|
25
26
|
// named declarative streams. The kernel retains ownership of dispatch pins
|
|
26
27
|
// and rejects a resume whose required offsets this worker does not hold.
|
|
27
|
-
//
|
|
28
|
+
// `local-workspace` starts at `local`. A deadline stamps that revision from
|
|
29
|
+
// the dirty tree, so a replacement worker still at `local` cannot take it.
|
|
28
30
|
const stream = requestedStream ?? `local-agent-${randomUUID()}`;
|
|
29
31
|
const worker = new AgentWorker(client, {
|
|
30
32
|
workerId: stream,
|
|
@@ -33,8 +35,10 @@ export async function attachLocalAgent(
|
|
|
33
35
|
// this same number (worker-slots.ts), so they never ask for more.
|
|
34
36
|
capacity,
|
|
35
37
|
dataDir, onPtyReady, runRoot, environment,
|
|
36
|
-
pins: {
|
|
37
|
-
|
|
38
|
+
pins: {
|
|
39
|
+
workspace: [{ surface: LOCAL_AGENT_WORKSPACE, revision_id: LOCAL_AGENT_REVISION }],
|
|
40
|
+
streams: [...new Set([stream, ...declaredStreams])].map(stream => ({ stream, read_offset: 0 })),
|
|
41
|
+
},
|
|
38
42
|
});
|
|
39
43
|
let failure: unknown;
|
|
40
44
|
// The cause travels with the close, so the flow's next request names it.
|
package/src/observer-link.ts
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* scoped, short-lived observer token on their behalf and hand *that* to the
|
|
12
12
|
* URL. The workspace admin key never leaves the operator's machine.
|
|
13
13
|
*
|
|
14
|
-
* Everything here is best-effort. A missing workspace key means
|
|
15
|
-
* link
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Everything here is best-effort. A missing workspace key means no observer
|
|
15
|
+
* link; the human-facing CLI explains the alternatives for a plain local run.
|
|
16
|
+
* A mint failure emits a labeled diagnostic to stderr and continues; a run
|
|
17
|
+
* must never fail because the observer link failed.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { readFileSync } from 'node:fs';
|
|
@@ -165,7 +165,7 @@ export interface ResolveObserverLinkEnvOptions {
|
|
|
165
165
|
* 2. Active workspace key from `~/.agentworkforce/relay/workspaces.json`
|
|
166
166
|
* (populated by `agent-relay cloud login` + `agent-relay workspace
|
|
167
167
|
* set_key`), when readable.
|
|
168
|
-
* 3. Nothing --
|
|
168
|
+
* 3. Nothing -- the caller decides whether its output mode warrants a hint.
|
|
169
169
|
*
|
|
170
170
|
* `FLOWS_NO_OBSERVER=1` still suppresses regardless of source.
|
|
171
171
|
*/
|
|
@@ -189,7 +189,7 @@ export function resolveObserverLinkEnv(
|
|
|
189
189
|
/**
|
|
190
190
|
* Read the observer-link config from the environment. Both trims and
|
|
191
191
|
* emptiness-checks the workspace key so a `RELAYCAST_WORKSPACE_KEY=` with no
|
|
192
|
-
* value reads the same as unset (no observer link
|
|
192
|
+
* value reads the same as unset (no observer link).
|
|
193
193
|
*/
|
|
194
194
|
export function readObserverLinkEnv(
|
|
195
195
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -258,7 +258,7 @@ export interface MintObserverOutcome {
|
|
|
258
258
|
/**
|
|
259
259
|
* A short diagnostic to log when mint failed. `undefined` on success or on
|
|
260
260
|
* "no observer link is possible" (missing key/suppressed) so the caller can
|
|
261
|
-
* distinguish
|
|
261
|
+
* distinguish an unavailable link from "warn about a mint failure".
|
|
262
262
|
*/
|
|
263
263
|
warning?: string;
|
|
264
264
|
}
|
package/src/worker-cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
3
|
-
import { diffWorkspaceFiles, snapshotWorkspaceFiles } from './agent-artifacts.js';
|
|
3
|
+
import { deadlineWorkspaceStamp, deletedWorkspaceFiles, diffWorkspaceFiles, snapshotWorkspaceFiles } from './agent-artifacts.js';
|
|
4
4
|
import { claudeResultOutcome, decodeProviderResult, decodeWrapperResult, requirePricedUsage } from './worker-usage.js';
|
|
5
5
|
import { openSidechannel, type SidechannelContext } from './pty-sidechannel.js';
|
|
6
6
|
import {
|
|
@@ -87,6 +87,18 @@ export interface WorkerCliResult {
|
|
|
87
87
|
* agent runs on another host.
|
|
88
88
|
*/
|
|
89
89
|
artifacts?: string[];
|
|
90
|
+
/**
|
|
91
|
+
* Content-and-deletion identity of the dirty tree, 16 hex characters.
|
|
92
|
+
* Present when the workspace changed. A deadline appends it to the end-pin
|
|
93
|
+
* revision so two different edits of the same paths, and a deletion with no
|
|
94
|
+
* new file, do not look like the clean start.
|
|
95
|
+
*/
|
|
96
|
+
workspace_stamp?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Paths removed during the attempt, cwd-relative and sorted. Journaled
|
|
99
|
+
* beside `artifacts`. Absent when nothing was deleted.
|
|
100
|
+
*/
|
|
101
|
+
deleted?: string[];
|
|
90
102
|
/**
|
|
91
103
|
* The attempt's transcript digest (`agent-transcript.ts`): provider result
|
|
92
104
|
* metadata, tool calls summarized, final text, failure excerpt, and — when
|
|
@@ -167,9 +179,27 @@ export async function runAgentCli(
|
|
|
167
179
|
const before = await snapshotWorkspaceFiles(artifactRoot);
|
|
168
180
|
const result = await execute();
|
|
169
181
|
const kernelData = sidechannel === undefined ? undefined : realPath(resolve(sidechannel.dataDir));
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
182
|
+
const after = await snapshotWorkspaceFiles(artifactRoot);
|
|
183
|
+
const visible = (files: Map<string, string>) => {
|
|
184
|
+
const out = new Map<string, string>();
|
|
185
|
+
for (const [path, signature] of files) {
|
|
186
|
+
// `canonicalTree` keeps a deleted path under the data dir excluded:
|
|
187
|
+
// the file is gone, so a plain realpath falls back to the spelling
|
|
188
|
+
// and can miss a symlinked data dir.
|
|
189
|
+
if (!under(canonicalTree(resolve(artifactRoot, path)), kernelData)) out.set(path, signature);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
};
|
|
193
|
+
const stampBefore = visible(before);
|
|
194
|
+
const stampAfter = visible(after);
|
|
195
|
+
const workspace_stamp = deadlineWorkspaceStamp(stampBefore, stampAfter);
|
|
196
|
+
const deleted = deletedWorkspaceFiles(stampBefore, stampAfter);
|
|
197
|
+
return {
|
|
198
|
+
...result,
|
|
199
|
+
artifacts: diffWorkspaceFiles(stampBefore, stampAfter),
|
|
200
|
+
...(workspace_stamp === undefined ? {} : { workspace_stamp }),
|
|
201
|
+
...(deleted.length === 0 ? {} : { deleted }),
|
|
202
|
+
};
|
|
173
203
|
});
|
|
174
204
|
|
|
175
205
|
async function execute(): Promise<WorkerCliResult> {
|
package/src/worker-input.ts
CHANGED
|
@@ -4,12 +4,24 @@ import type { StepDispatchEvent } from './protocol.js';
|
|
|
4
4
|
export function workerInstruction(instruction: string, dispatch: StepDispatchEvent): string {
|
|
5
5
|
const withMemory = dispatch.memory === undefined ? instruction
|
|
6
6
|
: `${instruction}\n\nMemory context (journaled):\n${JSON.stringify(dispatch.memory.pack)}`;
|
|
7
|
+
const noted = `${withMemory}${recoveryNote(dispatch)}`;
|
|
7
8
|
if (dispatch.input === undefined) {
|
|
8
9
|
if (typeof dispatch.spec === 'object' && dispatch.spec !== null
|
|
9
10
|
&& 'input' in dispatch.spec) {
|
|
10
11
|
throw new Error('Step declares input bindings but the kernel did not resolve them; upgrade relayflowd.');
|
|
11
12
|
}
|
|
12
|
-
return
|
|
13
|
+
return noted;
|
|
13
14
|
}
|
|
14
|
-
return `${
|
|
15
|
+
return `${noted}\n\ninput:\n${JSON.stringify(dispatch.input)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Absent on the first attempt. Null and undefined are both "the kernel had nothing". */
|
|
19
|
+
function recoveryNote(dispatch: StepDispatchEvent): string {
|
|
20
|
+
const reason = dispatch.recovery?.previous_completion_reason;
|
|
21
|
+
const tail = dispatch.recovery?.trajectory_tail;
|
|
22
|
+
if ((reason === undefined || reason === null) && (tail === undefined || tail === null)) return '';
|
|
23
|
+
return `\n\nPrevious attempt (journaled):\n${JSON.stringify({
|
|
24
|
+
completion_reason: reason ?? null,
|
|
25
|
+
trajectory_tail: tail ?? null,
|
|
26
|
+
})}`;
|
|
15
27
|
}
|
package/src/worker.ts
CHANGED
|
@@ -222,12 +222,32 @@ export class AgentWorker extends EventEmitter {
|
|
|
222
222
|
...(usage !== undefined ? { usage } : {}),
|
|
223
223
|
...(cost === undefined ? {} : { reported_cost: cost }),
|
|
224
224
|
started_pins: dispatch.pins,
|
|
225
|
-
end_pins:
|
|
225
|
+
end_pins: completionReason === 'timeout'
|
|
226
|
+
? deadlineEndPins(dispatch.pins, result.workspace_stamp)
|
|
227
|
+
: dispatch.pins,
|
|
226
228
|
},
|
|
227
229
|
);
|
|
228
230
|
}
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
/**
|
|
234
|
+
* A deadline's end pin names the dirty tree the next attempt must inspect.
|
|
235
|
+
* The stamp covers file contents and deletions, not the file bytes: the tail
|
|
236
|
+
* is capped and the bytes stay on this worker's disk. An empty diff keeps
|
|
237
|
+
* the start pin.
|
|
238
|
+
*/
|
|
239
|
+
function deadlineEndPins(pins: Pins, stamp: string | undefined): Pins {
|
|
240
|
+
const workspace = pins.workspace ?? [];
|
|
241
|
+
if (stamp === undefined || workspace.length === 0) return pins;
|
|
242
|
+
if (workspace.every(pin => pin.revision_id === '')) return pins;
|
|
243
|
+
return {
|
|
244
|
+
...pins,
|
|
245
|
+
workspace: workspace.map(pin => pin.revision_id === ''
|
|
246
|
+
? pin
|
|
247
|
+
: { ...pin, revision_id: `${pin.revision_id}+${stamp}` }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
231
251
|
/**
|
|
232
252
|
* The digest as journaled: the attempt and exit code (dropped from `output` on
|
|
233
253
|
* the JSON path by design) and the artifact paths folded in, then bounded to
|
|
@@ -238,9 +258,15 @@ function transcriptDigest(transcript: TranscriptDigest, attempt: number, result:
|
|
|
238
258
|
attempt,
|
|
239
259
|
exit_code: result.exit_code,
|
|
240
260
|
...transcript,
|
|
241
|
-
...(result.artifacts === undefined ? {} : {
|
|
242
|
-
|
|
243
|
-
|
|
261
|
+
...(result.artifacts === undefined && (result.deleted === undefined || result.deleted.length === 0) ? {} : {
|
|
262
|
+
artifacts: {
|
|
263
|
+
count: result.artifacts?.length ?? 0,
|
|
264
|
+
paths: (result.artifacts ?? []).slice(0, ARTIFACT_PATHS_MAX),
|
|
265
|
+
...(result.deleted === undefined || result.deleted.length === 0 ? {} : {
|
|
266
|
+
deleted: result.deleted.slice(0, ARTIFACT_PATHS_MAX),
|
|
267
|
+
}),
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
244
270
|
});
|
|
245
271
|
}
|
|
246
272
|
|