@relayflows/sdk 2.0.44 → 2.0.46
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/authored-flow-error.d.ts +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-helper-effect.d.ts.map +1 -1
- package/dist/authored-helper-effect.js +9 -0
- package/dist/authored-helper-effect.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +17 -0
- package/dist/authored-root.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +7 -0
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +31 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cloud-mirror-step.d.ts.map +1 -1
- package/dist/cloud-mirror-step.js +6 -1
- package/dist/cloud-mirror-step.js.map +1 -1
- package/dist/compile.d.ts +1 -0
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +2 -1
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +1 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +1 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-tool-adapters.d.ts +56 -0
- package/dist/flow-tool-adapters.d.ts.map +1 -0
- package/dist/flow-tool-adapters.js +42 -0
- package/dist/flow-tool-adapters.js.map +1 -0
- package/dist/flow-tool-client.d.ts +38 -0
- package/dist/flow-tool-client.d.ts.map +1 -0
- package/dist/flow-tool-client.js +105 -0
- package/dist/flow-tool-client.js.map +1 -0
- package/dist/flow-tool-contract.d.ts +103 -0
- package/dist/flow-tool-contract.d.ts.map +1 -0
- package/dist/flow-tool-contract.js +75 -0
- package/dist/flow-tool-contract.js.map +1 -0
- package/dist/flow-tool-definitions.d.ts +19 -0
- package/dist/flow-tool-definitions.d.ts.map +1 -0
- package/dist/flow-tool-definitions.js +12 -0
- package/dist/flow-tool-definitions.js.map +1 -0
- package/dist/flow-tool-http.d.ts +14 -0
- package/dist/flow-tool-http.d.ts.map +1 -0
- package/dist/flow-tool-http.js +175 -0
- package/dist/flow-tool-http.js.map +1 -0
- package/dist/flow-tool-kernel.d.ts +27 -0
- package/dist/flow-tool-kernel.d.ts.map +1 -0
- package/dist/flow-tool-kernel.js +310 -0
- package/dist/flow-tool-kernel.js.map +1 -0
- package/dist/flow-tool-manifest.d.ts +30 -0
- package/dist/flow-tool-manifest.d.ts.map +1 -0
- package/dist/flow-tool-manifest.js +79 -0
- package/dist/flow-tool-manifest.js.map +1 -0
- package/dist/flow-tool-schema.d.ts +15 -0
- package/dist/flow-tool-schema.d.ts.map +1 -0
- package/dist/flow-tool-schema.js +70 -0
- package/dist/flow-tool-schema.js.map +1 -0
- package/dist/flow-tool-wire.d.ts +26 -0
- package/dist/flow-tool-wire.d.ts.map +1 -0
- package/dist/flow-tool-wire.js +195 -0
- package/dist/flow-tool-wire.js.map +1 -0
- package/dist/helper-park.d.ts +14 -0
- package/dist/helper-park.d.ts.map +1 -0
- package/dist/helper-park.js +40 -0
- package/dist/helper-park.js.map +1 -0
- package/dist/helper-receipt.d.ts +11 -0
- package/dist/helper-receipt.d.ts.map +1 -0
- package/dist/helper-receipt.js +100 -0
- package/dist/helper-receipt.js.map +1 -0
- package/dist/helper-writeback.d.ts.map +1 -1
- package/dist/helper-writeback.js +7 -2
- package/dist/helper-writeback.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +7 -0
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +22 -1
- package/dist/journal-client.js.map +1 -1
- package/dist/protocol.d.ts +7 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/yaml-helper-effect.d.ts.map +1 -1
- package/dist/yaml-helper-effect.js +32 -23
- package/dist/yaml-helper-effect.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-flow-error.ts +1 -0
- package/src/authored-helper-effect.ts +9 -0
- package/src/authored-root.ts +17 -0
- package/src/cli/direct-run.ts +7 -0
- package/src/cli/run.ts +30 -2
- package/src/cloud-mirror-step.ts +6 -1
- package/src/compile.ts +2 -1
- package/src/failure-kinds.ts +1 -0
- package/src/flow-tool-adapters.ts +43 -0
- package/src/flow-tool-client.ts +115 -0
- package/src/flow-tool-contract.ts +153 -0
- package/src/flow-tool-definitions.ts +27 -0
- package/src/flow-tool-http.ts +134 -0
- package/src/flow-tool-kernel.ts +319 -0
- package/src/flow-tool-manifest.ts +104 -0
- package/src/flow-tool-schema.ts +66 -0
- package/src/flow-tool-wire.ts +152 -0
- package/src/helper-park.ts +45 -0
- package/src/helper-receipt.ts +90 -0
- package/src/helper-writeback.ts +8 -3
- package/src/index.ts +21 -0
- package/src/journal-client.ts +21 -1
- package/src/protocol.ts +7 -0
- package/src/yaml-helper-effect.ts +12 -5
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { JournalClient } from './journal-client.js';
|
|
2
|
+
import type { StepDispatchEvent } from './protocol.js';
|
|
3
|
+
import { HelperWritebackPending } from './helper-receipt.js';
|
|
4
|
+
|
|
5
|
+
export function helperReceiptWaitId(stepId: string, attempt: number): string {
|
|
6
|
+
return `helper-receipt:${stepId}:${attempt}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Journal a human-wait park before the worker connection closes.
|
|
10
|
+
* Closing without this is a transport failure, and the default budget is one
|
|
11
|
+
* extra retry: two receipt timeouts then fail the accepted write.
|
|
12
|
+
*/
|
|
13
|
+
export async function parkHelperReceipt(
|
|
14
|
+
client: JournalClient, dispatch: StepDispatchEvent, error: HelperWritebackPending,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
await client.stepWait(dispatch.run_id, dispatch.step_id, dispatch.attempt, dispatch.idempotency_key, {
|
|
17
|
+
wait_id: helperReceiptWaitId(dispatch.step_id, dispatch.attempt),
|
|
18
|
+
prompt: error.message,
|
|
19
|
+
requested_of: 'relayfile-receipt',
|
|
20
|
+
options: ['resume'],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Resume continues the same accepted write. Answering the park redispatches
|
|
25
|
+
* the attempt; it does not submit another provider draft.
|
|
26
|
+
*/
|
|
27
|
+
export async function releaseHelperReceiptWaits(client: JournalClient, runId: string): Promise<void> {
|
|
28
|
+
type WaitEntry = { seq?: number; entry_type: string; payload?: { wait_id?: string } };
|
|
29
|
+
const entries: WaitEntry[] = [];
|
|
30
|
+
let from = 1;
|
|
31
|
+
for (;;) {
|
|
32
|
+
const page = (await client.journalRead(runId, from, 1000)).entries as WaitEntry[];
|
|
33
|
+
if (page.length === 0) break;
|
|
34
|
+
entries.push(...page);
|
|
35
|
+
const last = page[page.length - 1]?.seq;
|
|
36
|
+
if (last === undefined || page.length < 1000) break;
|
|
37
|
+
from = last + 1;
|
|
38
|
+
}
|
|
39
|
+
const closed = new Set(entries.filter(entry => entry.entry_type === 'wait.completed').map(entry => entry.payload?.wait_id));
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const waitId = entry.payload?.wait_id;
|
|
42
|
+
if (entry.entry_type !== 'wait.human' || waitId === undefined || !waitId.startsWith('helper-receipt:') || closed.has(waitId)) continue;
|
|
43
|
+
await client.eventEmit(runId, waitId, { answer: 'resume', answeredBy: 'flows-resume' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, relative, isAbsolute } from 'node:path';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { writeJsonFile, type WritebackReceipt, type WritebackResult } from '@relayfile/adapter-core/vfs-client';
|
|
5
|
+
import { atomicJson, receiptPath, readHelperReceipt } from './helper-storage.js';
|
|
6
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
7
|
+
|
|
8
|
+
export class HelperWritebackPending extends AuthoredFlowExecutionError {
|
|
9
|
+
constructor(readonly writeId: string, readonly path: string, runId: string) {
|
|
10
|
+
super('helper_writeback_pending', `write ${writeId} accepted or submission interrupted; delivery receipt pending at ${path}. Resume to wait; do not re-issue the write.`, undefined, runId);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function helperReceiptTimeoutMs(): number {
|
|
15
|
+
const value = process.env.RELAYFLOW_HELPER_RECEIPT_TIMEOUT_MS;
|
|
16
|
+
if (value === undefined || value.trim() === '') return 60_000;
|
|
17
|
+
const timeout = Number(value);
|
|
18
|
+
if (!Number.isSafeInteger(timeout) || timeout <= 0) throw new Error('RELAYFLOW_HELPER_RECEIPT_TIMEOUT_MS must be a positive integer');
|
|
19
|
+
return timeout;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Persist intent before mount I/O: an interrupted submission is never blindly repeated. */
|
|
23
|
+
export async function writeHelperDraft(mount: string, provider: string, operation: string,
|
|
24
|
+
path: string, body: Record<string, unknown>, dataDir: string, runId: string, stepId: string,
|
|
25
|
+
signal: AbortSignal): Promise<WritebackResult> {
|
|
26
|
+
const timeout = helperReceiptTimeoutMs();
|
|
27
|
+
const writeId = `${runId}:${stepId}`;
|
|
28
|
+
const file = receiptPath(dataDir, runId, stepId) + '.pending';
|
|
29
|
+
let intent: { path: string; absolutePath: string; body: Record<string, unknown> };
|
|
30
|
+
try { intent = await readHelperReceipt(file) as typeof intent; }
|
|
31
|
+
catch (error) {
|
|
32
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
33
|
+
const absolutePath = resolve(mount, path.replace(/^\/+/, ''));
|
|
34
|
+
const inside = relative(mount, absolutePath);
|
|
35
|
+
if (inside === '..' || inside.startsWith('../') || isAbsolute(inside)) throw new Error('Helper path escapes mount root');
|
|
36
|
+
intent = { path, absolutePath, body };
|
|
37
|
+
await atomicJson(file, intent);
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
const submitted = await writeJsonFile({ relayfileMountRoot: mount, writebackTimeoutMs: 0 }, provider, operation, path, body);
|
|
40
|
+
if (providerReceipt(submitted.receipt, intent.body)) {
|
|
41
|
+
return { ...submitted, deliveryStatus: 'confirmed', receipt: stringReceipt(submitted.receipt) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const deadline = Date.now() + timeout;
|
|
45
|
+
do {
|
|
46
|
+
signal.throwIfAborted();
|
|
47
|
+
let receipt: WritebackReceipt | undefined;
|
|
48
|
+
try { receipt = JSON.parse(await readFile(intent.absolutePath, 'utf8')) as WritebackReceipt; }
|
|
49
|
+
catch (error) {
|
|
50
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
|
|
51
|
+
}
|
|
52
|
+
// Identity fields only. A path, timestamp, or error object written while
|
|
53
|
+
// the adapter is still flushing is not a provider delivery.
|
|
54
|
+
if (providerReceipt(receipt, intent.body)) {
|
|
55
|
+
return { path: intent.path, absolutePath: intent.absolutePath, deliveryStatus: 'confirmed', receipt: stringReceipt(receipt) };
|
|
56
|
+
}
|
|
57
|
+
const remaining = deadline - Date.now();
|
|
58
|
+
if (remaining <= 0) break;
|
|
59
|
+
await delay(Math.min(250, remaining), undefined, { signal });
|
|
60
|
+
} while (Date.now() <= deadline);
|
|
61
|
+
throw new HelperWritebackPending(writeId, intent.path, runId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A provider id the draft did not already carry. Path and created are not delivery. */
|
|
65
|
+
function providerReceipt(receipt: WritebackReceipt | undefined, draft: Record<string, unknown>): receipt is WritebackReceipt {
|
|
66
|
+
if (receipt === undefined || typeof receipt !== 'object' || Array.isArray(receipt)) return false;
|
|
67
|
+
return (['id', 'externalId', 'ts'] as const).some(key => {
|
|
68
|
+
const value = receiptIdentity(receipt[key]);
|
|
69
|
+
return value !== undefined && value !== receiptIdentity(draft[key]);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** GitHub mount files store comment and issue ids as JSON numbers. */
|
|
74
|
+
function receiptIdentity(value: unknown): string | undefined {
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
78
|
+
}
|
|
79
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) return String(value);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function stringReceipt(receipt: WritebackReceipt): WritebackReceipt {
|
|
84
|
+
const next: WritebackReceipt = { ...receipt };
|
|
85
|
+
for (const key of ['id', 'externalId', 'ts'] as const) {
|
|
86
|
+
const value = receiptIdentity(receipt[key]);
|
|
87
|
+
if (value !== undefined) next[key] = value;
|
|
88
|
+
}
|
|
89
|
+
return next;
|
|
90
|
+
}
|
package/src/helper-writeback.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { readJsonFile, listJsonFiles, type WritebackResult } from '@relayfile/adapter-core/vfs-client';
|
|
4
4
|
import type { RelayTransport, RelayTransportRequest } from '@relayfile/relay-helpers/transport';
|
|
5
5
|
import { helperClients, helperProviders, invokeHelper, type HelperCall } from '@relayflows/surface/runtime';
|
|
6
6
|
import { providerMount } from './slack-preflight.js';
|
|
7
7
|
import { atomicJson } from './helper-storage.js';
|
|
8
8
|
import type { SlackCall } from './slack-writeback.js';
|
|
9
|
+
import { writeHelperDraft, HelperWritebackPending } from './helper-receipt.js';
|
|
9
10
|
|
|
10
11
|
export class HelperDeliveryError extends Error {}
|
|
11
12
|
|
|
@@ -14,7 +15,10 @@ export async function helperWriteback(call: HelperCall, dataDir: string, runId:
|
|
|
14
15
|
if (!factory) throw new Error(`No writeback client for ${call.provider}`);
|
|
15
16
|
const { transport } = helperTransport(call, dataDir, runId, stepId, signal);
|
|
16
17
|
try { return await invokeHelper(factory, call, transport); }
|
|
17
|
-
catch (cause) {
|
|
18
|
+
catch (cause) {
|
|
19
|
+
if (cause instanceof HelperWritebackPending) throw cause;
|
|
20
|
+
throw new HelperDeliveryError(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
21
|
+
}
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/** One transport for every provider, including Slack. Only confirmed writes succeed. */
|
|
@@ -65,8 +69,9 @@ export function helperTransport(call: HelperCall | SlackCall, dataDir: string, r
|
|
|
65
69
|
runId, stepId, request: { ...request, body }, receipt: result.receipt,
|
|
66
70
|
});
|
|
67
71
|
} else {
|
|
68
|
-
try { result = await
|
|
72
|
+
try { result = await writeHelperDraft(options().relayfileMountRoot, call.provider, `write.${request.resource}`, draft, body, dataDir, runId, stepId, signal); }
|
|
69
73
|
catch (cause) {
|
|
74
|
+
if (cause instanceof HelperWritebackPending) throw cause;
|
|
70
75
|
// Upstream created() converts pending/terminal adapter errors into values.
|
|
71
76
|
// A journal effect must fail instead of confirming an undelivered write.
|
|
72
77
|
throw new Error(`${call.provider} writeback failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
package/src/index.ts
CHANGED
|
@@ -131,6 +131,16 @@ export { localFlowEvalExecutor, type LocalFlowEvalExecutorOptions } from './flow
|
|
|
131
131
|
export { cloudFlowEvalExecutor, type CloudFlowEvalExecutorOptions } from './flow-eval-cloud.js';
|
|
132
132
|
|
|
133
133
|
export { canonicalize, specHash } from './canonical.js';
|
|
134
|
+
export {
|
|
135
|
+
createFlowToolManifest, parseFlowToolManifest, canonicalFlowToolManifest,
|
|
136
|
+
validateFlowToolInput, validateFlowToolResult,
|
|
137
|
+
type FlowToolDeclarationV1, type FlowToolManifestV1, type FlowToolDigest,
|
|
138
|
+
} from './flow-tool-manifest.js';
|
|
139
|
+
export { FLOW_TOOL_SCHEMA_DIALECT, type FlowToolJson, type FlowToolObjectSchema } from './flow-tool-schema.js';
|
|
140
|
+
export {
|
|
141
|
+
flowToolFunctionDefinition, flowToolMcpDefinition,
|
|
142
|
+
type FlowToolFunctionDefinition, type FlowToolMcpDefinition,
|
|
143
|
+
} from './flow-tool-definitions.js';
|
|
134
144
|
export {
|
|
135
145
|
compileAndHash,
|
|
136
146
|
compileSpec,
|
|
@@ -295,6 +305,17 @@ export {
|
|
|
295
305
|
|
|
296
306
|
export { createFlow, type CreateFlowOptions, type CreatedFlow } from './create-flow.js';
|
|
297
307
|
|
|
308
|
+
export * from './flow-tool-contract.js';
|
|
309
|
+
export { parseFlowToolCatalog, canonicalFlowToolCatalog, parseFlowToolEntry, parseFlowToolRun, parseFlowToolEvent,
|
|
310
|
+
parseFlowToolEvidence, parseFlowToolInvocation, flowToolInputDigest, flowToolRunLinks } from './flow-tool-wire.js';
|
|
311
|
+
export { FlowToolClient, type FlowToolTransport, type FlowToolRequest, type FlowToolInvocationOptions } from './flow-tool-client.js';
|
|
312
|
+
export { createFlowToolHttpTransport, type FlowToolHttpOptions } from './flow-tool-http.js';
|
|
313
|
+
export { createFlowToolAdapters } from './flow-tool-adapters.js';
|
|
314
|
+
export {
|
|
315
|
+
createKernelFlowToolControlPlane, FLOW_TOOL_INPUT_PLACEHOLDER,
|
|
316
|
+
type KernelFlowToolDeployment, type KernelFlowToolControlPlaneOptions,
|
|
317
|
+
} from './flow-tool-kernel.js';
|
|
318
|
+
|
|
298
319
|
export { renderProgress, type ProgressEvent } from './progress.js';
|
|
299
320
|
export { webhookTriggerSpec } from './trigger-executor.js';
|
|
300
321
|
export { scheduleTriggerSpec, scheduleLowering, SCHEDULE_EXECUTOR, type ScheduleLowering } from './schedule-trigger.js';
|
package/src/journal-client.ts
CHANGED
|
@@ -132,12 +132,32 @@ export class JournalClient extends JournalConnection {
|
|
|
132
132
|
...(reuseFromRunId === undefined ? {} : { reuse_from_run_id: reuseFromRunId }),
|
|
133
133
|
...(admissionKey === undefined ? {} : { admission_key: admissionKey }),
|
|
134
134
|
...(watch ? { watch: true } : {}),
|
|
135
|
+
...this.stepEnv(),
|
|
135
136
|
}, null);
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
/** §3 memoized resume. */
|
|
139
140
|
runResume(runId: string, allowHumanInfluenced = false): Promise<VerbContract['run.resume']['result']> {
|
|
140
|
-
return this.request('run.resume', {
|
|
141
|
+
return this.request('run.resume', {
|
|
142
|
+
run_id: runId,
|
|
143
|
+
...(allowHumanInfluenced ? { allow_human_influenced: true } : {}),
|
|
144
|
+
...this.stepEnv(),
|
|
145
|
+
}, null);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* This process's environment, for the run's `f.run` steps. A shared daemon
|
|
150
|
+
* otherwise spawns them in the environment of whichever CLI started it
|
|
151
|
+
* (kernel/DAEMON-LIFECYCLE.md §3). Sent only to a daemon that lists
|
|
152
|
+
* `step_env`, so an older one never sees an unknown field.
|
|
153
|
+
*/
|
|
154
|
+
private stepEnv(): { env?: Record<string, string> } {
|
|
155
|
+
if (!this.features.has('step_env')) return {};
|
|
156
|
+
const env: Record<string, string> = {};
|
|
157
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
158
|
+
if (value !== undefined) env[key] = value;
|
|
159
|
+
}
|
|
160
|
+
return { env };
|
|
141
161
|
}
|
|
142
162
|
|
|
143
163
|
/** Durably request cancellation and return the terminal run fact. */
|
package/src/protocol.ts
CHANGED
|
@@ -126,6 +126,11 @@ export interface RunStartParams {
|
|
|
126
126
|
admission_key?: string;
|
|
127
127
|
/** Push the new run's entries to this connection from `run.spawned` on, as `run.watch` does. */
|
|
128
128
|
watch?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Environment for this run's deterministic steps, replacing the daemon's.
|
|
131
|
+
* Daemon feature `step_env`. Held in daemon memory only, never journaled.
|
|
132
|
+
*/
|
|
133
|
+
env?: Record<string, string>;
|
|
129
134
|
reuse_from_run_id?: string;
|
|
130
135
|
/**
|
|
131
136
|
* The kernel spec dialect — the ONE boundary shape `RunSpec::parse`
|
|
@@ -148,6 +153,8 @@ export type RunStartResult = RunOutcome;
|
|
|
148
153
|
|
|
149
154
|
export interface RunResumeParams {
|
|
150
155
|
allow_human_influenced?: boolean;
|
|
156
|
+
/** Replaces the run's step environment (feature `step_env`); see `RunStartParams.env`. */
|
|
157
|
+
env?: Record<string, string>;
|
|
151
158
|
run_id: string;
|
|
152
159
|
}
|
|
153
160
|
export type RunResumeResult = RunOutcome;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { HelperWritebackPending, writeHelperDraft } from './helper-receipt.js';
|
|
5
|
+
import { parkHelperReceipt } from './helper-park.js';
|
|
5
6
|
import type { RelayTransport } from '@relayfile/relay-helpers';
|
|
6
7
|
import type { JournalClient } from './journal-client.js';
|
|
7
8
|
import type { StepDispatchEvent } from './protocol.js';
|
|
@@ -37,8 +38,8 @@ async function writeback(call: HelperCall, dataDir: string, runId: string, stepI
|
|
|
37
38
|
// Item updates keep the client's canonical path. Creates use a stable draft.
|
|
38
39
|
const path = request.path.endsWith('.json') ? request.path
|
|
39
40
|
: `${request.path}/draft-${createHash('sha256').update(idempotencyKey).digest('hex')}.json`;
|
|
40
|
-
const result = await
|
|
41
|
-
`write.${request.resource}`, path, body);
|
|
41
|
+
const result = await writeHelperDraft(mount, request.provider,
|
|
42
|
+
`write.${request.resource}`, path, body, dataDir, runId, stepId, signal);
|
|
42
43
|
if (result.deliveryStatus !== 'confirmed' || !result.receipt) {
|
|
43
44
|
throw new Error(`${call.provider} writeback is pending; no delivery receipt`);
|
|
44
45
|
}
|
|
@@ -54,7 +55,9 @@ export async function completeHelperDispatch(
|
|
|
54
55
|
client: JournalClient, dispatch: StepDispatchEvent, call: HelperCall, dataDir: string,
|
|
55
56
|
): Promise<void> {
|
|
56
57
|
const surfacePath = `/${call.provider}`;
|
|
57
|
-
|
|
58
|
+
let output: { idempotencyKey: string; receipt: unknown };
|
|
59
|
+
try {
|
|
60
|
+
output = await withWorkerLease(client, dispatch, async signal => {
|
|
58
61
|
const file = receiptPath(dataDir, dispatch.run_id, dispatch.step_id);
|
|
59
62
|
let receipt: unknown;
|
|
60
63
|
await client.performEffect({
|
|
@@ -72,7 +75,11 @@ export async function completeHelperDispatch(
|
|
|
72
75
|
});
|
|
73
76
|
if (receipt === undefined) receipt = await readSlackReceipt(file);
|
|
74
77
|
return { ...call, idempotencyKey: `${dispatch.run_id}:${dispatch.step_id}`, receipt };
|
|
75
|
-
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof HelperWritebackPending) await parkHelperReceipt(client, dispatch, error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
76
83
|
await client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt,
|
|
77
84
|
dispatch.idempotency_key, 'success', { output, started_pins: dispatch.pins, end_pins: dispatch.pins, reported_cost: NO_MODEL_COST,
|
|
78
85
|
effects: [{ surface_path: surfacePath, idempotency_key: dispatch.idempotency_key }] });
|