@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.
@@ -3,25 +3,76 @@
3
3
  *
4
4
  * The parent control plane no longer holds an app-server, so `/models` can't be
5
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}).
6
+ * serve a config-derived Codex list and Traex's native `models --json` catalog.
7
+ * claude already has its own static list ({@link listClaudeModels}).
10
8
  */
11
- import { resolveRuntimeModel, } from "@rynx-ai/core";
9
+ import { execFile } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+ import { resolveRuntimeBinary, resolveRuntimeModel, } from "@rynx-ai/core";
12
12
  import { listClaudeModels } from "./claude/models.js";
13
+ const execFileAsync = promisify(execFile);
14
+ const TRAEX_MODELS_TIMEOUT_MS = 8_000;
15
+ const TRAEX_MODELS_MAX_BYTES = 2 * 1024 * 1024;
13
16
  /**
14
17
  * The model list for a runtime, without an execution backend.
15
- * Returns `null` when the runtime exposes no resolvable model (e.g. traex with
16
- * no `TRAEX_MODEL`), matching the prior "unsupported" semantics of `listModels`.
18
+ * Falls back to the configured model if live Traex discovery is unavailable.
17
19
  */
18
- export function listRuntimeModels(config, runtime) {
20
+ export async function listRuntimeModels(config, runtime, deps = {}) {
19
21
  if (runtime === "claude") {
20
22
  return listClaudeModels();
21
23
  }
24
+ if (runtime === "traex") {
25
+ try {
26
+ const value = await (deps.readTraexModels ?? readTraexModels)();
27
+ const models = normalizeTraexModels(value, resolveRuntimeModel(config, runtime).trim());
28
+ if (models.length > 0)
29
+ return { data: models };
30
+ }
31
+ catch {
32
+ // Keep the configured fallback usable when Traex is missing/logged out.
33
+ }
34
+ }
22
35
  const model = resolveRuntimeModel(config, runtime).trim();
23
36
  if (!model) {
24
37
  return null;
25
38
  }
26
39
  return { data: [{ id: model, model, isDefault: true }] };
27
40
  }
41
+ async function readTraexModels() {
42
+ const { stdout } = await execFileAsync(resolveRuntimeBinary("traex"), ["models", "--json"], {
43
+ encoding: "utf8",
44
+ timeout: TRAEX_MODELS_TIMEOUT_MS,
45
+ maxBuffer: TRAEX_MODELS_MAX_BYTES,
46
+ });
47
+ return JSON.parse(stdout);
48
+ }
49
+ function normalizeTraexModels(value, configuredDefault) {
50
+ if (!Array.isArray(value))
51
+ return [];
52
+ const models = [];
53
+ const seen = new Set();
54
+ for (const item of value) {
55
+ if (!item || typeof item !== "object" || Array.isArray(item))
56
+ continue;
57
+ const record = item;
58
+ const id = typeof record.name === "string" ? record.name.trim() : "";
59
+ if (!id || seen.has(id))
60
+ continue;
61
+ seen.add(id);
62
+ models.push({
63
+ id,
64
+ model: id,
65
+ ...(typeof record.description === "string" ? { description: record.description } : {}),
66
+ isDefault: configuredDefault ? id === configuredDefault : models.length === 0,
67
+ ...(typeof record.context_window === "number" ? { contextWindow: record.context_window } : {}),
68
+ ...(Array.isArray(record.supported_mime_types)
69
+ ? { supportedMimeTypes: record.supported_mime_types }
70
+ : {}),
71
+ ...(record._meta && typeof record._meta === "object" ? { _meta: record._meta } : {}),
72
+ });
73
+ }
74
+ if (configuredDefault && !seen.has(configuredDefault)) {
75
+ models.unshift({ id: configuredDefault, model: configuredDefault, isDefault: true });
76
+ }
77
+ return models;
78
+ }
@@ -161,9 +161,11 @@ export class RunnerSession {
161
161
  if (!this.terminals.get(`${msg.localThreadId}-main`)?.isAlive()) {
162
162
  await this.launchCodexPane(msg.localThreadId, msg.cols, msg.rows);
163
163
  }
164
- const ready = provider.waitLiveReady
165
- ? await provider.waitLiveReady(msg.localThreadId)
166
- : true;
164
+ const ready = msg.waitForReady === false
165
+ ? true
166
+ : provider.waitLiveReady
167
+ ? await provider.waitLiveReady(msg.localThreadId)
168
+ : true;
167
169
  this.transport.send({
168
170
  t: "live.ready",
169
171
  reqId: msg.reqId,
@@ -185,7 +187,8 @@ export class RunnerSession {
185
187
  async inject(msg) {
186
188
  const provider = this.liveProvider;
187
189
  try {
188
- const outcome = (await provider.injectMessage?.(msg.localThreadId, msg.text)) ?? "notLive";
190
+ const input = msg.input ?? msg.text;
191
+ const outcome = (await provider.injectMessage?.(msg.localThreadId, input)) ?? "notLive";
189
192
  this.transport.send({ t: "injected", reqId: msg.reqId, localThreadId: msg.localThreadId, outcome });
190
193
  }
191
194
  catch (error) {
@@ -16,7 +16,7 @@
16
16
  * channel.
17
17
  */
18
18
  import { spawn as nodeSpawn } from "node:child_process";
19
- import { type AgentCapabilities, type AgentRuntimeId, type AgentSpec, type AppConfig, type CapabilityResult, type ModelListResponse, type ReasoningEffort, type SessionInteractionResolution, type SessionEvent, type ThreadGoal } from "@rynx-ai/core";
19
+ import { type AgentCapabilities, type AgentRuntimeId, type AgentSpec, type AppConfig, type CapabilityResult, type ModelListResponse, type ReasoningEffort, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent, type ThreadGoal } from "@rynx-ai/core";
20
20
  import { type CodexSessionStore } from "../host.js";
21
21
  import type { ResolveInteractionResult } from "../interactions.js";
22
22
  import { type InjectOutcome, type TerminalOpenErrorCode, type TerminalRole } from "./protocol.js";
@@ -49,6 +49,10 @@ export interface RunnerManagerOptions {
49
49
  shutdownGraceMs?: number;
50
50
  /** Final bounded wait for child exit after SIGKILL. */
51
51
  shutdownKillGraceMs?: number;
52
+ /** Max wait for a setup-pane launch acknowledgement. */
53
+ liveStartTimeoutMs?: number;
54
+ /** Max wait for a Provider-native thread to become ready. */
55
+ liveReadyTimeoutMs?: number;
52
56
  /** Injected for tests. */
53
57
  spawn?: typeof nodeSpawn;
54
58
  now?: () => number;
@@ -124,6 +128,8 @@ export declare class RunnerManager implements AgentCapabilities {
124
128
  private readonly reapTimer;
125
129
  private readonly shutdownGraceMs;
126
130
  private readonly shutdownKillGraceMs;
131
+ private readonly liveStartTimeoutMs;
132
+ private readonly liveReadyTimeoutMs;
127
133
  private stopping;
128
134
  private stopPromise;
129
135
  /** Sink for mirrored {@link SessionEvent}s from every session's forwarder. */
@@ -176,6 +182,19 @@ export declare class RunnerManager implements AgentCapabilities {
176
182
  agentName?: string;
177
183
  agentSpec?: AgentSpec;
178
184
  }): Promise<boolean>;
185
+ /** Start the Provider pane without waiting for login/onboarding to create a
186
+ * native thread. This is the setup-terminal gate; callers may attach as soon
187
+ * as it resolves, while normal message delivery still uses ensureLiveSession. */
188
+ startLiveSession(localThreadId: string, opts?: {
189
+ cwd?: string;
190
+ cols?: number;
191
+ rows?: number;
192
+ runtime?: AgentRuntimeId;
193
+ reasoningEffort?: ReasoningEffort;
194
+ agentName?: string;
195
+ agentSpec?: AgentSpec;
196
+ }): Promise<boolean>;
197
+ private requestLiveSession;
179
198
  lastLiveSessionError(localThreadId: string): string | undefined;
180
199
  /**
181
200
  * Inject a user turn into a session's live codex thread — reference implementation's
@@ -183,7 +202,7 @@ export declare class RunnerManager implements AgentCapabilities {
183
202
  * all output. Resolves true when the app-server accepted the turn, false when
184
203
  * the session has no live forwarder (caller falls back to the run path).
185
204
  */
186
- injectMessage(localThreadId: string, text: string): Promise<InjectOutcome>;
205
+ injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectOutcome>;
187
206
  /**
188
207
  * Interrupt a session's active live turn — the web Stop button (codex
189
208
  * `turn/interrupt`, claude Escape). Best-effort: resolves false with NO spawn
@@ -32,6 +32,10 @@ const STDERR_TAIL_LINES = 40;
32
32
  const DEFAULT_SHUTDOWN_GRACE_MS = 5_000;
33
33
  /** Time allowed for exit after SIGKILL before shutdown reports failure. */
34
34
  const DEFAULT_SHUTDOWN_KILL_GRACE_MS = 5_000;
35
+ /** Setup-pane launch should acknowledge quickly; never pin its HTTP request. */
36
+ const DEFAULT_LIVE_START_TIMEOUT_MS = 10_000;
37
+ /** Thread readiness may legitimately wait through Provider startup. */
38
+ const DEFAULT_LIVE_READY_TIMEOUT_MS = 25_000;
35
39
  /** Keep per-Session spawn context small enough to remain an environment handoff,
36
40
  * not an unbounded transport. */
37
41
  const MAX_SESSION_CONTEXT_ENV_ENTRIES = 32;
@@ -144,6 +148,8 @@ export class RunnerManager {
144
148
  reapTimer;
145
149
  shutdownGraceMs;
146
150
  shutdownKillGraceMs;
151
+ liveStartTimeoutMs;
152
+ liveReadyTimeoutMs;
147
153
  stopping = false;
148
154
  stopPromise;
149
155
  /** Sink for mirrored {@link SessionEvent}s from every session's forwarder. */
@@ -166,6 +172,8 @@ export class RunnerManager {
166
172
  this.defaultRuntime = opts.config.AGENT_RUNTIME ?? "codex";
167
173
  this.shutdownGraceMs = Math.max(0, opts.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS);
168
174
  this.shutdownKillGraceMs = Math.max(0, opts.shutdownKillGraceMs ?? DEFAULT_SHUTDOWN_KILL_GRACE_MS);
175
+ this.liveStartTimeoutMs = Math.max(1, opts.liveStartTimeoutMs ?? DEFAULT_LIVE_START_TIMEOUT_MS);
176
+ this.liveReadyTimeoutMs = Math.max(1, opts.liveReadyTimeoutMs ?? DEFAULT_LIVE_READY_TIMEOUT_MS);
169
177
  const reapIntervalMs = opts.reapIntervalMs ?? 60_000;
170
178
  if (reapIntervalMs > 0) {
171
179
  this.reapTimer = setInterval(() => this.reapIdle(), reapIntervalMs);
@@ -244,12 +252,37 @@ export class RunnerManager {
244
252
  * non-codex / non-live session (the caller then uses the normal run path).
245
253
  */
246
254
  ensureLiveSession(localThreadId, opts) {
255
+ return this.requestLiveSession(localThreadId, opts, true);
256
+ }
257
+ /** Start the Provider pane without waiting for login/onboarding to create a
258
+ * native thread. This is the setup-terminal gate; callers may attach as soon
259
+ * as it resolves, while normal message delivery still uses ensureLiveSession. */
260
+ startLiveSession(localThreadId, opts) {
261
+ return this.requestLiveSession(localThreadId, opts, false);
262
+ }
263
+ requestLiveSession(localThreadId, opts, waitForReady) {
247
264
  const handle = this.getOrSpawn(localThreadId);
248
265
  handle.lastUsedAt = this.now();
249
266
  this.liveSessionKeys.add(localThreadId);
250
267
  const reqId = randomUUID();
251
268
  return new Promise((resolve) => {
269
+ const timeoutMs = waitForReady ? this.liveReadyTimeoutMs : this.liveStartTimeoutMs;
270
+ const timeout = setTimeout(() => {
271
+ if (!handle.live.delete(reqId))
272
+ return;
273
+ const reason = `runner did not acknowledge ${waitForReady ? "live readiness" : "terminal start"} within ${timeoutMs}ms`;
274
+ this.liveErrors.set(localThreadId, reason);
275
+ this.liveSessionKeys.delete(localThreadId);
276
+ resolve(false);
277
+ // A child that cannot answer a bounded control round-trip is unsafe to
278
+ // reuse. Reap it so the next click gets a fresh runner.
279
+ void this.terminateHandle(handle, reason).catch((error) => {
280
+ logTerminationFailure(handle, error);
281
+ });
282
+ }, timeoutMs);
283
+ timeout.unref?.();
252
284
  handle.live.set(reqId, (res) => {
285
+ clearTimeout(timeout);
253
286
  const ok = res.ok ?? false;
254
287
  if (ok) {
255
288
  this.liveErrors.delete(localThreadId);
@@ -270,6 +303,7 @@ export class RunnerManager {
270
303
  ...(opts?.reasoningEffort ? { reasoningEffort: opts.reasoningEffort } : {}),
271
304
  ...(opts?.agentName ? { agentName: opts.agentName } : {}),
272
305
  ...(opts?.agentSpec ? { agentSpec: opts.agentSpec } : {}),
306
+ waitForReady,
273
307
  });
274
308
  });
275
309
  }
@@ -282,13 +316,15 @@ export class RunnerManager {
282
316
  * all output. Resolves true when the app-server accepted the turn, false when
283
317
  * the session has no live forwarder (caller falls back to the run path).
284
318
  */
285
- injectMessage(localThreadId, text) {
319
+ injectMessage(localThreadId, input) {
286
320
  const handle = this.getOrSpawn(localThreadId);
287
321
  handle.lastUsedAt = this.now();
288
322
  const reqId = randomUUID();
289
323
  return new Promise((resolve) => {
290
324
  handle.live.set(reqId, (res) => resolve(res.outcome ?? "failed"));
291
- handle.transport.send({ t: "inject", reqId, localThreadId, text });
325
+ handle.transport.send(typeof input === "string"
326
+ ? { t: "inject", reqId, localThreadId, text: input }
327
+ : { t: "inject", reqId, localThreadId, input });
292
328
  });
293
329
  }
294
330
  /**
@@ -6,7 +6,7 @@
6
6
  * (`live.ensure` / `inject` / `live.interrupt`) plus per-thread capabilities and
7
7
  * terminal attachment; the reply channels mirror each request's `reqId`.
8
8
  */
9
- import { AgentRuntimeError, type AgentRuntimeId, type AgentSpec, type ReasoningEffort, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
9
+ import { AgentRuntimeError, type AgentRuntimeId, type AgentSpec, type ReasoningEffort, type RuntimeUserInput, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
10
10
  import type { ResolveInteractionResult } from "../interactions.js";
11
11
  /** A runtime error flattened for the wire; rebuilt parent-side as `AgentRuntimeError`. */
12
12
  export interface WireError {
@@ -92,16 +92,24 @@ export type ToChild = {
92
92
  /** An inline agent spec (console session created from an inline config
93
93
  * rather than a preset id) — same purpose as `agentName`. */
94
94
  agentSpec?: AgentSpec;
95
+ /** Setup terminals only need the Provider pane to exist. Message delivery
96
+ * keeps the default and waits until the native thread is actually bound. */
97
+ waitForReady?: boolean;
95
98
  }
96
99
  /** Inject a user turn into the session's live codex thread (`turn/start` or
97
100
  * `turn/steer`). The forwarder mirrors the output — this is the single-writer
98
101
  * web send path. The child replies `injected` (echoing `reqId`). */
99
- | {
102
+ | ({
100
103
  t: "inject";
101
104
  reqId: string;
102
105
  localThreadId: string;
106
+ } & ({
107
+ input: RuntimeUserInput;
108
+ text?: never;
109
+ } | {
103
110
  text: string;
104
- }
111
+ input?: never;
112
+ }))
105
113
  /** Interrupt the session's active turn — the web Stop button. codex: app-server
106
114
  * `turn/interrupt`; claude: an Escape keystroke to the pane. The child replies
107
115
  * `interrupted` (echoing `reqId`). */
@@ -163,8 +171,8 @@ export type FromChild = {
163
171
  cwd?: string;
164
172
  parentSessionId?: string;
165
173
  }
166
- /** Result of a `live.ensure`: `ok` once the codex thread is bound (or a no-op
167
- * for a non-codex / non-live session). Echoes the request `reqId`. */
174
+ /** Result of a `live.ensure`: `ok` once the requested gate is reached (pane
175
+ * started for setup, otherwise native thread bound). Echoes `reqId`. */
168
176
  | {
169
177
  t: "live.ready";
170
178
  reqId: string;
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@rynx-ai/runtime",
3
- "version": "0.1.9",
3
+ "version": "0.1.10-beta.3",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/rynx-ai/rynx.git",
7
+ "directory": "packages/runtime"
8
+ },
4
9
  "description": "Runtime host that drives local coding-agent CLIs (codex / traex / claude) behind one AgentExecutor + AgentCapabilities contract.",
5
10
  "type": "module",
6
11
  "publishConfig": {
@@ -21,7 +26,7 @@
21
26
  "dependencies": {
22
27
  "node-pty": "^1.0.0",
23
28
  "ws": "^8.21.0",
24
- "@rynx-ai/core": "0.1.9"
29
+ "@rynx-ai/core": "0.1.10-beta.3"
25
30
  },
26
31
  "devDependencies": {
27
32
  "@types/ws": "^8.18.1"