@nylorun/runtime 0.2.1-beta → 0.3.0-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 +6 -0
- package/README.md +18 -9
- package/dist/cli.js +15 -4
- package/dist/dev.d.ts +2 -0
- package/dist/dev.js +121 -0
- package/dist/model/configure.js +12 -2
- package/dist/model/settings.js +12 -1
- package/dist/server/host.d.ts +1 -1
- package/dist/server/host.js +50 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9c350be: Provide `nylorun dev` with optional Studio and browser opening, automatic development loopback CORS, and inferred Hono mount paths. Move local model selection to `.env/model.json` with legacy fallback and migration. Generate starters without copied launcher scripts, a top-level config directory, or a separate TypeScript build config. Release preparation must update the creator's Runtime compatibility pin together with these changes.
|
|
8
|
+
|
|
3
9
|
## 0.2.1-beta
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ import { Runtime, serveAgents } from "@nylorun/runtime";
|
|
|
8
8
|
const runtime = new Runtime();
|
|
9
9
|
app.route(
|
|
10
10
|
"/agents",
|
|
11
|
-
serveAgents({ agents, runtime
|
|
11
|
+
serveAgents({ agents, runtime })
|
|
12
12
|
);
|
|
13
13
|
```
|
|
14
14
|
|
|
@@ -16,22 +16,31 @@ app.route(
|
|
|
16
16
|
|
|
17
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.
|
|
18
18
|
|
|
19
|
-
Runtime publishes root-relative discovery and endpoint URLs.
|
|
19
|
+
Runtime publishes root-relative discovery and endpoint URLs. It infers the Hono
|
|
20
|
+
mount from each request URL (so a separate consumer `hono` install still works).
|
|
21
|
+
Mount at `/agents` or `/api/agents` without repeating that path in `serveAgents`.
|
|
22
|
+
Pass explicit `basePath` when a reverse proxy rewrites the public prefix.
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
app.route(
|
|
23
|
-
"/api/agents",
|
|
24
|
-
serveAgents({ agents, runtime, basePath: "/api/agents" })
|
|
25
|
-
);
|
|
26
|
-
```
|
|
24
|
+
`nylorun dev` enables local Studio connections automatically by setting `NYLORUN_DEV=1` for its child application. This allows HTTP/HTTPS browser origins on `localhost`, `127.0.0.1`, or `[::1]`, including Studio's fallback ports. Ordinary production startup does not enable this policy; the application owns production CORS and authorization.
|
|
27
25
|
|
|
28
26
|
`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.
|
|
29
27
|
|
|
30
28
|
## Commands
|
|
31
29
|
|
|
32
30
|
- `nylorun configure`
|
|
31
|
+
- `nylorun dev [--no-studio] [--no-open]`
|
|
33
32
|
- `nylorun studio --agent-url http://localhost:3000/agents [--port 4161] [--no-open]`
|
|
34
33
|
|
|
35
34
|
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.
|
|
36
35
|
|
|
37
|
-
Provider credentials are stored in `.env/auth.json`, and selection in
|
|
36
|
+
Provider credentials are stored in `.env/auth.json`, and selection in `.env/model.json`. `nylorun configure` can run before an agent graph is importable.
|
|
37
|
+
|
|
38
|
+
`nylorun dev` runs project-local `tsx watch src/index.ts`, waits for `/agents/v1/agents`, then starts project-local Studio. `PORT` defaults to 3000. `--no-studio` runs just the application; `--no-open` keeps the browser closed. Both flags can be combined. Ctrl-C stops both processes.
|
|
39
|
+
|
|
40
|
+
### Upgrading an existing starter
|
|
41
|
+
|
|
42
|
+
After upgrading Runtime to a release containing `nylorun dev`, change the development script to `"dev": "nylorun dev"` and remove `dev:app` and `scripts/dev.mjs`. Remove the duplicated `basePath` option for normal Hono mounts.
|
|
43
|
+
|
|
44
|
+
Run `npm run configure` to move model selection to `.env/model.json`. Runtime reads the legacy `config/model.json` only when the new file is absent. Successful configuration removes the legacy file and its directory only if empty; credentials remain in `.env/auth.json`. Do not set `NYLORUN_DEV` in production.
|
|
45
|
+
|
|
46
|
+
The starter now uses one `tsconfig.json` with `rootDir: "."` and `outDir: "dist"`. Remove `noEmit` from that file and use `tsc --noEmit` for checks. Change the build's compiler invocation to `tsc -p tsconfig.json` before deleting `tsconfig.build.json`. Keep your existing asset-copy step. Projects whose checks include tests may retain separate build configuration.
|
package/dist/cli.js
CHANGED
|
@@ -4,8 +4,10 @@ import { loadEnvFile } from "node:process";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { ConfigurationCancelled, configureProvider } from "./model/configure.js";
|
|
8
|
-
|
|
7
|
+
import { ConfigurationCancelled, configureProvider, } from "./model/configure.js";
|
|
8
|
+
import { develop } from "./dev.js";
|
|
9
|
+
const usage = `nylorun <configure|dev|studio>
|
|
10
|
+
dev [--no-studio] [--no-open]
|
|
9
11
|
configure
|
|
10
12
|
studio --agent-url <http(s)-url> [--port <n>] [--no-open]`;
|
|
11
13
|
async function startStudio(agentServerUrl, open, port) {
|
|
@@ -17,7 +19,11 @@ async function startStudio(agentServerUrl, open, port) {
|
|
|
17
19
|
throw new Error("Install @nylorun/studio to use the Studio dashboard.");
|
|
18
20
|
}
|
|
19
21
|
const studio = await import(pathToFileURL(entry).href);
|
|
20
|
-
return studio.startStudio({
|
|
22
|
+
return studio.startStudio({
|
|
23
|
+
agentServerUrl,
|
|
24
|
+
open,
|
|
25
|
+
...(port === undefined ? {} : { port }),
|
|
26
|
+
});
|
|
21
27
|
}
|
|
22
28
|
function parsePort(value) {
|
|
23
29
|
if (value === undefined)
|
|
@@ -31,6 +37,10 @@ async function main() {
|
|
|
31
37
|
const [command, ...args] = process.argv.slice(2);
|
|
32
38
|
if (!command || command === "--help" || command === "-h")
|
|
33
39
|
return void console.log(usage);
|
|
40
|
+
if (command === "dev") {
|
|
41
|
+
process.exitCode = await develop(args);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
34
44
|
if (command === "configure") {
|
|
35
45
|
if (args.length)
|
|
36
46
|
throw new Error(usage);
|
|
@@ -80,5 +90,6 @@ async function main() {
|
|
|
80
90
|
}
|
|
81
91
|
void main().catch((error) => {
|
|
82
92
|
console.error(error instanceof Error ? error.message : String(error));
|
|
83
|
-
process.exitCode =
|
|
93
|
+
process.exitCode =
|
|
94
|
+
error instanceof ConfigurationCancelled ? error.exitCode : 1;
|
|
84
95
|
});
|
package/dist/dev.d.ts
ADDED
package/dist/dev.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
/** Runs project development tooling without copying a supervisor into each application. */
|
|
7
|
+
export async function develop(args) {
|
|
8
|
+
const usage = "Usage: nylorun dev [--no-studio] [--no-open]";
|
|
9
|
+
if (new Set(args).size !== args.length ||
|
|
10
|
+
args.some((arg) => !["--no-studio", "--no-open"].includes(arg)))
|
|
11
|
+
throw new Error(usage);
|
|
12
|
+
const port = Number(process.env.PORT ?? "3000");
|
|
13
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
14
|
+
throw new Error("PORT must be an integer between 1 and 65535.");
|
|
15
|
+
const require = createRequire(join(process.cwd(), "package.json"));
|
|
16
|
+
let tsx;
|
|
17
|
+
try {
|
|
18
|
+
tsx = require.resolve("tsx/cli");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error("Install tsx in your project to use nylorun dev.");
|
|
22
|
+
}
|
|
23
|
+
if (!args.includes("--no-studio")) {
|
|
24
|
+
try {
|
|
25
|
+
require.resolve("@nylorun/studio");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error("Install @nylorun/studio or use nylorun dev --no-studio.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const children = new Set();
|
|
33
|
+
const exits = [];
|
|
34
|
+
let result = 0;
|
|
35
|
+
let force;
|
|
36
|
+
const stop = (code, signal = "SIGTERM") => {
|
|
37
|
+
if (controller.signal.aborted)
|
|
38
|
+
return;
|
|
39
|
+
result = code;
|
|
40
|
+
controller.abort();
|
|
41
|
+
for (const child of children)
|
|
42
|
+
child.kill(signal);
|
|
43
|
+
force = setTimeout(() => {
|
|
44
|
+
for (const child of children)
|
|
45
|
+
child.kill("SIGKILL");
|
|
46
|
+
}, 5_000);
|
|
47
|
+
force.unref();
|
|
48
|
+
};
|
|
49
|
+
const interrupt = () => stop(130, "SIGINT");
|
|
50
|
+
const terminate = () => stop(143);
|
|
51
|
+
process.once("SIGINT", interrupt);
|
|
52
|
+
process.once("SIGTERM", terminate);
|
|
53
|
+
const launch = (argv) => {
|
|
54
|
+
const child = spawn(process.execPath, argv, {
|
|
55
|
+
stdio: "inherit",
|
|
56
|
+
env: { ...process.env, NYLORUN_DEV: "1" },
|
|
57
|
+
});
|
|
58
|
+
children.add(child);
|
|
59
|
+
exits.push(new Promise((resolve) => {
|
|
60
|
+
child.once("error", (error) => {
|
|
61
|
+
console.error(`Could not start development process: ${error.message}`);
|
|
62
|
+
stop(1);
|
|
63
|
+
});
|
|
64
|
+
child.once("close", (code, signal) => {
|
|
65
|
+
children.delete(child);
|
|
66
|
+
stop(code ?? (signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1));
|
|
67
|
+
resolve();
|
|
68
|
+
});
|
|
69
|
+
}));
|
|
70
|
+
};
|
|
71
|
+
try {
|
|
72
|
+
launch([tsx, "watch", "src/index.ts"]);
|
|
73
|
+
if (!args.includes("--no-studio")) {
|
|
74
|
+
const url = `http://127.0.0.1:${port}/agents/v1/agents`;
|
|
75
|
+
const deadline = Date.now() + 20_000;
|
|
76
|
+
let ready = false;
|
|
77
|
+
while (!controller.signal.aborted && Date.now() < deadline) {
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(url, {
|
|
80
|
+
signal: AbortSignal.any([
|
|
81
|
+
controller.signal,
|
|
82
|
+
AbortSignal.timeout(500),
|
|
83
|
+
]),
|
|
84
|
+
});
|
|
85
|
+
ready = response.ok;
|
|
86
|
+
await response.body?.cancel();
|
|
87
|
+
if (ready)
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* Watch mode may still be compiling or restarting the app. */
|
|
92
|
+
}
|
|
93
|
+
await delay(50, undefined, { signal: controller.signal }).catch(() => { });
|
|
94
|
+
}
|
|
95
|
+
if (!controller.signal.aborted) {
|
|
96
|
+
if (!ready)
|
|
97
|
+
throw new Error(`Application did not become ready at ${url} within 20 seconds.`);
|
|
98
|
+
launch([
|
|
99
|
+
fileURLToPath(new URL("./cli.js", import.meta.url)),
|
|
100
|
+
"studio",
|
|
101
|
+
"--agent-url",
|
|
102
|
+
`http://localhost:${port}/agents`,
|
|
103
|
+
...(args.includes("--no-open") ? ["--no-open"] : []),
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await Promise.all(exits);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
stop(1);
|
|
111
|
+
await Promise.all(exits);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
if (force !== undefined)
|
|
116
|
+
clearTimeout(force);
|
|
117
|
+
process.removeListener("SIGINT", interrupt);
|
|
118
|
+
process.removeListener("SIGTERM", terminate);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
package/dist/model/configure.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, writeFile, rename, rm } from "node:fs/promises";
|
|
1
|
+
import { mkdir, writeFile, rename, rm, rmdir } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
@@ -98,7 +98,7 @@ export async function configureProvider(options = {}) {
|
|
|
98
98
|
}
|
|
99
99
|
async function save(selection) {
|
|
100
100
|
signal.throwIfAborted();
|
|
101
|
-
const directory = join(root, "
|
|
101
|
+
const directory = join(root, ".env");
|
|
102
102
|
const temporary = join(directory, `.model-${randomUUID()}.json`);
|
|
103
103
|
try {
|
|
104
104
|
await mkdir(directory, { recursive: true });
|
|
@@ -107,6 +107,16 @@ export async function configureProvider(options = {}) {
|
|
|
107
107
|
});
|
|
108
108
|
signal.throwIfAborted();
|
|
109
109
|
await rename(temporary, join(directory, "model.json"));
|
|
110
|
+
await rm(join(root, "config", "model.json"), { force: true });
|
|
111
|
+
try {
|
|
112
|
+
await rmdir(join(root, "config"));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (!(error instanceof Error) ||
|
|
116
|
+
!("code" in error) ||
|
|
117
|
+
!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(String(error.code)))
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
110
120
|
}
|
|
111
121
|
finally {
|
|
112
122
|
await rm(temporary, { force: true });
|
package/dist/model/settings.js
CHANGED
|
@@ -2,7 +2,18 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
export function modelSelection(root = process.cwd()) {
|
|
4
4
|
try {
|
|
5
|
-
|
|
5
|
+
let contents;
|
|
6
|
+
try {
|
|
7
|
+
contents = readFileSync(join(root, ".env", "model.json"), "utf8");
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (!(error instanceof Error) ||
|
|
11
|
+
!("code" in error) ||
|
|
12
|
+
error.code !== "ENOENT")
|
|
13
|
+
throw error;
|
|
14
|
+
contents = readFileSync(join(root, "config", "model.json"), "utf8");
|
|
15
|
+
}
|
|
16
|
+
const value = JSON.parse(contents);
|
|
6
17
|
if (typeof value.provider === "string" &&
|
|
7
18
|
value.provider &&
|
|
8
19
|
typeof value.model === "string" &&
|
package/dist/server/host.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type RuntimeActor = Readonly<{
|
|
|
6
6
|
context?: Record<string, JsonValue>;
|
|
7
7
|
}>;
|
|
8
8
|
export type AgentRouterOptions = Readonly<{
|
|
9
|
-
/**
|
|
9
|
+
/** Override the public URL prefix; defaults to the current Hono mount path. */
|
|
10
10
|
basePath?: string;
|
|
11
11
|
getActor?: (context: Context) => RuntimeActor | undefined | Promise<RuntimeActor | undefined>;
|
|
12
12
|
getRequestMetadata?: (context: Context) => Record<string, JsonValue> | undefined | Promise<Record<string, JsonValue> | undefined>;
|
package/dist/server/host.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
|
+
import { cors } from "hono/cors";
|
|
3
4
|
import { HTTPException } from "hono/http-exception";
|
|
4
5
|
import { agUiEvents, sse } from "./ag-ui.js";
|
|
5
6
|
import { observedPayload } from "./digests.js";
|
|
@@ -38,9 +39,34 @@ export class Runtime {
|
|
|
38
39
|
const configuredObserver = this.#config.observer;
|
|
39
40
|
const redact = (value) => scrub(value, projectSecrets());
|
|
40
41
|
const live = new Map();
|
|
41
|
-
let publicPath = (path) => path;
|
|
42
42
|
const routerOptions = options;
|
|
43
43
|
const app = new Hono();
|
|
44
|
+
if (process.env.NYLORUN_DEV === "1") {
|
|
45
|
+
app.use("*", cors({
|
|
46
|
+
origin: (origin) => {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(origin);
|
|
49
|
+
return ["http:", "https:"].includes(url.protocol) &&
|
|
50
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) &&
|
|
51
|
+
url.origin === origin
|
|
52
|
+
? origin
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
allowMethods: [
|
|
60
|
+
"GET",
|
|
61
|
+
"HEAD",
|
|
62
|
+
"POST",
|
|
63
|
+
"PUT",
|
|
64
|
+
"PATCH",
|
|
65
|
+
"DELETE",
|
|
66
|
+
"OPTIONS",
|
|
67
|
+
],
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
44
70
|
app.onError((error, context) => context.json({ error: String(redact(error.message)) }, error instanceof HTTPException ? error.status : 500));
|
|
45
71
|
app.use("*", async (context, next) => {
|
|
46
72
|
await next();
|
|
@@ -52,19 +78,22 @@ export class Runtime {
|
|
|
52
78
|
});
|
|
53
79
|
}
|
|
54
80
|
});
|
|
55
|
-
|
|
81
|
+
// Infer the public mount from this request URL and the local route path.
|
|
82
|
+
// Do not use hono/route basePath: consumers often install a separate `hono`
|
|
83
|
+
// copy, and that helper's match-result Symbol then misses the parent's match.
|
|
84
|
+
const publicPath = (context, routePath, path) => `${normalizeBasePath(routerOptions.basePath ?? inferMountPath(context, routePath))}${path}`;
|
|
56
85
|
app.get("/v1/agents", (context) => context.json({
|
|
57
86
|
protocolVersion: 2,
|
|
58
87
|
agents: agents.map((agent) => ({
|
|
59
88
|
id: agent.id,
|
|
60
|
-
manifestUrl: publicPath(`/${agent.id}/manifest.json`),
|
|
89
|
+
manifestUrl: publicPath(context, "/v1/agents", `/${agent.id}/manifest.json`),
|
|
61
90
|
})),
|
|
62
91
|
}));
|
|
63
92
|
app.get("/:agentId/manifest.json", (context) => {
|
|
64
93
|
const agent = byId.get(context.req.param("agentId"));
|
|
65
94
|
return agent === undefined
|
|
66
95
|
? context.json({ error: "unknown agent" }, 404)
|
|
67
|
-
: context.json(manifest(agent, media !== undefined, publicPath));
|
|
96
|
+
: context.json(manifest(agent, media !== undefined, (path) => publicPath(context, "/:agentId/manifest.json", path)));
|
|
68
97
|
});
|
|
69
98
|
app.get("/:agentId/v1/media/:session/:assetId", async (context) => {
|
|
70
99
|
const agent = requireAgent(context.req.param("agentId"));
|
|
@@ -602,3 +631,20 @@ function normalizeBasePath(value) {
|
|
|
602
631
|
throw new Error("basePath must start with / and must not end with /.");
|
|
603
632
|
return value;
|
|
604
633
|
}
|
|
634
|
+
/** Public mount prefix for this request, derived without hono/route Symbols. */
|
|
635
|
+
function inferMountPath(context, routePath) {
|
|
636
|
+
const pathname = new URL(context.req.url).pathname;
|
|
637
|
+
const suffix = routePath.replace(/:([A-Za-z0-9_]+)/g, (_, key) => {
|
|
638
|
+
const value = context.req.param(key);
|
|
639
|
+
if (value === undefined)
|
|
640
|
+
throw new Error(`Unable to infer Runtime mount path from ${pathname}. Pass basePath matching the Hono mount.`);
|
|
641
|
+
return value;
|
|
642
|
+
});
|
|
643
|
+
if (suffix !== "/" && pathname.endsWith(suffix)) {
|
|
644
|
+
const base = pathname.slice(0, -suffix.length);
|
|
645
|
+
return base === "" ? "/" : base;
|
|
646
|
+
}
|
|
647
|
+
if (pathname === suffix || `${pathname}/` === suffix)
|
|
648
|
+
return "/";
|
|
649
|
+
throw new Error(`Unable to infer Runtime mount path from ${pathname}. Pass basePath matching the Hono mount.`);
|
|
650
|
+
}
|