super-dsh 0.1.0 → 0.1.2
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/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- package/client.js +0 -4
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex runtime app home bootstrap — idempotently creates the codex home and
|
|
3
|
+
* seeds it with the Codex CLI's own config set (config.toml / model catalog /
|
|
4
|
+
* auth) copied once from the native `~/.codex`.
|
|
5
|
+
*
|
|
6
|
+
* Why this is required, not cosmetic: a Codex home without `config.toml` makes
|
|
7
|
+
* the CLI fall back to its built-in OpenAI provider, so every turn dies on
|
|
8
|
+
* `wss://api.openai.com/v1/responses` with a region block (403 "Country,
|
|
9
|
+
* region, or territory not supported"). The native home is where the user's
|
|
10
|
+
* provider wiring lives (e.g. a local proxy: `model_provider = "custom"`,
|
|
11
|
+
* `base_url = "http://127.0.0.1:15721/v1"`, `model = "glm-5.3-flash"`), and the
|
|
12
|
+
* world deliberately runs with its own CODEX_HOME — so the config has to travel.
|
|
13
|
+
*
|
|
14
|
+
* Same manners as the OMP twin: never overwrites an existing file (a
|
|
15
|
+
* provisioned app home wins), skips missing sources, and is fail-soft — the app
|
|
16
|
+
* home is derived state, never a boot gate.
|
|
17
|
+
*/
|
|
18
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { resolveCodexHome } from "./codex-store.js";
|
|
22
|
+
/**
|
|
23
|
+
* Config files seeded into the codex home. `config.toml` names the provider and
|
|
24
|
+
* model, `cc-switch-model-catalog.json` is what its `model_catalog_json` key
|
|
25
|
+
* points at (a relative path, hence the same directory), and `auth.json` is the
|
|
26
|
+
* CLI's own credential record.
|
|
27
|
+
*/
|
|
28
|
+
export const CODEX_HOME_CONFIG_FILES = ["config.toml", "cc-switch-model-catalog.json", "auth.json"];
|
|
29
|
+
/** Native codex home — the copy source (`AW_CODEX_SEED_HOME` overrides). */
|
|
30
|
+
export function defaultCodexSeedHome() {
|
|
31
|
+
const configured = process.env.AW_CODEX_SEED_HOME;
|
|
32
|
+
return configured !== undefined && configured !== "" ? configured : join(homedir(), ".codex");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Ensure the codex home exists and carries the CLI config set.
|
|
36
|
+
*
|
|
37
|
+
* @param options - target/source/files overrides.
|
|
38
|
+
* @returns the resolved codex home path (always, even on a failed seed).
|
|
39
|
+
*/
|
|
40
|
+
export function ensureCodexAppHome(options = {}) {
|
|
41
|
+
const codexHome = options.codexHome ?? resolveCodexHome();
|
|
42
|
+
const source = options.seedHome ?? defaultCodexSeedHome();
|
|
43
|
+
const files = options.files ?? CODEX_HOME_CONFIG_FILES;
|
|
44
|
+
const seeded = [];
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(codexHome, { recursive: true });
|
|
47
|
+
for (const name of files) {
|
|
48
|
+
const dest = join(codexHome, name);
|
|
49
|
+
if (existsSync(dest))
|
|
50
|
+
continue;
|
|
51
|
+
const src = join(source, name);
|
|
52
|
+
if (!existsSync(src))
|
|
53
|
+
continue;
|
|
54
|
+
copyFileSync(src, dest);
|
|
55
|
+
seeded.push(name);
|
|
56
|
+
}
|
|
57
|
+
if (seeded.length > 0) {
|
|
58
|
+
options.warn?.(`codex-provider: app home ${codexHome} seeded from ${source} (${seeded.join(", ")})`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
options.warn?.(`codex-provider: app-home bootstrap failed (fail-soft): ${String(error)}`);
|
|
63
|
+
}
|
|
64
|
+
return codexHome;
|
|
65
|
+
}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexSdkClient — the SDK-line client seam (AW-E Task 3).
|
|
3
|
+
*
|
|
4
|
+
* Public surface mirrors `OmpSdkClient` (agent-omp/src/sdk-client.ts) exactly —
|
|
5
|
+
* `spawn` / `on` / `onFailure` / `sendRaw` / `send` / `getState` / `getMessages`
|
|
6
|
+
* / `getSessionStats` / `getSubagents` / `prompt` / `followUp` / `steer` /
|
|
7
|
+
* `setModel` / `abort` / `newSession` / `close` / `ensureStarted` / `spawned` —
|
|
8
|
+
* so the Task 7 agent.ts/index.ts port compiles against this client the way it
|
|
9
|
+
* compiles against the omp one. Instead of one `omp --mode rpc` sidecar per
|
|
10
|
+
* session, ALL clients multiplex ONE module-level refcounted `Codex` instance
|
|
11
|
+
* (same acquire()/release() pattern as the omp sidecar; the SDK spawns its own
|
|
12
|
+
* per-turn codex process, so dropping the shared instance needs no stop call).
|
|
13
|
+
*
|
|
14
|
+
* Codex home: the shared instance is constructed with `{ env: { ...process.env } }`
|
|
15
|
+
* (SDK env is REPLACE semantics — Task 1 spike fact, so the spread is
|
|
16
|
+
* mandatory) and carries NO `CODEX_HOME`: the CLI resolves its own native
|
|
17
|
+
* `~/.codex` (user ruling 2026-09-17 — never redirect the native app home at
|
|
18
|
+
* spawn; tests redirect via `process.env.CODEX_HOME`). The SDK constructor
|
|
19
|
+
* itself is injectable via {@link setCodexFactory}.
|
|
20
|
+
*
|
|
21
|
+
* Wire events flow through `projectThreadEvent` (codex-events.ts) before
|
|
22
|
+
* reaching listeners, so Task 7's agent.ts switch is unchanged from omp.
|
|
23
|
+
*/
|
|
24
|
+
import { Codex } from "@openai/codex-sdk";
|
|
25
|
+
import { projectThreadEvent } from "./codex-events.js";
|
|
26
|
+
let codexFactory = (options) => new Codex(options);
|
|
27
|
+
/** Replace the SDK constructor (tests inject a fake Codex). */
|
|
28
|
+
export function setCodexFactory(factory) {
|
|
29
|
+
codexFactory = factory;
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Shared Codex instance (module-level singleton, refcounted — omp sidecar pattern)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
let sharedCodex = null;
|
|
35
|
+
let refcount = 0;
|
|
36
|
+
function acquire() {
|
|
37
|
+
if (sharedCodex === null) {
|
|
38
|
+
sharedCodex = codexFactory({ env: { ...process.env } });
|
|
39
|
+
}
|
|
40
|
+
refcount += 1;
|
|
41
|
+
return sharedCodex;
|
|
42
|
+
}
|
|
43
|
+
function release() {
|
|
44
|
+
refcount = Math.max(0, refcount - 1);
|
|
45
|
+
if (refcount === 0)
|
|
46
|
+
sharedCodex = null;
|
|
47
|
+
}
|
|
48
|
+
/** Test/observability hook: current shared-instance refcount (0 = none alive). */
|
|
49
|
+
export function codexClientRefCount() {
|
|
50
|
+
return refcount;
|
|
51
|
+
}
|
|
52
|
+
function ok(command) {
|
|
53
|
+
return { type: "response", command, success: true };
|
|
54
|
+
}
|
|
55
|
+
function asRecord(value) {
|
|
56
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
57
|
+
return null;
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse the omp-style spawn args. `--approval-mode <v>` is accepted and
|
|
62
|
+
* applied verbatim as the thread's launch `approvalPolicy` — the 3-preset
|
|
63
|
+
* mapping resolves the VALUE provider-side (Task 7's permission.ts) and the
|
|
64
|
+
* thread carries it on every construction (start, resume, setModel rebuild).
|
|
65
|
+
* `--resume-thread <id>` constructs the client on `codex.resumeThread(id, …)`
|
|
66
|
+
* — the thread-identity resume (plan Task 7), strictly distinct from the
|
|
67
|
+
* rejected omp-style `--resume <file>`, which keeps failing loudly exactly
|
|
68
|
+
* like unknown args.
|
|
69
|
+
*/
|
|
70
|
+
function parseSpawnArgs(args) {
|
|
71
|
+
const out = {};
|
|
72
|
+
for (let i = 0; i < args.length; i++) {
|
|
73
|
+
if (args[i] === "--approval-mode" && args[i + 1] !== undefined) {
|
|
74
|
+
out.approvalMode = args[++i];
|
|
75
|
+
}
|
|
76
|
+
else if (args[i] === "--resume-thread" && args[i + 1] !== undefined) {
|
|
77
|
+
out.resumeThreadId = args[++i];
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
throw new Error(`CodexSdkClient: unsupported spawn arg "${String(args[i])}" (SDK line)`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
export class CodexSdkClient {
|
|
87
|
+
#codex;
|
|
88
|
+
#listeners = new Set();
|
|
89
|
+
#failureListeners = new Set();
|
|
90
|
+
#threadOptions;
|
|
91
|
+
#messages = [];
|
|
92
|
+
#queue = [];
|
|
93
|
+
#approvalMode;
|
|
94
|
+
#thread;
|
|
95
|
+
#closed = false;
|
|
96
|
+
#failure = null;
|
|
97
|
+
#inFlight = false;
|
|
98
|
+
#currentAbort = null;
|
|
99
|
+
#currentRun = null;
|
|
100
|
+
#pendingModel = null;
|
|
101
|
+
#appliedModel = null;
|
|
102
|
+
#lastUsage = null;
|
|
103
|
+
/** Bumped by newSession()/close() to orphan runs started on an earlier thread. */
|
|
104
|
+
#sessionEpoch = 0;
|
|
105
|
+
constructor(codex, cwd, approvalMode, resumeThreadId) {
|
|
106
|
+
this.#codex = codex;
|
|
107
|
+
this.#approvalMode = approvalMode;
|
|
108
|
+
// skipGitRepoCheck: adapters spawn from arbitrary test/worktree cwds (Task 1 spike precedent).
|
|
109
|
+
// Launch-only approval mapping (Task 7): present only when the provider
|
|
110
|
+
// resolved a preset, so plain spawns keep the bare ThreadOptions shape.
|
|
111
|
+
this.#threadOptions = {
|
|
112
|
+
workingDirectory: cwd,
|
|
113
|
+
skipGitRepoCheck: true,
|
|
114
|
+
...(approvalMode === undefined ? {} : { approvalPolicy: approvalMode }),
|
|
115
|
+
};
|
|
116
|
+
// `--resume-thread <id>` re-attaches to the existing Codex thread (the
|
|
117
|
+
// model context lives in Codex's rollout); plain spawn starts a fresh one.
|
|
118
|
+
this.#thread =
|
|
119
|
+
resumeThreadId === undefined ? codex.startThread(this.#threadOptions) : codex.resumeThread(resumeThreadId, this.#threadOptions);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Start a session on the shared Codex instance and resolve once ready.
|
|
123
|
+
* Mirrors `OmpSdkClient.spawn(args, cwd)`.
|
|
124
|
+
*/
|
|
125
|
+
static async spawn(args = [], cwd) {
|
|
126
|
+
const { approvalMode, resumeThreadId } = parseSpawnArgs(args);
|
|
127
|
+
const codex = acquire();
|
|
128
|
+
try {
|
|
129
|
+
return new CodexSdkClient(codex, cwd, approvalMode, resumeThreadId);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
release();
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Register a wire event listener; returns the unsubscriber. */
|
|
137
|
+
on(listener) {
|
|
138
|
+
this.#listeners.add(listener);
|
|
139
|
+
return () => {
|
|
140
|
+
this.#listeners.delete(listener);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Register a failure listener; returns the unsubscriber. */
|
|
144
|
+
onFailure(listener) {
|
|
145
|
+
this.#failureListeners.add(listener);
|
|
146
|
+
return () => {
|
|
147
|
+
this.#failureListeners.delete(listener);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Raw-command escape hatch. The SDK line has no stdio command channel, so
|
|
152
|
+
* RPC-specific records (e.g. `extension_ui_response` approval answers) are
|
|
153
|
+
* accepted and logged under CODEX_TRACE=1 — approval flows route through
|
|
154
|
+
* SDK-side approval work (Task 7 parity item).
|
|
155
|
+
*/
|
|
156
|
+
sendRaw(command) {
|
|
157
|
+
if (this.#failure !== null || this.#closed)
|
|
158
|
+
return;
|
|
159
|
+
if (process.env.CODEX_TRACE === "1") {
|
|
160
|
+
process.stderr.write(`[codex-sdk] sendRaw (no-op on SDK line): ${JSON.stringify(command).slice(0, 160)}\n`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Generic command sender mapping the omp command table onto client methods. */
|
|
164
|
+
async send(command) {
|
|
165
|
+
switch (command.type) {
|
|
166
|
+
case "prompt":
|
|
167
|
+
await this.prompt(String(command.message));
|
|
168
|
+
return ok("prompt");
|
|
169
|
+
case "steer":
|
|
170
|
+
await this.steer(String(command.message));
|
|
171
|
+
return ok("steer");
|
|
172
|
+
case "follow_up":
|
|
173
|
+
await this.followUp(String(command.message));
|
|
174
|
+
return ok("follow_up");
|
|
175
|
+
case "abort":
|
|
176
|
+
await this.abort();
|
|
177
|
+
return ok("abort");
|
|
178
|
+
case "set_model":
|
|
179
|
+
await this.setModel(String(command.provider), String(command.modelId));
|
|
180
|
+
return ok("set_model");
|
|
181
|
+
case "new_session":
|
|
182
|
+
await this.newSession();
|
|
183
|
+
return ok("new_session");
|
|
184
|
+
case "get_state":
|
|
185
|
+
return { type: "response", command: "get_state", success: true, data: await this.getState() };
|
|
186
|
+
case "get_messages":
|
|
187
|
+
return { type: "response", command: "get_messages", success: true, data: { messages: await this.getMessages() } };
|
|
188
|
+
default:
|
|
189
|
+
throw new Error(`CodexSdkClient.send: unsupported command "${String(command.type)}" (SDK line)`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Query session state; returns the `get_state`-shaped payload. */
|
|
193
|
+
async getState() {
|
|
194
|
+
this.#ensureUsable();
|
|
195
|
+
return { isStreaming: this.#inFlight };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Best-effort conversation mirror accumulated from the wire (user echoes at
|
|
199
|
+
* acceptance, assistant message_end, toolResult). The authoritative
|
|
200
|
+
* transcript comes from the rollout scan (codex-store.ts); reasoning-only
|
|
201
|
+
* messages are not mirrored. Fail-soft: [] when nothing accumulated.
|
|
202
|
+
*/
|
|
203
|
+
async getMessages() {
|
|
204
|
+
try {
|
|
205
|
+
return [...this.#messages];
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Live token accounting from the last turn_end usage (fail-soft {}). */
|
|
212
|
+
async getSessionStats() {
|
|
213
|
+
try {
|
|
214
|
+
const usage = this.#lastUsage;
|
|
215
|
+
if (usage === null)
|
|
216
|
+
return {};
|
|
217
|
+
const tokens = {};
|
|
218
|
+
const map = [
|
|
219
|
+
["input", "input_tokens"],
|
|
220
|
+
["output", "output_tokens"],
|
|
221
|
+
["cacheRead", "cached_input_tokens"],
|
|
222
|
+
["cacheWrite", "cache_write_input_tokens"],
|
|
223
|
+
];
|
|
224
|
+
for (const [wireKey, codexKey] of map) {
|
|
225
|
+
const value = usage[codexKey];
|
|
226
|
+
if (typeof value === "number")
|
|
227
|
+
tokens[wireKey] = value;
|
|
228
|
+
}
|
|
229
|
+
return { tokens };
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Codex has no subagent registry (fail-soft constant, omp surface parity). */
|
|
236
|
+
async getSubagents() {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
/** Start a turn with a user prompt (fire-and-forget acceptance). */
|
|
240
|
+
async prompt(message) {
|
|
241
|
+
this.#ensureUsable();
|
|
242
|
+
this.#enqueueOrRun(message);
|
|
243
|
+
return ok("prompt");
|
|
244
|
+
}
|
|
245
|
+
/** Queue a follow-up on a busy session (runs immediately when idle). */
|
|
246
|
+
async followUp(message) {
|
|
247
|
+
this.#ensureUsable();
|
|
248
|
+
this.#enqueueOrRun(message);
|
|
249
|
+
return ok("follow_up");
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Steer an in-flight turn — 2026-09-10 gap table: the SDK line has no steer,
|
|
253
|
+
* so this is the queued-followUp approximation (plan Global Constraints:
|
|
254
|
+
* "无 steer(→ followUp 队列近似)"); while idle it degrades to a plain
|
|
255
|
+
* prompt, while in flight the message runs right after the current turn
|
|
256
|
+
* settles.
|
|
257
|
+
*/
|
|
258
|
+
async steer(message) {
|
|
259
|
+
this.#ensureUsable();
|
|
260
|
+
if (this.#inFlight && process.env.CODEX_TRACE === "1") {
|
|
261
|
+
process.stderr.write("[codex-sdk] steer approximated as queued followUp (SDK gap)\n");
|
|
262
|
+
}
|
|
263
|
+
this.#enqueueOrRun(message);
|
|
264
|
+
return ok("steer");
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Select the active model. SDK has no live setModel (2026-09-10 gap table):
|
|
268
|
+
* the id is stored and applied as the `model` ThreadOption on the NEXT run —
|
|
269
|
+
* via resumeThread when the thread already has an id, so conversation
|
|
270
|
+
* context survives the model switch. `provider` is accepted for surface
|
|
271
|
+
* parity; codex resolves models from its own catalog.
|
|
272
|
+
*/
|
|
273
|
+
async setModel(provider, modelId) {
|
|
274
|
+
this.#ensureUsable();
|
|
275
|
+
void provider;
|
|
276
|
+
this.#pendingModel = modelId;
|
|
277
|
+
return ok("set_model");
|
|
278
|
+
}
|
|
279
|
+
/** Abort the current streaming run (AbortController per run, turn-level signal). */
|
|
280
|
+
async abort() {
|
|
281
|
+
this.#currentAbort?.abort();
|
|
282
|
+
const run = this.#currentRun;
|
|
283
|
+
if (run !== null)
|
|
284
|
+
await run; // settle first so state is deterministically idle
|
|
285
|
+
return ok("abort");
|
|
286
|
+
}
|
|
287
|
+
/** Discard the current thread and message mirror; start fresh. */
|
|
288
|
+
async newSession() {
|
|
289
|
+
this.#ensureUsable();
|
|
290
|
+
// Invalidate THIS run's queue-drain before aborting it: its finally must
|
|
291
|
+
// not start a queued message on the thread we are about to discard.
|
|
292
|
+
this.#sessionEpoch += 1;
|
|
293
|
+
this.#currentAbort?.abort();
|
|
294
|
+
const run = this.#currentRun;
|
|
295
|
+
if (run !== null)
|
|
296
|
+
await run;
|
|
297
|
+
this.#queue.length = 0;
|
|
298
|
+
this.#messages.length = 0;
|
|
299
|
+
this.#thread = this.#codex.startThread(this.#threadOptions);
|
|
300
|
+
this.#appliedModel = null; // a pending model still applies at the next run
|
|
301
|
+
return ok("new_session");
|
|
302
|
+
}
|
|
303
|
+
/** Lazy-surface parity: a real client is born live (see OmpAgentRpc.spawned). */
|
|
304
|
+
ensureStarted() {
|
|
305
|
+
return Promise.resolve();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* The current Codex thread id, exactly as the SDK thread exposes it (null
|
|
309
|
+
* until the thread materializes one). The provider keeps the
|
|
310
|
+
* threadId ↔ Dash-session-id index from this (Task 7).
|
|
311
|
+
*/
|
|
312
|
+
get threadId() {
|
|
313
|
+
return this.#thread.id;
|
|
314
|
+
}
|
|
315
|
+
/** Lazy-surface parity: a real client is born live (see OmpAgentRpc.spawned). */
|
|
316
|
+
get spawned() {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
/** Release the shared-instance refcount and fail all future operations. */
|
|
320
|
+
close() {
|
|
321
|
+
if (this.#closed)
|
|
322
|
+
return;
|
|
323
|
+
this.#closed = true;
|
|
324
|
+
this.#sessionEpoch += 1; // a settled run must not drain the queue after close
|
|
325
|
+
this.#currentAbort?.abort();
|
|
326
|
+
release();
|
|
327
|
+
this.#fail(new Error("codex sdk client closed"));
|
|
328
|
+
}
|
|
329
|
+
// -------------------------------------------------------------------------
|
|
330
|
+
#ensureUsable() {
|
|
331
|
+
if (this.#failure !== null)
|
|
332
|
+
throw this.#failure;
|
|
333
|
+
if (this.#closed)
|
|
334
|
+
throw new Error("codex sdk client is closed");
|
|
335
|
+
}
|
|
336
|
+
/** Queue behind the in-flight turn, or start a run now. */
|
|
337
|
+
#enqueueOrRun(message) {
|
|
338
|
+
if (this.#inFlight) {
|
|
339
|
+
this.#queue.push(message);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.#startRun(message);
|
|
343
|
+
}
|
|
344
|
+
#startRun(message) {
|
|
345
|
+
this.#messages.push({ role: "user", content: [{ type: "text", text: message }] });
|
|
346
|
+
const run = this.#runTurn(message);
|
|
347
|
+
this.#currentRun = run;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* One streamed turn: runStreamed on the thread, pump raw SDK events through
|
|
351
|
+
* projectThreadEvent, mirror the conversation, settle to agent_end. Mirrors
|
|
352
|
+
* the real SDK abort contract: an aborted turn just ENDS the stream (Task 1
|
|
353
|
+
* spike — no throw guarantee), so it settles like a normal end; thrown errors
|
|
354
|
+
* and the fatal top-level `error` event go to onFailure.
|
|
355
|
+
*/
|
|
356
|
+
async #runTurn(message) {
|
|
357
|
+
// Session epoch: bumped by newSession()/close() so a run that started on
|
|
358
|
+
// the previous thread never drains the queue onto the new one.
|
|
359
|
+
const epoch = this.#sessionEpoch;
|
|
360
|
+
this.#inFlight = true;
|
|
361
|
+
const controller = new AbortController();
|
|
362
|
+
this.#currentAbort = controller;
|
|
363
|
+
try {
|
|
364
|
+
this.#applyPendingModel();
|
|
365
|
+
const streamed = await this.#thread.runStreamed(message, { signal: controller.signal });
|
|
366
|
+
for await (const raw of streamed.events) {
|
|
367
|
+
const record = asRecord(raw);
|
|
368
|
+
if (record !== null && record.type === "error") {
|
|
369
|
+
// Fatal stream error: SDK contract surfaces it here, not as a throw.
|
|
370
|
+
this.#fail(new Error(typeof record.message === "string" ? record.message : "codex stream error"));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const wire = projectThreadEvent(raw);
|
|
374
|
+
if (wire !== null) {
|
|
375
|
+
const events = Array.isArray(wire) ? wire : [wire];
|
|
376
|
+
for (const event of events) {
|
|
377
|
+
this.#emit(event);
|
|
378
|
+
this.#mirror(event);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
this.#emit({ type: "agent_end" });
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
if (controller.signal.aborted) {
|
|
386
|
+
// Real SDK: the stream ends behind an abort — settle, stay idle.
|
|
387
|
+
this.#emit({ type: "agent_end" });
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
this.#fail(error instanceof Error ? error : new Error(String(error)));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
this.#inFlight = false;
|
|
395
|
+
this.#currentAbort = null;
|
|
396
|
+
this.#currentRun = null;
|
|
397
|
+
const next = this.#queue.shift();
|
|
398
|
+
if (next !== undefined && epoch === this.#sessionEpoch && this.#failure === null && !this.#closed) {
|
|
399
|
+
this.#startRun(next);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/** Rebuild the thread with the pending model when one is pending (next run). */
|
|
404
|
+
#applyPendingModel() {
|
|
405
|
+
if (this.#pendingModel === this.#appliedModel)
|
|
406
|
+
return;
|
|
407
|
+
const model = this.#pendingModel;
|
|
408
|
+
const options = model === null ? { ...this.#threadOptions } : { ...this.#threadOptions, model };
|
|
409
|
+
const currentId = this.#thread.id;
|
|
410
|
+
this.#thread =
|
|
411
|
+
currentId !== null ? this.#codex.resumeThread(currentId, options) : this.#codex.startThread(options);
|
|
412
|
+
this.#appliedModel = model;
|
|
413
|
+
}
|
|
414
|
+
/** Mirror wire events into the getMessages accumulator (best effort). */
|
|
415
|
+
#mirror(wire) {
|
|
416
|
+
if (wire.type === "message_end") {
|
|
417
|
+
const message = asRecord(wire.message);
|
|
418
|
+
if (message !== null && message.role === "assistant") {
|
|
419
|
+
// Reasoning-only messages are not mirrored (see getMessages doc).
|
|
420
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
421
|
+
const reasoningOnly = blocks.length > 0 && blocks.every((block) => block.type === "thinking");
|
|
422
|
+
if (!reasoningOnly)
|
|
423
|
+
this.#messages.push(message);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (wire.type === "tool_execution_end") {
|
|
428
|
+
const result = asRecord(wire.result);
|
|
429
|
+
this.#messages.push({
|
|
430
|
+
role: "toolResult",
|
|
431
|
+
toolCallId: wire.toolCallId,
|
|
432
|
+
content: Array.isArray(result?.content) ? result.content : [],
|
|
433
|
+
isError: wire.isError === true,
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (wire.type === "turn_end") {
|
|
438
|
+
const data = asRecord(wire.data);
|
|
439
|
+
const usage = asRecord(data?.usage);
|
|
440
|
+
if (usage !== null)
|
|
441
|
+
this.#lastUsage = usage;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
#emit(event) {
|
|
445
|
+
if (this.#closed)
|
|
446
|
+
return;
|
|
447
|
+
for (const listener of [...this.#listeners]) {
|
|
448
|
+
try {
|
|
449
|
+
listener(event);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
process.stderr.write(`codex sdk listener error: ${String(error)}\n`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
#fail(error) {
|
|
457
|
+
if (this.#failure !== null)
|
|
458
|
+
return;
|
|
459
|
+
this.#failure = error;
|
|
460
|
+
for (const listener of [...this.#failureListeners]) {
|
|
461
|
+
try {
|
|
462
|
+
listener(error);
|
|
463
|
+
}
|
|
464
|
+
catch (listenerError) {
|
|
465
|
+
process.stderr.write(`codex sdk failure listener error: ${String(listenerError)}\n`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|