@parcha/agentrun-kernel 0.0.0-bootstrap.0 → 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 +53 -0
  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 +76 -1
  82. package/vendor/durable-harness/UPSTREAM.json +67 -0
@@ -0,0 +1,76 @@
1
+ import { type ReplayPolicy, type SessionState, type Submission } from '../kernel/index.js';
2
+ import { type OpenTurn, openTurn } from './records.js';
3
+ export { type OpenTurn, openTurn };
4
+ /**
5
+ * The recovery note: what a process harness tells its model when it resumes
6
+ * a turn a crash cut off. A CLI harness resumes its own session natively,
7
+ * but it never knows what happened to the calls that were in flight (Codex
8
+ * needs an output injected for a dangling call), and a resumed session needs a new
9
+ * user message to move. The note is that message, built from the log: the
10
+ * calls since the turn began, each with what the log knows about it.
11
+ */
12
+ export interface RecoveryCall {
13
+ callId: string;
14
+ name: string;
15
+ input: unknown;
16
+ replay: ReplayPolicy;
17
+ /**
18
+ * - `completed`: the result is recorded (`output`);
19
+ * - `failed`: the tool failed and said so (`error`);
20
+ * - `interrupted`: a `replay: 'never'` call whose outcome was lost: it may
21
+ * have taken effect;
22
+ * - `retryable`: a `replay: 'safe'` call that was interrupted: safe to repeat;
23
+ * - `running`: still open in the log (build the note after the kernel's
24
+ * recovery has resolved intents; a drive's own context always has).
25
+ */
26
+ status: 'completed' | 'failed' | 'interrupted' | 'retryable' | 'running';
27
+ output?: string;
28
+ error?: string;
29
+ }
30
+ export interface RecoveryOptions {
31
+ /** Pending submissions to fold into the note. Default: every pending one. Pass `[]` to place them separately. */
32
+ pending?: Submission[];
33
+ /**
34
+ * Quote the prompts placed in the interrupted turn. For a harness whose
35
+ * native session may have lost them (the crash came before the record of
36
+ * the message was in the log).
37
+ */
38
+ prompts?: boolean;
39
+ /** How much of a tool's output, input or error to quote. Default 1500 characters. */
40
+ clipChars?: number;
41
+ }
42
+ export interface RecoveryNote {
43
+ /** The message to send the harness, as a user message. */
44
+ text: string;
45
+ calls: RecoveryCall[];
46
+ /** The pending submissions the note carries: place these with it (`placeSubmissions`). */
47
+ pending: Submission[];
48
+ turn: OpenTurn;
49
+ }
50
+ /** The note for the open turn, or `undefined` if no turn is open. */
51
+ export declare function recoveryNote(state: SessionState, options?: RecoveryOptions): RecoveryNote | undefined;
52
+ /**
53
+ * The note as a harness carries it outside the user's turn (a system prompt, a developer item): from the harness, about the
54
+ * turn before, in the harness's own words only. It names each call and says how it ended, and never quotes what a tool was
55
+ * given or answered with (that is data the agent read, not something the harness may speak for), a prompt, or a pending message.
56
+ * `undefined` when no turn is open.
57
+ */
58
+ export declare function recoverySystemNote(state: SessionState): string | undefined;
59
+ /** The user's turn that resumes a cut-off turn: only the user's words (the prompts the session never got, then the messages sent since), or a bare nudge. */
60
+ export declare function recoveryUserTurn(lost: string[], pending: string[]): string;
61
+ /**
62
+ * The live event a harness sends as it resumes a turn a crash cut off,
63
+ * before the model answers again: the model starts the step the crash
64
+ * interrupted over, so what a reader saw of its reply is replaced.
65
+ * `text` is the turn's text as the log kept it (the assistant text of its
66
+ * finished steps, and the text before each tool call); a reader shows it in
67
+ * place of what it showed of the turn so far (everything since the last
68
+ * `idle`), and appends what streams after it. `parts` is the same text, one
69
+ * entry per kept step, for a reader that shows tool calls between them: a
70
+ * turn on screen with more text than `parts` has a cut-off reply last.
71
+ */
72
+ export declare function restartEvent(turn: OpenTurn): {
73
+ type: 'restart';
74
+ text: string;
75
+ parts: string[];
76
+ };
@@ -0,0 +1,139 @@
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/recovery.ts at ea8aa4060c (UPSTREAM.json records its blob and sha256).
3
+ // Changed by Parcha, imports rewritten for AgentRun; patches: 003-comments-name-this-host, 009-recovery-system-note, 010-recovery-note-statuses-only
4
+ import { pendingSubmissions } from '../kernel/index.js';
5
+ import { openTurn } from './records.js';
6
+ export { openTurn };
7
+ /** The note for the open turn, or `undefined` if no turn is open. */
8
+ export function recoveryNote(state, options = {}) {
9
+ const turn = openTurn(state);
10
+ if (!turn)
11
+ return undefined;
12
+ const max = options.clipChars ?? 1500;
13
+ const calls = turn.calls.map((e) => recoveryCall(e, max));
14
+ const pending = options.pending ?? pendingSubmissions(state);
15
+ const lines = [
16
+ '[Recovery] Your previous turn was interrupted: the process running it stopped, and this session was resumed from its durable log.',
17
+ ];
18
+ if (options.prompts) {
19
+ const texts = turn.placed
20
+ .map((id) => state.submissions.find((s) => s.id === id)?.text)
21
+ .filter((t) => !!t);
22
+ if (texts.length)
23
+ lines.push('', 'The user\'s message in that turn:', ...texts.map((t) => quote(clip(t, max))));
24
+ }
25
+ lines.push('');
26
+ if (calls.length === 0)
27
+ lines.push('No tool call had started in that turn.');
28
+ else {
29
+ lines.push('What the log records about the tool calls since that turn began:');
30
+ for (const c of calls)
31
+ lines.push(...describe(c));
32
+ }
33
+ lines.push('', 'Check the actual state before repeating anything that may have taken effect, then carry on with the task.');
34
+ if (pending.length) {
35
+ lines.push('', 'New messages since then:');
36
+ for (const s of pending)
37
+ lines.push(quote(s.text ?? ''));
38
+ }
39
+ return { text: lines.join('\n'), calls, pending, turn };
40
+ }
41
+ /**
42
+ * The note as a harness carries it outside the user's turn (a system prompt, a developer item): from the harness, about the
43
+ * turn before, in the harness's own words only. It names each call and says how it ended, and never quotes what a tool was
44
+ * given or answered with (that is data the agent read, not something the harness may speak for), a prompt, or a pending message.
45
+ * `undefined` when no turn is open.
46
+ */
47
+ export function recoverySystemNote(state) {
48
+ const turn = openTurn(state);
49
+ if (!turn)
50
+ return undefined;
51
+ const calls = turn.calls.map((e) => recoveryCall(e, 0));
52
+ const lines = [
53
+ 'The harness, not the user, adds this about the turn before this one:',
54
+ '[Recovery] Your previous turn was interrupted: the process running it stopped, and this session was resumed from its durable log.',
55
+ '',
56
+ ];
57
+ if (calls.length === 0)
58
+ lines.push('No tool call had started in that turn.');
59
+ else {
60
+ lines.push('What the log records about the tool calls since that turn began (none was run again):');
61
+ for (const c of calls)
62
+ lines.push(`- ${c.name}, call ${c.callId}: ${ended(c)}`);
63
+ lines.push('', 'The output of those calls is not shown here. Check the actual state before relying on it.');
64
+ }
65
+ lines.push('', 'Check the actual state before repeating anything that may have taken effect, then carry on with the task.');
66
+ return lines.join('\n');
67
+ }
68
+ function ended(c) {
69
+ switch (c.status) {
70
+ case 'completed':
71
+ return 'completed.';
72
+ case 'failed':
73
+ return 'failed.';
74
+ case 'interrupted':
75
+ return 'interrupted before it reported a result. It is not safe to repeat, and it may have taken effect.';
76
+ case 'retryable':
77
+ return 'interrupted before it reported a result. It is safe to repeat.';
78
+ case 'running':
79
+ return 'interrupted, and it may still be running. It may have taken effect.';
80
+ }
81
+ }
82
+ /** The user's turn that resumes a cut-off turn: only the user's words (the prompts the session never got, then the messages sent since), or a bare nudge. */
83
+ export function recoveryUserTurn(lost, pending) {
84
+ const words = [...lost, ...pending].filter((t) => t !== '');
85
+ return words.length ? words.join('\n\n') : 'Continue.';
86
+ }
87
+ function recoveryCall(e, max) {
88
+ const base = {
89
+ callId: e.key.startsWith('tool:') ? e.key.slice(5) : e.key,
90
+ name: e.name.slice('tool.'.length),
91
+ input: e.input,
92
+ replay: e.replay,
93
+ };
94
+ if (e.status === 'completed')
95
+ return { ...base, status: 'completed', output: clip(text(e.output), max) };
96
+ if (e.status === 'pending')
97
+ return { ...base, status: 'retryable' };
98
+ if (e.status === 'running')
99
+ return { ...base, status: 'running' };
100
+ if (e.error?.interrupted)
101
+ return { ...base, status: e.replay === 'safe' ? 'retryable' : 'interrupted' };
102
+ return { ...base, status: 'failed', error: clip(e.error?.message ?? 'failed', max) };
103
+ }
104
+ function describe(c) {
105
+ const head = `- ${c.name}(${clip(text(c.input), 300)}), call ${c.callId}:`;
106
+ switch (c.status) {
107
+ case 'completed':
108
+ return [`${head} completed. Its result:`, ...indent(c.output ?? '')];
109
+ case 'failed':
110
+ return [`${head} failed: ${c.error}`];
111
+ case 'interrupted':
112
+ return [`${head} interrupted before it reported a result. It is not safe to repeat, and it may have taken effect: check before repeating it.`];
113
+ case 'retryable':
114
+ return [`${head} interrupted before it reported a result. It is safe to repeat.`];
115
+ case 'running':
116
+ return [`${head} interrupted, and it may still be running. It may have taken effect: check before repeating it.`];
117
+ }
118
+ }
119
+ const text = (v) => (typeof v === 'string' ? v : JSON.stringify(v) ?? String(v));
120
+ const quote = (t) => t.split('\n').map((l) => `> ${l}`).join('\n');
121
+ const indent = (t) => t.split('\n').map((l) => ` ${l}`);
122
+ function clip(t, max) {
123
+ return t.length <= max ? t : `${t.slice(0, max)}… [${t.length - max} more characters]`;
124
+ }
125
+ /**
126
+ * The live event a harness sends as it resumes a turn a crash cut off,
127
+ * before the model answers again: the model starts the step the crash
128
+ * interrupted over, so what a reader saw of its reply is replaced.
129
+ * `text` is the turn's text as the log kept it (the assistant text of its
130
+ * finished steps, and the text before each tool call); a reader shows it in
131
+ * place of what it showed of the turn so far (everything since the last
132
+ * `idle`), and appends what streams after it. `parts` is the same text, one
133
+ * entry per kept step, for a reader that shows tool calls between them: a
134
+ * turn on screen with more text than `parts` has a cut-off reply last.
135
+ */
136
+ export function restartEvent(turn) {
137
+ const parts = turn.entries.flatMap((r) => (r.data?.type === 'message' && r.data.role === 'assistant' && r.data.text ? [r.data.text] : []));
138
+ return { type: 'restart', text: parts.join('\n\n'), parts };
139
+ }
@@ -0,0 +1,11 @@
1
+ import { type Drive } from "../kernel/index.js";
2
+ /**
3
+ * Records the session's open turn as ended in its transcript, the record a turn that ends writes (so `openTurn` finds
4
+ * none, and the next turn carries no recovery note for it), and runs nothing. Input the turn never placed (a stop that
5
+ * landed before the harness took its prompt) is withdrawn first, as the kernel's follow-up settle does, so no later open
6
+ * finds it pending, and the next message is sent alone. It is the write behind
7
+ * `AdapterSession.settleOpenTurn()` for a turn the person stopped, which is never resumed. The adapter checks that a
8
+ * turn is open. The end is the kernel's for the session's own stop (`abortedOutcome` of its `SessionHalt`), the one
9
+ * path an interrupted turn's end takes, though the turn never ran in this process.
10
+ */
11
+ export declare function settleTurn(drive: Drive): Promise<void>;
@@ -0,0 +1,20 @@
1
+ // AgentRun's own (not copied): how a process harness settles a turn its session left open.
2
+ import { abortedOutcome, pendingSubmissions, SessionHalt } from "../kernel/index.js";
3
+ import { NativeJournal } from "./journal.js";
4
+ /**
5
+ * Records the session's open turn as ended in its transcript, the record a turn that ends writes (so `openTurn` finds
6
+ * none, and the next turn carries no recovery note for it), and runs nothing. Input the turn never placed (a stop that
7
+ * landed before the harness took its prompt) is withdrawn first, as the kernel's follow-up settle does, so no later open
8
+ * finds it pending, and the next message is sent alone. It is the write behind
9
+ * `AdapterSession.settleOpenTurn()` for a turn the person stopped, which is never resumed. The adapter checks that a
10
+ * turn is open. The end is the kernel's for the session's own stop (`abortedOutcome` of its `SessionHalt`), the one
11
+ * path an interrupted turn's end takes, though the turn never ran in this process.
12
+ */
13
+ export async function settleTurn(drive) {
14
+ for (const submission of pendingSubmissions(drive.state()))
15
+ await drive.submit({ id: `withdraw:${submission.id}`, kind: "withdraw", target: submission.id });
16
+ const { status, error } = abortedOutcome(AbortSignal.abort(new SessionHalt("interrupted")));
17
+ const journal = new NativeJournal(drive);
18
+ journal.message({ type: "turn", status, ...(error === undefined ? {} : { error }) });
19
+ await journal.flush();
20
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A line-complete tail of an append-only file (a JSONL transcript, a
3
+ * rollout): `read()` returns the lines completed since the last read and
4
+ * keeps a partial last line for the next one. For mirroring a native
5
+ * session file into the journal as the harness writes it.
6
+ */
7
+ export declare class FileTail {
8
+ readonly path: string;
9
+ /** Byte offset already consumed (the length of what the journal already holds). */
10
+ offset: number;
11
+ constructor(path: string,
12
+ /** Byte offset already consumed (the length of what the journal already holds). */
13
+ offset?: number);
14
+ /**
15
+ * The complete lines appended since the last read. Throws if the file is
16
+ * now shorter than what was read: it was rewritten, not appended to, and
17
+ * the journal can no longer mirror it line by line.
18
+ */
19
+ read(): string[];
20
+ }
@@ -0,0 +1,53 @@
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/tail.ts at ea8aa4060c (UPSTREAM.json records its blob and sha256).
3
+ // Changed by Parcha, patches: 002-message-prefix
4
+ import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
5
+ /**
6
+ * A line-complete tail of an append-only file (a JSONL transcript, a
7
+ * rollout): `read()` returns the lines completed since the last read and
8
+ * keeps a partial last line for the next one. For mirroring a native
9
+ * session file into the journal as the harness writes it.
10
+ */
11
+ export class FileTail {
12
+ path;
13
+ offset;
14
+ constructor(path,
15
+ /** Byte offset already consumed (the length of what the journal already holds). */
16
+ offset = 0) {
17
+ this.path = path;
18
+ this.offset = offset;
19
+ }
20
+ /**
21
+ * The complete lines appended since the last read. Throws if the file is
22
+ * now shorter than what was read: it was rewritten, not appended to, and
23
+ * the journal can no longer mirror it line by line.
24
+ */
25
+ read() {
26
+ let fd;
27
+ try {
28
+ fd = openSync(this.path, 'r');
29
+ }
30
+ catch (err) {
31
+ if (err.code === 'ENOENT')
32
+ return [];
33
+ throw err;
34
+ }
35
+ try {
36
+ const size = fstatSync(fd).size;
37
+ if (size < this.offset)
38
+ throw new Error(`agentrun-kernel: ${this.path} shrank from ${this.offset} to ${size} bytes`);
39
+ if (size === this.offset)
40
+ return [];
41
+ const buf = Buffer.alloc(size - this.offset);
42
+ readSync(fd, buf, 0, buf.length, this.offset);
43
+ const last = buf.lastIndexOf(0x0a);
44
+ if (last < 0)
45
+ return [];
46
+ this.offset += last + 1;
47
+ return buf.subarray(0, last).toString('utf8').split('\n');
48
+ }
49
+ finally {
50
+ closeSync(fd);
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,105 @@
1
+ import { type DriveContext, type EffectError, type HostTool, type ReplayPolicy } from '../kernel/index.js';
2
+ import { type ToolImage } from '../types.js';
3
+ /**
4
+ * The tool bridge: `HostTool`s as a harness-neutral list a port maps onto its
5
+ * harness's own tool mechanism (Codex's dynamic tools).
6
+ *
7
+ * Each call runs the kernel's effect sandwich keyed by the harness's own
8
+ * tool-call id, `tool:<callId>`, step name `tool.<name>`, with the tool's
9
+ * replay policy: a call the log already finished returns its recorded
10
+ * result without running (a resumed harness that re-issues a call gets the
11
+ * same answer), a `replay: 'never'` call interrupted by a crash comes back
12
+ * as an interrupted result, never a second run. With the harness's built-in
13
+ * tools turned off, these are its only tools, so every side effect it can
14
+ * cause has an intent in the log before it runs.
15
+ *
16
+ * Every result a harness gets passes here, so here it is held to the host's
17
+ * bound: a text past HOST_RESULT_MAX_CHARS keeps its start and says where it
18
+ * was cut (`boundResult`), the call's record keeping it whole.
19
+ */
20
+ /** What a call returns to the harness. */
21
+ export interface ToolResult {
22
+ text: string;
23
+ /** The call failed (its recorded error) or was interrupted. */
24
+ isError: boolean;
25
+ /** The call's intent was committed and its outcome lost (see `EffectError.interrupted`). */
26
+ interrupted?: boolean;
27
+ /** Images the tool answered with, as it gave them (recorded with its outcome). */
28
+ images?: readonly ToolImage[];
29
+ /** The text was cut at HOST_RESULT_MAX_CHARS (`boundResult`); it says so in its last line. */
30
+ truncated?: true;
31
+ }
32
+ /** One tool, harness-neutral. */
33
+ export interface BridgedTool {
34
+ name: string;
35
+ description: string;
36
+ /** JSON Schema for the input (`zodShapeFromJsonSchema` turns it into zod for SDKs that want that). */
37
+ inputSchema: Record<string, unknown>;
38
+ replay: ReplayPolicy;
39
+ /**
40
+ * Run the call. `callId` is the harness's id for this call (Codex's
41
+ * call_id); it must be stable across a resume of
42
+ * the harness's session, since it is the memo key. `signal` is the
43
+ * harness's own cancellation, if it has one; the drive's is always applied.
44
+ */
45
+ call(args: unknown, callId: string, signal?: AbortSignal): Promise<ToolResult>;
46
+ }
47
+ export interface ToolBridgeOptions {
48
+ /**
49
+ * Runs before a `replay: 'never'` call that the log has not finished:
50
+ * the place for `journal.flush()`, so the harness's record of the call is
51
+ * in the log before the call's intent is.
52
+ */
53
+ beforeNever?: (call: {
54
+ name: string;
55
+ callId: string;
56
+ args: unknown;
57
+ }) => Promise<void>;
58
+ /** Serialize calls (a per-drive mutex). Default true: concurrent effects in one drive are not exercised yet. */
59
+ serial?: boolean;
60
+ /** Publish `tool_start` / `tool_end` on `ctx.live` for calls that run. Default true. */
61
+ live?: boolean;
62
+ /** How much of a partial output an interrupted result quotes. Default 2000 characters. */
63
+ partialChars?: number;
64
+ }
65
+ export interface ToolBridge {
66
+ tools: BridgedTool[];
67
+ get(name: string): BridgedTool | undefined;
68
+ /** Run a call by tool name. An unknown name is an error result, not a throw. */
69
+ call(name: string, args: unknown, callId: string, signal?: AbortSignal): Promise<ToolResult>;
70
+ }
71
+ /** A promise-chain mutex. */
72
+ export declare class Mutex {
73
+ private tail;
74
+ run<T>(fn: () => Promise<T>): Promise<T>;
75
+ }
76
+ /** The memo key of a tool call. */
77
+ export declare const toolKey: (callId: string) => string;
78
+ /**
79
+ * Build the bridge for one drive. The calls' effects are this drive's: build
80
+ * a new bridge per `drive(ctx)`.
81
+ *
82
+ * A call whose effect throws anything but a committed failure (the drive was
83
+ * preempted, a recovery resolved the call first, the World refused a write)
84
+ * halts the drive (`ctx.halt`) and rethrows: the harness must not be handed
85
+ * a result the log does not have, and its runtime must stop.
86
+ */
87
+ export declare function toolBridge(ctx: DriveContext, tools: HostTool[], options?: ToolBridgeOptions): ToolBridge;
88
+ /**
89
+ * The text a harness gets for a failed call: the error, or for an
90
+ * interrupted one, what the kernel recorded ("may or may not have taken
91
+ * effect") plus the last progress it reported.
92
+ */
93
+ export declare function failureText(error: EffectError, partialChars?: number): string;
94
+ /**
95
+ * `text` within HOST_RESULT_MAX_CHARS characters, as a harness gets a call's
96
+ * result: a longer one keeps its start, cut before a character written as a
97
+ * surrogate pair rather than between its halves, and ends with a line saying
98
+ * so, `[cut at N of M characters]`, counted in the bound. `truncated` when cut.
99
+ */
100
+ export declare function boundResult(text: string): {
101
+ text: string;
102
+ truncated?: true;
103
+ };
104
+ /** At most `max` characters, with a marker when cut. */
105
+ export declare function clip(text: string, max: number): string;
@@ -0,0 +1,120 @@
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/tools.ts at ea8aa4060c (UPSTREAM.json records its blob and sha256).
3
+ // Changed by Parcha, imports rewritten for AgentRun; patches: 002-message-prefix, 003-comments-name-this-host, 008-tool-images, 011-result-bound
4
+ import { EffectFailure, hostToolIO } from '../kernel/index.js';
5
+ import { HOST_RESULT_MAX_CHARS } from '../types.js';
6
+ /** A promise-chain mutex. */
7
+ export class Mutex {
8
+ tail = Promise.resolve();
9
+ run(fn) {
10
+ const run = this.tail.then(fn, fn);
11
+ this.tail = run.catch(() => { });
12
+ return run;
13
+ }
14
+ }
15
+ /** The memo key of a tool call. */
16
+ export const toolKey = (callId) => `tool:${callId}`;
17
+ /**
18
+ * Build the bridge for one drive. The calls' effects are this drive's: build
19
+ * a new bridge per `drive(ctx)`.
20
+ *
21
+ * A call whose effect throws anything but a committed failure (the drive was
22
+ * preempted, a recovery resolved the call first, the World refused a write)
23
+ * halts the drive (`ctx.halt`) and rethrows: the harness must not be handed
24
+ * a result the log does not have, and its runtime must stop.
25
+ */
26
+ export function toolBridge(ctx, tools, options = {}) {
27
+ const mutex = new Mutex();
28
+ const serial = options.serial ?? true;
29
+ const live = options.live ?? true;
30
+ const bridged = tools.map((tool) => {
31
+ const run = async (args, callId, signal) => {
32
+ if (!callId)
33
+ throw new TypeError(`agentrun-kernel: a call to ${tool.name} has no call id; derive a stable one`);
34
+ const key = toolKey(callId);
35
+ const fresh = !ctx.isMemoized(key);
36
+ if (fresh && tool.replay === 'never')
37
+ await options.beforeNever?.({ name: tool.name, callId, args });
38
+ if (fresh && live)
39
+ ctx.live({ type: 'tool_start', name: tool.name, args, callId });
40
+ try {
41
+ const output = await ctx.effect(key, {
42
+ name: `tool.${tool.name}`,
43
+ replay: tool.replay,
44
+ input: args,
45
+ ...(tool.requires ? { requires: tool.requires } : {}),
46
+ ...(tool.needs?.length ? { needs: tool.needs } : {}),
47
+ }, (io) => tool.execute(args, hostToolIO(io, ctx, { signal: signal ? AbortSignal.any([signal, io.signal]) : io.signal })));
48
+ // A tool answers in text, or in text with images (a screenshot): the images come back as recorded.
49
+ const { text: whole, images } = typeof output === 'object' && output !== null ? output : { text: String(output), images: undefined };
50
+ const { text, truncated } = boundResult(String(whole));
51
+ if (fresh && live)
52
+ ctx.live({ type: 'tool_end', name: tool.name, callId, result: text, ...(truncated ? { truncated } : {}) });
53
+ return { text, isError: false, ...(images?.length ? { images } : {}), ...(truncated ? { truncated } : {}) };
54
+ }
55
+ catch (err) {
56
+ if (err instanceof EffectFailure) {
57
+ const { text, truncated } = boundResult(failureText(err.error, options.partialChars));
58
+ if (fresh && live)
59
+ ctx.live({ type: 'tool_end', name: tool.name, callId, isError: true, result: text, ...(truncated ? { truncated } : {}) });
60
+ return { text, isError: true, ...(err.error.interrupted ? { interrupted: true } : {}), ...(truncated ? { truncated } : {}) };
61
+ }
62
+ ctx.halt?.(err);
63
+ throw err;
64
+ }
65
+ };
66
+ return {
67
+ name: tool.name,
68
+ description: tool.description,
69
+ inputSchema: tool.inputSchema,
70
+ replay: tool.replay,
71
+ call: (args, callId, signal) => (serial ? mutex.run(() => run(args, callId, signal)) : run(args, callId, signal)),
72
+ };
73
+ });
74
+ const byName = new Map(bridged.map((t) => [t.name, t]));
75
+ return {
76
+ tools: bridged,
77
+ get: (name) => byName.get(name),
78
+ call: async (name, args, callId, signal) => {
79
+ const tool = byName.get(name);
80
+ if (!tool)
81
+ return { text: `unknown tool: ${name}`, isError: true };
82
+ return tool.call(args, callId, signal);
83
+ },
84
+ };
85
+ }
86
+ /**
87
+ * The text a harness gets for a failed call: the error, or for an
88
+ * interrupted one, what the kernel recorded ("may or may not have taken
89
+ * effect") plus the last progress it reported.
90
+ */
91
+ export function failureText(error, partialChars = 2000) {
92
+ const message = error.message || 'the tool failed with no message';
93
+ if (!error.interrupted)
94
+ return message;
95
+ const last = error.partial?.at(-1);
96
+ if (last === undefined)
97
+ return message;
98
+ const text = typeof last === 'string' ? last : JSON.stringify(last);
99
+ return `${message}\nPartial output before the interruption:\n${clip(text, partialChars)}`;
100
+ }
101
+ /**
102
+ * `text` within HOST_RESULT_MAX_CHARS characters, as a harness gets a call's
103
+ * result: a longer one keeps its start, cut before a character written as a
104
+ * surrogate pair rather than between its halves, and ends with a line saying
105
+ * so, `[cut at N of M characters]`, counted in the bound. `truncated` when cut.
106
+ */
107
+ export function boundResult(text) {
108
+ if (text.length <= HOST_RESULT_MAX_CHARS)
109
+ return { text };
110
+ const note = (kept) => `\n[cut at ${kept} of ${text.length} characters]`;
111
+ // The note's own length counted at the bound: what is kept is never longer than that, so neither is its note.
112
+ let kept = HOST_RESULT_MAX_CHARS - note(HOST_RESULT_MAX_CHARS).length;
113
+ if ((text.charCodeAt(kept - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(kept) & 0xfc00) === 0xdc00)
114
+ kept -= 1;
115
+ return { text: `${text.slice(0, kept)}${note(kept)}`, truncated: true };
116
+ }
117
+ /** At most `max` characters, with a marker when cut. */
118
+ export function clip(text, max) {
119
+ return text.length <= max ? text : `${text.slice(0, max)}… [${text.length - max} more characters]`;
120
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * JSON Schema to zod, for harness SDKs whose tool APIs take zod (the Claude
3
+ * Agent SDK's in-process MCP tools, OpenCode's plugin tools). `HostTool`s carry
4
+ * JSON Schema; this converts the subset tool schemas use.
5
+ *
6
+ * zod is passed in (`import { z } from 'zod'`, v3 or v4) so this package
7
+ * does not pin one: use the one the harness SDK was built against.
8
+ *
9
+ * Adapted from the AI SDK's bridge (vercel/ai cbb6b49,
10
+ * `packages/harness-claude-code/src/bridge/json-schema-to-zod.ts`,
11
+ * Apache-2.0); changed to take `z` as a parameter and to export the shape.
12
+ */
13
+ type Z = any;
14
+ type ZodType = any;
15
+ export interface JsonSchemaObject {
16
+ type?: string | string[];
17
+ description?: string;
18
+ properties?: Record<string, JsonSchemaObject>;
19
+ additionalProperties?: boolean | JsonSchemaObject;
20
+ required?: string[];
21
+ items?: JsonSchemaObject | JsonSchemaObject[];
22
+ enum?: unknown[];
23
+ const?: unknown;
24
+ oneOf?: JsonSchemaObject[];
25
+ anyOf?: JsonSchemaObject[];
26
+ nullable?: boolean;
27
+ }
28
+ /** A zod object for a JSON Schema object (extra properties allowed unless `additionalProperties: false`). */
29
+ export declare function zodFromJsonSchema(schema: unknown, z: Z): ZodType;
30
+ /** The raw shape (`{ key: zodType }`) of a JSON Schema object, for APIs that take a shape. */
31
+ export declare function zodShapeFromJsonSchema(schema: unknown, z: Z): Record<string, ZodType>;
32
+ export {};