@nylorun/runtime 0.1.2-beta → 0.2.1-beta

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1-beta
4
+
5
+ ### Patch Changes
6
+
7
+ - fd24b00: Flatten Runtime agent routes to `/:id/...` and pass matching `basePath` from the Hono mount so discovery, manifests, and AG-UI resolve at `/agents/:id/...` for Studio.
8
+
9
+ ## 0.2.0-beta
10
+
11
+ ### Minor Changes
12
+
13
+ - 4badb5b: Move model execution to session startup, provide Runtime as a mountable Hono router, and generate Hono-first projects with supervised application and Studio development. Studio now resolves root-relative Runtime endpoints correctly for custom mount paths.
14
+
3
15
  ## 0.1.2-beta
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -1,52 +1,37 @@
1
1
  # @nylorun/runtime
2
2
 
3
- Portable agent hosting, pi-ai model providers, and the `nylorun` CLI. Runtime has no Harness dependency. Studio is loaded from the application only when requested.
3
+ Portable agent lifecycle, a mountable Hono protocol router, pi-ai model providers, and the `nylorun` CLI. Runtime has no Harness dependency.
4
4
 
5
5
  ```ts
6
- import { defineRuntime } from "@nylorun/runtime";
7
- import { agents } from "./agent/registry.js";
8
- export default defineRuntime({ agents });
9
- ```
10
-
11
- Applications install their engine and Runtime directly, with Studio as a development dependency. An agent may bind Runtime's `piModel()` through its engine's model interface. `piModel({ media })` uses the explicitly supplied media adapter to resolve opaque image references. It never fetches arbitrary media URLs.
12
-
13
- ## Commands
14
-
15
- Run from the directory containing `nylorun.config.ts`:
16
-
17
- - `nylorun dev [--no-studio] [--no-open] [--port 4111] [--host 127.0.0.1] [--allowed-hosts a,b]`
18
- - `nylorun studio --agent-url http://127.0.0.1:4111 [--port 3000] [--no-open]`
19
- - `nylorun configure`
20
- - `nylorun inspect`
21
- - `nylorun build`
22
- - `nylorun start [--port 4111] [--host 127.0.0.1] [--allowed-hosts a,b]`
6
+ import { Runtime, serveAgents } from "@nylorun/runtime";
23
7
 
24
- `PORT`, `HOST` and `ALLOWED_HOSTS` environment variables supply the same settings. By default the host binds to loopback and answers only to `localhost`, `127.0.0.1` and `[::1]` on the chosen port, so changing `--port` needs no other setup and Studio connects to whatever address is printed. To publish, bind with `--host 0.0.0.0` (every Host header is then served) or keep the loopback bind behind a reverse proxy and list the public names in `--allowed-hosts`; `*` accepts any Host header. Development retains existing sessions across reloads; new sessions use the latest definitions. A failed reload leaves the previous runtime active. Studio attachment does not start or own the remote runtime.
25
-
26
- Build emits `dist/nylorun.config.js` and application assets under `dist/agent/`. Deploy `dist/`, your package manifest and lockfile, installed production dependencies, and private configuration. Production startup does not import Studio. Use `projectAsset("agent/skills/catalog")` for assets that must resolve in source and built deployments.
27
-
28
- ## Configuration and adapters
8
+ const runtime = new Runtime();
9
+ app.route(
10
+ "/agents",
11
+ serveAgents({ agents, runtime, basePath: "/agents" })
12
+ );
13
+ ```
29
14
 
30
- The default host uses in-memory history and text input. Add `persistence: localJsonl()` and `media: localMedia()` explicitly for local storage under `.data/sessions` and `.data/media`. Share the same media adapter with `piModel({ media })` and domain tools. Persistence preserves history across restarts; archived sessions cannot resume execution and require a new conversation.
15
+ `Runtime` is the primitive stack: model adapter (`piModel` by default), observer (`jsonlObserver` per session by default), and durability (`localJsonl` by default). Session files live together under `.data/sessions/<agent>/<session>/` as `events.jsonl` and `observe.jsonl`. Agents bind only when served.
31
16
 
32
- The creator runs `nylorun configure` after installation and before development unless `--skip-config` is passed. Standalone configuration also supports scripted input. EOF fails setup; Ctrl-C/SIGTERM cancels prompts and authentication with exit status 130/143. A cancelled setup does not proceed to development.
17
+ The application owns Hono composition, authentication, CORS, logging, process lifecycle, and deployment. Runtime owns agent sessions, durability, media, and AG-UI/session protocol routes. Graceful shutdown is optional: if the application installs signal handlers and wants to drain live sessions, flush pending journal writes, and run optional agent cleanup, it should await `runtime.close()`. An application that does not install handlers exits normally on its host's shutdown policy; `runtime.close()` does not run on crash, OOM, or SIGKILL.
33
18
 
34
- `nylorun configure` stores provider credentials in `.env/auth.json` and selection in `config/model.json`. Agents can be imported before setup; provider authentication is resolved when the model is called. Optional integration environment variables are loaded from `.env/integrations.env`. Existing flat `.env` files must be relocated before creating the vault; never commit credentials. Image-editing and external coding integrations may use their own credentials independently.
19
+ Runtime publishes root-relative discovery and endpoint URLs. `basePath` must match the Hono mount path so advertised manifests and AG-UI endpoints resolve. Agent-scoped routes are `/:id/...` inside the router, so mounting at `/agents` with `basePath: "/agents"` yields `/agents/:id/...`. Pass the same prefix for other mounts:
35
20
 
36
- ## Portable contracts
21
+ ```ts
22
+ app.route(
23
+ "/api/agents",
24
+ serveAgents({ agents, runtime, basePath: "/api/agents" })
25
+ );
26
+ ```
37
27
 
38
- `RuntimeAgent` supplies `id`, `name`, a matching `manifest`, and `run({ id })`. Its session supplies `input`, `stream`, `observe`, and `stop`. An input handle's `completed` promise settles after that input's execution or pause, with ordered lifecycle events and a status. Runtime uses completion events for terminal output, interactions, and failures, and observation events for optional diagnostics. Error or tripwire completions and rejected, cancelled, or stopped statuses produce a terminal AG-UI error; repeated diagnostic and completion reports do not duplicate the failure. Engines need not emit model/tool diagnostics or middleware metadata. Runtime records input before execution and flushes persistence before returning an HTTP reply. `stop()` releases session resources; optional agent `close()` runs after session shutdown. Shutdown is idempotent.
28
+ `getActor(context)` supplies an optional actor id and session context for newly created sessions. `getRequestMetadata(context)` supplies JSON-safe metadata for inbound messages. Application middleware remains responsible for authorizing every agent route.
39
29
 
40
- `RuntimeModelAdapter` accepts ordered prompt items, tools, model controls, opaque media references, and an abort signal, returning portable text/reasoning/tool-call outputs. It has no engine import. Integration tests in the creator check assignment in both directions with Harness without casts. Runtime's own tests use an independent engine.
30
+ ## Commands
41
31
 
42
- Discovery and manifest documents use protocol version 2. The agent's manifest is under `manifest`; no engine-branded envelope is required. Existing agent-scoped HTTP endpoint paths remain unchanged. Studio also understands legacy version-1 `harness.manifest` documents.
32
+ - `nylorun configure`
33
+ - `nylorun studio --agent-url http://localhost:3000/agents [--port 4161] [--no-open]`
43
34
 
44
- Repository development: [contributing](../CONTRIBUTING.md). Package publication: [releasing](../RELEASING.md).
35
+ Studio attaches to an application you run. Use your own TypeScript/build tooling and a Node adapter such as `@hono/node-server` when applicable. `projectAsset("agents/skills/catalog")` resolves bundled application assets from source or a compiled `dist/` deployment.
45
36
 
46
- Provider continuation state (for example Gemini thought signatures) travels as
47
- opaque JSON `providerMetadata` on assistant text, reasoning, and tool-call
48
- blocks. Engines integrating `piModel()` must preserve it on the corresponding
49
- assistant prompt parts, including empty signed blocks and their order. Runtime
50
- only reuses signatures with the originating provider and model. Middleware that
51
- rewrites a signed block must remove its metadata; signatures describe the
52
- original provider output. Unsigned reasoning remains diagnostic output.
37
+ Provider credentials are stored in `.env/auth.json`, and selection in `config/model.json`. `nylorun configure` can run before an agent graph is importable.
@@ -12,24 +12,24 @@ export type SessionSummary = Readonly<{
12
12
  startedAt: number;
13
13
  endedAt?: number;
14
14
  }>;
15
- /** Explicit local-only JSONL persistence. Raw provider payloads never enter this service. */
15
+ /** Explicit local-only JSONL durability. Raw provider payloads never enter this service. */
16
16
  export declare class JsonlJournal {
17
17
  private readonly root;
18
18
  private readonly secrets;
19
19
  constructor(root: string, secrets: readonly string[]);
20
- append(agent: string, event: CanonicalEvent): Promise<void>;
21
- events(agent: string, session: string): Promise<readonly CanonicalEvent[]>;
22
- list(agent: string): Promise<readonly SessionSummary[]>;
20
+ append(agentId: string, event: CanonicalEvent): Promise<void>;
21
+ events(agentId: string, sessionId: string): Promise<readonly CanonicalEvent[]>;
22
+ list(agentId: string): Promise<readonly SessionSummary[]>;
23
23
  private file;
24
24
  }
25
25
  export declare function scrub(value: unknown, secrets: readonly string[]): unknown;
26
- export interface RuntimePersistence {
27
- append(agent: string, event: CanonicalEvent): Promise<void>;
28
- events(agent: string, session: string): Promise<readonly CanonicalEvent[]>;
29
- list(agent: string): Promise<readonly SessionSummary[]>;
26
+ export interface RuntimeDurability {
27
+ append(agentId: string, event: CanonicalEvent): Promise<void>;
28
+ events(agentId: string, sessionId: string): Promise<readonly CanonicalEvent[]>;
29
+ list(agentId: string): Promise<readonly SessionSummary[]>;
30
30
  }
31
31
  export declare function localJsonl(options?: {
32
32
  root?: string;
33
33
  secrets?: readonly string[];
34
- }): RuntimePersistence;
35
- export declare function memoryHistory(): RuntimePersistence;
34
+ }): RuntimeDurability;
35
+ export declare function memoryHistory(): RuntimeDurability;
@@ -1,6 +1,6 @@
1
1
  import { appendFile, mkdir, readFile, readdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- /** Explicit local-only JSONL persistence. Raw provider payloads never enter this service. */
3
+ /** Explicit local-only JSONL durability. Raw provider payloads never enter this service. */
4
4
  export class JsonlJournal {
5
5
  root;
6
6
  secrets;
@@ -8,14 +8,14 @@ export class JsonlJournal {
8
8
  this.root = root;
9
9
  this.secrets = secrets;
10
10
  }
11
- async append(agent, event) {
12
- const file = this.file(agent, event.session);
13
- await mkdir(join(this.root, agent, event.session), { recursive: true });
11
+ async append(agentId, event) {
12
+ const file = this.file(agentId, event.session);
13
+ await mkdir(join(this.root, agentId, event.session), { recursive: true });
14
14
  await appendFile(file, `${JSON.stringify(scrub(event, this.secrets))}\n`);
15
15
  }
16
- async events(agent, session) {
16
+ async events(agentId, sessionId) {
17
17
  try {
18
- return Object.freeze((await readFile(this.file(agent, session), "utf8"))
18
+ return Object.freeze((await readFile(this.file(agentId, sessionId), "utf8"))
19
19
  .split("\n")
20
20
  .filter(Boolean)
21
21
  .flatMap((line) => {
@@ -31,15 +31,15 @@ export class JsonlJournal {
31
31
  return Object.freeze([]);
32
32
  }
33
33
  }
34
- async list(agent) {
34
+ async list(agentId) {
35
35
  try {
36
- const ids = await readdir(join(this.root, agent));
37
- const summaries = await Promise.all(ids.map(async (session) => {
38
- const events = await this.events(agent, session);
36
+ const ids = await readdir(join(this.root, agentId));
37
+ const summaries = await Promise.all(ids.map(async (sessionId) => {
38
+ const events = await this.events(agentId, sessionId);
39
39
  const final = events.findLast((event) => event.type === "final");
40
40
  const title = sessionTitle(events);
41
41
  return {
42
- session,
42
+ session: sessionId,
43
43
  ...(title === undefined ? {} : { title }),
44
44
  status: sessionStatus(events),
45
45
  startedAt: events[0] ? Date.parse(events[0].ts) : 0,
@@ -52,8 +52,8 @@ export class JsonlJournal {
52
52
  return Object.freeze([]);
53
53
  }
54
54
  }
55
- file(agent, session) {
56
- return join(this.root, safe(agent), safe(session), "events.jsonl");
55
+ file(agentId, sessionId) {
56
+ return join(this.root, safe(agentId), safe(sessionId), "events.jsonl");
57
57
  }
58
58
  }
59
59
  function sessionTitle(events) {
@@ -106,18 +106,18 @@ export function localJsonl(options = {}) {
106
106
  export function memoryHistory() {
107
107
  const agents = new Map();
108
108
  return {
109
- async append(agent, event) {
110
- const sessions = agents.get(agent) ?? new Map();
111
- agents.set(agent, sessions);
109
+ async append(agentId, event) {
110
+ const sessions = agents.get(agentId) ?? new Map();
111
+ agents.set(agentId, sessions);
112
112
  const events = sessions.get(event.session) ?? [];
113
113
  events.push(event);
114
114
  sessions.set(event.session, events);
115
115
  },
116
- async events(agent, session) {
117
- return agents.get(agent)?.get(session) ?? [];
116
+ async events(agentId, sessionId) {
117
+ return agents.get(agentId)?.get(sessionId) ?? [];
118
118
  },
119
- async list(agent) {
120
- return [...(agents.get(agent) ?? [])]
119
+ async list(agentId) {
120
+ return [...(agents.get(agentId) ?? [])]
121
121
  .map(([session, events]) => ({
122
122
  session,
123
123
  title: sessionTitle(events),
@@ -0,0 +1,10 @@
1
+ /** Local JSONL observer. Writes raw engine events beside session durability files. */
2
+ export declare function jsonlObserver(options: {
3
+ readonly agentId: string;
4
+ readonly sessionId: string;
5
+ readonly root?: string;
6
+ }): {
7
+ (event: {
8
+ readonly type: string;
9
+ }): void;
10
+ };
@@ -0,0 +1,23 @@
1
+ import { appendFile, mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { scrub } from "./journal.js";
4
+ import { projectSecrets } from "../model/settings.js";
5
+ /** Local JSONL observer. Writes raw engine events beside session durability files. */
6
+ export function jsonlObserver(options) {
7
+ const root = options.root ?? join(process.cwd(), ".data", "sessions");
8
+ const directory = join(root, safe(options.agentId), safe(options.sessionId));
9
+ const file = join(directory, "observe.jsonl");
10
+ const secrets = projectSecrets();
11
+ return (event) => {
12
+ const line = `${JSON.stringify(scrub(event, secrets))}\n`;
13
+ void mkdir(directory, { recursive: true })
14
+ .then(() => appendFile(file, line))
15
+ .catch(() => { });
16
+ };
17
+ }
18
+ function safe(value) {
19
+ if (value === "." || value === ".." || !/^[a-zA-Z0-9._-]+$/u.test(value)) {
20
+ throw new Error("Refusing a path-shaped ID.");
21
+ }
22
+ return value;
23
+ }
package/dist/cli.js CHANGED
@@ -1,319 +1,84 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from "node:fs";
3
3
  import { loadEnvFile } from "node:process";
4
- import { cp, mkdir } from "node:fs/promises";
5
- import { createRequire } from "node:module";
6
- import { resolve, join, relative, isAbsolute, sep } from "node:path";
4
+ import { join } from "node:path";
7
5
  import { pathToFileURL } from "node:url";
8
- import { serve } from "@hono/node-server";
9
- import { build, createServer } from "vite";
10
- import { defineRuntime } from "./config.js";
11
- import { allowedHost, createRuntime, loopbackHosts } from "./server/host.js";
12
- import { configureProvider, ConfigurationCancelled, } from "./model/configure.js";
13
- import { modelSelection } from "./model/settings.js";
14
- import { modelsFor } from "./model/models.js";
15
- import { ProjectCredentialStore } from "./model/auth-store.js";
16
- const usage = `nylorun <dev|studio|configure|inspect|build|start>
17
- dev [--no-studio] [--no-open] [--port <n>] [--host <address>] [--allowed-hosts <list>]
18
- studio --agent-url <http(s)-url> [--port <n>] [--no-open]
19
- start [--port <n>] [--host <address>] [--allowed-hosts <list>]
20
- Run from the directory containing nylorun.config.ts.
21
- PORT, HOST and ALLOWED_HOSTS environment variables supply the same settings.`;
22
- async function studio(agentServerUrl, open, port) {
6
+ import { createRequire } from "node:module";
7
+ import { ConfigurationCancelled, configureProvider } from "./model/configure.js";
8
+ const usage = `nylorun <configure|studio>
9
+ configure
10
+ studio --agent-url <http(s)-url> [--port <n>] [--no-open]`;
11
+ async function startStudio(agentServerUrl, open, port) {
23
12
  let entry;
24
13
  try {
25
14
  entry = createRequire(join(process.cwd(), "package.json")).resolve("@nylorun/studio");
26
15
  }
27
16
  catch {
28
- throw new Error("Install @nylorun/studio in this project, or run nylorun dev --no-studio.");
17
+ throw new Error("Install @nylorun/studio to use the Studio dashboard.");
29
18
  }
30
- const module = await import(pathToFileURL(entry).href);
31
- return module.startStudio({
32
- agentServerUrl,
33
- open,
34
- ...(port === undefined ? {} : { port }),
35
- });
36
- }
37
- function port(value, fallback) {
38
- const result = value === undefined ? fallback : Number(value);
39
- if (!Number.isInteger(result) || result < 1 || result > 65535)
40
- throw new Error("Port must be an integer between 1 and 65535.");
41
- return result;
19
+ const studio = await import(pathToFileURL(entry).href);
20
+ return studio.startStudio({ agentServerUrl, open, ...(port === undefined ? {} : { port }) });
42
21
  }
43
- /**
44
- * Loopback binds answer only to their own address on the chosen port, so
45
- * development needs no setup. A published bind or an explicit allowed-host
46
- * list is the deployment decision; "*" accepts any Host header.
47
- */
48
- function bindAddress(host, allowed, hostPort) {
49
- const hostname = (host?.trim() || "127.0.0.1").replace(/^\[(.*)\]$/u, "$1");
50
- const loopback = ["127.0.0.1", "localhost", "::1"].includes(hostname);
51
- const configured = (allowed ?? "")
52
- .split(",")
53
- .map((entry) => entry.trim())
54
- .filter(Boolean);
55
- const hosts = configured.length
56
- ? [...(loopback ? loopbackHosts(hostPort) : []), ...configured]
57
- : loopback
58
- ? loopbackHosts(hostPort)
59
- : ["*"];
60
- const unspecified = ["0.0.0.0", "::"].includes(hostname);
61
- const reachable = unspecified
62
- ? "127.0.0.1"
63
- : hostname.includes(":")
64
- ? `[${hostname}]`
65
- : hostname;
66
- return { hostname, reachable, loopback, hosts };
67
- }
68
- async function sourceLoader() {
69
- const vite = await createServer({
70
- configFile: false,
71
- appType: "custom",
72
- optimizeDeps: { noDiscovery: true, include: [] },
73
- server: { middlewareMode: true, hmr: false, ws: false },
74
- ssr: { external: true },
75
- });
76
- return {
77
- vite,
78
- async load() {
79
- const module = await vite.ssrLoadModule("/nylorun.config.ts");
80
- return defineRuntime(module.default);
81
- },
82
- };
22
+ function parsePort(value) {
23
+ if (value === undefined)
24
+ return undefined;
25
+ const port = Number(value);
26
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
27
+ throw new Error("--port must be an integer between 1 and 65535.");
28
+ return port;
83
29
  }
84
30
  async function main() {
85
31
  const [command, ...args] = process.argv.slice(2);
86
- if (!command || command === "--help" || command === "-h") {
87
- console.log(usage);
88
- return;
89
- }
90
- const allowed = {
91
- dev: ["--no-studio", "--no-open", "--port", "--host", "--allowed-hosts"],
92
- studio: ["--agent-url", "--port", "--no-open"],
93
- start: ["--port", "--host", "--allowed-hosts"],
94
- configure: [],
95
- inspect: [],
96
- build: [],
97
- };
98
- if (!(command in allowed))
99
- throw new Error(usage);
100
- const flags = new Map();
101
- for (let i = 0; i < args.length; i++) {
102
- const arg = args[i];
103
- if (!allowed[command].includes(arg) || flags.has(arg))
104
- throw new Error(`Invalid option ${arg}\n${usage}`);
105
- if (["--port", "--agent-url", "--host", "--allowed-hosts"].includes(arg)) {
106
- const value = args[++i];
107
- if (!value || value.startsWith("--"))
108
- throw new Error(`${arg} requires a value.`);
109
- flags.set(arg, value);
110
- }
111
- else
112
- flags.set(arg, true);
113
- }
114
- const requestedPort = flags.get("--port");
32
+ if (!command || command === "--help" || command === "-h")
33
+ return void console.log(usage);
115
34
  if (command === "configure") {
35
+ if (args.length)
36
+ throw new Error(usage);
116
37
  const controller = new AbortController();
117
- const onInt = () => controller.abort(new ConfigurationCancelled("SIGINT"));
118
- const onTerm = () => controller.abort(new ConfigurationCancelled("SIGTERM"));
119
- process.on("SIGINT", onInt);
120
- process.on("SIGTERM", onTerm);
121
- try {
122
- const integrations = join(process.cwd(), ".env", "integrations.env");
123
- if (existsSync(integrations))
124
- loadEnvFile(integrations);
125
- await configureProvider({ signal: controller.signal });
126
- }
127
- finally {
128
- process.removeListener("SIGINT", onInt);
129
- process.removeListener("SIGTERM", onTerm);
130
- }
131
- return;
132
- }
133
- let stopping = false;
134
- const shutdown = [];
135
- const stop = async () => {
136
- if (stopping)
137
- return;
138
- stopping = true;
139
- const results = await Promise.allSettled(shutdown.map((close) => close()));
140
- if (results.some((result) => result.status === "rejected"))
141
- process.exitCode = 1;
142
- };
143
- process.once("SIGINT", () => void stop());
144
- process.once("SIGTERM", () => void stop());
145
- try {
38
+ const cancel = (signal) => controller.abort(new ConfigurationCancelled(signal));
39
+ process.once("SIGINT", () => cancel("SIGINT"));
40
+ process.once("SIGTERM", () => cancel("SIGTERM"));
146
41
  const integrations = join(process.cwd(), ".env", "integrations.env");
147
42
  if (existsSync(integrations))
148
43
  loadEnvFile(integrations);
149
- if (command === "studio") {
150
- const url = flags.get("--agent-url");
151
- if (typeof url !== "string")
152
- throw new Error("--agent-url is required.");
153
- const dashboard = await studio(url, !flags.has("--no-open"), requestedPort ? port(requestedPort, 0) : undefined);
154
- shutdown.push(() => dashboard.close());
155
- console.log(`Studio on ${dashboard.address}`);
156
- return;
157
- }
158
- if (command === "build") {
159
- await build({
160
- configFile: false,
161
- build: {
162
- target: "node22",
163
- ssr: "nylorun.config.ts",
164
- outDir: "dist",
165
- rollupOptions: { output: { entryFileNames: "nylorun.config.js" } },
166
- },
167
- ssr: { external: true },
168
- });
169
- if (existsSync("agent")) {
170
- await mkdir("dist/agent", { recursive: true });
171
- await cp("agent", "dist/agent", {
172
- recursive: true,
173
- filter: (source) => !source.split(/[\\/]/).includes("node_modules"),
174
- });
175
- }
176
- return;
177
- }
178
- let loader;
179
- let config;
180
- if (command === "start")
181
- config = defineRuntime((await import(pathToFileURL(resolve("dist/nylorun.config.js")).href))
182
- .default);
183
- else {
184
- loader = await sourceLoader();
185
- shutdown.push(() => loader.vite.close());
186
- config = await loader.load();
187
- }
188
- if (command === "inspect") {
189
- let setup = "required";
190
- try {
191
- const selected = modelSelection();
192
- setup = (await modelsFor(selected, new ProjectCredentialStore()).checkAuth(selected.provider))
193
- ? "ready"
194
- : "required";
195
- }
196
- catch {
197
- /* Unconfigured is a valid inspection state. */
198
- }
199
- console.log(JSON.stringify({ agents: config.agents.map((agent) => agent.manifest), setup }, null, 2));
200
- await stop();
201
- return;
202
- }
203
- let current = await createRuntime(config);
204
- const retained = [current];
205
- shutdown.push(async () => {
206
- await Promise.all(retained.map((runtime) => runtime.close()));
207
- });
208
- const fetch = async (request) => {
209
- const path = new URL(request.url).pathname;
210
- const collection = path.match(/^\/agents\/([^/]+)\/v1\/sessions$/);
211
- if (request.method === "GET" && collection && retained.length > 1) {
212
- const response = await current.app.fetch(request);
213
- if (!response.ok)
214
- return response;
215
- const agentId = collection[1];
216
- const document = (await response.json());
217
- const summaries = new Map(document.sessions.map((item) => [item.session, item]));
218
- // A journal may be shared across reloads, but only the owning runtime
219
- // knows whether a retained session is still running or waiting.
220
- for (const runtime of retained) {
221
- if (runtime === current)
222
- continue;
223
- const previous = await runtime.app.fetch(request.clone());
224
- if (!previous.ok)
225
- continue;
226
- const history = (await previous.json());
227
- for (const summary of history.sessions) {
228
- if (runtime.hasSession(agentId, summary.session) ||
229
- !summaries.has(summary.session))
230
- summaries.set(summary.session, summary);
231
- }
232
- }
233
- return new Response(JSON.stringify({
234
- sessions: [...summaries.values()].sort((a, b) => b.startedAt - a.startedAt),
235
- }), { status: response.status, headers: response.headers });
236
- }
237
- const match = path.match(/^\/agents\/([^/]+)\/v1\/(?:sessions|ag-ui\/sessions|media)\/([^/]+)/);
238
- let agentId = match?.[1];
239
- let sessionId = match?.[2];
240
- if (!match && request.method === "POST" && path.endsWith("/v1/ag-ui")) {
241
- agentId = path.split("/")[2];
242
- const body = await request
243
- .clone()
244
- .json()
245
- .catch(() => ({}));
246
- sessionId =
247
- typeof body.threadId === "string" ? body.threadId : undefined;
248
- }
249
- const runtime = agentId && sessionId
250
- ? [...retained]
251
- .reverse()
252
- .find((item) => item.hasSession(agentId, sessionId)) ?? current
253
- : current;
254
- return runtime.app.fetch(request);
255
- };
256
- const hostPort = port(requestedPort ?? process.env.PORT, 4111);
257
- const binding = bindAddress(flags.get("--host") ?? process.env.HOST, flags.get("--allowed-hosts") ??
258
- process.env.ALLOWED_HOSTS, hostPort);
259
- const guarded = async (request) => allowedHost(request.headers.get("host") ?? undefined, binding.hosts)
260
- ? fetch(request)
261
- : Response.json({
262
- error: "Host header is not allowed. Set --allowed-hosts or ALLOWED_HOSTS.",
263
- }, { status: 421 });
264
- const server = serve({
265
- fetch: guarded,
266
- hostname: binding.hostname,
267
- port: hostPort,
268
- });
269
- shutdown.push(() => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))));
270
- await new Promise((resolve, reject) => {
271
- server.once("listening", resolve);
272
- server.once("error", reject);
273
- });
274
- const address = `http://${binding.reachable}:${hostPort}`;
275
- console.log(`Agent runtime on ${address}`);
276
- if (!binding.loopback)
277
- console.log(binding.hosts.includes("*")
278
- ? "Serving every Host header; add TLS and access control at the network boundary."
279
- : `Serving Host headers: ${binding.hosts.join(", ")}`);
280
- if (command === "dev" && !flags.has("--no-studio")) {
281
- const dashboard = await studio(address, !flags.has("--no-open"));
282
- shutdown.push(() => dashboard.close());
283
- console.log(`Studio on ${dashboard.address}`);
44
+ await configureProvider({ signal: controller.signal });
45
+ return;
46
+ }
47
+ if (command !== "studio")
48
+ throw new Error(usage);
49
+ let agentUrl;
50
+ let port;
51
+ let open = true;
52
+ for (let index = 0; index < args.length; index += 1) {
53
+ const arg = args[index];
54
+ if (arg === "--agent-url") {
55
+ if (agentUrl !== undefined)
56
+ throw new Error("--agent-url may only be supplied once.");
57
+ agentUrl = args[++index];
58
+ if (!agentUrl || agentUrl.startsWith("--"))
59
+ throw new Error("--agent-url requires a value.");
284
60
  }
285
- if (loader) {
286
- let reload = Promise.resolve();
287
- loader.vite.watcher.on("all", (event, file) => {
288
- const path = relative(process.cwd(), file).split(sep).join("/");
289
- if (stopping ||
290
- !["add", "change", "unlink"].includes(event) ||
291
- isAbsolute(path) ||
292
- path.startsWith("..") ||
293
- !(path === "nylorun.config.ts" || path.startsWith("agent/")))
294
- return;
295
- reload = reload.then(async () => {
296
- try {
297
- loader.vite.moduleGraph.invalidateAll();
298
- const replacement = await createRuntime(await loader.load());
299
- current = replacement;
300
- retained.push(replacement);
301
- console.log(`Reloaded ${path}`);
302
- }
303
- catch (error) {
304
- console.error(`Reload failed; existing agents remain active: ${error instanceof Error ? error.message : String(error)}`);
305
- }
306
- });
307
- });
61
+ else if (arg === "--port") {
62
+ if (port !== undefined)
63
+ throw new Error("--port may only be supplied once.");
64
+ port = parsePort(args[++index]);
308
65
  }
66
+ else if (arg === "--no-open" && open)
67
+ open = false;
68
+ else
69
+ throw new Error(usage);
309
70
  }
310
- catch (error) {
311
- await stop();
312
- throw error;
313
- }
71
+ if (!agentUrl)
72
+ throw new Error("--agent-url is required.");
73
+ const dashboard = await startStudio(agentUrl, open, port);
74
+ console.log(`Studio on ${dashboard.address}`);
75
+ await new Promise((resolve, reject) => {
76
+ const close = () => void dashboard.close().then(resolve, reject);
77
+ process.once("SIGINT", close);
78
+ process.once("SIGTERM", close);
79
+ });
314
80
  }
315
81
  void main().catch((error) => {
316
82
  console.error(error instanceof Error ? error.message : String(error));
317
- process.exitCode =
318
- error instanceof ConfigurationCancelled ? error.exitCode : 1;
83
+ process.exitCode = error instanceof ConfigurationCancelled ? error.exitCode : 1;
319
84
  });