@karowanorg/orc-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -0
- package/README.md +5 -0
- package/dist/canonical.d.ts +11 -0
- package/dist/canonical.js +71 -0
- package/dist/compile.d.ts +12 -0
- package/dist/compile.js +27 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +42 -0
- package/dist/contracts.d.ts +405 -0
- package/dist/contracts.js +28 -0
- package/dist/cost.d.ts +34 -0
- package/dist/cost.js +51 -0
- package/dist/engine.d.ts +54 -0
- package/dist/engine.js +474 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/jsonschema.d.ts +3 -0
- package/dist/jsonschema.js +31 -0
- package/dist/rundir.d.ts +53 -0
- package/dist/rundir.js +299 -0
- package/dist/status.d.ts +10 -0
- package/dist/status.js +147 -0
- package/dist/supervisor.d.ts +34 -0
- package/dist/supervisor.js +1072 -0
- package/package.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
BSD Zero Clause License (0BSD)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kyle Rowan
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @karowanorg/orc-core
|
|
2
|
+
|
|
3
|
+
The orc engine: deterministic QuickJS event loop, journaled leaves, replay/resume, supervisor, contracts.
|
|
4
|
+
|
|
5
|
+
Part of orc - model-authored promise-native agent programs with deterministic replay, live monitoring, and pluggable harnesses. See the orc repository README for the full picture.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Json } from "./contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical JSON: sorted object keys, no whitespace. One rule used for
|
|
4
|
+
* journal digests, result content-addressing, and spec hashing.
|
|
5
|
+
* Numbers pass through JS semantics; integers beyond 2^53 are a pinned design
|
|
6
|
+
* decision (host-side only — programs receive f64 like any JSON consumer).
|
|
7
|
+
*/
|
|
8
|
+
export declare function canonicalJson(value: Json): string;
|
|
9
|
+
export declare function sha256Hex(data: string | Buffer): string;
|
|
10
|
+
export declare function digestJson(value: Json): string;
|
|
11
|
+
export declare function boundString(s: string, maxBytes: number): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical JSON: sorted object keys, no whitespace. One rule used for
|
|
4
|
+
* journal digests, result content-addressing, and spec hashing.
|
|
5
|
+
* Numbers pass through JS semantics; integers beyond 2^53 are a pinned design
|
|
6
|
+
* decision (host-side only — programs receive f64 like any JSON consumer).
|
|
7
|
+
*/
|
|
8
|
+
export function canonicalJson(value) {
|
|
9
|
+
return stringify(value, 0, new WeakSet());
|
|
10
|
+
}
|
|
11
|
+
const MAX_JSON_DEPTH = 256;
|
|
12
|
+
function stringify(v, depth, ancestors) {
|
|
13
|
+
if (depth > MAX_JSON_DEPTH)
|
|
14
|
+
throw new TypeError(`JSON nesting exceeds ${MAX_JSON_DEPTH}`);
|
|
15
|
+
if (v === null || typeof v === "boolean")
|
|
16
|
+
return JSON.stringify(v);
|
|
17
|
+
if (typeof v === "number") {
|
|
18
|
+
if (!Number.isFinite(v))
|
|
19
|
+
throw new TypeError("JSON numbers must be finite");
|
|
20
|
+
return JSON.stringify(v);
|
|
21
|
+
}
|
|
22
|
+
if (typeof v === "string")
|
|
23
|
+
return JSON.stringify(v);
|
|
24
|
+
if (typeof v !== "object")
|
|
25
|
+
throw new TypeError(`unsupported JSON value: ${typeof v}`);
|
|
26
|
+
if (ancestors.has(v))
|
|
27
|
+
throw new TypeError("JSON value contains a cycle");
|
|
28
|
+
ancestors.add(v);
|
|
29
|
+
try {
|
|
30
|
+
if (Array.isArray(v)) {
|
|
31
|
+
const items = [];
|
|
32
|
+
for (let i = 0; i < v.length; i++) {
|
|
33
|
+
const descriptor = Object.getOwnPropertyDescriptor(v, i);
|
|
34
|
+
if (!descriptor)
|
|
35
|
+
throw new TypeError("JSON arrays cannot be sparse");
|
|
36
|
+
if (!("value" in descriptor))
|
|
37
|
+
throw new TypeError("JSON properties cannot be accessors");
|
|
38
|
+
items.push(stringify(descriptor.value, depth + 1, ancestors));
|
|
39
|
+
}
|
|
40
|
+
return `[${items.join(",")}]`;
|
|
41
|
+
}
|
|
42
|
+
const prototype = Object.getPrototypeOf(v);
|
|
43
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
44
|
+
throw new TypeError("JSON objects must be plain objects");
|
|
45
|
+
}
|
|
46
|
+
const keys = Object.keys(v).sort();
|
|
47
|
+
return `{${keys
|
|
48
|
+
.map((k) => {
|
|
49
|
+
const descriptor = Object.getOwnPropertyDescriptor(v, k);
|
|
50
|
+
if (!("value" in descriptor))
|
|
51
|
+
throw new TypeError("JSON properties cannot be accessors");
|
|
52
|
+
return `${JSON.stringify(k)}:${stringify(descriptor.value, depth + 1, ancestors)}`;
|
|
53
|
+
})
|
|
54
|
+
.join(",")}}`;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
ancestors.delete(v);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function sha256Hex(data) {
|
|
61
|
+
return createHash("sha256").update(data).digest("hex");
|
|
62
|
+
}
|
|
63
|
+
export function digestJson(value) {
|
|
64
|
+
return sha256Hex(canonicalJson(value));
|
|
65
|
+
}
|
|
66
|
+
export function boundString(s, maxBytes) {
|
|
67
|
+
const buf = Buffer.from(s, "utf8");
|
|
68
|
+
if (buf.byteLength <= maxBytes)
|
|
69
|
+
return s;
|
|
70
|
+
return buf.subarray(0, maxBytes).toString("utf8") + `…(truncated ${buf.byteLength - maxBytes} bytes)`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile a .orc.ts / .ts / .js program into a frozen single-file bundle.
|
|
3
|
+
* The emitted bytes are the pinned program: sha256(bundle) goes in the manifest
|
|
4
|
+
* and every execution (live or replay) runs exactly these bytes.
|
|
5
|
+
*
|
|
6
|
+
* Programs may use `import type` freely (erased); runtime imports are rejected
|
|
7
|
+
* (the sandbox has no module loader).
|
|
8
|
+
*/
|
|
9
|
+
export declare function compileProgram(entryPath: string): Promise<{
|
|
10
|
+
bundle: string;
|
|
11
|
+
sha256: string;
|
|
12
|
+
}>;
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { sha256Hex } from "./canonical.js";
|
|
4
|
+
/**
|
|
5
|
+
* Compile a .orc.ts / .ts / .js program into a frozen single-file bundle.
|
|
6
|
+
* The emitted bytes are the pinned program: sha256(bundle) goes in the manifest
|
|
7
|
+
* and every execution (live or replay) runs exactly these bytes.
|
|
8
|
+
*
|
|
9
|
+
* Programs may use `import type` freely (erased); runtime imports are rejected
|
|
10
|
+
* (the sandbox has no module loader).
|
|
11
|
+
*/
|
|
12
|
+
export async function compileProgram(entryPath) {
|
|
13
|
+
if (!fs.existsSync(entryPath))
|
|
14
|
+
throw new Error(`program not found: ${entryPath}`);
|
|
15
|
+
const result = await build({
|
|
16
|
+
entryPoints: [entryPath],
|
|
17
|
+
bundle: true,
|
|
18
|
+
format: "iife",
|
|
19
|
+
globalName: "__orc_mod",
|
|
20
|
+
platform: "neutral",
|
|
21
|
+
target: "es2020",
|
|
22
|
+
write: false,
|
|
23
|
+
logLevel: "silent",
|
|
24
|
+
});
|
|
25
|
+
const bundle = result.outputFiles[0].text;
|
|
26
|
+
return { bundle, sha256: sha256Hex(bundle) };
|
|
27
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ExtensionLeaf, Harness } from "./contracts.js";
|
|
2
|
+
import type { ModelRate } from "./cost.js";
|
|
3
|
+
/**
|
|
4
|
+
* Optional config. Zero config is the default: built-ins (claude, codex) are
|
|
5
|
+
* discovered natively; there is deliberately NO ambient plugin scanning —
|
|
6
|
+
* registering a custom harness or extension is an explicit config entry.
|
|
7
|
+
*/
|
|
8
|
+
export interface OrcConfig {
|
|
9
|
+
/** Custom harnesses: Harness objects, or { exec } NDJSON executables. */
|
|
10
|
+
harnesses?: Array<Harness | {
|
|
11
|
+
exec: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
}>;
|
|
14
|
+
defaultHarness?: string;
|
|
15
|
+
extensions?: ExtensionLeaf[];
|
|
16
|
+
/** Per-model USD/1M rate overrides for cost estimation (codex). */
|
|
17
|
+
costRates?: Record<string, ModelRate>;
|
|
18
|
+
}
|
|
19
|
+
/** Identity helper with types — `defineLeaf({...})` in orc.config.js. */
|
|
20
|
+
export declare function defineLeaf(leaf: ExtensionLeaf): ExtensionLeaf;
|
|
21
|
+
export declare function loadConfig(cwd: string): Promise<OrcConfig>;
|
|
22
|
+
/**
|
|
23
|
+
* Caller-affinity default harness: an orchestration launched from a claude
|
|
24
|
+
* session defaults to claude leaves; from codex, codex. Resolution order is
|
|
25
|
+
* per-call harness → launch flag → config.defaultHarness → caller affinity →
|
|
26
|
+
* codex → any available. `mcpClientName` lets the MCP head pass clientInfo.
|
|
27
|
+
*/
|
|
28
|
+
export declare function callerAffinity(mcpClientName?: string): string | undefined;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { orcHome } from "./rundir.js";
|
|
5
|
+
/** Identity helper with types — `defineLeaf({...})` in orc.config.js. */
|
|
6
|
+
export function defineLeaf(leaf) {
|
|
7
|
+
return leaf;
|
|
8
|
+
}
|
|
9
|
+
const CONFIG_BASENAMES = ["orc.config.js", "orc.config.mjs", "orc.config.json"];
|
|
10
|
+
export async function loadConfig(cwd) {
|
|
11
|
+
for (const dir of [cwd, orcHome()]) {
|
|
12
|
+
for (const base of CONFIG_BASENAMES) {
|
|
13
|
+
const file = path.join(dir, base);
|
|
14
|
+
if (!fs.existsSync(file))
|
|
15
|
+
continue;
|
|
16
|
+
if (base.endsWith(".json")) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
18
|
+
}
|
|
19
|
+
const mod = (await import(pathToFileURL(file).href));
|
|
20
|
+
return mod.default ?? {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Caller-affinity default harness: an orchestration launched from a claude
|
|
27
|
+
* session defaults to claude leaves; from codex, codex. Resolution order is
|
|
28
|
+
* per-call harness → launch flag → config.defaultHarness → caller affinity →
|
|
29
|
+
* codex → any available. `mcpClientName` lets the MCP head pass clientInfo.
|
|
30
|
+
*/
|
|
31
|
+
export function callerAffinity(mcpClientName) {
|
|
32
|
+
const name = (mcpClientName ?? "").toLowerCase();
|
|
33
|
+
if (name.includes("claude"))
|
|
34
|
+
return "claude";
|
|
35
|
+
if (name.includes("codex"))
|
|
36
|
+
return "codex";
|
|
37
|
+
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT)
|
|
38
|
+
return "claude";
|
|
39
|
+
if (process.env.CODEX_SANDBOX || process.env.CODEX_HOME || process.env.CODEX_CI)
|
|
40
|
+
return "codex";
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contracts for orc.
|
|
3
|
+
* Everything here is consumed by core, executors, harnesses, ops, and ui.
|
|
4
|
+
*/
|
|
5
|
+
import type { Readable, Writable } from "node:stream";
|
|
6
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
7
|
+
[k: string]: Json;
|
|
8
|
+
};
|
|
9
|
+
export type ApprovalMode = "manual" | "accept-edits" | "auto" | "bypass";
|
|
10
|
+
export interface ApprovalRequest {
|
|
11
|
+
id: string;
|
|
12
|
+
runId: string;
|
|
13
|
+
seq: number;
|
|
14
|
+
toolName: string;
|
|
15
|
+
input: Json;
|
|
16
|
+
requestedAtMs: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ApprovalDecision {
|
|
19
|
+
behavior: "allow" | "deny";
|
|
20
|
+
message?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface SpawnOptions {
|
|
23
|
+
cwd?: string;
|
|
24
|
+
env?: Record<string, string>;
|
|
25
|
+
stdin?: "pipe" | "ignore";
|
|
26
|
+
}
|
|
27
|
+
export interface Proc {
|
|
28
|
+
stdin: Writable | null;
|
|
29
|
+
stdout: Readable;
|
|
30
|
+
stderr: Readable;
|
|
31
|
+
/** Resolves with the exit code (or -1 on signal kill). Never rejects. */
|
|
32
|
+
exited: Promise<number>;
|
|
33
|
+
/** SIGTERM the process group, grace, then SIGKILL. Idempotent. */
|
|
34
|
+
kill(): void;
|
|
35
|
+
pid: number | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface Executor {
|
|
38
|
+
/** undefined for local; the ssh destination string otherwise (e.g. "build@ci-box"). */
|
|
39
|
+
readonly host: string | undefined;
|
|
40
|
+
spawn(cmd: string[], opts?: SpawnOptions): Proc;
|
|
41
|
+
/** Run a command to completion, capturing output. */
|
|
42
|
+
run(cmd: string[], opts?: SpawnOptions & {
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
code: number;
|
|
46
|
+
stdout: string;
|
|
47
|
+
stderr: string;
|
|
48
|
+
}>;
|
|
49
|
+
exists(path: string): Promise<boolean>;
|
|
50
|
+
readFile(path: string): Promise<string>;
|
|
51
|
+
writeFile(path: string, data: string): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/** One model and the reasoning efforts THAT model supports (per-model). */
|
|
54
|
+
export interface ModelCapability {
|
|
55
|
+
id: string;
|
|
56
|
+
displayName?: string;
|
|
57
|
+
/** Reasoning efforts valid for this model — empty if it takes no effort param. */
|
|
58
|
+
reasoningEfforts: string[];
|
|
59
|
+
default?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface HarnessCapabilities {
|
|
62
|
+
available: boolean;
|
|
63
|
+
version?: string;
|
|
64
|
+
/** Discovered natively, never hardcoded; each model carries its own efforts. */
|
|
65
|
+
models: ModelCapability[];
|
|
66
|
+
approvalModes: ApprovalMode[];
|
|
67
|
+
structuredOutput: boolean;
|
|
68
|
+
sessions: boolean;
|
|
69
|
+
detail?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface LeafRequest {
|
|
72
|
+
runId: string;
|
|
73
|
+
seq: number;
|
|
74
|
+
id?: string;
|
|
75
|
+
prompt: string;
|
|
76
|
+
system: string;
|
|
77
|
+
brief: string;
|
|
78
|
+
schema?: Json;
|
|
79
|
+
model?: string;
|
|
80
|
+
reasoningEffort?: string;
|
|
81
|
+
readOnly: boolean;
|
|
82
|
+
cwd: string;
|
|
83
|
+
host?: string;
|
|
84
|
+
approvalMode: ApprovalMode;
|
|
85
|
+
sessionId?: string;
|
|
86
|
+
idleTimeoutMs?: number | false;
|
|
87
|
+
/**
|
|
88
|
+
* Filesystem confinement for WRITE leaves. Default false: a write leaf is an
|
|
89
|
+
* unconfined subagent that can write wherever the caller can (builds write to
|
|
90
|
+
* caches outside the workspace). When true, writes are confined to `cwd` plus
|
|
91
|
+
* `sandboxDirs` (override dirs, e.g. cache roots). Read-only leaves deny the
|
|
92
|
+
* built-in command/file mutation paths regardless — that's the readOnly
|
|
93
|
+
* contract, not sandboxing. Configured hooks and MCP tools are not disabled,
|
|
94
|
+
* so this is not a global side-effect sandbox.
|
|
95
|
+
*/
|
|
96
|
+
sandbox?: boolean;
|
|
97
|
+
sandboxDirs?: string[];
|
|
98
|
+
}
|
|
99
|
+
export type HarnessEvent = {
|
|
100
|
+
kind: "tool-call-open";
|
|
101
|
+
id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
input?: Json;
|
|
104
|
+
atMs: number;
|
|
105
|
+
} | {
|
|
106
|
+
kind: "tool-call-close";
|
|
107
|
+
id: string;
|
|
108
|
+
status: "ok" | "error";
|
|
109
|
+
result?: Json;
|
|
110
|
+
atMs: number;
|
|
111
|
+
} | {
|
|
112
|
+
kind: "text";
|
|
113
|
+
delta: string;
|
|
114
|
+
atMs: number;
|
|
115
|
+
} | {
|
|
116
|
+
kind: "usage";
|
|
117
|
+
tokensIn?: number;
|
|
118
|
+
tokensOut?: number;
|
|
119
|
+
costUsd?: number;
|
|
120
|
+
costEstimated?: boolean;
|
|
121
|
+
} | {
|
|
122
|
+
kind: "model";
|
|
123
|
+
model?: string;
|
|
124
|
+
reasoningEffort?: string;
|
|
125
|
+
} | {
|
|
126
|
+
kind: "session";
|
|
127
|
+
sessionId: string;
|
|
128
|
+
} | {
|
|
129
|
+
kind: "denied";
|
|
130
|
+
toolName: string;
|
|
131
|
+
reason: string;
|
|
132
|
+
atMs: number;
|
|
133
|
+
} | {
|
|
134
|
+
kind: "result";
|
|
135
|
+
output: Json;
|
|
136
|
+
} | {
|
|
137
|
+
kind: "error";
|
|
138
|
+
message: string;
|
|
139
|
+
};
|
|
140
|
+
export interface HarnessContext {
|
|
141
|
+
executor: Executor;
|
|
142
|
+
requestApproval(req: Omit<ApprovalRequest, "id" | "requestedAtMs">): Promise<ApprovalDecision>;
|
|
143
|
+
signal: AbortSignal;
|
|
144
|
+
log(message: string): void;
|
|
145
|
+
}
|
|
146
|
+
export interface Harness {
|
|
147
|
+
readonly name: string;
|
|
148
|
+
discover(ctx: {
|
|
149
|
+
executor: Executor;
|
|
150
|
+
}): Promise<HarnessCapabilities>;
|
|
151
|
+
invoke(req: LeafRequest, ctx: HarnessContext): AsyncIterable<HarnessEvent>;
|
|
152
|
+
/**
|
|
153
|
+
* Optional static check of a leaf's structured-output JSON Schema against
|
|
154
|
+
* whatever the harness enforces at runtime (e.g. OpenAI/codex strict mode).
|
|
155
|
+
* Returns human-readable problems so `orc validate` can reject a schema the
|
|
156
|
+
* model would otherwise fail on at invocation time. Empty/undefined = no
|
|
157
|
+
* harness-specific restriction.
|
|
158
|
+
*/
|
|
159
|
+
lintOutputSchema?(schema: Json): string[];
|
|
160
|
+
}
|
|
161
|
+
export interface ExtensionLeaf {
|
|
162
|
+
name: string;
|
|
163
|
+
inputSchema?: Json;
|
|
164
|
+
readOnly: boolean;
|
|
165
|
+
execute(payload: Json, ctx: HarnessContext): Promise<Json>;
|
|
166
|
+
}
|
|
167
|
+
export interface ThunkSpec {
|
|
168
|
+
kind: "agent" | `ext:${string}`;
|
|
169
|
+
prompt?: string;
|
|
170
|
+
payload?: Json;
|
|
171
|
+
id?: string;
|
|
172
|
+
harness?: string;
|
|
173
|
+
model?: string;
|
|
174
|
+
reasoningEffort?: string;
|
|
175
|
+
schema?: Json;
|
|
176
|
+
readOnly: boolean;
|
|
177
|
+
cwd?: string;
|
|
178
|
+
host?: string;
|
|
179
|
+
phase?: string;
|
|
180
|
+
idleTimeoutMs?: number | false;
|
|
181
|
+
groupId?: string;
|
|
182
|
+
}
|
|
183
|
+
export interface CallRecord {
|
|
184
|
+
t: "call";
|
|
185
|
+
seq: number;
|
|
186
|
+
kind: ThunkSpec["kind"];
|
|
187
|
+
id?: string;
|
|
188
|
+
phase?: string;
|
|
189
|
+
readOnly: boolean;
|
|
190
|
+
/** sha256 of the canonical ThunkSpec — replay verifies this. */
|
|
191
|
+
specDigest: string;
|
|
192
|
+
}
|
|
193
|
+
export interface CompletionRecord {
|
|
194
|
+
t: "done";
|
|
195
|
+
seq: number;
|
|
196
|
+
status: "ok" | "error";
|
|
197
|
+
/** sha256 of the canonical result body in results/<sha>.json (ok only). */
|
|
198
|
+
resultSha?: string;
|
|
199
|
+
sizeBytes?: number;
|
|
200
|
+
error?: string;
|
|
201
|
+
attempt: number;
|
|
202
|
+
}
|
|
203
|
+
/** Durable start of one leaf attempt, written before the harness is invoked. */
|
|
204
|
+
export interface AttemptRecord {
|
|
205
|
+
t: "attempt";
|
|
206
|
+
seq: number;
|
|
207
|
+
attempt: number;
|
|
208
|
+
atMs: number;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Latest cumulative cost reported for one attempt. Multiple records may be
|
|
212
|
+
* written; replay keeps the last value for each (seq, attempt) pair.
|
|
213
|
+
*/
|
|
214
|
+
export interface CostRecord {
|
|
215
|
+
t: "cost";
|
|
216
|
+
seq: number;
|
|
217
|
+
attempt: number;
|
|
218
|
+
costUsd: number;
|
|
219
|
+
costEstimated: boolean;
|
|
220
|
+
atMs: number;
|
|
221
|
+
}
|
|
222
|
+
export interface FinishRecord {
|
|
223
|
+
t: "finish";
|
|
224
|
+
status: "completed" | "failed" | "cancelled";
|
|
225
|
+
resultSha?: string;
|
|
226
|
+
error?: string;
|
|
227
|
+
/** Number of control records consumed by the ownership epoch that finished. */
|
|
228
|
+
controlOffset?: number;
|
|
229
|
+
/** Leaf whose delivered error directly became the program's terminal error. */
|
|
230
|
+
errorSeq?: number;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Fail-forward resume marker (append-only — history is never rewritten):
|
|
234
|
+
* completion records for these seqs written BEFORE this marker are void for
|
|
235
|
+
* replay; the seqs re-dispatch with a fresh attempt.
|
|
236
|
+
*/
|
|
237
|
+
export interface RetryRecord {
|
|
238
|
+
t: "retry";
|
|
239
|
+
seqs: number[];
|
|
240
|
+
atMs: number;
|
|
241
|
+
}
|
|
242
|
+
export type JournalRecord = CallRecord | AttemptRecord | CostRecord | CompletionRecord | FinishRecord | RetryRecord;
|
|
243
|
+
export interface ToolCallTrace {
|
|
244
|
+
id: string;
|
|
245
|
+
name: string;
|
|
246
|
+
input?: Json;
|
|
247
|
+
result?: Json;
|
|
248
|
+
startMs?: number;
|
|
249
|
+
endMs?: number;
|
|
250
|
+
status: "running" | "ok" | "error";
|
|
251
|
+
}
|
|
252
|
+
export interface LeafTraceRecord {
|
|
253
|
+
t: "leaf";
|
|
254
|
+
seq: number;
|
|
255
|
+
attempt: number;
|
|
256
|
+
rev: number;
|
|
257
|
+
status: "running" | "ok" | "error";
|
|
258
|
+
id?: string;
|
|
259
|
+
phase?: string;
|
|
260
|
+
kind: string;
|
|
261
|
+
harness?: string;
|
|
262
|
+
model?: string;
|
|
263
|
+
reasoningEffort?: string;
|
|
264
|
+
host?: string;
|
|
265
|
+
cwd?: string;
|
|
266
|
+
readOnly: boolean;
|
|
267
|
+
startMs: number;
|
|
268
|
+
endMs?: number;
|
|
269
|
+
prompt?: string;
|
|
270
|
+
brief?: string;
|
|
271
|
+
output?: Json;
|
|
272
|
+
error?: string;
|
|
273
|
+
sessionId?: string;
|
|
274
|
+
toolCalls?: ToolCallTrace[];
|
|
275
|
+
tokensIn?: number;
|
|
276
|
+
tokensOut?: number;
|
|
277
|
+
costUsd?: number;
|
|
278
|
+
costEstimated?: boolean;
|
|
279
|
+
reoriented?: boolean;
|
|
280
|
+
}
|
|
281
|
+
export interface RunEventRecord {
|
|
282
|
+
t: "event";
|
|
283
|
+
atMs: number;
|
|
284
|
+
event: {
|
|
285
|
+
kind: "log";
|
|
286
|
+
message: string;
|
|
287
|
+
} | {
|
|
288
|
+
kind: "phase";
|
|
289
|
+
name: string;
|
|
290
|
+
} | {
|
|
291
|
+
kind: "approval-requested";
|
|
292
|
+
approval: ApprovalRequest;
|
|
293
|
+
} | {
|
|
294
|
+
kind: "approval-resolved";
|
|
295
|
+
approvalId: string;
|
|
296
|
+
decision: ApprovalDecision;
|
|
297
|
+
by: string;
|
|
298
|
+
} | {
|
|
299
|
+
kind: "denied";
|
|
300
|
+
seq: number;
|
|
301
|
+
toolName: string;
|
|
302
|
+
reason: string;
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* A harness's own stderr/tracing line, attributed to one leaf. Kept OUT of the
|
|
307
|
+
* run's event feed (which carries only the orchestration narrative) and shown
|
|
308
|
+
* in the leaf's drawer under a collapsed "Harness log" section, deduped.
|
|
309
|
+
*/
|
|
310
|
+
export interface HarnessLogRecord {
|
|
311
|
+
t: "hlog";
|
|
312
|
+
seq: number;
|
|
313
|
+
atMs: number;
|
|
314
|
+
message: string;
|
|
315
|
+
}
|
|
316
|
+
export type TraceRecord = LeafTraceRecord | RunEventRecord | HarnessLogRecord;
|
|
317
|
+
export interface RunManifest {
|
|
318
|
+
runId: string;
|
|
319
|
+
name?: string;
|
|
320
|
+
programPath: string;
|
|
321
|
+
programSha256: string;
|
|
322
|
+
cwd: string;
|
|
323
|
+
host?: string;
|
|
324
|
+
brief: string;
|
|
325
|
+
allowWrites: boolean;
|
|
326
|
+
approvalMode: ApprovalMode;
|
|
327
|
+
sandbox: boolean;
|
|
328
|
+
sandboxDirs: string[];
|
|
329
|
+
maxParallel: number;
|
|
330
|
+
idleTimeoutMs: number | false;
|
|
331
|
+
/** Reactive USD cap: the run fails once observed estimated cost exceeds this. */
|
|
332
|
+
budgetUsd?: number;
|
|
333
|
+
defaultHarness: string;
|
|
334
|
+
createdAtMs: number;
|
|
335
|
+
orcVersion: string;
|
|
336
|
+
}
|
|
337
|
+
export type RunState = "running" | "completed" | "failed" | "cancelled";
|
|
338
|
+
export interface LeafStatus {
|
|
339
|
+
seq: number;
|
|
340
|
+
id?: string;
|
|
341
|
+
phase?: string;
|
|
342
|
+
kind: string;
|
|
343
|
+
status: "pending" | "running" | "ok" | "error";
|
|
344
|
+
readOnly: boolean;
|
|
345
|
+
harness?: string;
|
|
346
|
+
host?: string;
|
|
347
|
+
startMs?: number;
|
|
348
|
+
endMs?: number;
|
|
349
|
+
error?: string;
|
|
350
|
+
resultSha?: string;
|
|
351
|
+
attempt?: number;
|
|
352
|
+
}
|
|
353
|
+
export interface RunStatus {
|
|
354
|
+
runId: string;
|
|
355
|
+
state: RunState;
|
|
356
|
+
totalCalls: number;
|
|
357
|
+
ok: number;
|
|
358
|
+
failed: number;
|
|
359
|
+
running: number;
|
|
360
|
+
approvalsPending: number;
|
|
361
|
+
leaves: LeafStatus[];
|
|
362
|
+
resultSha?: string;
|
|
363
|
+
error?: string;
|
|
364
|
+
startedAtMs: number;
|
|
365
|
+
endedAtMs?: number;
|
|
366
|
+
approvalMode: ApprovalMode;
|
|
367
|
+
allowWrites: boolean;
|
|
368
|
+
}
|
|
369
|
+
export type ControlMessage = {
|
|
370
|
+
t: "cancel";
|
|
371
|
+
atMs: number;
|
|
372
|
+
} | {
|
|
373
|
+
t: "approval";
|
|
374
|
+
approvalId: string;
|
|
375
|
+
decision: ApprovalDecision;
|
|
376
|
+
by: string;
|
|
377
|
+
atMs: number;
|
|
378
|
+
};
|
|
379
|
+
export interface Policy {
|
|
380
|
+
maxCommands: number;
|
|
381
|
+
maxParallel: number;
|
|
382
|
+
maxPromptBytes: number;
|
|
383
|
+
maxResultBytes: number;
|
|
384
|
+
maxErrorBytes: number;
|
|
385
|
+
stepBudgetPerTurn: number;
|
|
386
|
+
memoryLimitBytes: number;
|
|
387
|
+
maxStackBytes: number;
|
|
388
|
+
readOnlyRetries: number;
|
|
389
|
+
}
|
|
390
|
+
export declare const DEFAULT_POLICY: Policy;
|
|
391
|
+
export declare class DivergenceError extends Error {
|
|
392
|
+
readonly detail: {
|
|
393
|
+
seq?: number;
|
|
394
|
+
expected?: string;
|
|
395
|
+
got?: string;
|
|
396
|
+
};
|
|
397
|
+
constructor(message: string, detail: {
|
|
398
|
+
seq?: number;
|
|
399
|
+
expected?: string;
|
|
400
|
+
got?: string;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
export declare class PolicyError extends Error {
|
|
404
|
+
constructor(message: string);
|
|
405
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const DEFAULT_POLICY = {
|
|
2
|
+
maxCommands: 128,
|
|
3
|
+
maxParallel: 8,
|
|
4
|
+
maxPromptBytes: 1 << 20,
|
|
5
|
+
maxResultBytes: 1 << 20,
|
|
6
|
+
maxErrorBytes: 4096,
|
|
7
|
+
stepBudgetPerTurn: 100_000,
|
|
8
|
+
memoryLimitBytes: 64 << 20,
|
|
9
|
+
maxStackBytes: 1 << 20,
|
|
10
|
+
readOnlyRetries: 2,
|
|
11
|
+
};
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Errors
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export class DivergenceError extends Error {
|
|
16
|
+
detail;
|
|
17
|
+
constructor(message, detail) {
|
|
18
|
+
super(`replay divergence: ${message}`);
|
|
19
|
+
this.detail = detail;
|
|
20
|
+
this.name = "DivergenceError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class PolicyError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(`policy violation: ${message}`);
|
|
26
|
+
this.name = "PolicyError";
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/cost.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost estimation for harnesses that report tokens but not dollars.
|
|
3
|
+
*
|
|
4
|
+
* Claude's Agent SDK returns an exact `total_cost_usd`; codex's app-server
|
|
5
|
+
* reports only token counts. For codex, orc estimates cost from a per-model
|
|
6
|
+
* rate table (USD per 1M tokens). Rates are ESTIMATES — the number is marked
|
|
7
|
+
* `estimated: true` so the UI can render it as "~$… est" rather than implying a
|
|
8
|
+
* billed figure. Override via config (`costRates`) or `ORC_COST_RATES` (JSON).
|
|
9
|
+
*
|
|
10
|
+
* If a model has no rate, cost is omitted (never fabricated as zero).
|
|
11
|
+
*/
|
|
12
|
+
export interface ModelRate {
|
|
13
|
+
/** USD per 1M input (prompt) tokens. */
|
|
14
|
+
inputPer1M: number;
|
|
15
|
+
/** USD per 1M output (completion) tokens. */
|
|
16
|
+
outputPer1M: number;
|
|
17
|
+
/** USD per 1M cached input tokens (defaults to inputPer1M when absent). */
|
|
18
|
+
cachedInputPer1M?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TokenBreakdown {
|
|
21
|
+
inputTokens?: number;
|
|
22
|
+
cachedInputTokens?: number;
|
|
23
|
+
outputTokens?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Default list-price ESTIMATES for the gpt-5.x families, keyed by longest
|
|
27
|
+
* matching prefix. These are editable placeholders — real billing depends on
|
|
28
|
+
* your account/agreement. Override with config `costRates` or ORC_COST_RATES.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_COST_RATES: Record<string, ModelRate>;
|
|
31
|
+
export declare function loadCostRates(override?: Record<string, ModelRate>): Record<string, ModelRate>;
|
|
32
|
+
/** Longest-prefix match so "gpt-5.6-sol" resolves to the "gpt-5.6" rate. */
|
|
33
|
+
export declare function rateForModel(rates: Record<string, ModelRate>, model: string | undefined): ModelRate | undefined;
|
|
34
|
+
export declare function estimateCostUsd(rates: Record<string, ModelRate>, model: string | undefined, tokens: TokenBreakdown): number | undefined;
|