@parcha/agentrun-kernel 0.0.0-stage → 0.1.0-beta.29

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 (82) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +52 -2
  5. package/dist/delegate.d.ts +87 -0
  6. package/dist/delegate.js +143 -0
  7. package/dist/errors.d.ts +36 -0
  8. package/dist/errors.js +37 -0
  9. package/dist/host/alive.d.ts +2 -0
  10. package/dist/host/alive.js +28 -0
  11. package/dist/host/box-slot.d.ts +26 -0
  12. package/dist/host/box-slot.js +112 -0
  13. package/dist/host/box.d.ts +104 -0
  14. package/dist/host/box.js +16 -0
  15. package/dist/host/egress-proxy.d.ts +45 -0
  16. package/dist/host/egress-proxy.js +416 -0
  17. package/dist/host/egress-table.d.ts +24 -0
  18. package/dist/host/egress-table.js +165 -0
  19. package/dist/host/index.d.ts +16 -0
  20. package/dist/host/index.js +12 -0
  21. package/dist/host/overhead.d.ts +53 -0
  22. package/dist/host/overhead.js +75 -0
  23. package/dist/host/parent-death.d.ts +15 -0
  24. package/dist/host/parent-death.js +42 -0
  25. package/dist/host/parent-watch.d.ts +1 -0
  26. package/dist/host/parent-watch.js +38 -0
  27. package/dist/host/pinned-binary.d.ts +6 -0
  28. package/dist/host/pinned-binary.js +48 -0
  29. package/dist/host/proxy.d.ts +179 -0
  30. package/dist/host/proxy.js +629 -0
  31. package/dist/host/serial.d.ts +3 -0
  32. package/dist/host/serial.js +10 -0
  33. package/dist/index.d.ts +11 -0
  34. package/dist/index.js +6 -0
  35. package/dist/kernel/blobs.d.ts +16 -0
  36. package/dist/kernel/blobs.js +59 -0
  37. package/dist/kernel/drive.d.ts +35 -0
  38. package/dist/kernel/drive.js +202 -0
  39. package/dist/kernel/errors.d.ts +27 -0
  40. package/dist/kernel/errors.js +42 -0
  41. package/dist/kernel/follow-up.d.ts +67 -0
  42. package/dist/kernel/follow-up.js +146 -0
  43. package/dist/kernel/halt.d.ts +11 -0
  44. package/dist/kernel/halt.js +17 -0
  45. package/dist/kernel/index.d.ts +8 -0
  46. package/dist/kernel/index.js +5 -0
  47. package/dist/kernel/types.d.ts +120 -0
  48. package/dist/kernel/types.js +1 -0
  49. package/dist/process/builtins.d.ts +142 -0
  50. package/dist/process/builtins.js +207 -0
  51. package/dist/process/inbox.d.ts +29 -0
  52. package/dist/process/inbox.js +52 -0
  53. package/dist/process/index.d.ts +9 -0
  54. package/dist/process/index.js +11 -0
  55. package/dist/process/journal.d.ts +111 -0
  56. package/dist/process/journal.js +232 -0
  57. package/dist/process/records.d.ts +143 -0
  58. package/dist/process/records.js +142 -0
  59. package/dist/process/recovery.d.ts +76 -0
  60. package/dist/process/recovery.js +139 -0
  61. package/dist/process/settle.d.ts +11 -0
  62. package/dist/process/settle.js +20 -0
  63. package/dist/process/tail.d.ts +20 -0
  64. package/dist/process/tail.js +53 -0
  65. package/dist/process/tools.d.ts +105 -0
  66. package/dist/process/tools.js +120 -0
  67. package/dist/process/zod.d.ts +32 -0
  68. package/dist/process/zod.js +109 -0
  69. package/dist/runner.d.ts +154 -0
  70. package/dist/runner.js +547 -0
  71. package/dist/schema-check-cases.d.ts +7 -0
  72. package/dist/schema-check-cases.js +73 -0
  73. package/dist/schema-check.d.ts +3 -0
  74. package/dist/schema-check.js +88 -0
  75. package/dist/skill-files.d.ts +23 -0
  76. package/dist/skill-files.js +136 -0
  77. package/dist/testing.d.ts +91 -0
  78. package/dist/testing.js +196 -0
  79. package/dist/types.d.ts +237 -0
  80. package/dist/types.js +10 -0
  81. package/package.json +74 -4
  82. package/vendor/durable-harness/UPSTREAM.json +67 -0
@@ -0,0 +1,120 @@
1
+ import type { JsonValue, SessionWriter, ToolImage } from "../types.js";
2
+ export type ReplayPolicy = "safe" | "never";
3
+ export type SubmissionKind = "prompt" | "steer" | "followUp" | "withdraw";
4
+ /** One input a person sent the session, as the host recorded it. */
5
+ export interface Submission {
6
+ id: string;
7
+ kind: SubmissionKind;
8
+ text?: string;
9
+ /** For a withdraw: the submission it withdraws. */
10
+ target?: string;
11
+ receivedAt: Date;
12
+ }
13
+ export type EffectStatus = "pending" | "running" | "completed" | "failed";
14
+ export interface EffectError {
15
+ message: string;
16
+ /** The effect's drive went away before it reported an outcome: it may or may not have taken effect. */
17
+ interrupted?: boolean;
18
+ /** What it reported before it was interrupted. */
19
+ partial?: unknown[];
20
+ }
21
+ /** One effect as the session's records hold it: its intent, then at most one outcome. */
22
+ export interface EffectRecord {
23
+ key: string;
24
+ stepId: string;
25
+ name: string;
26
+ replay: ReplayPolicy;
27
+ input: unknown;
28
+ status: EffectStatus;
29
+ output?: unknown;
30
+ error?: EffectError;
31
+ /** 0 for the first run; one more each time a `safe` effect a crash cut off is run again. */
32
+ attempt: number;
33
+ startedAt?: Date;
34
+ /** Submissions this effect placed: taken out of `pending()` at its intent. */
35
+ placed: string[];
36
+ }
37
+ /** The session as its records fold. */
38
+ export interface SessionState {
39
+ runId: string;
40
+ status: "running";
41
+ submissions: Submission[];
42
+ placed: Set<string>;
43
+ withdrawn: Set<string>;
44
+ /** By key: the first intent for a key wins. */
45
+ effects: Map<string, EffectRecord>;
46
+ /** In the order their intents were written. */
47
+ effectOrder: EffectRecord[];
48
+ }
49
+ export interface EffectOptions {
50
+ name: string;
51
+ replay: ReplayPolicy;
52
+ input: unknown;
53
+ placed?: string[];
54
+ /** Where the effect must run and what it needs; recorded with the intent, not acted on by this kernel. */
55
+ requires?: string;
56
+ needs?: string[];
57
+ }
58
+ export interface EffectIO {
59
+ signal: AbortSignal;
60
+ progress(chunk: unknown): void;
61
+ stepId: string;
62
+ }
63
+ /** A blob store for records too large to keep inline: pieces of text, content-addressed, kept in the session's own
64
+ * records, written through its writer. */
65
+ export interface KernelWorld {
66
+ readonly store: SessionWriter;
67
+ }
68
+ /** What a copied harness drive gets: its session's state, effects that run at most once (or again, when `safe`, after a
69
+ * crash), its pending input, and its signal. One drive holds a session at a time: its writer is fenced once a newer
70
+ * drive opens, and the stale drive's next write is a `Restart`. */
71
+ export interface DriveContext {
72
+ readonly runId: string;
73
+ readonly world: KernelWorld;
74
+ readonly config: unknown;
75
+ state(): SessionState;
76
+ effect<T>(key: string, options: EffectOptions, fn: (io: EffectIO) => Promise<T>): Promise<T>;
77
+ isMemoized(key: string): boolean;
78
+ pending(): Submission[];
79
+ /** Re-folds this drive's records; it reads nothing. This drive is the session's only writer (a newer `open` fences
80
+ * it), and door input is already folded by `submit`, which also wakes `onHint`. A fenced drive learns it on its
81
+ * next append, not here. */
82
+ refresh(): Promise<void>;
83
+ live(event: unknown): void;
84
+ /** Called when new input may be waiting (a submission was recorded through this drive); returns the unsubscribe. */
85
+ onHint?(listener: (hint: {
86
+ eventType: "hook_received" | "reconnect";
87
+ }) => void): () => void;
88
+ readonly signal: AbortSignal;
89
+ halt?(reason: unknown): void;
90
+ /** How many times this session's drive has been rebuilt in this process (0 first). */
91
+ readonly attempt: number;
92
+ /** When this drive must have let go, on Date.now()'s clock, if it has a deadline. */
93
+ readonly deadline?: number;
94
+ }
95
+ /** A tool the host runs for the harness, as the copied tool bridge calls it. */
96
+ export interface HostTool<Input = any> {
97
+ name: string;
98
+ label?: string;
99
+ description: string;
100
+ inputSchema: object;
101
+ replay: ReplayPolicy;
102
+ /** Where the tool must run, and what it needs: passed to the effect's intent. */
103
+ requires?: string;
104
+ needs?: string[];
105
+ /** Its answer: the text, or the text with the images it shows. Recorded as the call's outcome, so a call answered
106
+ * from the record answers with the same images. */
107
+ execute(input: Input, io: HostToolIO): Promise<string | ToolOutput>;
108
+ }
109
+ /** A tool's answer with images. */
110
+ export interface ToolOutput {
111
+ text: string;
112
+ images?: readonly ToolImage[];
113
+ }
114
+ export interface HostToolIO {
115
+ signal: AbortSignal;
116
+ progress(chunk: unknown): void;
117
+ ctx: DriveContext;
118
+ stepId?: string;
119
+ }
120
+ export type { JsonValue };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,142 @@
1
+ import { type DriveContext, type HostToolIO, type ReplayPolicy } from '../kernel/index.js';
2
+ import { type ToolResult } from './tools.js';
3
+ /**
4
+ * A harness's own (built-in) tools, in the log.
5
+ *
6
+ * With its built-in tools on, a harness runs tools the drive never sees run:
7
+ * Codex's shell and `apply_patch` run inside app-server. The drive sees
8
+ * them through the harness's own hooks: one before a call runs (a Codex
9
+ * approval request), one after it (`item/completed`). `BuiltinCalls` makes that pair the kernel's effect
10
+ * sandwich for the call, keyed by the harness's own call id
11
+ * (`tool:<callId>`, step name `tool.<name>`, like a bridged tool):
12
+ *
13
+ * - `begin` commits the call's intent and resolves once it is committed
14
+ * (for a `never` call: durable, after the harness's own record of the call
15
+ * is in the log, `beforeNever`). Only then does the hook let the call run.
16
+ * - `end` commits its outcome: the result the harness reports (clipped: the
17
+ * full result is in the harness's own transcript, which the log carries).
18
+ *
19
+ * What that buys, and what it does not:
20
+ *
21
+ * - Every built-in call that ran has an intent in the log first. After a
22
+ * crash, the recovery note lists each call of the turn with what the log
23
+ * knows: completed (its result), failed, or interrupted, "may or may not
24
+ * have taken effect" (`never`) / "safe to repeat" (`safe`).
25
+ * - Nothing reruns a built-in call by itself. The harness resumes its own
26
+ * session, which answers a call that was in flight "interrupted, outcome
27
+ * unknown" (Codex gets it injected).
28
+ * A harness that dispatches the same call id again gets the log's answer
29
+ * (`begin` returns it) and must not run it. For a built-in, `safe` only
30
+ * changes the note's wording: the model decides what to repeat.
31
+ * - What a call did is not in the log. The working directory is a disk on
32
+ * the machine that drives the session (see `LocalWorkspace`), and a
33
+ * process a tool started does not outlive the drive (`ProcessHost`'s
34
+ * group registry kills it).
35
+ */
36
+ export interface BuiltinCall {
37
+ /** The harness's own id for the call (stable across a resume of its session: it is the memo key). */
38
+ callId: string;
39
+ /** The harness's tool name (`Bash`, `shell`, `edit`, ...). */
40
+ name: string;
41
+ input: unknown;
42
+ /** Default: `BuiltinCallsOptions.replay(name)`. */
43
+ replay?: ReplayPolicy;
44
+ }
45
+ export interface BuiltinCallsOptions {
46
+ /** A built-in's replay policy, by name. Default `never`. */
47
+ replay?: (name: string) => ReplayPolicy;
48
+ /**
49
+ * Runs before the intent of a `never` call: the native journal's barrier,
50
+ * so the harness's own record of the call is in the log before its intent.
51
+ */
52
+ beforeNever?: (call: BuiltinCall) => Promise<void>;
53
+ /** Publish `tool_start` / `tool_end` on `ctx.live`. Default true. */
54
+ live?: boolean;
55
+ /** How much of a result the step records. Default 8000 characters. */
56
+ outputChars?: number;
57
+ /**
58
+ * Run each call's effect body inside this wrapper: `run` lets the call go
59
+ * (`begin` resolves once the wrapper calls it) and resolves with the
60
+ * result the harness reports (`end`), or rejects with its failure. What
61
+ * the wrapper returns is the call's recorded outcome, and what it throws
62
+ * its failure. For a harness whose built-ins work on files the drive
63
+ * keeps: sync the files in before `run`, and commit what the call changed
64
+ * with its outcome. A wrapper that throws before
65
+ * calling `run` fails the call: `begin` answers with that failure, and the
66
+ * call never runs. One that returns without calling `run` decides the
67
+ * outcome itself: `begin` answers with it, and the call never runs either
68
+ * (nothing on the live stream). One that throws after `run` fails the call
69
+ * although the harness ran it (the step records the failure, `end`
70
+ * resolves). Calling `run` again returns the first call's promise. Default:
71
+ * none (the call runs as soon as its intent is committed).
72
+ */
73
+ around?: (call: BuiltinCall, run: () => Promise<string>, io: HostToolIO) => Promise<string>;
74
+ }
75
+ /** What `begin` says the hook should do. */
76
+ export type BuiltinStart =
77
+ /** The intent is committed: let the call run. */
78
+ {
79
+ run: true;
80
+ }
81
+ /** The log already has an outcome for this call id: do not run it, answer with this. */
82
+ | {
83
+ run: false;
84
+ result: ToolResult;
85
+ };
86
+ /**
87
+ * The built-in calls of one drive. Build one per `drive(ctx)`.
88
+ *
89
+ * A call the drive leaves open (the harness's process died mid-call, the
90
+ * drive stopped) is dealt with by `close`: interrupted if the drive is
91
+ * still live, else left to the kernel (a stopped drive's intent is released
92
+ * or recovered as interrupted by the next delivery).
93
+ */
94
+ export declare class BuiltinCalls {
95
+ private readonly ctx;
96
+ private readonly options;
97
+ private readonly open;
98
+ /** Each call id's first `begin`, reserved before it awaits anything. */
99
+ private readonly begun;
100
+ private readonly replay;
101
+ private readonly live;
102
+ private readonly outputChars;
103
+ constructor(ctx: DriveContext, options?: BuiltinCallsOptions);
104
+ /** Calls begun and not ended. */
105
+ get size(): number;
106
+ /** Whether this call is open in this drive. */
107
+ has(callId: string): boolean;
108
+ /**
109
+ * The harness is about to run `call`: commit its intent. Resolves once the
110
+ * intent is committed (`{ run: true }`), or at once with the log's answer
111
+ * when the call id already has an outcome (`{ run: false }`). Rejects if
112
+ * the intent cannot be committed (the drive lost the session, a write
113
+ * failed): the drive is halted, and the hook must refuse the call.
114
+ */
115
+ begin(call: BuiltinCall): Promise<BuiltinStart>;
116
+ /** The same call begun again: never run it twice. Wait for the first begin, then for its outcome, from the log. */
117
+ private again;
118
+ private beginOnce;
119
+ /**
120
+ * The call ended: commit its outcome. `isError` (the tool reported a
121
+ * failure) commits a failed step with `text` as its error. Resolves once
122
+ * the outcome is committed (or the drive stopped first). A call this
123
+ * drive never began (a hook that fired without its `begin`) is begun and
124
+ * ended here: recorded after the fact.
125
+ */
126
+ end(callId: string, result: {
127
+ name?: string;
128
+ input?: unknown;
129
+ text: string;
130
+ isError?: boolean;
131
+ }): Promise<void>;
132
+ /**
133
+ * The harness can no longer report these calls (its process exited mid-call):
134
+ * commit each open call as interrupted, "may or may not have taken effect".
135
+ * A no-op once the drive stopped (the kernel resolves its intents).
136
+ */
137
+ interruptAll(reason?: string): Promise<void>;
138
+ /** Wait for every open call to settle, or `ms` to pass. */
139
+ settled(ms: number): Promise<void>;
140
+ }
141
+ /** A tool name list as a replay classifier: these are `safe`, the rest `never`. */
142
+ export declare function replayBy(safe: Iterable<string>): (name: string) => ReplayPolicy;
@@ -0,0 +1,207 @@
1
+ // Copyright 2026 Vercel Inc. Licensed under the Apache License, Version 2.0; see LICENSE in this package.
2
+ // From github.com/vercel-labs/durable-harness, packages/harness-process/src/builtins.ts at ea8aa4060c (UPSTREAM.json records its blob and sha256).
3
+ // Changed by Parcha, imports rewritten for AgentRun; patches: 001-begin-reservation, 002-message-prefix, 003-comments-name-this-host
4
+ import { EffectFailure, EffectInterrupted, Fatal, hostToolIO } from '../kernel/index.js';
5
+ import { clip, failureText, toolKey } from './tools.js';
6
+ /**
7
+ * The built-in calls of one drive. Build one per `drive(ctx)`.
8
+ *
9
+ * A call the drive leaves open (the harness's process died mid-call, the
10
+ * drive stopped) is dealt with by `close`: interrupted if the drive is
11
+ * still live, else left to the kernel (a stopped drive's intent is released
12
+ * or recovered as interrupted by the next delivery).
13
+ */
14
+ export class BuiltinCalls {
15
+ ctx;
16
+ options;
17
+ open = new Map();
18
+ /** Each call id's first `begin`, reserved before it awaits anything. */
19
+ begun = new Map();
20
+ replay;
21
+ live;
22
+ outputChars;
23
+ constructor(ctx, options = {}) {
24
+ this.ctx = ctx;
25
+ this.options = options;
26
+ this.replay = options.replay ?? (() => 'never');
27
+ this.live = options.live ?? true;
28
+ this.outputChars = options.outputChars ?? 8000;
29
+ }
30
+ /** Calls begun and not ended. */
31
+ get size() {
32
+ return this.open.size;
33
+ }
34
+ /** Whether this call is open in this drive. */
35
+ has(callId) {
36
+ return this.open.has(callId);
37
+ }
38
+ /**
39
+ * The harness is about to run `call`: commit its intent. Resolves once the
40
+ * intent is committed (`{ run: true }`), or at once with the log's answer
41
+ * when the call id already has an outcome (`{ run: false }`). Rejects if
42
+ * the intent cannot be committed (the drive lost the session, a write
43
+ * failed): the drive is halted, and the hook must refuse the call.
44
+ */
45
+ begin(call) {
46
+ if (!call.callId)
47
+ return Promise.reject(new TypeError(`agentrun-kernel: a built-in ${call.name} call has no call id`));
48
+ const first = this.begun.get(call.callId);
49
+ if (first)
50
+ return this.again(call.callId, first);
51
+ const start = this.beginOnce(call);
52
+ this.begun.set(call.callId, start);
53
+ // A call that will not run is done with now; one that runs is, once its outcome is in (beginOnce's `done`).
54
+ start.then((s) => { if (!s.run)
55
+ this.begun.delete(call.callId); }, () => this.begun.delete(call.callId));
56
+ return start;
57
+ }
58
+ /** The same call begun again: never run it twice. Wait for the first begin, then for its outcome, from the log. */
59
+ async again(callId, first) {
60
+ const start = await first;
61
+ if (!start.run)
62
+ return start;
63
+ await this.open.get(callId)?.done;
64
+ // The first call's outcome could not be written: the drive stopped, and this caller stops with it.
65
+ if (this.ctx.signal.aborted)
66
+ throw this.ctx.signal.reason;
67
+ const known = this.ctx.state().effects.get(toolKey(callId));
68
+ if (known?.status === 'completed')
69
+ return { run: false, result: { text: String(known.output ?? ''), isError: false } };
70
+ if (known?.status === 'failed' && known.error)
71
+ return { run: false, result: { text: failureText(known.error), isError: true, ...(known.error.interrupted ? { interrupted: true } : {}) } };
72
+ throw new Fatal(`agentrun-kernel: built-in call ${callId} has no recorded outcome`);
73
+ }
74
+ async beginOnce(call) {
75
+ const key = toolKey(call.callId);
76
+ const known = this.ctx.state().effects.get(key);
77
+ if (known?.status === 'completed')
78
+ return { run: false, result: { text: String(known.output ?? ''), isError: false } };
79
+ if (known?.status === 'failed' && known.error)
80
+ return { run: false, result: { text: failureText(known.error), isError: true, ...(known.error.interrupted ? { interrupted: true } : {}) } };
81
+ const replay = call.replay ?? this.replay(call.name);
82
+ try {
83
+ if (replay === 'never')
84
+ await this.options.beforeNever?.(call);
85
+ }
86
+ catch (err) {
87
+ this.ctx.halt?.(err);
88
+ throw err;
89
+ }
90
+ let started;
91
+ const committed = new Promise((r) => (started = r));
92
+ let settle;
93
+ const outcome = new Promise((r) => (settle = r));
94
+ let failed;
95
+ let ran = false;
96
+ const around = this.options.around;
97
+ const effect = this.ctx.effect(key, { name: `tool.${call.name}`, replay, input: call.input }, async (io) => {
98
+ let running;
99
+ // The call itself, once: a wrapper that calls `run` again gets the same promise.
100
+ const body = () => (running ??= (async () => {
101
+ ran = true;
102
+ started();
103
+ if (this.live)
104
+ this.ctx.live({ type: 'tool_start', name: call.name, args: call.input, callId: call.callId });
105
+ const result = await new Promise((resolve, reject) => {
106
+ const stop = () => reject(io.signal.reason ?? new Error('the drive stopped'));
107
+ if (io.signal.aborted)
108
+ return stop();
109
+ io.signal.addEventListener('abort', stop, { once: true });
110
+ void outcome.then((o) => {
111
+ io.signal.removeEventListener('abort', stop);
112
+ resolve(o);
113
+ });
114
+ });
115
+ if (!result.ok)
116
+ throw result.error;
117
+ return result.text;
118
+ })());
119
+ return around ? around(call, body, hostToolIO(io, this.ctx)) : body();
120
+ });
121
+ const done = effect.then(() => {
122
+ // A call its wrapper settled without letting it run never started, on the live stream either.
123
+ if (this.live && (ran || !around))
124
+ this.ctx.live({ type: 'tool_end', name: call.name, callId: call.callId });
125
+ }, (err) => {
126
+ if (err instanceof EffectFailure) {
127
+ // Nor one its wrapper failed before it ran.
128
+ if (this.live && (ran || !around))
129
+ this.ctx.live({ type: 'tool_end', name: call.name, callId: call.callId, isError: true });
130
+ return;
131
+ }
132
+ // Not this call's outcome: the drive stopped, or a write failed. Stop the drive.
133
+ failed = err;
134
+ this.ctx.halt?.(err);
135
+ });
136
+ this.open.set(call.callId, { settle, done });
137
+ void done.finally(() => { this.open.delete(call.callId); this.begun.delete(call.callId); });
138
+ await Promise.race([committed, done]);
139
+ if (failed !== undefined)
140
+ throw failed;
141
+ // Settled before its intent (replayed from the log by a concurrent read), or by a wrapper that never let it run: answer from the log.
142
+ const after = this.ctx.state().effects.get(key);
143
+ if (!this.open.has(call.callId) && after?.status === 'completed')
144
+ return { run: false, result: { text: String(after.output ?? ''), isError: false } };
145
+ // The wrapper failed the call before letting it run: answer with that failure.
146
+ if (around && !ran) {
147
+ const error = after?.status === 'failed' && after.error ? failureText(after.error) : 'the tool failed before it ran';
148
+ return { run: false, result: { text: error, isError: true } };
149
+ }
150
+ return { run: true };
151
+ }
152
+ /**
153
+ * The call ended: commit its outcome. `isError` (the tool reported a
154
+ * failure) commits a failed step with `text` as its error. Resolves once
155
+ * the outcome is committed (or the drive stopped first). A call this
156
+ * drive never began (a hook that fired without its `begin`) is begun and
157
+ * ended here: recorded after the fact.
158
+ */
159
+ async end(callId, result) {
160
+ let open = this.open.get(callId);
161
+ // Its begin is still committing the intent: end that call, not a second one.
162
+ if (!open && this.begun.has(callId)) {
163
+ await this.begun.get(callId).catch(() => undefined);
164
+ open = this.open.get(callId);
165
+ }
166
+ if (!open) {
167
+ if (this.ctx.isMemoized(toolKey(callId)))
168
+ return;
169
+ const started = await this.begin({ callId, name: result.name ?? 'builtin', input: result.input ?? null, replay: 'safe' }).catch(() => undefined);
170
+ if (!started?.run)
171
+ return;
172
+ open = this.open.get(callId);
173
+ if (!open)
174
+ return;
175
+ }
176
+ const text = clip(result.text, this.outputChars);
177
+ open.settle(result.isError ? { ok: false, error: new Error(text || 'the tool failed') } : { ok: true, text });
178
+ await open.done;
179
+ }
180
+ /**
181
+ * The harness can no longer report these calls (its process exited mid-call):
182
+ * commit each open call as interrupted, "may or may not have taken effect".
183
+ * A no-op once the drive stopped (the kernel resolves its intents).
184
+ */
185
+ async interruptAll(reason = 'the harness stopped before this call reported its result: it may or may not have taken effect') {
186
+ const open = [...this.open.values()];
187
+ if (this.ctx.signal.aborted)
188
+ return;
189
+ for (const o of open)
190
+ o.settle({ ok: false, error: new EffectInterrupted(reason) });
191
+ await Promise.all(open.map((o) => o.done));
192
+ }
193
+ /** Wait for every open call to settle, or `ms` to pass. */
194
+ async settled(ms) {
195
+ const open = [...this.open.values()].map((o) => o.done);
196
+ if (!open.length)
197
+ return;
198
+ let timer;
199
+ await Promise.race([Promise.all(open), new Promise((r) => (timer = setTimeout(r, ms)))]);
200
+ clearTimeout(timer);
201
+ }
202
+ }
203
+ /** A tool name list as a replay classifier: these are `safe`, the rest `never`. */
204
+ export function replayBy(safe) {
205
+ const set = new Set(safe);
206
+ return (name) => (set.has(name) ? 'safe' : 'never');
207
+ }
@@ -0,0 +1,29 @@
1
+ import type { DriveContext, Submission } from '../kernel/index.js';
2
+ /**
3
+ * Place submissions before the harness's runtime sees them: one record-only
4
+ * effect `place:<first id>` whose `placed` names them. Placement is what
5
+ * takes a submission off `ctx.pending()` and what a withdraw races (a
6
+ * withdraw committed first wins, and this throws `Restart`: the drive
7
+ * rebuilds without the withdrawn submission). Sending only after placing
8
+ * means the kernel's withdraw rule is exact, and a crash between the two
9
+ * re-sends instead of losing the message.
10
+ *
11
+ * The demo's transcript shows a placed submission where its placement is in
12
+ * the log, so a harness that places right before sending gets its prompts
13
+ * in the right place for free.
14
+ */
15
+ export declare function placeSubmissions(ctx: DriveContext, submissions: Submission[], name?: string): Promise<void>;
16
+ export interface WaitOptions {
17
+ /** How often to read the inbox when the kernel offers no hints. Default 500 ms (5 s with hints). */
18
+ pollMs?: number;
19
+ /** Stop waiting (resolves `[]`). `ctx.signal` always stops it too. */
20
+ signal?: AbortSignal;
21
+ }
22
+ /**
23
+ * The submissions waiting to be placed, as soon as there are any: refreshes
24
+ * the log on every hint (`ctx.onHint`) and on a poll. Resolves `[]` when
25
+ * `ctx.signal` or `options.signal` aborts. A drive that has nothing left to
26
+ * do should return instead of waiting; this is for a harness that keeps its
27
+ * runtime up while a turn runs (steers) or for a short idle grace.
28
+ */
29
+ export declare function waitForSubmissions(ctx: DriveContext, options?: WaitOptions): Promise<Submission[]>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Place submissions before the harness's runtime sees them: one record-only
3
+ * effect `place:<first id>` whose `placed` names them. Placement is what
4
+ * takes a submission off `ctx.pending()` and what a withdraw races (a
5
+ * withdraw committed first wins, and this throws `Restart`: the drive
6
+ * rebuilds without the withdrawn submission). Sending only after placing
7
+ * means the kernel's withdraw rule is exact, and a crash between the two
8
+ * re-sends instead of losing the message.
9
+ *
10
+ * The demo's transcript shows a placed submission where its placement is in
11
+ * the log, so a harness that places right before sending gets its prompts
12
+ * in the right place for free.
13
+ */
14
+ export async function placeSubmissions(ctx, submissions, name = 'place') {
15
+ if (submissions.length === 0)
16
+ return;
17
+ const ids = submissions.map((s) => s.id);
18
+ await ctx.effect(`place:${ids[0]}`, { name, replay: 'safe', input: { ids }, placed: ids }, async () => true);
19
+ }
20
+ /**
21
+ * The submissions waiting to be placed, as soon as there are any: refreshes
22
+ * the log on every hint (`ctx.onHint`) and on a poll. Resolves `[]` when
23
+ * `ctx.signal` or `options.signal` aborts. A drive that has nothing left to
24
+ * do should return instead of waiting; this is for a harness that keeps its
25
+ * runtime up while a turn runs (steers) or for a short idle grace.
26
+ */
27
+ export async function waitForSubmissions(ctx, options = {}) {
28
+ const signals = [ctx.signal, options.signal].filter((s) => !!s);
29
+ const stop = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
30
+ const pollMs = options.pollMs ?? (ctx.onHint ? 5000 : 500);
31
+ for (;;) {
32
+ if (stop.aborted)
33
+ return [];
34
+ const pending = ctx.pending();
35
+ if (pending.length > 0)
36
+ return pending;
37
+ await new Promise((resolve) => {
38
+ const timer = setTimeout(done, pollMs);
39
+ const off = ctx.onHint?.(() => done());
40
+ stop.addEventListener('abort', done, { once: true });
41
+ function done() {
42
+ clearTimeout(timer);
43
+ off?.();
44
+ stop.removeEventListener('abort', done);
45
+ resolve();
46
+ }
47
+ });
48
+ if (stop.aborted)
49
+ return [];
50
+ await ctx.refresh();
51
+ }
52
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./records.js";
2
+ export * from "./recovery.js";
3
+ export * from "./journal.js";
4
+ export * from "./tools.js";
5
+ export * from "./builtins.js";
6
+ export * from "./inbox.js";
7
+ export * from "./tail.js";
8
+ export * from "./zod.js";
9
+ export * from "./settle.js";
@@ -0,0 +1,11 @@
1
+ // The shared layer for harnesses that run as a child process, copied from another project (each file's header
2
+ // says from where, and UPSTREAM.json records it), on AgentRun's kernel (../kernel).
3
+ export * from "./records.js";
4
+ export * from "./recovery.js";
5
+ export * from "./journal.js";
6
+ export * from "./tools.js";
7
+ export * from "./builtins.js";
8
+ export * from "./inbox.js";
9
+ export * from "./tail.js";
10
+ export * from "./zod.js";
11
+ export * from "./settle.js";