nah-studio 0.0.1-beta.1
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/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/cli-AoWZGiY6.js +19512 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +5 -0
- package/dist/endpoint.d.ts +49 -0
- package/dist/evals.d.ts +82 -0
- package/dist/index-CkZPim_p-DruQhqoF.js +1320 -0
- package/dist/index-DSFrg_AU-DdZcTs3s.js +1146 -0
- package/dist/index-DlioFBtF-Dcq_xX6f.js +6195 -0
- package/dist/index-dTL1wsK0-B6Ehb7qF.js +9096 -0
- package/dist/launcher.d.ts +67 -0
- package/dist/run.d.ts +29 -0
- package/dist/sandbox-C5dJwMTq-Cv8w7UGu.js +96 -0
- package/dist/server.d.ts +63 -0
- package/dist/store.d.ts +301 -0
- package/dist/stream.d.ts +14 -0
- package/dist/ui/assets/index-BF2-fvLG.js +42 -0
- package/dist/ui/assets/index-D0JWML8R.css +1 -0
- package/dist/ui/index.html +16 -0
- package/package.json +65 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { StudioStore } from './store.js';
|
|
3
|
+
import { Agent } from './wire.js';
|
|
4
|
+
export type Launcher = {
|
|
5
|
+
/** The thing being spawned: a `nah` binary, or this process's own node. */
|
|
6
|
+
command: string;
|
|
7
|
+
/** Arguments that always come first, e.g. the script path when running under node. */
|
|
8
|
+
prefixArgs: string[];
|
|
9
|
+
/** Human-readable form, for the UI. */
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
export type LaunchRequest = {
|
|
13
|
+
store: StudioStore;
|
|
14
|
+
agentId: string;
|
|
15
|
+
cwd: string;
|
|
16
|
+
prompt: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
permissions?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Find the `nah` to launch.
|
|
22
|
+
*
|
|
23
|
+
* Checked in the order that produces the version you are debugging: the binary
|
|
24
|
+
* recorded by the `nah` that started this Studio, then a `nah` on PATH. The
|
|
25
|
+
* recorded one matters because a global `nah` from npm and a `nah` built from
|
|
26
|
+
* source send different trace shapes, and a dashboard that quietly launched the
|
|
27
|
+
* other one would show you runs you never made.
|
|
28
|
+
*
|
|
29
|
+
* A recorded path ending in `.js` is a script rather than a binary — which is what
|
|
30
|
+
* `argv[1]` is for a dev checkout or an `npx` run — so it is launched through this
|
|
31
|
+
* process's own node. Depending on the file's executable bit would work on a
|
|
32
|
+
* globally installed CLI and fail on every checkout, which is the opposite of the
|
|
33
|
+
* order that matters.
|
|
34
|
+
*/
|
|
35
|
+
export declare const resolveNahBinary: (recorded: string | undefined, options?: {
|
|
36
|
+
platform?: NodeJS.Platform;
|
|
37
|
+
execPath?: string;
|
|
38
|
+
which?: (command: string) => string | undefined;
|
|
39
|
+
}) => Launcher | null;
|
|
40
|
+
/**
|
|
41
|
+
* Argument list for one launched agent.
|
|
42
|
+
*
|
|
43
|
+
* `--mode json` because the output is a machine-readable event stream, not a
|
|
44
|
+
* terminal: this process has no TTY, so the pretty renderer would have nothing to
|
|
45
|
+
* render into and the agent's reasoning would be invisible in the log pane.
|
|
46
|
+
*/
|
|
47
|
+
export declare const launchArgs: (request: Pick<LaunchRequest, "prompt" | "model" | "permissions">) => string[];
|
|
48
|
+
export type LaunchResult = {
|
|
49
|
+
started: boolean;
|
|
50
|
+
reason?: string;
|
|
51
|
+
pid?: number;
|
|
52
|
+
};
|
|
53
|
+
export declare const launchAgent: (launcher: Launcher, request: LaunchRequest, options?: {
|
|
54
|
+
spawnProcess?: typeof spawn;
|
|
55
|
+
}) => Promise<LaunchResult>;
|
|
56
|
+
export type StopResult = {
|
|
57
|
+
stopped: boolean;
|
|
58
|
+
reason?: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Ask a launched agent to stop.
|
|
62
|
+
*
|
|
63
|
+
* `SIGTERM` first, because a `nah` process restores the terminal on the way out
|
|
64
|
+
* and `SIGKILL` would leave an alternate screen behind. Only agents the Studio
|
|
65
|
+
* started are eligible, and the server checks the host before calling this.
|
|
66
|
+
*/
|
|
67
|
+
export declare const stopAgentProcess: (agent: Agent, store: StudioStore) => StopResult;
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { StudioAgent } from 'nah/agent';
|
|
2
|
+
import { StudioHandle } from './server.js';
|
|
3
|
+
export type ServeOptions = {
|
|
4
|
+
cwd: string;
|
|
5
|
+
port?: number;
|
|
6
|
+
host?: string;
|
|
7
|
+
/** Studio database. Defaults to ~/.nah/studio/studio.sqlite. */
|
|
8
|
+
dbPath?: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
/** Shared secret required on every request. Set it before binding 0.0.0.0. */
|
|
11
|
+
token?: string;
|
|
12
|
+
/** The `nah` to launch agents with. Resolved from PATH when absent. */
|
|
13
|
+
nahBin?: string;
|
|
14
|
+
/** Do not publish `~/.nah/studio.json`, so no agent finds this Studio. */
|
|
15
|
+
noPublish?: boolean;
|
|
16
|
+
out?: NodeJS.WriteStream;
|
|
17
|
+
/** Injected in tests. */
|
|
18
|
+
createAgent?: (options: {
|
|
19
|
+
cwd: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
}) => Promise<StudioAgent>;
|
|
22
|
+
};
|
|
23
|
+
export type ServeRun = {
|
|
24
|
+
output: string;
|
|
25
|
+
toolsCalled: string[];
|
|
26
|
+
filesChanged: string[];
|
|
27
|
+
traceId?: string;
|
|
28
|
+
};
|
|
29
|
+
export declare const runServe: (options: ServeOptions) => Promise<StudioHandle>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { DEFAULT_CAPS as x } from "@astracollab/not-another-harness";
|
|
2
|
+
import { posix as l } from "node:path";
|
|
3
|
+
const u = (process.env.BL_WORKSPACE ?? process.env.BLAXEL_WORKSPACE ?? "").trim(), b = async (s = {}) => {
|
|
4
|
+
if (!(process.env.BL_API_KEY ?? process.env.BLAXEL_API_KEY ?? "").trim())
|
|
5
|
+
throw new Error(
|
|
6
|
+
"--sandbox needs a Blaxel API key: set BL_API_KEY (workspace is inferred from the key, or set BL_WORKSPACE)."
|
|
7
|
+
);
|
|
8
|
+
u && (process.env.BL_WORKSPACE = u);
|
|
9
|
+
let p;
|
|
10
|
+
try {
|
|
11
|
+
p = await import("@blaxel/core");
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error("--sandbox needs the optional dependency @blaxel/core installed.");
|
|
14
|
+
}
|
|
15
|
+
const c = s.sandboxName ?? `nah-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`, i = l.resolve(s.repoDir ?? "/workspace/repo"), f = s.sandboxName == null, o = s.sandboxName ? await p.SandboxInstance.get(c) : await p.SandboxInstance.create({
|
|
16
|
+
name: c,
|
|
17
|
+
image: s.image ?? "blaxel/ts-app:latest",
|
|
18
|
+
memory: 4096,
|
|
19
|
+
region: (process.env.BL_REGION ?? "us-pdx-1").trim()
|
|
20
|
+
}), m = async (e, r, a) => {
|
|
21
|
+
const t = await o.process.exec({
|
|
22
|
+
command: e,
|
|
23
|
+
...r ? { workingDir: r } : {},
|
|
24
|
+
timeout: a ?? 120,
|
|
25
|
+
waitForCompletion: !0
|
|
26
|
+
});
|
|
27
|
+
return typeof t == "string" ? { stdout: t, stderr: "", exitCode: 0 } : {
|
|
28
|
+
stdout: typeof t.stdout == "string" ? t.stdout : "",
|
|
29
|
+
stderr: typeof t.stderr == "string" ? t.stderr : "",
|
|
30
|
+
exitCode: typeof t.exitCode == "number" ? t.exitCode : 0
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
await m(`mkdir -p ${d(i)}`, "/").catch(() => {
|
|
34
|
+
});
|
|
35
|
+
const w = (e, r) => m(e, i, r), n = (e) => {
|
|
36
|
+
const r = e.replace(/\\/g, "/");
|
|
37
|
+
if (r.startsWith("/") || /^[A-Za-z]:/.test(r))
|
|
38
|
+
throw new Error(`absolute paths are not allowed in the sandbox repo: ${e}`);
|
|
39
|
+
const a = l.normalize(r || ".");
|
|
40
|
+
if (a === ".." || a.startsWith("../"))
|
|
41
|
+
throw new Error(`path escapes sandbox repo: ${e}`);
|
|
42
|
+
const t = l.resolve(i, a);
|
|
43
|
+
if (t !== i && !t.startsWith(`${i}/`))
|
|
44
|
+
throw new Error(`path escapes sandbox repo: ${e}`);
|
|
45
|
+
return t;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
env: {
|
|
49
|
+
readFile: (e) => o.fs.read(n(e)),
|
|
50
|
+
writeFile: async (e, r) => {
|
|
51
|
+
await o.fs.write(n(e), r);
|
|
52
|
+
},
|
|
53
|
+
deleteFile: async (e) => {
|
|
54
|
+
const r = o.fs;
|
|
55
|
+
if (!r.rm) throw new Error("the configured sandbox SDK does not support file removal");
|
|
56
|
+
await r.rm(n(e));
|
|
57
|
+
},
|
|
58
|
+
exists: async (e) => {
|
|
59
|
+
try {
|
|
60
|
+
return await o.fs.read(n(e)), !0;
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return await o.fs.ls(n(e)), !0;
|
|
65
|
+
} catch {
|
|
66
|
+
return !1;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
readdir: async (e) => {
|
|
70
|
+
const r = await o.fs.ls(n(e)), a = (r.files ?? []).map((t) => ({
|
|
71
|
+
name: t.path.split("/").pop() ?? t.path,
|
|
72
|
+
type: "file"
|
|
73
|
+
}));
|
|
74
|
+
return [...(r.subdirectories ?? []).map((t) => ({
|
|
75
|
+
name: t.path.split("/").pop() ?? t.path,
|
|
76
|
+
type: "directory"
|
|
77
|
+
})), ...a];
|
|
78
|
+
},
|
|
79
|
+
grep: async ({ pattern: e, path: r, ignoreCase: a }) => {
|
|
80
|
+
const t = ["-rnI", "-m", String(x.grep.maxPerFile)];
|
|
81
|
+
a && t.push("-i"), t.push("-E", "--", e);
|
|
82
|
+
const h = r?.trim() ? n(r.trim()) : ".", y = `git grep -nI ${a ? "-i " : ""}-m ${x.grep.maxPerFile} -E -- ${d(e)} ${d(h)} 2>/dev/null || grep ${t.join(" ")} ${d(h)}`;
|
|
83
|
+
return (await w(y, 60)).stdout ?? "";
|
|
84
|
+
},
|
|
85
|
+
exec: (e, r) => w(e, r?.timeoutSeconds)
|
|
86
|
+
},
|
|
87
|
+
sandboxName: c,
|
|
88
|
+
destroy: async () => {
|
|
89
|
+
!f || process.env.NAH_KEEP_SANDBOX === "1" || await p.SandboxInstance.delete?.(c).catch(() => {
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}, d = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
94
|
+
export {
|
|
95
|
+
b as createBlaxelEnvironment
|
|
96
|
+
};
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
import { StudioStore } from './store.js';
|
|
3
|
+
import { StreamHub } from './stream.js';
|
|
4
|
+
import { Launcher } from './launcher.js';
|
|
5
|
+
import { Scorer } from './evals.js';
|
|
6
|
+
export type StudioServerOptions = {
|
|
7
|
+
store: StudioStore;
|
|
8
|
+
/**
|
|
9
|
+
* Directory holding the built Studio (`index.html` plus hashed assets).
|
|
10
|
+
*
|
|
11
|
+
* The UI is built by the package's own second Vite pass into `dist/ui`, so the
|
|
12
|
+
* server serves bytes from beside its entry point rather than shipping a
|
|
13
|
+
* second copy of the source.
|
|
14
|
+
*/
|
|
15
|
+
assetDir?: string;
|
|
16
|
+
port?: number;
|
|
17
|
+
host?: string;
|
|
18
|
+
/** Reuse an already-listening server in tests. */
|
|
19
|
+
server?: Server;
|
|
20
|
+
/**
|
|
21
|
+
* Runs one prompt through the real agent, used by the chat tab and by
|
|
22
|
+
* experiments. Supplied by `run.ts`, which owns model resolution.
|
|
23
|
+
*/
|
|
24
|
+
execute?: (input: string) => Promise<{
|
|
25
|
+
output: string;
|
|
26
|
+
toolsCalled: string[];
|
|
27
|
+
filesChanged: string[];
|
|
28
|
+
traceId?: string;
|
|
29
|
+
}>;
|
|
30
|
+
/** Model the server is running, recorded on experiments. */
|
|
31
|
+
model?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Shared secret for every request. Set when the server is not bound to
|
|
34
|
+
* loopback, because the store holds prompts, file paths and tool output.
|
|
35
|
+
*/
|
|
36
|
+
token?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Spawning agents. Absent in tests and whenever no `nah` binary can be found,
|
|
39
|
+
* in which case the launch route reports that instead of pretending.
|
|
40
|
+
*/
|
|
41
|
+
launcher?: Launcher;
|
|
42
|
+
/** The live event stream. Supplied by `startStudioServer`, which owns its lifetime. */
|
|
43
|
+
stream?: StreamHub;
|
|
44
|
+
/** The directory the built-in agent works in, reported by `/api/info`. */
|
|
45
|
+
cwd?: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Scorers this process can actually run.
|
|
49
|
+
*
|
|
50
|
+
* A registry rather than instances in the database, because a scorer is code and
|
|
51
|
+
* a database row cannot be code. The row carries the name and the description;
|
|
52
|
+
* this map decides what runs.
|
|
53
|
+
*/
|
|
54
|
+
export declare const registeredScorers: Map<string, Scorer>;
|
|
55
|
+
/** Register the scorers that need no model, so a fresh server has something to run. */
|
|
56
|
+
export declare const registerBuiltinScorers: () => void;
|
|
57
|
+
export declare const createStudioServer: (options: StudioServerOptions) => Server;
|
|
58
|
+
export type StudioHandle = {
|
|
59
|
+
server: Server;
|
|
60
|
+
url: string;
|
|
61
|
+
close(): Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
export declare const startStudioServer: (options: StudioServerOptions) => Promise<StudioHandle>;
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { Agent, AgentLogLine, AgentStatus, AgentSummary, Span, Trace } from './wire.js';
|
|
2
|
+
export type TraceRow = Trace;
|
|
3
|
+
export type DatasetItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
input: string;
|
|
6
|
+
expected?: string;
|
|
7
|
+
metadata: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
export type ScoreRecord = {
|
|
10
|
+
scorerId: string;
|
|
11
|
+
score: number;
|
|
12
|
+
reason?: string;
|
|
13
|
+
/** Scorers may decline rather than guess; recorded so averages stay honest. */
|
|
14
|
+
skipped?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type ExperimentResult = {
|
|
17
|
+
id: string;
|
|
18
|
+
itemId: string;
|
|
19
|
+
input: string;
|
|
20
|
+
status: "passed" | "failed" | "error";
|
|
21
|
+
output?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
scores: ScoreRecord[];
|
|
24
|
+
traceId?: string;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
/** Provider-level retries before this item ran (or gave up). */
|
|
27
|
+
attempts: number;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* What changed, for whoever is watching.
|
|
31
|
+
*
|
|
32
|
+
* The store is synchronous and knows nothing about HTTP, so it announces facts
|
|
33
|
+
* and the server decides who to tell. That is what keeps the live dashboard from
|
|
34
|
+
* needing a second source of truth: there is no polling loop to drift from the
|
|
35
|
+
* writes that actually happened.
|
|
36
|
+
*/
|
|
37
|
+
export type StoreEvent = {
|
|
38
|
+
type: "trace";
|
|
39
|
+
trace: Trace;
|
|
40
|
+
agentId: string | null;
|
|
41
|
+
} | {
|
|
42
|
+
type: "agent";
|
|
43
|
+
agentId: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: "log";
|
|
46
|
+
agentId: string;
|
|
47
|
+
line: AgentLogLine;
|
|
48
|
+
};
|
|
49
|
+
export declare class StudioStore {
|
|
50
|
+
private readonly db;
|
|
51
|
+
private readonly listeners;
|
|
52
|
+
constructor(options: {
|
|
53
|
+
path: string;
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* Bring an older file up to the current shape.
|
|
57
|
+
*
|
|
58
|
+
* `CREATE TABLE IF NOT EXISTS` only helps a store that does not exist yet, and
|
|
59
|
+
* everyone who ran the Studio last week has one. SQLite has no
|
|
60
|
+
* `ADD COLUMN IF NOT EXISTS`, so the columns are checked first: an `ALTER` on
|
|
61
|
+
* an existing column is an error, and a dashboard that refuses to start
|
|
62
|
+
* because it ran twice is worse than one that starts twice.
|
|
63
|
+
*/
|
|
64
|
+
private migrate;
|
|
65
|
+
/** Watch for writes. Returns the unsubscribe, so a caller cannot leak a listener. */
|
|
66
|
+
on(listener: (event: StoreEvent) => void): () => void;
|
|
67
|
+
/**
|
|
68
|
+
* Tell the listeners, and never let one of them fail the write.
|
|
69
|
+
*
|
|
70
|
+
* A watcher is a viewer. A dashboard that has gone away mid-turn must not turn
|
|
71
|
+
* a completed run into an error for the agent that produced it.
|
|
72
|
+
*/
|
|
73
|
+
private emit;
|
|
74
|
+
/** Persist a finished trace and all of its spans, as one unit. */
|
|
75
|
+
saveTrace(trace: Trace, spans: Span[], agentId?: string | null): void;
|
|
76
|
+
listTraces(filter?: {
|
|
77
|
+
limit?: number;
|
|
78
|
+
search?: string;
|
|
79
|
+
status?: string;
|
|
80
|
+
tag?: string;
|
|
81
|
+
since?: number;
|
|
82
|
+
until?: number;
|
|
83
|
+
/** One agent, or every agent. */
|
|
84
|
+
agentId?: string;
|
|
85
|
+
sort?: "startTime" | "name" | "costUsd";
|
|
86
|
+
order?: "asc" | "desc";
|
|
87
|
+
}): TraceRow[];
|
|
88
|
+
getTrace(id: string): {
|
|
89
|
+
trace: TraceRow;
|
|
90
|
+
spans: Span[];
|
|
91
|
+
} | null;
|
|
92
|
+
/**
|
|
93
|
+
* Bucketed history for the dashboard charts.
|
|
94
|
+
*
|
|
95
|
+
* Buckets rather than a raw series: a week of runs is thousands of points, and
|
|
96
|
+
* a sparkline cannot show that anyway. `NULLS`-free arithmetic keeps an empty
|
|
97
|
+
* hour at zero rather than absent, which is the difference between "no runs"
|
|
98
|
+
* and "the query lost them".
|
|
99
|
+
*/
|
|
100
|
+
timeseries(buckets?: number, filter?: {
|
|
101
|
+
agentId?: string;
|
|
102
|
+
}): Array<{
|
|
103
|
+
t: number;
|
|
104
|
+
traces: number;
|
|
105
|
+
errors: number;
|
|
106
|
+
costUsd: number;
|
|
107
|
+
medianMs: number;
|
|
108
|
+
}>;
|
|
109
|
+
/**
|
|
110
|
+
* Per-tool aggregates across every trace.
|
|
111
|
+
*
|
|
112
|
+
* "Which tool is slow or failing" is a question about the whole history, not
|
|
113
|
+
* about one run, so it is answered in SQL rather than by loading spans.
|
|
114
|
+
*/
|
|
115
|
+
toolStats(filter?: {
|
|
116
|
+
agentId?: string;
|
|
117
|
+
}): Array<{
|
|
118
|
+
tool: string;
|
|
119
|
+
calls: number;
|
|
120
|
+
errors: number;
|
|
121
|
+
errorRate: number;
|
|
122
|
+
p50Ms: number;
|
|
123
|
+
p95Ms: number;
|
|
124
|
+
totalMs: number;
|
|
125
|
+
}>;
|
|
126
|
+
/** Totals for the dashboard. One pass, so it stays cheap as the table grows. */
|
|
127
|
+
overview(filter?: {
|
|
128
|
+
agentId?: string;
|
|
129
|
+
}): {
|
|
130
|
+
traces: number;
|
|
131
|
+
errors: number;
|
|
132
|
+
costUsd: number;
|
|
133
|
+
inputTokens: number;
|
|
134
|
+
outputTokens: number;
|
|
135
|
+
medianDurationMs: number;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Register, or refresh, one agent.
|
|
139
|
+
*
|
|
140
|
+
* An id is the identity: a process asks with the same id every time it
|
|
141
|
+
* heartbeats, so a re-registration updates one row instead of filling the list
|
|
142
|
+
* with copies of the same agent. A caller with no id gets a new one, which is
|
|
143
|
+
* what a fresh `nah` session does.
|
|
144
|
+
*/
|
|
145
|
+
registerAgent(registration: {
|
|
146
|
+
id?: string;
|
|
147
|
+
name?: string;
|
|
148
|
+
cwd: string;
|
|
149
|
+
host?: string;
|
|
150
|
+
pid?: number;
|
|
151
|
+
model?: string | null;
|
|
152
|
+
version?: string | null;
|
|
153
|
+
source?: "session" | "launch";
|
|
154
|
+
metadata?: Record<string, unknown>;
|
|
155
|
+
status?: AgentStatus;
|
|
156
|
+
}): Agent;
|
|
157
|
+
/**
|
|
158
|
+
* Move an agent to a new status and mark it seen.
|
|
159
|
+
*
|
|
160
|
+
* Separate from registration because a heartbeat must not be able to rewrite
|
|
161
|
+
* the fields it has no opinion about — a status change should not blank a model.
|
|
162
|
+
*/
|
|
163
|
+
setAgentState(id: string, patch: {
|
|
164
|
+
status?: AgentStatus;
|
|
165
|
+
exitCode?: number | null;
|
|
166
|
+
metadata?: Record<string, unknown>;
|
|
167
|
+
}): Agent | null;
|
|
168
|
+
getAgent(id: string): Agent | null;
|
|
169
|
+
/**
|
|
170
|
+
* Agents, each with its own totals, newest activity first.
|
|
171
|
+
*
|
|
172
|
+
* `runningBefore` is what makes the online dot honest: a process that stopped
|
|
173
|
+
* reporting is offline even though nothing ever wrote "offline", because a
|
|
174
|
+
* machine that was unplugged cannot file that report.
|
|
175
|
+
*/
|
|
176
|
+
listAgents(filter?: {
|
|
177
|
+
runningBefore?: number;
|
|
178
|
+
cwd?: string;
|
|
179
|
+
}): AgentSummary[];
|
|
180
|
+
/**
|
|
181
|
+
* One agent's summary.
|
|
182
|
+
*
|
|
183
|
+
* Separate from filtering the list because the live stream needs this per
|
|
184
|
+
* event, and re-aggregating every agent to describe the one that just moved is
|
|
185
|
+
* how a ten-agent dashboard spends its time on nine agents nobody is watching.
|
|
186
|
+
*/
|
|
187
|
+
getAgentSummary(id: string, runningBefore?: number): AgentSummary | null;
|
|
188
|
+
private summarize;
|
|
189
|
+
deleteAgent(id: string): boolean;
|
|
190
|
+
/** Append one line of an agent's output, and hand it to whoever is watching. */
|
|
191
|
+
appendAgentLog(agentId: string, line: {
|
|
192
|
+
stream: "stdout" | "stderr" | "system";
|
|
193
|
+
text: string;
|
|
194
|
+
}): AgentLogLine;
|
|
195
|
+
listAgentLogs(agentId: string, limit?: number): AgentLogLine[];
|
|
196
|
+
/**
|
|
197
|
+
* Drop the rows nobody is looking at any more.
|
|
198
|
+
*
|
|
199
|
+
* Agents age out far faster than traces: a process from last month is not a
|
|
200
|
+
* thing to keep a row for, and the trace it produced is the part worth keeping.
|
|
201
|
+
*/
|
|
202
|
+
pruneAgents(olderThanMs: number): number;
|
|
203
|
+
saveMessage(message: {
|
|
204
|
+
id: string;
|
|
205
|
+
traceId?: string;
|
|
206
|
+
agentId?: string;
|
|
207
|
+
role: string;
|
|
208
|
+
content: string;
|
|
209
|
+
metadata?: Record<string, unknown>;
|
|
210
|
+
}): void;
|
|
211
|
+
/**
|
|
212
|
+
* Messages newest first, with a monotonic sequence number.
|
|
213
|
+
*
|
|
214
|
+
* `created_at` is millisecond-resolution, and a short reply writes its user and
|
|
215
|
+
* assistant messages inside the same millisecond — so timestamp alone is not a
|
|
216
|
+
* total order and a thread rebuilt from it can come back reversed. `seq` is
|
|
217
|
+
* SQLite's insertion counter: it is monotonic, so sorting by it puts the thread
|
|
218
|
+
* back in the order it was actually said in. The sort order *within* equal
|
|
219
|
+
* timestamps is left unspecified on purpose rather than papered over.
|
|
220
|
+
*/
|
|
221
|
+
listMessages(filter?: {
|
|
222
|
+
traceId?: string;
|
|
223
|
+
limit?: number;
|
|
224
|
+
}): Array<{
|
|
225
|
+
id: string;
|
|
226
|
+
traceId?: string;
|
|
227
|
+
role: string;
|
|
228
|
+
content: string;
|
|
229
|
+
createdAt: number;
|
|
230
|
+
seq: number;
|
|
231
|
+
}>;
|
|
232
|
+
/** Bounded retention. Traces are diagnostic; a year of them is a liability. */
|
|
233
|
+
prune(olderThanMs: number): number;
|
|
234
|
+
createDataset(input: {
|
|
235
|
+
id?: string;
|
|
236
|
+
name: string;
|
|
237
|
+
description?: string;
|
|
238
|
+
}): {
|
|
239
|
+
id: string;
|
|
240
|
+
version: number;
|
|
241
|
+
};
|
|
242
|
+
listDatasets(): Array<{
|
|
243
|
+
id: string;
|
|
244
|
+
name: string;
|
|
245
|
+
description: string;
|
|
246
|
+
version: number;
|
|
247
|
+
items: number;
|
|
248
|
+
}>;
|
|
249
|
+
addDatasetItems(datasetId: string, items: Array<{
|
|
250
|
+
id?: string;
|
|
251
|
+
input: string;
|
|
252
|
+
expected?: string;
|
|
253
|
+
metadata?: Record<string, unknown>;
|
|
254
|
+
}>): number;
|
|
255
|
+
listDatasetItems(datasetId: string): DatasetItem[];
|
|
256
|
+
deleteDataset(id: string): boolean;
|
|
257
|
+
saveScorer(scorer: {
|
|
258
|
+
id: string;
|
|
259
|
+
name: string;
|
|
260
|
+
description?: string;
|
|
261
|
+
kind: string;
|
|
262
|
+
config?: Record<string, unknown>;
|
|
263
|
+
}): void;
|
|
264
|
+
listScorers(): Array<{
|
|
265
|
+
id: string;
|
|
266
|
+
name: string;
|
|
267
|
+
description: string;
|
|
268
|
+
kind: string;
|
|
269
|
+
config: Record<string, unknown>;
|
|
270
|
+
}>;
|
|
271
|
+
startExperiment(input: {
|
|
272
|
+
id?: string;
|
|
273
|
+
datasetId: string;
|
|
274
|
+
model: string;
|
|
275
|
+
}): string;
|
|
276
|
+
saveExperimentResult(result: ExperimentResult & {
|
|
277
|
+
experimentId: string;
|
|
278
|
+
}): void;
|
|
279
|
+
finishExperiment(id: string, summary: Record<string, unknown>, error?: string): void;
|
|
280
|
+
getExperiment(id: string): {
|
|
281
|
+
id: string;
|
|
282
|
+
datasetId: string;
|
|
283
|
+
status: string;
|
|
284
|
+
model: string;
|
|
285
|
+
startedAt: number;
|
|
286
|
+
finishedAt: number | null;
|
|
287
|
+
summary: Record<string, unknown>;
|
|
288
|
+
error?: string;
|
|
289
|
+
results: ExperimentResult[];
|
|
290
|
+
} | null;
|
|
291
|
+
listExperiments(limit?: number): Array<{
|
|
292
|
+
id: string;
|
|
293
|
+
datasetId: string;
|
|
294
|
+
status: string;
|
|
295
|
+
model: string;
|
|
296
|
+
startedAt: number;
|
|
297
|
+
finishedAt: number | null;
|
|
298
|
+
summary: Record<string, unknown>;
|
|
299
|
+
}>;
|
|
300
|
+
close(): void;
|
|
301
|
+
}
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ServerResponse } from 'node:http';
|
|
2
|
+
import { StudioStore } from './store.js';
|
|
3
|
+
export type StreamHub = {
|
|
4
|
+
/** Take over a response and keep it open as an event stream. */
|
|
5
|
+
add: (response: ServerResponse) => void;
|
|
6
|
+
/** End every open stream. Called when the server closes. */
|
|
7
|
+
close: () => void;
|
|
8
|
+
/** Clients currently attached, for tests. */
|
|
9
|
+
readonly size: () => number;
|
|
10
|
+
};
|
|
11
|
+
export declare const createStreamHub: (store: StudioStore, options?: {
|
|
12
|
+
livenessMs?: number;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
}) => StreamHub;
|