@rynx-ai/runtime 0.1.9 → 0.1.10-beta.3
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/claude/native-hook-main.js +45 -8
- package/dist/claude/native-integration.d.ts +45 -11
- package/dist/claude/native-integration.js +211 -59
- package/dist/claude/transcript.js +11 -0
- package/dist/codex/rollout-synth.d.ts +8 -3
- package/dist/codex/rollout-synth.js +64 -31
- package/dist/codex-app-server/client.d.ts +2 -1
- package/dist/codex-app-server/client.js +6 -0
- package/dist/codex-app-server/forwarder.d.ts +29 -3
- package/dist/codex-app-server/forwarder.js +135 -23
- package/dist/codex-app-server/mapping.js +37 -3
- package/dist/codex-app-server/protocol.d.ts +32 -1
- package/dist/codex-home.d.ts +10 -0
- package/dist/codex-home.js +38 -6
- package/dist/host.d.ts +5 -6
- package/dist/host.js +101 -36
- package/dist/input-resources.d.ts +13 -0
- package/dist/input-resources.js +67 -0
- package/dist/models-catalog.d.ts +5 -13
- package/dist/models-catalog.js +59 -8
- package/dist/runner/child.js +7 -4
- package/dist/runner/manager.d.ts +21 -2
- package/dist/runner/manager.js +38 -2
- package/dist/runner/protocol.d.ts +13 -5
- package/package.json +7 -2
|
@@ -200,6 +200,20 @@ export interface DynamicToolCallItem extends ThreadItemBase {
|
|
|
200
200
|
export interface WebSearchItem extends ThreadItemBase {
|
|
201
201
|
type: "webSearch";
|
|
202
202
|
query: string;
|
|
203
|
+
action?: {
|
|
204
|
+
type: "search";
|
|
205
|
+
query: string | null;
|
|
206
|
+
queries: string[] | null;
|
|
207
|
+
} | {
|
|
208
|
+
type: "openPage";
|
|
209
|
+
url: string | null;
|
|
210
|
+
} | {
|
|
211
|
+
type: "findInPage";
|
|
212
|
+
url: string | null;
|
|
213
|
+
pattern: string | null;
|
|
214
|
+
} | {
|
|
215
|
+
type: "other";
|
|
216
|
+
} | null;
|
|
203
217
|
}
|
|
204
218
|
export type ThreadItem = UserMessageItem | AgentMessageItem | ReasoningItem | PlanItem | CommandExecutionItem | FileChangeItem | McpToolCallItem | DynamicToolCallItem | WebSearchItem | (ThreadItemBase & Record<string, unknown>);
|
|
205
219
|
export interface ThreadSummary {
|
|
@@ -406,6 +420,15 @@ export interface ErrorNotificationParams {
|
|
|
406
420
|
threadId: string;
|
|
407
421
|
turnId: string;
|
|
408
422
|
}
|
|
423
|
+
/** Traex backend-capacity queue update. */
|
|
424
|
+
export interface QueueStatusNotificationParams {
|
|
425
|
+
threadId: string;
|
|
426
|
+
turnId: string;
|
|
427
|
+
/** `queued` | `waiting` | `ready`. */
|
|
428
|
+
state: string;
|
|
429
|
+
position: number | null;
|
|
430
|
+
message: string | null;
|
|
431
|
+
}
|
|
409
432
|
export type ServerNotification = {
|
|
410
433
|
method: "thread/started";
|
|
411
434
|
params: ThreadStartedNotificationParams;
|
|
@@ -445,6 +468,9 @@ export type ServerNotification = {
|
|
|
445
468
|
} | {
|
|
446
469
|
method: "thread/tokenUsage/updated";
|
|
447
470
|
params: ThreadTokenUsageUpdatedNotificationParams;
|
|
471
|
+
} | {
|
|
472
|
+
method: "queue/status";
|
|
473
|
+
params: QueueStatusNotificationParams;
|
|
448
474
|
} | {
|
|
449
475
|
method: "error";
|
|
450
476
|
params: ErrorNotificationParams;
|
|
@@ -528,6 +554,8 @@ export interface ToolRequestUserInputQuestion {
|
|
|
528
554
|
question: string;
|
|
529
555
|
isOther: boolean;
|
|
530
556
|
isSecret: boolean;
|
|
557
|
+
/** Traex extension; absent in Codex 0.144. */
|
|
558
|
+
multiSelect?: boolean;
|
|
531
559
|
options: Array<{
|
|
532
560
|
label: string;
|
|
533
561
|
description: string;
|
|
@@ -538,7 +566,10 @@ export interface ToolRequestUserInputParams {
|
|
|
538
566
|
turnId: string;
|
|
539
567
|
itemId: string;
|
|
540
568
|
questions: ToolRequestUserInputQuestion[];
|
|
541
|
-
|
|
569
|
+
/** Codex supplies an optional auto-resolution deadline. */
|
|
570
|
+
autoResolutionMs?: number | null;
|
|
571
|
+
/** Traex 0.200 supplies blocking semantics instead of a deadline. */
|
|
572
|
+
isBlocking?: boolean;
|
|
542
573
|
}
|
|
543
574
|
export interface ToolRequestUserInputResponse {
|
|
544
575
|
answers: Record<string, {
|
package/dist/codex-home.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type AgentRuntimeId } from "@rynx-ai/core";
|
|
2
|
+
export type CodexLineageRuntime = Exclude<AgentRuntimeId, "claude">;
|
|
1
3
|
/**
|
|
2
4
|
* The deterministic private CODEX_HOME path for a rynx session. PER-SESSION
|
|
3
5
|
* (uid-scoped + `sha256(sessionId)[:32]`), mirroring reference implementation's per-session
|
|
@@ -6,6 +8,8 @@
|
|
|
6
8
|
* (app-server) both locate the SAME session's home from `sessionId` alone.
|
|
7
9
|
*/
|
|
8
10
|
export declare function codexHomePath(sessionId: string): string;
|
|
11
|
+
/** Deterministic private home for a Codex-lineage runtime. */
|
|
12
|
+
export declare function runtimeHomePath(sessionId: string, runtime: CodexLineageRuntime): string;
|
|
9
13
|
/** The OLD uid-scoped shared home (pre per-session). Kept ONLY for back-compat
|
|
10
14
|
* resume fallback — a session's rollout may still live under here. */
|
|
11
15
|
export declare function legacyCodexHomePath(): string;
|
|
@@ -22,6 +26,12 @@ export declare function legacyCodexHomePath(): string;
|
|
|
22
26
|
* call so a re-login/config change propagates. Returns the private home dir.
|
|
23
27
|
*/
|
|
24
28
|
export declare function prepareCodexHome(sessionId: string, realHome?: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Prepare a private runtime home while inheriting only the login and settings
|
|
31
|
+
* files required by the selected CLI. Mutable update/NUX state remains in the
|
|
32
|
+
* real home and cannot wedge a managed app-server/TUI pair.
|
|
33
|
+
*/
|
|
34
|
+
export declare function prepareRuntimeHome(sessionId: string, runtime: CodexLineageRuntime, realHome?: string): string;
|
|
25
35
|
/**
|
|
26
36
|
* Link a resolved skill set into `<codexHome>/skills/<name>/` so the native Codex
|
|
27
37
|
* discovers them at `$CODEX_HOME/skills/` — the SAME filesystem mechanism reference implementation
|
package/dist/codex-home.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, rmSync, symlinkSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { getRuntimeProfile, resolveRuntimeHome, } from "@rynx-ai/core";
|
|
5
6
|
/** Inherit the user's LIVE login by symlink (stays in sync). */
|
|
6
7
|
const SYMLINK_FILES = ["auth.json"];
|
|
7
8
|
/** Inherit the user's settings by snapshot copy (not the mutable NUX/update state). */
|
|
8
9
|
const COPY_FILES = ["config.toml"];
|
|
10
|
+
const RUNTIME_HOME_FILES = {
|
|
11
|
+
codex: { symlink: SYMLINK_FILES, copy: COPY_FILES },
|
|
12
|
+
traex: {
|
|
13
|
+
// Traex keeps credentials below `cli/`, while its user configuration lives
|
|
14
|
+
// at the TRAE_HOME root. Keep both the current TOML name and the legacy YAML
|
|
15
|
+
// name so existing installations remain usable without an implicit migrate.
|
|
16
|
+
symlink: ["cli/auth.json"],
|
|
17
|
+
copy: ["traecli.toml", "traecli.yaml"],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
9
20
|
/** The user's real CODEX_HOME (env override, else `~/.codex`). */
|
|
10
21
|
function realCodexHome() {
|
|
11
22
|
return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
|
|
@@ -26,6 +37,13 @@ export function codexHomePath(sessionId) {
|
|
|
26
37
|
const digest = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
27
38
|
return join(rynxUidRoot(), "codex-native", digest, "codex-home");
|
|
28
39
|
}
|
|
40
|
+
/** Deterministic private home for a Codex-lineage runtime. */
|
|
41
|
+
export function runtimeHomePath(sessionId, runtime) {
|
|
42
|
+
if (runtime === "codex")
|
|
43
|
+
return codexHomePath(sessionId);
|
|
44
|
+
const digest = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
45
|
+
return join(rynxUidRoot(), "traex-native", digest, "trae-home");
|
|
46
|
+
}
|
|
29
47
|
/** The OLD uid-scoped shared home (pre per-session). Kept ONLY for back-compat
|
|
30
48
|
* resume fallback — a session's rollout may still live under here. */
|
|
31
49
|
export function legacyCodexHomePath() {
|
|
@@ -44,13 +62,25 @@ export function legacyCodexHomePath() {
|
|
|
44
62
|
* call so a re-login/config change propagates. Returns the private home dir.
|
|
45
63
|
*/
|
|
46
64
|
export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
|
|
47
|
-
|
|
65
|
+
return prepareRuntimeHome(sessionId, "codex", realHome);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Prepare a private runtime home while inheriting only the login and settings
|
|
69
|
+
* files required by the selected CLI. Mutable update/NUX state remains in the
|
|
70
|
+
* real home and cannot wedge a managed app-server/TUI pair.
|
|
71
|
+
*/
|
|
72
|
+
export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "codex"
|
|
73
|
+
? realCodexHome()
|
|
74
|
+
: resolveRuntimeHome(getRuntimeProfile(runtime))) {
|
|
75
|
+
const dir = runtimeHomePath(sessionId, runtime);
|
|
48
76
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
49
|
-
|
|
77
|
+
const files = RUNTIME_HOME_FILES[runtime];
|
|
78
|
+
for (const name of files.symlink) {
|
|
50
79
|
const src = join(realHome, name);
|
|
51
80
|
const dst = join(dir, name);
|
|
81
|
+
mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
|
|
52
82
|
try {
|
|
53
|
-
rmSync(dst, { force: true });
|
|
83
|
+
rmSync(dst, { recursive: true, force: true });
|
|
54
84
|
}
|
|
55
85
|
catch {
|
|
56
86
|
// absent — fine
|
|
@@ -64,11 +94,13 @@ export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
|
|
|
64
94
|
}
|
|
65
95
|
}
|
|
66
96
|
}
|
|
67
|
-
for (const name of
|
|
97
|
+
for (const name of files.copy) {
|
|
68
98
|
const src = join(realHome, name);
|
|
69
99
|
if (existsSync(src)) {
|
|
70
100
|
try {
|
|
71
|
-
|
|
101
|
+
const dst = join(dir, name);
|
|
102
|
+
mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
|
|
103
|
+
copyFileSync(src, dst);
|
|
72
104
|
}
|
|
73
105
|
catch {
|
|
74
106
|
// best-effort
|
package/dist/host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentSpec, type ReasoningEffort, type ResolvedExecutionBudget, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
|
|
1
|
+
import { type AgentSpec, type ReasoningEffort, type ResolvedExecutionBudget, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
|
|
2
2
|
import { type AgentRuntimeId } from "@rynx-ai/core";
|
|
3
3
|
import { type AppConfig } from "@rynx-ai/core";
|
|
4
4
|
import { createCodexChildEnv } from "./codex-child-env.js";
|
|
@@ -140,7 +140,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
140
140
|
private readonly sessionId;
|
|
141
141
|
private sessionSandbox?;
|
|
142
142
|
private sessionApprovalPolicy?;
|
|
143
|
-
private
|
|
143
|
+
private readonly runtimeHomes;
|
|
144
144
|
private readonly liveSessions;
|
|
145
145
|
private readonly liveClaudeSessions;
|
|
146
146
|
/** Claude forwarders stopped before their terminal is killed. Runner shutdown
|
|
@@ -175,8 +175,8 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
175
175
|
*/
|
|
176
176
|
private reapIdleBackends;
|
|
177
177
|
private createBackend;
|
|
178
|
-
/** This session's private
|
|
179
|
-
private
|
|
178
|
+
/** This session's private native home, shared by its app-server + TUI. */
|
|
179
|
+
private runtimeHome;
|
|
180
180
|
private createAppServerClient;
|
|
181
181
|
/** Resolve a pending native question/approval without opening a new Turn. */
|
|
182
182
|
resolveInteraction(localThreadId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<ResolveInteractionResult>;
|
|
@@ -241,7 +241,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
241
241
|
* forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
|
|
242
242
|
* the bridge instead of a short race that falls back to a second output path.
|
|
243
243
|
*/
|
|
244
|
-
injectMessage(localThreadId: string,
|
|
244
|
+
injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
|
|
245
245
|
/**
|
|
246
246
|
* Interrupt the session's active turn — the web Stop button. codex: the
|
|
247
247
|
* app-server `turn/interrupt` on the active `{threadId, turnId}` (exactly what
|
|
@@ -316,7 +316,6 @@ export declare function parseCodexLoginStatus(exitCode: number, output: string):
|
|
|
316
316
|
authMode: string | null;
|
|
317
317
|
issues: string[];
|
|
318
318
|
};
|
|
319
|
-
export declare function getCodexModel(config: AppConfig, override?: string): string;
|
|
320
319
|
export { createCodexChildEnv };
|
|
321
320
|
export declare function isUnsupportedMethodError(error: unknown): boolean;
|
|
322
321
|
/** A `thread/resume` failure meaning the thread has no (or an empty) rollout yet
|
package/dist/host.js
CHANGED
|
@@ -7,10 +7,10 @@ import { resolveAgent, resolveAgentExecution, buildAgentSkillEnvironment, Sessio
|
|
|
7
7
|
import { getRuntimeProfile, } from "@rynx-ai/core";
|
|
8
8
|
import { resolveRuntimeBinary, resolveRuntimeModel, } from "@rynx-ai/core";
|
|
9
9
|
import { createCodexChildEnv } from "./codex-child-env.js";
|
|
10
|
-
import {
|
|
10
|
+
import { prepareRuntimeHome, populateCodexSkills, } from "./codex-home.js";
|
|
11
11
|
import { materializeSkillPlugin } from "./claude/executor.js";
|
|
12
12
|
import { listClaudeModels } from "./claude/models.js";
|
|
13
|
-
import { CodexAppServerClient,
|
|
13
|
+
import { CodexAppServerClient, buildRuntimeUserInput, } from "./codex-app-server/client.js";
|
|
14
14
|
import { buildAppServerBaseArgs } from "./codex-app-server/transport.js";
|
|
15
15
|
import { WsRpcChannel, ExternalWsChannel } from "./codex-app-server/ws-channel.js";
|
|
16
16
|
import { CodexSessionForwarder } from "./codex-app-server/forwarder.js";
|
|
@@ -18,6 +18,7 @@ import { buildCodexRemoteArgs } from "./terminal/codex-tui.js";
|
|
|
18
18
|
import { buildClaudeTuiArgs } from "./terminal/claude-tui.js";
|
|
19
19
|
import { ensureProjectTrusted } from "./claude/trust.js";
|
|
20
20
|
import { claudeTranscriptPath } from "./claude/transcript.js";
|
|
21
|
+
import { claudeAttachmentToken, claudeInputText, runtimeUserContent, } from "./input-resources.js";
|
|
21
22
|
import { claudeBridgeDir, prepareClaudeBridgeDir, removeManagedClaudeSettings, writeManagedClaudeSettings, } from "./claude/native-bridge.js";
|
|
22
23
|
import { ClaudeLiveSession, injectViaTerminal, } from "./claude/native-integration.js";
|
|
23
24
|
import { buildClaudeHookSettings } from "./claude/native-hooks.js";
|
|
@@ -234,9 +235,9 @@ export class LocalAgentHost {
|
|
|
234
235
|
// this agent). Absent ⇒ fall back to the daemon `config.*` default.
|
|
235
236
|
sessionSandbox;
|
|
236
237
|
sessionApprovalPolicy;
|
|
237
|
-
//
|
|
238
|
-
// inherited) for this session's
|
|
239
|
-
|
|
238
|
+
// Private CODEX_HOME / TRAE_HOME directories (login symlinked, settings
|
|
239
|
+
// copied, update/NUX state NOT inherited) for this session's app-server + TUI.
|
|
240
|
+
runtimeHomes = new Map();
|
|
240
241
|
// Per-session codex-native live forwarders, keyed by localThreadId. The
|
|
241
242
|
// single-writer of the session's canonical events + the target of turn injection.
|
|
242
243
|
liveSessions = new Map();
|
|
@@ -329,9 +330,14 @@ export class LocalAgentHost {
|
|
|
329
330
|
: this.createAppServerClient(runtime, retryEnv);
|
|
330
331
|
return { runtime, profile, appServerClient, commandRunner };
|
|
331
332
|
}
|
|
332
|
-
/** This session's private
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
/** This session's private native home, shared by its app-server + TUI. */
|
|
334
|
+
runtimeHome(runtime) {
|
|
335
|
+
const existing = this.runtimeHomes.get(runtime);
|
|
336
|
+
if (existing)
|
|
337
|
+
return existing;
|
|
338
|
+
const prepared = prepareRuntimeHome(this.sessionId, runtime);
|
|
339
|
+
this.runtimeHomes.set(runtime, prepared);
|
|
340
|
+
return prepared;
|
|
335
341
|
}
|
|
336
342
|
createAppServerClient(runtime, extraEnv) {
|
|
337
343
|
const cliPath = resolveRuntimeBinary(runtime);
|
|
@@ -343,12 +349,16 @@ export class LocalAgentHost {
|
|
|
343
349
|
// Native co-drive: run the app-server on a loopback ws (multi-client) so a
|
|
344
350
|
// `codex --remote` TUI can attach to the same thread (stdio admits only one
|
|
345
351
|
// client). This is the sole transport — always native, aligning with reference implementation.
|
|
346
|
-
// Point it at the private
|
|
347
|
-
// never trips the real home's update/NUX state.
|
|
352
|
+
// Point it at the runtime's private home so it shares the TUI's
|
|
353
|
+
// login/settings and never trips the real home's update/NUX state.
|
|
354
|
+
const profile = getRuntimeProfile(runtime);
|
|
348
355
|
const channel = new WsRpcChannel({
|
|
349
356
|
cliPath,
|
|
350
357
|
baseArgs: buildAppServerBaseArgs(sandbox),
|
|
351
|
-
extraEnv: {
|
|
358
|
+
extraEnv: {
|
|
359
|
+
...(extraEnv ?? {}),
|
|
360
|
+
[profile.homeEnvVar]: this.runtimeHome(runtime),
|
|
361
|
+
},
|
|
352
362
|
});
|
|
353
363
|
return new CodexAppServerClient({
|
|
354
364
|
channel,
|
|
@@ -448,6 +458,7 @@ export class LocalAgentHost {
|
|
|
448
458
|
if (live?.instructions) {
|
|
449
459
|
configOverrides.push(`developer_instructions=${JSON.stringify(live.instructions)}`);
|
|
450
460
|
}
|
|
461
|
+
const profile = getRuntimeProfile(runtime);
|
|
451
462
|
return {
|
|
452
463
|
command: resolveRuntimeBinary(runtime),
|
|
453
464
|
args: buildCodexRemoteArgs({
|
|
@@ -461,7 +472,7 @@ export class LocalAgentHost {
|
|
|
461
472
|
// scopes agent-run CLIs (rynx-emulator) to this session at the daemon.
|
|
462
473
|
env: {
|
|
463
474
|
...createCodexChildEnv(process.env),
|
|
464
|
-
|
|
475
|
+
[profile.homeEnvVar]: this.runtimeHome(runtime),
|
|
465
476
|
RYNX_SESSION_ID: localThreadId,
|
|
466
477
|
},
|
|
467
478
|
};
|
|
@@ -539,7 +550,7 @@ export class LocalAgentHost {
|
|
|
539
550
|
// session-scoped dir (see resolveLiveAgentConfig); no spec / no `skills` means
|
|
540
551
|
// zero skills — the owner's catalog is never consulted.
|
|
541
552
|
const liveSkills = liveCfg.selectedSkills.map((s) => ({ name: s.name, dir: s.dir }));
|
|
542
|
-
populateCodexSkills(this.
|
|
553
|
+
populateCodexSkills(this.runtimeHome(runtime), liveSkills);
|
|
543
554
|
// Apply this agent's sandbox / approval policy to the session's app-server
|
|
544
555
|
// BEFORE it boots (getBackend below lazily creates it). Host is per-session,
|
|
545
556
|
// so these knobs are the agent's own — no cross-session bleed. Set even when
|
|
@@ -584,7 +595,7 @@ export class LocalAgentHost {
|
|
|
584
595
|
// A resumed thread keeps its stored model; otherwise use the agent-spec model (or
|
|
585
596
|
// the runtime default). The model rides a launch `-c model=` on the TUI (see
|
|
586
597
|
// `codexTerminalSpec`) / `turn/start` on injection, since rynx doesn't create the thread.
|
|
587
|
-
const model =
|
|
598
|
+
const model = resolveRuntimeModel(this.config, runtime, record?.model ?? liveCfg.model);
|
|
588
599
|
const hasCurrentEffortSource = Boolean(opts?.agentName || opts?.agentSpec || opts?.reasoningEffort);
|
|
589
600
|
const reasoningEffort = hasCurrentEffortSource
|
|
590
601
|
? liveCfg.reasoningEffort
|
|
@@ -632,7 +643,10 @@ export class LocalAgentHost {
|
|
|
632
643
|
// `turn/started` land on ONE response instead of splitting into random
|
|
633
644
|
// ids (the DB `resp_codex_<uuid>` doubling). Mirrors reference implementation `_response_id`.
|
|
634
645
|
responseId,
|
|
635
|
-
model
|
|
646
|
+
// Traex intentionally leaves the launch model empty to use its own
|
|
647
|
+
// config. Canonical/Direct events still require a printable producer
|
|
648
|
+
// label, so report the runtime when the exact model is unknown.
|
|
649
|
+
model: model || runtime,
|
|
636
650
|
});
|
|
637
651
|
return normalizer;
|
|
638
652
|
};
|
|
@@ -833,9 +847,9 @@ export class LocalAgentHost {
|
|
|
833
847
|
};
|
|
834
848
|
const sink = {
|
|
835
849
|
onTurnStart: (turnId) => startNormalizer(turnId),
|
|
836
|
-
onUserMessage: (
|
|
850
|
+
onUserMessage: (content) => {
|
|
837
851
|
const n = normalizer ?? startNormalizer();
|
|
838
|
-
for (const se of n.userInput(
|
|
852
|
+
for (const se of n.userInput(content))
|
|
839
853
|
emit(se);
|
|
840
854
|
},
|
|
841
855
|
onEvent: (event) => {
|
|
@@ -859,7 +873,20 @@ export class LocalAgentHost {
|
|
|
859
873
|
closeCanonicalInteractions();
|
|
860
874
|
if (!normalizer)
|
|
861
875
|
return;
|
|
862
|
-
|
|
876
|
+
const responseStopped = error.message === "Codex turn was interrupted";
|
|
877
|
+
const providerName = runtime === "traex" ? "Traex" : "Codex";
|
|
878
|
+
const message = responseStopped
|
|
879
|
+
? "Response stopped"
|
|
880
|
+
: runtime === "traex"
|
|
881
|
+
? error.message.replace(/^Codex\b/, providerName)
|
|
882
|
+
: error.message;
|
|
883
|
+
for (const se of normalizer.fail({
|
|
884
|
+
code: responseStopped
|
|
885
|
+
? "response_stopped"
|
|
886
|
+
: runtime === "traex" ? "traex_error" : "codex_error",
|
|
887
|
+
message,
|
|
888
|
+
source: "execution",
|
|
889
|
+
}))
|
|
863
890
|
emit(se);
|
|
864
891
|
normalizer = null;
|
|
865
892
|
currentResponseId = null;
|
|
@@ -870,7 +897,16 @@ export class LocalAgentHost {
|
|
|
870
897
|
onThreadStarted: (threadId) => this.onLiveThreadStarted(live, localThreadId, opts?.agentName ?? record?.agent, threadId),
|
|
871
898
|
onThreadActive: () => live.releaseActive?.(),
|
|
872
899
|
};
|
|
873
|
-
const forwarder = new CodexSessionForwarder(forwarderClient, sink
|
|
900
|
+
const forwarder = new CodexSessionForwarder(forwarderClient, sink, {
|
|
901
|
+
// Traex 0.200 can publish a final reasoning item immediately after
|
|
902
|
+
// `turn/completed`; without this grace frame it reopens the response and
|
|
903
|
+
// leaves Chat permanently running even though the TUI is idle. It also
|
|
904
|
+
// completes the final message before that preceding reasoning item, so
|
|
905
|
+
// hold the message over the same window to preserve semantic order.
|
|
906
|
+
turnCompletionGraceMs: runtime === "traex" ? 150 : 0,
|
|
907
|
+
assistantMessageGraceMs: runtime === "traex" ? 150 : 0,
|
|
908
|
+
surfaceQueueStatus: runtime === "traex",
|
|
909
|
+
});
|
|
874
910
|
live.forwarder = forwarder;
|
|
875
911
|
bindInteractionClient(injectClient);
|
|
876
912
|
bindInteractionClient(forwarderClient);
|
|
@@ -901,11 +937,11 @@ export class LocalAgentHost {
|
|
|
901
937
|
const previousCleanup = live.skillsCleanup;
|
|
902
938
|
try {
|
|
903
939
|
const liveSkills = liveCfg.selectedSkills.map((s) => ({ name: s.name, dir: s.dir }));
|
|
904
|
-
populateCodexSkills(this.
|
|
940
|
+
populateCodexSkills(this.runtimeHome(live.runtime), liveSkills);
|
|
905
941
|
void previousCleanup?.().catch(() => undefined);
|
|
906
942
|
this.sessionSandbox = liveCfg.sandbox;
|
|
907
943
|
this.sessionApprovalPolicy = liveCfg.approvalPolicy;
|
|
908
|
-
live.model =
|
|
944
|
+
live.model = resolveRuntimeModel(this.config, live.runtime, liveCfg.model);
|
|
909
945
|
live.reasoningEffort = liveCfg.reasoningEffort;
|
|
910
946
|
live.instructions = liveCfg.instructions;
|
|
911
947
|
live.skillsCleanup = liveCfg.skillsCleanup;
|
|
@@ -917,7 +953,7 @@ export class LocalAgentHost {
|
|
|
917
953
|
await live.injectClient
|
|
918
954
|
.threadSettingsUpdate({
|
|
919
955
|
threadId: live.threadId,
|
|
920
|
-
model: live.model,
|
|
956
|
+
...(live.model ? { model: live.model } : {}),
|
|
921
957
|
effort: live.reasoningEffort ?? null,
|
|
922
958
|
})
|
|
923
959
|
.catch(() => undefined);
|
|
@@ -1034,10 +1070,25 @@ export class LocalAgentHost {
|
|
|
1034
1070
|
* forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
|
|
1035
1071
|
* the bridge instead of a short race that falls back to a second output path.
|
|
1036
1072
|
*/
|
|
1037
|
-
async injectMessage(localThreadId,
|
|
1073
|
+
async injectMessage(localThreadId, input) {
|
|
1074
|
+
const runtimeInput = typeof input === "string"
|
|
1075
|
+
? { content: [{ type: "text", text: input }] }
|
|
1076
|
+
: input;
|
|
1038
1077
|
const claude = this.liveClaudeSessions.get(localThreadId);
|
|
1039
|
-
if (claude)
|
|
1040
|
-
|
|
1078
|
+
if (claude) {
|
|
1079
|
+
const text = claudeInputText(runtimeInput);
|
|
1080
|
+
const token = claudeAttachmentToken(text);
|
|
1081
|
+
if (token) {
|
|
1082
|
+
claude.pendingImageInputs.set(token, runtimeUserContent(runtimeInput));
|
|
1083
|
+
const expiry = setTimeout(() => claude.pendingImageInputs.delete(token), 5 * 60_000);
|
|
1084
|
+
expiry.unref?.();
|
|
1085
|
+
}
|
|
1086
|
+
const outcome = await this.injectClaude(claude, localThreadId, text);
|
|
1087
|
+
if (token && (outcome === "notLive" || outcome === "notReady")) {
|
|
1088
|
+
claude.pendingImageInputs.delete(token);
|
|
1089
|
+
}
|
|
1090
|
+
return outcome;
|
|
1091
|
+
}
|
|
1041
1092
|
const live = this.liveSessions.get(localThreadId);
|
|
1042
1093
|
if (!live)
|
|
1043
1094
|
return "notLive";
|
|
@@ -1048,13 +1099,17 @@ export class LocalAgentHost {
|
|
|
1048
1099
|
const threadId = live.threadId ?? live.forwarder.threadId();
|
|
1049
1100
|
if (!bound || !threadId)
|
|
1050
1101
|
return "notReady";
|
|
1051
|
-
const
|
|
1102
|
+
const nativeInput = buildRuntimeUserInput(runtimeInput);
|
|
1052
1103
|
try {
|
|
1053
1104
|
// Inject via the BACKEND client (the forwarder connection only observes).
|
|
1054
1105
|
if (live.forwarder.isTurnOpen()) {
|
|
1055
1106
|
const turnId = live.forwarder.currentTurnId();
|
|
1056
1107
|
if (turnId) {
|
|
1057
|
-
await live.injectClient.turnSteer({
|
|
1108
|
+
await live.injectClient.turnSteer({
|
|
1109
|
+
threadId,
|
|
1110
|
+
expectedTurnId: turnId,
|
|
1111
|
+
input: nativeInput,
|
|
1112
|
+
});
|
|
1058
1113
|
return "injected";
|
|
1059
1114
|
}
|
|
1060
1115
|
}
|
|
@@ -1062,9 +1117,9 @@ export class LocalAgentHost {
|
|
|
1062
1117
|
// agent's model even if the TUI's config default differs.
|
|
1063
1118
|
await live.injectClient.turnStart({
|
|
1064
1119
|
threadId,
|
|
1065
|
-
input,
|
|
1120
|
+
input: nativeInput,
|
|
1066
1121
|
cwd: live.cwd,
|
|
1067
|
-
model: live.model,
|
|
1122
|
+
...(live.model ? { model: live.model } : {}),
|
|
1068
1123
|
...(live.reasoningEffort ? { effort: live.reasoningEffort } : {}),
|
|
1069
1124
|
});
|
|
1070
1125
|
return "injected";
|
|
@@ -1391,6 +1446,7 @@ export class LocalAgentHost {
|
|
|
1391
1446
|
cwd,
|
|
1392
1447
|
bridgeDir,
|
|
1393
1448
|
injectLock: Promise.resolve(),
|
|
1449
|
+
pendingImageInputs: new Map(),
|
|
1394
1450
|
ready,
|
|
1395
1451
|
markReady,
|
|
1396
1452
|
discoveredReady: false,
|
|
@@ -1405,7 +1461,11 @@ export class LocalAgentHost {
|
|
|
1405
1461
|
onTurnStart: (turnId) => startNormalizer(turnId),
|
|
1406
1462
|
onUserMessage: (text) => {
|
|
1407
1463
|
const n = normalizer ?? startNormalizer();
|
|
1408
|
-
|
|
1464
|
+
const token = claudeAttachmentToken(text);
|
|
1465
|
+
const content = token ? live.pendingImageInputs.get(token) : undefined;
|
|
1466
|
+
if (token && content)
|
|
1467
|
+
live.pendingImageInputs.delete(token);
|
|
1468
|
+
for (const se of n.userInput(content ?? text))
|
|
1409
1469
|
emit(se);
|
|
1410
1470
|
},
|
|
1411
1471
|
onTerminalCommand: (cmd) => {
|
|
@@ -1458,10 +1518,14 @@ export class LocalAgentHost {
|
|
|
1458
1518
|
});
|
|
1459
1519
|
}
|
|
1460
1520
|
},
|
|
1461
|
-
onTurnError: (
|
|
1521
|
+
onTurnError: () => {
|
|
1462
1522
|
if (!normalizer)
|
|
1463
1523
|
return;
|
|
1464
|
-
for (const se of normalizer.fail({
|
|
1524
|
+
for (const se of normalizer.fail({
|
|
1525
|
+
code: "agent_error",
|
|
1526
|
+
message: "Agent turn failed",
|
|
1527
|
+
source: "execution",
|
|
1528
|
+
}))
|
|
1465
1529
|
emit(se);
|
|
1466
1530
|
normalizer = null;
|
|
1467
1531
|
currentResponseId = undefined;
|
|
@@ -1559,7 +1623,11 @@ export class LocalAgentHost {
|
|
|
1559
1623
|
const abort = new AbortController();
|
|
1560
1624
|
live.injectAbort = abort;
|
|
1561
1625
|
try {
|
|
1562
|
-
const
|
|
1626
|
+
const submissionCheckpoint = live.forwarder.beginSubmissionObservation();
|
|
1627
|
+
const ok = await injectViaTerminal(injector, text, {
|
|
1628
|
+
signal: abort.signal,
|
|
1629
|
+
submissionObserved: () => live.forwarder.hasObservedSubmissionAfter(submissionCheckpoint, text),
|
|
1630
|
+
});
|
|
1563
1631
|
return ok ? "injected" : "failed";
|
|
1564
1632
|
}
|
|
1565
1633
|
catch {
|
|
@@ -1739,9 +1807,6 @@ function stableValue(value) {
|
|
|
1739
1807
|
}
|
|
1740
1808
|
return value;
|
|
1741
1809
|
}
|
|
1742
|
-
export function getCodexModel(config, override) {
|
|
1743
|
-
return override?.trim() || config.CODEX_MODEL;
|
|
1744
|
-
}
|
|
1745
1810
|
export { createCodexChildEnv };
|
|
1746
1811
|
export function isUnsupportedMethodError(error) {
|
|
1747
1812
|
if (!error || typeof error !== "object") {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InputImageContentPart, RuntimeUserInput, UserContentPart } from "@rynx-ai/core";
|
|
2
|
+
import type { UserInput } from "./codex-app-server/protocol.js";
|
|
3
|
+
/** Convert the provider-native user echo back to resource references. Unknown
|
|
4
|
+
* local paths and remote URLs are deliberately omitted rather than exposed. */
|
|
5
|
+
export declare function codexUserContent(input: readonly UserInput[]): UserContentPart[];
|
|
6
|
+
/** Claude's native TUI has no structured image RPC. The target daemon supplies
|
|
7
|
+
* only managed local paths and the marker makes the transcript echo reversible. */
|
|
8
|
+
export declare function claudeInputText(input: RuntimeUserInput, attachmentToken?: string): string;
|
|
9
|
+
/** Extract only the one-time token. The host resolves it through its in-memory
|
|
10
|
+
* pending-injection map; user-authored lookalikes have no entry and remain text. */
|
|
11
|
+
export declare function claudeAttachmentToken(text: string): string | undefined;
|
|
12
|
+
export declare function runtimeUserContent(input: RuntimeUserInput): UserContentPart[];
|
|
13
|
+
export declare function managedImagePart(path: string): InputImageContentPart | undefined;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
const CLAUDE_ATTACHMENT_TOKEN = "RYNX_ATTACHMENT_SET";
|
|
4
|
+
const CLAUDE_ATTACHMENT_TOKEN_RE = /^\[RYNX_ATTACHMENT_SET (att_[0-9a-f-]{36}): inspect each RYNX_IMAGE_RESOURCE path with the Read tool before answering\.\]$/m;
|
|
5
|
+
/** Convert the provider-native user echo back to resource references. Unknown
|
|
6
|
+
* local paths and remote URLs are deliberately omitted rather than exposed. */
|
|
7
|
+
export function codexUserContent(input) {
|
|
8
|
+
const parts = [];
|
|
9
|
+
for (const part of input) {
|
|
10
|
+
if (part.type === "text") {
|
|
11
|
+
if (part.text)
|
|
12
|
+
parts.push({ type: "input_text", text: part.text });
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (part.type === "localImage") {
|
|
16
|
+
const image = managedImagePart(part.path);
|
|
17
|
+
if (image)
|
|
18
|
+
parts.push(image);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return parts;
|
|
22
|
+
}
|
|
23
|
+
/** Claude's native TUI has no structured image RPC. The target daemon supplies
|
|
24
|
+
* only managed local paths and the marker makes the transcript echo reversible. */
|
|
25
|
+
export function claudeInputText(input, attachmentToken = `att_${randomUUID()}`) {
|
|
26
|
+
const images = input.content.filter((part) => part.type === "local_image");
|
|
27
|
+
const text = input.content
|
|
28
|
+
.filter((part) => part.type === "text")
|
|
29
|
+
.map((part) => part.text)
|
|
30
|
+
.join("");
|
|
31
|
+
if (images.length === 0)
|
|
32
|
+
return text;
|
|
33
|
+
if (!/^att_[0-9a-f-]{36}$/.test(attachmentToken)) {
|
|
34
|
+
throw new Error("Claude attachment token is invalid");
|
|
35
|
+
}
|
|
36
|
+
const preamble = `[${CLAUDE_ATTACHMENT_TOKEN} ${attachmentToken}: inspect each RYNX_IMAGE_RESOURCE path with the Read tool before answering.]`;
|
|
37
|
+
const markers = images.map((part) => `[[RYNX_IMAGE_RESOURCE ${JSON.stringify({ path: part.path })}]]`);
|
|
38
|
+
return [preamble, ...markers, text]
|
|
39
|
+
.filter((part, index) => index <= markers.length || part.length > 0)
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|
|
42
|
+
/** Extract only the one-time token. The host resolves it through its in-memory
|
|
43
|
+
* pending-injection map; user-authored lookalikes have no entry and remain text. */
|
|
44
|
+
export function claudeAttachmentToken(text) {
|
|
45
|
+
return text.match(CLAUDE_ATTACHMENT_TOKEN_RE)?.[1];
|
|
46
|
+
}
|
|
47
|
+
export function runtimeUserContent(input) {
|
|
48
|
+
return input.content.map((part) => part.type === "text"
|
|
49
|
+
? { type: "input_text", text: part.text }
|
|
50
|
+
: { ...part.resource });
|
|
51
|
+
}
|
|
52
|
+
export function managedImagePart(path) {
|
|
53
|
+
const extension = extname(path).toLowerCase();
|
|
54
|
+
const mediaType = extension === ".png"
|
|
55
|
+
? "image/png"
|
|
56
|
+
: extension === ".jpg" || extension === ".jpeg"
|
|
57
|
+
? "image/jpeg"
|
|
58
|
+
: extension === ".webp"
|
|
59
|
+
? "image/webp"
|
|
60
|
+
: undefined;
|
|
61
|
+
if (!mediaType)
|
|
62
|
+
return undefined;
|
|
63
|
+
const resourceId = basename(path, extension);
|
|
64
|
+
if (!/^res_[0-9a-f-]{36}$/i.test(resourceId))
|
|
65
|
+
return undefined;
|
|
66
|
+
return { type: "input_image", resourceId, mediaType };
|
|
67
|
+
}
|
package/dist/models-catalog.d.ts
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backend-free model listing for codex/traex.
|
|
3
|
-
*
|
|
4
|
-
* The parent control plane no longer holds an app-server, so `/models` can't be
|
|
5
|
-
* a live `model/list` RPC anymore. Following reference implementation's static-catalog model, we
|
|
6
|
-
* serve a config-derived list: the runtime's configured default model (the one
|
|
7
|
-
* `resolveRuntimeModel` would pick), marked `isDefault`. This is intentionally
|
|
8
|
-
* minimal — a fuller curated catalogue can be added here later without touching
|
|
9
|
-
* any caller. claude already has its own static list ({@link listClaudeModels}).
|
|
10
|
-
*/
|
|
11
1
|
import { type AgentRuntimeId, type AppConfig } from "@rynx-ai/core";
|
|
12
2
|
import type { ModelListResponse } from "./codex-app-server/protocol.js";
|
|
3
|
+
export interface RuntimeModelCatalogDeps {
|
|
4
|
+
readTraexModels?: () => Promise<unknown>;
|
|
5
|
+
}
|
|
13
6
|
/**
|
|
14
7
|
* The model list for a runtime, without an execution backend.
|
|
15
|
-
*
|
|
16
|
-
* no `TRAEX_MODEL`), matching the prior "unsupported" semantics of `listModels`.
|
|
8
|
+
* Falls back to the configured model if live Traex discovery is unavailable.
|
|
17
9
|
*/
|
|
18
|
-
export declare function listRuntimeModels(config: AppConfig, runtime: AgentRuntimeId): ModelListResponse | null
|
|
10
|
+
export declare function listRuntimeModels(config: AppConfig, runtime: AgentRuntimeId, deps?: RuntimeModelCatalogDeps): Promise<ModelListResponse | null>;
|