orcareplay 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/dist/api.d.ts +127 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +159 -0
- package/dist/api.js.map +1 -0
- package/dist/args.d.ts +24 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +124 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/compare.d.ts +27 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/compare.js +182 -0
- package/dist/commands/compare.js.map +1 -0
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +358 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/gc.d.ts +24 -0
- package/dist/commands/gc.d.ts.map +1 -0
- package/dist/commands/gc.js +326 -0
- package/dist/commands/gc.js.map +1 -0
- package/dist/commands/inspect.d.ts +24 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +262 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/record.d.ts +17 -0
- package/dist/commands/record.d.ts.map +1 -0
- package/dist/commands/record.js +497 -0
- package/dist/commands/record.js.map +1 -0
- package/dist/commands/replay.d.ts +43 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +800 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/scrub.d.ts +34 -0
- package/dist/commands/scrub.d.ts.map +1 -0
- package/dist/commands/scrub.js +452 -0
- package/dist/commands/scrub.js.map +1 -0
- package/dist/commands/setup.d.ts +23 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +177 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/config.d.ts +80 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +117 -0
- package/dist/config.js.map +1 -0
- package/dist/exchange-events.d.ts +72 -0
- package/dist/exchange-events.d.ts.map +1 -0
- package/dist/exchange-events.js +170 -0
- package/dist/exchange-events.js.map +1 -0
- package/dist/flags.d.ts +10 -0
- package/dist/flags.d.ts.map +1 -0
- package/dist/flags.js +83 -0
- package/dist/flags.js.map +1 -0
- package/dist/fs-events.d.ts +19 -0
- package/dist/fs-events.d.ts.map +1 -0
- package/dist/fs-events.js +53 -0
- package/dist/fs-events.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +282 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp-server.d.ts +44 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +245 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp.d.ts +111 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +221 -0
- package/dist/mcp.js.map +1 -0
- package/dist/out.d.ts +69 -0
- package/dist/out.d.ts.map +1 -0
- package/dist/out.js +138 -0
- package/dist/out.js.map +1 -0
- package/dist/rasterize.d.ts +62 -0
- package/dist/rasterize.d.ts.map +1 -0
- package/dist/rasterize.js +154 -0
- package/dist/rasterize.js.map +1 -0
- package/dist/serial.d.ts +19 -0
- package/dist/serial.d.ts.map +1 -0
- package/dist/serial.js +34 -0
- package/dist/serial.js.map +1 -0
- package/dist/share-card.d.ts +73 -0
- package/dist/share-card.d.ts.map +1 -0
- package/dist/share-card.js +413 -0
- package/dist/share-card.js.map +1 -0
- package/dist/snapshot.d.ts +30 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +43 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/tls-capture.d.ts +57 -0
- package/dist/tls-capture.d.ts.map +1 -0
- package/dist/tls-capture.js +198 -0
- package/dist/tls-capture.js.map +1 -0
- package/dist/upstream.d.ts +20 -0
- package/dist/upstream.d.ts.map +1 -0
- package/dist/upstream.js +31 -0
- package/dist/upstream.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { modelInfoFor } from '@orcareplay/providers';
|
|
3
|
+
import { configPath, gatewayHeaders, ORCAROUTER_CONSOLE, ORCAROUTER_URL, readConfig, writeConfig, } from '../config.js';
|
|
4
|
+
/** OpenAI-compatible model listing, which every gateway worth pointing orca at implements. */
|
|
5
|
+
async function probeModels(gateway, headers) {
|
|
6
|
+
const res = await fetch(`${gateway.replace(/\/+$/, '')}/v1/models`, { headers });
|
|
7
|
+
if (!res.ok)
|
|
8
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
9
|
+
const body = (await res.json());
|
|
10
|
+
return (body.data ?? [])
|
|
11
|
+
.map((m) => m.id)
|
|
12
|
+
.filter((id) => typeof id === 'string')
|
|
13
|
+
.sort();
|
|
14
|
+
}
|
|
15
|
+
/** Prompts on a real terminal, and refuses to be one when nothing is attached. */
|
|
16
|
+
function terminalAsk() {
|
|
17
|
+
if (!process.stdin.isTTY)
|
|
18
|
+
return undefined;
|
|
19
|
+
return async (question) => {
|
|
20
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
21
|
+
try {
|
|
22
|
+
return (await rl.question(question)).trim();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
rl.close();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function setupCommand(args, out, deps = {}) {
|
|
30
|
+
const env = deps.env ?? process.env;
|
|
31
|
+
const probe = deps.probe ?? probeModels;
|
|
32
|
+
const ask = deps.ask ?? terminalAsk();
|
|
33
|
+
let url = args.str('gateway');
|
|
34
|
+
let key = args.str('key');
|
|
35
|
+
const keyEnv = args.str('key-env');
|
|
36
|
+
// OrcaRouter fills the blank, and Enter accepts it. Offered rather than imposed: the whole point
|
|
37
|
+
// of a gateway is that one origin serves several models, and most people asking for that do not
|
|
38
|
+
// have one already — but anyone who does types over it, and `--gateway` skips the question.
|
|
39
|
+
if (!url && ask)
|
|
40
|
+
url = await ask(`Gateway URL (serves the model APIs) [${ORCAROUTER_URL}]: `);
|
|
41
|
+
if (!url)
|
|
42
|
+
url = ORCAROUTER_URL;
|
|
43
|
+
if (!key && !keyEnv && ask) {
|
|
44
|
+
if (sameOrigin(url, ORCAROUTER_URL)) {
|
|
45
|
+
out.plain(` get a key at ${ORCAROUTER_CONSOLE} — OrcaRouter keys start sk-orca-`);
|
|
46
|
+
}
|
|
47
|
+
key = await ask('API key (stored 0600; leave blank for none): ');
|
|
48
|
+
}
|
|
49
|
+
const gateway = { url };
|
|
50
|
+
// Stored key wins if both are given, and only one is ever written: keeping both would leave a
|
|
51
|
+
// credential on disk for someone who explicitly asked not to have one.
|
|
52
|
+
if (key)
|
|
53
|
+
gateway.api_key = key;
|
|
54
|
+
else if (keyEnv)
|
|
55
|
+
gateway.api_key_env = keyEnv;
|
|
56
|
+
const existing = await readConfig(env);
|
|
57
|
+
const config = { ...existing, gateway };
|
|
58
|
+
// Ask before saving is tempting, but saving first means an unreachable gateway still leaves you
|
|
59
|
+
// configured — being offline should not stop you setting up the thing you will use online.
|
|
60
|
+
const path = await writeConfig(config, env);
|
|
61
|
+
// `auth:`, not `key:` — the terminal guard redacts any field named `key`, which is right in
|
|
62
|
+
// general and would hide the one thing this line exists to tell you: whether a key was stored at
|
|
63
|
+
// all, and where it came from. The value is a description, never the credential.
|
|
64
|
+
out.info('config.saved', { path, mode: '0600', gateway: url, auth: describeKey(gateway) });
|
|
65
|
+
let available = [];
|
|
66
|
+
try {
|
|
67
|
+
available = await probe(url, gatewayHeaders(config, env));
|
|
68
|
+
if (available.length === 0) {
|
|
69
|
+
out.warn('gateway.no_models', { note: 'reachable, but it listed no models' });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
out.plain('');
|
|
73
|
+
out.plain(` ${available.length} models available:`);
|
|
74
|
+
for (const m of available.slice(0, 20))
|
|
75
|
+
out.plain(` ${m}`);
|
|
76
|
+
if (available.length > 20)
|
|
77
|
+
out.plain(` … and ${available.length - 20} more — orca models`);
|
|
78
|
+
}
|
|
79
|
+
// Ask which of them to compare by default, so `orca compare` needs no flags afterwards. This
|
|
80
|
+
// is the half that makes setup worth running: a gateway with nothing chosen still leaves you
|
|
81
|
+
// typing a model list on every invocation, which is what the command exists to remove.
|
|
82
|
+
const chosen = args.str('models') ?? (ask ? await askModels(ask, available) : undefined);
|
|
83
|
+
if (chosen) {
|
|
84
|
+
config.models = chosen
|
|
85
|
+
.split(',')
|
|
86
|
+
.map((m) => m.trim())
|
|
87
|
+
.filter((m) => m !== '');
|
|
88
|
+
if (config.models.length > 0) {
|
|
89
|
+
await writeConfig(config, env);
|
|
90
|
+
out.info('config.models', { models: config.models.join(',') });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
out.warn('gateway.unreachable', {
|
|
96
|
+
why: String(err instanceof Error ? err.message : err),
|
|
97
|
+
note: 'the config was saved; fix the URL or key and run orca setup again',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
out.plain('');
|
|
101
|
+
// Built from what the gateway actually serves, never from two model names picked here. Model ids
|
|
102
|
+
// are gateway-specific — OrcaRouter namespaces them by provider, a direct provider does not — so a
|
|
103
|
+
// hardcoded pair is a copyable line that fails against the gateway orca just configured.
|
|
104
|
+
out.plain(nextStep(config, available));
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
107
|
+
/** The line worth copying next, using real model ids wherever we have them. */
|
|
108
|
+
function nextStep(config, available) {
|
|
109
|
+
if (config.models && config.models.length > 0)
|
|
110
|
+
return ' orca compare last --verify "npm test"';
|
|
111
|
+
if (available.length > 0) {
|
|
112
|
+
return ` orca compare last --models ${available.slice(0, 2).join(',')} --verify "npm test"`;
|
|
113
|
+
}
|
|
114
|
+
return ' orca models # what this gateway serves, then compare two of them';
|
|
115
|
+
}
|
|
116
|
+
/** Offer the gateway's own list, so the answer is a choice rather than a spelling test. */
|
|
117
|
+
async function askModels(ask, available) {
|
|
118
|
+
const suggestion = available.slice(0, 3).join(',');
|
|
119
|
+
const answer = await ask(`Models to compare by default${suggestion ? ` [${suggestion}]` : ''} (comma-separated, blank to skip): `);
|
|
120
|
+
return answer === '' ? (suggestion === '' ? undefined : suggestion) : answer;
|
|
121
|
+
}
|
|
122
|
+
export async function modelsCommand(args, out, deps = {}) {
|
|
123
|
+
const env = deps.env ?? process.env;
|
|
124
|
+
const probe = deps.probe ?? probeModels;
|
|
125
|
+
const config = await readConfig(env);
|
|
126
|
+
if (!config.gateway?.url) {
|
|
127
|
+
out.plain('no gateway configured');
|
|
128
|
+
out.plain('');
|
|
129
|
+
out.plain(` orca setup # ${ORCAROUTER_URL}, or any gateway you name`);
|
|
130
|
+
out.plain(` orca setup --gateway <url> --key <key>`);
|
|
131
|
+
out.plain('');
|
|
132
|
+
out.plain(` a key for the default gateway: ${ORCAROUTER_CONSOLE}`);
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
let models;
|
|
136
|
+
try {
|
|
137
|
+
models = await probe(config.gateway.url, gatewayHeaders(config, env));
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
out.failure({
|
|
141
|
+
event: 'gateway.unreachable',
|
|
142
|
+
what: `could not reach ${config.gateway.url}`,
|
|
143
|
+
why: String(err instanceof Error ? err.message : err),
|
|
144
|
+
next: `check the URL and key in ${configPath(env)}, or run orca setup again`,
|
|
145
|
+
});
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
// Price where we know it, a dash where we do not. Inventing a number for an unknown model is
|
|
149
|
+
// how a comparison table ends up quoting a cost that was never real.
|
|
150
|
+
out.table(['MODEL', '$/MTOK IN', '$/MTOK OUT'], models.map((id) => {
|
|
151
|
+
const info = modelInfoFor(id);
|
|
152
|
+
return [
|
|
153
|
+
id,
|
|
154
|
+
info ? String(info.input_price_per_mtok) : '—',
|
|
155
|
+
info ? String(info.output_price_per_mtok) : '—',
|
|
156
|
+
];
|
|
157
|
+
}));
|
|
158
|
+
return models;
|
|
159
|
+
}
|
|
160
|
+
/** Same origin, tolerating a trailing slash — used only to decide whether to print the key hint. */
|
|
161
|
+
function sameOrigin(a, b) {
|
|
162
|
+
try {
|
|
163
|
+
return new URL(a).origin === new URL(b).origin;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return a.replace(/\/+$/, '') === b.replace(/\/+$/, '');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Says whether a key is set and where it came from, never what it is. */
|
|
170
|
+
function describeKey(gateway) {
|
|
171
|
+
if (gateway.api_key)
|
|
172
|
+
return 'stored';
|
|
173
|
+
if (gateway.api_key_env)
|
|
174
|
+
return `from $${gateway.api_key_env}`;
|
|
175
|
+
return 'none';
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ParsedArgs } from './args.js';
|
|
2
|
+
/**
|
|
3
|
+
* User-level configuration, which exists for exactly one job: let
|
|
4
|
+
* `orca compare --models a,b,c` reach several models without re-typing a gateway URL and a key on
|
|
5
|
+
* every invocation.
|
|
6
|
+
*
|
|
7
|
+
* That means this file holds a credential, and everything here follows from it. The file is
|
|
8
|
+
* `0600` inside a `0700` directory, the key can live in an environment variable instead of on
|
|
9
|
+
* disk, and nothing ever prints it back. The key itself never enters a trace either — the proxy
|
|
10
|
+
* adds it to the outbound request only, while what gets recorded is derived from the *incoming*
|
|
11
|
+
* request with auth stripped, so a gateway key orca injects is invisible to the recording by
|
|
12
|
+
* construction rather than by a rule someone has to remember.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* OrcaRouter, the gateway orca suggests when you do not name one.
|
|
16
|
+
*
|
|
17
|
+
* A default, never a redirect. It fills in the blank when you ask for a gateway — `orca setup` with
|
|
18
|
+
* no `--gateway` — and nothing more: a run with no gateway configured still proxies the agent's own
|
|
19
|
+
* traffic straight to the provider the agent was already talking to, on the agent's own key. Sending
|
|
20
|
+
* that somewhere the user never named would mean posting their source code to a third party as a
|
|
21
|
+
* side effect of pressing record, and their existing provider key would not authenticate there
|
|
22
|
+
* anyway. Naming a default is a recommendation; rerouting unconfigured traffic would be a decision
|
|
23
|
+
* taken on someone's behalf.
|
|
24
|
+
*
|
|
25
|
+
* The **origin**, deliberately without the `/v1` an OpenAI SDK wants. That SDK is configured with
|
|
26
|
+
* `base_url=https://api.orcarouter.ai/v1` because it appends only `/chat/completions`; orca appends
|
|
27
|
+
* the whole dialect path (`/v1/messages` or `/v1/chat/completions`) and probes `/v1/models`, so a
|
|
28
|
+
* `/v1` here would produce `/v1/v1/chat/completions`. Confirmed against the maintainers' own
|
|
29
|
+
* published action, whose `orcarouter-url` input defaults to
|
|
30
|
+
* `https://api.orcarouter.ai/v1/chat/completions`.
|
|
31
|
+
*
|
|
32
|
+
* Model ids there are namespaced by provider — `anthropic/claude-sonnet-4.6`,
|
|
33
|
+
* `openai/gpt-4o-mini`. Both places that read a model id already cope: dialect selection matches
|
|
34
|
+
* `(?:.*\/)?claude[-.]`, and `resolveModelId` strips the namespace before pricing.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ORCAROUTER_URL = "https://api.orcarouter.ai";
|
|
37
|
+
/** Where a person gets a key for the default gateway. Printed, never fetched. */
|
|
38
|
+
export declare const ORCAROUTER_CONSOLE = "https://www.orcarouter.ai/console/token";
|
|
39
|
+
export interface GatewayConfig {
|
|
40
|
+
/** Origin that serves the model APIs. One gateway usually serves both wire formats. */
|
|
41
|
+
url: string;
|
|
42
|
+
/** Stored key. Prefer `api_key_env` if you would rather not keep a credential on disk. */
|
|
43
|
+
api_key?: string;
|
|
44
|
+
/** Name of an environment variable to read the key from at call time. */
|
|
45
|
+
api_key_env?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface OrcaConfig {
|
|
48
|
+
gateway?: GatewayConfig;
|
|
49
|
+
/** Default model list for `orca compare` when `--models` is not given. */
|
|
50
|
+
models?: string[];
|
|
51
|
+
}
|
|
52
|
+
/** XDG, with the documented fallback. Honours `XDG_CONFIG_HOME` so tests need no real home. */
|
|
53
|
+
export declare function configPath(env?: NodeJS.ProcessEnv): string;
|
|
54
|
+
/**
|
|
55
|
+
* Read the config, or an empty one.
|
|
56
|
+
*
|
|
57
|
+
* Never throws. A hand-edited file with a stray comma should cost you the gateway setting, not
|
|
58
|
+
* every orca command — and a tool people reach for when something is already broken is the worst
|
|
59
|
+
* possible thing to have its own unrecoverable failure mode.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readConfig(env?: NodeJS.ProcessEnv): Promise<OrcaConfig>;
|
|
62
|
+
export declare function writeConfig(config: OrcaConfig, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
63
|
+
/**
|
|
64
|
+
* The auth header for the configured gateway, or nothing.
|
|
65
|
+
*
|
|
66
|
+
* Returns `{}` rather than an empty bearer when no key is available. An `Authorization: Bearer `
|
|
67
|
+
* with nothing after it is worse than sending none: a gateway may accept it as an anonymous
|
|
68
|
+
* session, and the user never learns their key was not applied.
|
|
69
|
+
*/
|
|
70
|
+
export declare function gatewayHeaders(config: OrcaConfig, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
71
|
+
/**
|
|
72
|
+
* Where live model calls go: flag, then environment, then the configured gateway.
|
|
73
|
+
*
|
|
74
|
+
* Needed by record *and* by replay — `--loose` and any fork continue live, and a fork that ignored
|
|
75
|
+
* the override would quietly talk to the real provider instead of the gateway the user pointed it
|
|
76
|
+
* at. Same precedence, whichever command is running: the more specific and more recent the
|
|
77
|
+
* instruction, the more it wins.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveUpstream(args: ParsedArgs, env?: NodeJS.ProcessEnv): Promise<Record<string, string> | undefined>;
|
|
80
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C;;;;;;;;;;;GAWG;AACH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,cAAc,8BAA8B,CAAC;AAE1D,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,4CAA4C,CAAC;AAE5E,MAAM,WAAW,aAAa;IAC5B,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC;IACZ,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,+FAA+F;AAC/F,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAGvE;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ1F;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,MAAM,CAAC,CASjB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,UAAU,EAChB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAgB7C"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* User-level configuration, which exists for exactly one job: let
|
|
6
|
+
* `orca compare --models a,b,c` reach several models without re-typing a gateway URL and a key on
|
|
7
|
+
* every invocation.
|
|
8
|
+
*
|
|
9
|
+
* That means this file holds a credential, and everything here follows from it. The file is
|
|
10
|
+
* `0600` inside a `0700` directory, the key can live in an environment variable instead of on
|
|
11
|
+
* disk, and nothing ever prints it back. The key itself never enters a trace either — the proxy
|
|
12
|
+
* adds it to the outbound request only, while what gets recorded is derived from the *incoming*
|
|
13
|
+
* request with auth stripped, so a gateway key orca injects is invisible to the recording by
|
|
14
|
+
* construction rather than by a rule someone has to remember.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* OrcaRouter, the gateway orca suggests when you do not name one.
|
|
18
|
+
*
|
|
19
|
+
* A default, never a redirect. It fills in the blank when you ask for a gateway — `orca setup` with
|
|
20
|
+
* no `--gateway` — and nothing more: a run with no gateway configured still proxies the agent's own
|
|
21
|
+
* traffic straight to the provider the agent was already talking to, on the agent's own key. Sending
|
|
22
|
+
* that somewhere the user never named would mean posting their source code to a third party as a
|
|
23
|
+
* side effect of pressing record, and their existing provider key would not authenticate there
|
|
24
|
+
* anyway. Naming a default is a recommendation; rerouting unconfigured traffic would be a decision
|
|
25
|
+
* taken on someone's behalf.
|
|
26
|
+
*
|
|
27
|
+
* The **origin**, deliberately without the `/v1` an OpenAI SDK wants. That SDK is configured with
|
|
28
|
+
* `base_url=https://api.orcarouter.ai/v1` because it appends only `/chat/completions`; orca appends
|
|
29
|
+
* the whole dialect path (`/v1/messages` or `/v1/chat/completions`) and probes `/v1/models`, so a
|
|
30
|
+
* `/v1` here would produce `/v1/v1/chat/completions`. Confirmed against the maintainers' own
|
|
31
|
+
* published action, whose `orcarouter-url` input defaults to
|
|
32
|
+
* `https://api.orcarouter.ai/v1/chat/completions`.
|
|
33
|
+
*
|
|
34
|
+
* Model ids there are namespaced by provider — `anthropic/claude-sonnet-4.6`,
|
|
35
|
+
* `openai/gpt-4o-mini`. Both places that read a model id already cope: dialect selection matches
|
|
36
|
+
* `(?:.*\/)?claude[-.]`, and `resolveModelId` strips the namespace before pricing.
|
|
37
|
+
*/
|
|
38
|
+
export const ORCAROUTER_URL = 'https://api.orcarouter.ai';
|
|
39
|
+
/** Where a person gets a key for the default gateway. Printed, never fetched. */
|
|
40
|
+
export const ORCAROUTER_CONSOLE = 'https://www.orcarouter.ai/console/token';
|
|
41
|
+
/** XDG, with the documented fallback. Honours `XDG_CONFIG_HOME` so tests need no real home. */
|
|
42
|
+
export function configPath(env = process.env) {
|
|
43
|
+
const base = env.XDG_CONFIG_HOME ?? join(env.HOME ?? homedir(), '.config');
|
|
44
|
+
return join(base, 'orca', 'config.json');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read the config, or an empty one.
|
|
48
|
+
*
|
|
49
|
+
* Never throws. A hand-edited file with a stray comma should cost you the gateway setting, not
|
|
50
|
+
* every orca command — and a tool people reach for when something is already broken is the worst
|
|
51
|
+
* possible thing to have its own unrecoverable failure mode.
|
|
52
|
+
*/
|
|
53
|
+
export async function readConfig(env = process.env) {
|
|
54
|
+
try {
|
|
55
|
+
const raw = await readFile(configPath(env), 'utf8');
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : {};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function writeConfig(config, env = process.env) {
|
|
64
|
+
const path = configPath(env);
|
|
65
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
66
|
+
// Set explicitly as well as passed to mkdir: an existing directory keeps whatever mode it had,
|
|
67
|
+
// and a 0755 directory makes the file's 0600 decoration.
|
|
68
|
+
await chmod(dirname(path), 0o700).catch(() => { });
|
|
69
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
70
|
+
await chmod(path, 0o600);
|
|
71
|
+
return path;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The auth header for the configured gateway, or nothing.
|
|
75
|
+
*
|
|
76
|
+
* Returns `{}` rather than an empty bearer when no key is available. An `Authorization: Bearer `
|
|
77
|
+
* with nothing after it is worse than sending none: a gateway may accept it as an anonymous
|
|
78
|
+
* session, and the user never learns their key was not applied.
|
|
79
|
+
*/
|
|
80
|
+
export function gatewayHeaders(config, env = process.env) {
|
|
81
|
+
const gateway = config.gateway;
|
|
82
|
+
if (!gateway)
|
|
83
|
+
return {};
|
|
84
|
+
const key = gateway.api_key ?? (gateway.api_key_env ? env[gateway.api_key_env] : undefined) ?? '';
|
|
85
|
+
if (key === '')
|
|
86
|
+
return {};
|
|
87
|
+
// Both header names, because a gateway fronting both wire formats reads whichever its callers
|
|
88
|
+
// send — and sending an extra header costs nothing while guessing wrong costs a confusing 401.
|
|
89
|
+
return { authorization: `Bearer ${key}`, 'x-api-key': key };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Where live model calls go: flag, then environment, then the configured gateway.
|
|
93
|
+
*
|
|
94
|
+
* Needed by record *and* by replay — `--loose` and any fork continue live, and a fork that ignored
|
|
95
|
+
* the override would quietly talk to the real provider instead of the gateway the user pointed it
|
|
96
|
+
* at. Same precedence, whichever command is running: the more specific and more recent the
|
|
97
|
+
* instruction, the more it wins.
|
|
98
|
+
*/
|
|
99
|
+
export async function resolveUpstream(args, env = process.env) {
|
|
100
|
+
const config = await readConfig(env);
|
|
101
|
+
const gateway = config.gateway?.url;
|
|
102
|
+
const out = {};
|
|
103
|
+
const anthropic = args.str('upstream-anthropic') ?? env.ORCA_UPSTREAM_ANTHROPIC ?? gateway;
|
|
104
|
+
const openai = args.str('upstream-openai') ?? env.ORCA_UPSTREAM_OPENAI ?? gateway;
|
|
105
|
+
if (anthropic)
|
|
106
|
+
out.anthropic = anthropic;
|
|
107
|
+
if (openai) {
|
|
108
|
+
out.openai = openai;
|
|
109
|
+
// The proxy resolves an origin by dialect id, and chat completions and the Responses API are
|
|
110
|
+
// two dialects sharing one provider — so a map holding only `openai` sends every Codex and
|
|
111
|
+
// Agents-SDK call straight past a gateway the user configured, on their own key, silently.
|
|
112
|
+
// `--upstream-openai` names a provider, not a wire format.
|
|
113
|
+
out['openai-responses'] = openai;
|
|
114
|
+
}
|
|
115
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { RecordedExchange } from '@orcareplay/proxy';
|
|
2
|
+
import type { TraceWriter } from '@orcareplay/core';
|
|
3
|
+
/**
|
|
4
|
+
* Turning intercepted model exchanges into trace events.
|
|
5
|
+
*
|
|
6
|
+
* This is where the project's central observation is cashed in. A tool result never appears in the
|
|
7
|
+
* response that requested it — the harness runs the tool and hands the result back on the *next*
|
|
8
|
+
* request. So we hold each `tool_use` open and close it when a later request carries the matching
|
|
9
|
+
* `tool_result`, which is how a proxy that only sees model traffic still reconstructs the whole
|
|
10
|
+
* tool loop without touching the agent.
|
|
11
|
+
*
|
|
12
|
+
* The consequence to keep in mind: a tool call whose result never comes back (the run ended, the
|
|
13
|
+
* agent crashed) stays unresolved, and `unresolved()` reports it rather than silently dropping it.
|
|
14
|
+
*/
|
|
15
|
+
export interface PendingToolCall {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
input: unknown;
|
|
19
|
+
/** seq of the tool.call event, so the tool.result can point back at it. */
|
|
20
|
+
seq: number;
|
|
21
|
+
turn: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DerivedEvent {
|
|
24
|
+
type: 'model.request' | 'model.response' | 'tool.call' | 'tool.result';
|
|
25
|
+
actor: 'agent' | 'model' | 'harness';
|
|
26
|
+
attrs: Record<string, unknown>;
|
|
27
|
+
payload?: unknown;
|
|
28
|
+
/** Index into the pending map, resolved to a real seq by the caller. */
|
|
29
|
+
causesToolId?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Positions of earlier events *in this same batch* that caused this one.
|
|
32
|
+
*
|
|
33
|
+
* Seqs do not exist until the writer has appended, so the deriver cannot name them. It names
|
|
34
|
+
* positions in the array it is about to return instead, and the recorder swaps each one for the
|
|
35
|
+
* seq it wrote. Both edges this carries are observed rather than inferred: a `tool_use` block is
|
|
36
|
+
* inside the response that emitted it, and a `tool_result` block is inside the request that
|
|
37
|
+
* carried it back.
|
|
38
|
+
*/
|
|
39
|
+
causesIndex?: number[];
|
|
40
|
+
}
|
|
41
|
+
export declare class ExchangeEventDeriver {
|
|
42
|
+
#private;
|
|
43
|
+
/** Tool calls that were issued but whose result never came back. */
|
|
44
|
+
unresolved(): PendingToolCall[];
|
|
45
|
+
/**
|
|
46
|
+
* Derive the events for one exchange. Returns them in the order they should be appended;
|
|
47
|
+
* `tool.result` events come first because they describe work that happened *before* this
|
|
48
|
+
* request was made.
|
|
49
|
+
*/
|
|
50
|
+
derive(exchange: RecordedExchange, turn: number): DerivedEvent[];
|
|
51
|
+
markPending(toolUseId: string, seq: number): void;
|
|
52
|
+
seqOf(toolUseId: string): number | undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Derive one exchange's events, resolve their causal references, and write them.
|
|
56
|
+
*
|
|
57
|
+
* Shared because it has two callers — `orca record` and the live half of a fork — and they drifted:
|
|
58
|
+
* the fork wrote every model exchange with no `causes` at all, so a forked run could be replayed
|
|
59
|
+
* and forked again but not explained. The seq resolution is the part that is easy to leave out,
|
|
60
|
+
* since it cannot happen until the writer has appended, so it lives here rather than in either
|
|
61
|
+
* caller.
|
|
62
|
+
*/
|
|
63
|
+
export declare function appendDerivedEvents(writer: TraceWriter, deriver: ExchangeEventDeriver, exchange: RecordedExchange, turn: number,
|
|
64
|
+
/**
|
|
65
|
+
* Each derived event as it is written.
|
|
66
|
+
*
|
|
67
|
+
* The recorder needs to know whether the agent ran any commands, so it can say when the shell
|
|
68
|
+
* layer was on and saw none of them. That is only knowable from what the model asked for, and
|
|
69
|
+
* this is the one place the derived events exist.
|
|
70
|
+
*/
|
|
71
|
+
onDerived?: (derived: DerivedEvent) => void): Promise<void>;
|
|
72
|
+
//# sourceMappingURL=exchange-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exchange-events.d.ts","sourceRoot":"","sources":["../src/exchange-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,eAAe,GAAG,gBAAgB,GAAG,WAAW,GAAG,aAAa,CAAC;IACvE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,qBAAa,oBAAoB;;IAS/B,oEAAoE;IACpE,UAAU,IAAI,eAAe,EAAE;IAO/B;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE;IA+EhE,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAKjD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAI7C;AAyBD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,MAAM;AACZ;;;;;;GAMG;AACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,GAC1C,OAAO,CAAC,IAAI,CAAC,CA2Bf"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export class ExchangeEventDeriver {
|
|
2
|
+
#pending = new Map();
|
|
3
|
+
/**
|
|
4
|
+
* Tool results already turned into events. Necessary because the conversation is resent in
|
|
5
|
+
* full every turn: without this, a result would be re-emitted on every subsequent request and
|
|
6
|
+
* a long run would accumulate one duplicate per turn per tool call.
|
|
7
|
+
*/
|
|
8
|
+
#closed = new Set();
|
|
9
|
+
/** Tool calls that were issued but whose result never came back. */
|
|
10
|
+
unresolved() {
|
|
11
|
+
// A pending entry now outlives the result that answered it, so that the recorder can still
|
|
12
|
+
// resolve the call's seq for `causes` after derive has returned. `#closed` is the authority on
|
|
13
|
+
// whether a call was actually answered; "still in #pending" no longer means "unanswered".
|
|
14
|
+
return [...this.#pending.values()].filter((p) => !this.#closed.has(p.id));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Derive the events for one exchange. Returns them in the order they should be appended;
|
|
18
|
+
* `tool.result` events come first because they describe work that happened *before* this
|
|
19
|
+
* request was made.
|
|
20
|
+
*/
|
|
21
|
+
derive(exchange, turn) {
|
|
22
|
+
const events = [];
|
|
23
|
+
/** Where the results this batch produced ended up, so the request can name them. */
|
|
24
|
+
const resultsAt = [];
|
|
25
|
+
for (const result of collectToolResults(exchange.canonicalRequest)) {
|
|
26
|
+
// Already accounted for on an earlier turn — the resent conversation is not new information.
|
|
27
|
+
if (this.#closed.has(result.tool_use_id))
|
|
28
|
+
continue;
|
|
29
|
+
// No pending call either: recording began mid-conversation, so this result is the first
|
|
30
|
+
// thing we know about that tool use. Record it rather than lose it.
|
|
31
|
+
const pending = this.#pending.get(result.tool_use_id);
|
|
32
|
+
events.push({
|
|
33
|
+
type: 'tool.result',
|
|
34
|
+
actor: 'harness',
|
|
35
|
+
attrs: {
|
|
36
|
+
tool_use_id: result.tool_use_id,
|
|
37
|
+
name: pending?.name ?? 'unknown',
|
|
38
|
+
is_error: result.is_error ?? false,
|
|
39
|
+
bytes: result.content.length,
|
|
40
|
+
},
|
|
41
|
+
payload: result.content,
|
|
42
|
+
causesToolId: result.tool_use_id,
|
|
43
|
+
});
|
|
44
|
+
resultsAt.push(events.length - 1);
|
|
45
|
+
// Deliberately not deleted here. The recorder resolves the call's seq *after* derive
|
|
46
|
+
// returns — it has to, since the seq only exists once the event is written — so dropping the
|
|
47
|
+
// entry in this loop meant `causes` came back empty on every tool result ever recorded, with
|
|
48
|
+
// no error to notice. `#closed` is what stops a resent result being re-emitted, so the
|
|
49
|
+
// pending entry can outlive the result it answered without any duplication.
|
|
50
|
+
this.#closed.add(result.tool_use_id);
|
|
51
|
+
}
|
|
52
|
+
events.push({
|
|
53
|
+
type: 'model.request',
|
|
54
|
+
actor: 'agent',
|
|
55
|
+
attrs: {
|
|
56
|
+
model: exchange.canonicalRequest.model,
|
|
57
|
+
dialect: exchange.dialect,
|
|
58
|
+
path: exchange.path,
|
|
59
|
+
messages: exchange.canonicalRequest.messages.length,
|
|
60
|
+
tools: exchange.canonicalRequest.tools?.length ?? 0,
|
|
61
|
+
},
|
|
62
|
+
payload: exchange.rawRequest,
|
|
63
|
+
// Absent rather than empty when nothing came back: an edge list naming nothing is noise a
|
|
64
|
+
// reader has to interpret, and `causes` is optional precisely so it can be left off.
|
|
65
|
+
...(resultsAt.length > 0 ? { causesIndex: resultsAt } : {}),
|
|
66
|
+
});
|
|
67
|
+
const response = exchange.canonicalResponse;
|
|
68
|
+
events.push({
|
|
69
|
+
type: 'model.response',
|
|
70
|
+
actor: 'model',
|
|
71
|
+
attrs: {
|
|
72
|
+
model: response?.model ?? exchange.canonicalRequest.model,
|
|
73
|
+
stop_reason: response?.stop_reason ?? 'unknown',
|
|
74
|
+
input_tokens: response?.usage.input_tokens ?? 0,
|
|
75
|
+
output_tokens: response?.usage.output_tokens ?? 0,
|
|
76
|
+
status: exchange.status,
|
|
77
|
+
duration_ms: exchange.durationMs ?? 0,
|
|
78
|
+
streamed: exchange.streamed,
|
|
79
|
+
},
|
|
80
|
+
payload: exchange.rawResponse,
|
|
81
|
+
});
|
|
82
|
+
const responseAt = events.length - 1;
|
|
83
|
+
for (const use of collectToolUses(response)) {
|
|
84
|
+
events.push({
|
|
85
|
+
type: 'tool.call',
|
|
86
|
+
actor: 'model',
|
|
87
|
+
attrs: { tool_use_id: use.id, name: use.name, input: use.input },
|
|
88
|
+
causesIndex: [responseAt],
|
|
89
|
+
});
|
|
90
|
+
// seq is assigned by the writer; the caller patches it back in via markPending.
|
|
91
|
+
this.#pending.set(use.id, { id: use.id, name: use.name, input: use.input, seq: -1, turn });
|
|
92
|
+
}
|
|
93
|
+
return events;
|
|
94
|
+
}
|
|
95
|
+
markPending(toolUseId, seq) {
|
|
96
|
+
const p = this.#pending.get(toolUseId);
|
|
97
|
+
if (p)
|
|
98
|
+
p.seq = seq;
|
|
99
|
+
}
|
|
100
|
+
seqOf(toolUseId) {
|
|
101
|
+
const seq = this.#pending.get(toolUseId)?.seq;
|
|
102
|
+
return seq === undefined || seq < 0 ? undefined : seq;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function collectToolResults(req) {
|
|
106
|
+
const out = [];
|
|
107
|
+
// Only the trailing user message can carry results for the calls we are still holding open;
|
|
108
|
+
// scanning the whole conversation would re-emit every earlier result on every turn.
|
|
109
|
+
const last = req.messages[req.messages.length - 1];
|
|
110
|
+
if (!last || last.role !== 'user')
|
|
111
|
+
return out;
|
|
112
|
+
for (const block of last.content) {
|
|
113
|
+
if (block.type === 'tool_result')
|
|
114
|
+
out.push(block);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
function collectToolUses(res) {
|
|
119
|
+
if (!res)
|
|
120
|
+
return [];
|
|
121
|
+
return res.content.filter((b) => b.type === 'tool_use');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Derive one exchange's events, resolve their causal references, and write them.
|
|
125
|
+
*
|
|
126
|
+
* Shared because it has two callers — `orca record` and the live half of a fork — and they drifted:
|
|
127
|
+
* the fork wrote every model exchange with no `causes` at all, so a forked run could be replayed
|
|
128
|
+
* and forked again but not explained. The seq resolution is the part that is easy to leave out,
|
|
129
|
+
* since it cannot happen until the writer has appended, so it lives here rather than in either
|
|
130
|
+
* caller.
|
|
131
|
+
*/
|
|
132
|
+
export async function appendDerivedEvents(writer, deriver, exchange, turn,
|
|
133
|
+
/**
|
|
134
|
+
* Each derived event as it is written.
|
|
135
|
+
*
|
|
136
|
+
* The recorder needs to know whether the agent ran any commands, so it can say when the shell
|
|
137
|
+
* layer was on and saw none of them. That is only knowable from what the model asked for, and
|
|
138
|
+
* this is the one place the derived events exist.
|
|
139
|
+
*/
|
|
140
|
+
onDerived) {
|
|
141
|
+
/** `written[i]` is the seq of the i-th derived event, which is what `causesIndex` refers to. */
|
|
142
|
+
const written = [];
|
|
143
|
+
for (const derived of deriver.derive(exchange, turn)) {
|
|
144
|
+
const causes = [];
|
|
145
|
+
if (derived.causesToolId) {
|
|
146
|
+
const seq = deriver.seqOf(derived.causesToolId);
|
|
147
|
+
if (seq !== undefined)
|
|
148
|
+
causes.push(seq);
|
|
149
|
+
}
|
|
150
|
+
for (const index of derived.causesIndex ?? []) {
|
|
151
|
+
const seq = written[index];
|
|
152
|
+
if (seq !== undefined)
|
|
153
|
+
causes.push(seq);
|
|
154
|
+
}
|
|
155
|
+
const event = await writer.append({
|
|
156
|
+
type: derived.type,
|
|
157
|
+
actor: derived.actor,
|
|
158
|
+
turn,
|
|
159
|
+
attrs: derived.attrs,
|
|
160
|
+
payload: derived.payload,
|
|
161
|
+
...(causes.length > 0 ? { causes } : {}),
|
|
162
|
+
});
|
|
163
|
+
written.push(event.seq);
|
|
164
|
+
onDerived?.(derived);
|
|
165
|
+
if (derived.type === 'tool.call') {
|
|
166
|
+
deriver.markPending(String(derived.attrs['tool_use_id']), event.seq);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=exchange-events.js.map
|