@plasm_lang/vercel-agent 0.3.63
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/README.md +235 -0
- package/agent/.plasm/archives/runs/runs/pr2d5c4a99707b4c19b650553d50229a1d600d28e3d98a9c58f18e5026cecc86ca.json +64 -0
- package/agent/.plasm/archives/runs/runs/pr2e0c0d8ad443c63c82da7435ee1a002b0e0fa718b640263c0a9d3e6e5944812f.json +64 -0
- package/agent/.plasm/archives/runs/runs/pr2faedb8354f40ee6d828e3af07b421fda9ccda973a4f7347fce3639f03a0a869.json +64 -0
- package/agent/.plasm/archives/runs/runs/pr586b47c55547b0702c572bce4255558b22dbe5e682d6359169577e0ea75fe98f.json +64 -0
- package/agent/.plasm/archives/runs/runs/pr76212356445e3b00fcf256835aaec18bac68576324b90d0be92d47f0b4a862a7.json +56 -0
- package/agent/.plasm/archives/runs/runs/pr9ec805d689e00db9270a9539858f2fb7216c24acbfea943d450e37b641149da1.json +64 -0
- package/agent/.plasm/archives/runs/runs/prc3c0c4ba2e28fc94ed6d37b6796e277a7997d9cb3184640d14c35c98bc6d136f.json +64 -0
- package/agent/.plasm/archives/runs/runs/prf04de32522f2fdcb17818907d91bccce7dcaecbd1259041cc448d447b6993244.json +64 -0
- package/agent/.plasm/archives/traces/traces/local/00000000000000000000000000000000/records.ndjson +1 -0
- package/agent/.plasm/archives/traces/traces/local/00000000000000000000000000000000/summary.json +13 -0
- package/agent/.plasm/discovery/manifest.json +126 -0
- package/agent/.plasm/sessions/TGlzdCBwcm9kdWN0cyBmcm9tIHRoZSBleGVjdXRlX3RpbnkgY2F0YWxvZw.json +44 -0
- package/agent/.plasm/sessions/TGlzdCBwcm9kdWN0cyBmcm9tIHRoZSBleGVjdXRlX3RpbnkgY2F0YWxvZw.teaching.tsv +23 -0
- package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHM.json +151 -0
- package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHM.teaching.tsv +131 -0
- package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHMgYXN5bmMgdHJhbnNwb3J0.json +44 -0
- package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHMgYXN5bmMgdHJhbnNwb3J0.teaching.tsv +23 -0
- package/agent/.plasm/stubs/.gitkeep +0 -0
- package/agent/.plasm/stubs/execute_tiny.ts +107 -0
- package/agent/agent.ts +52 -0
- package/agent/catalogs/README.md +15 -0
- package/agent/channels/.gitkeep +0 -0
- package/agent/channels/execute-tiny-webhook.ts +59 -0
- package/agent/channels/health.ts +16 -0
- package/agent/hooks/.gitkeep +0 -0
- package/agent/hooks/trace-log.ts +10 -0
- package/agent/instructions.md +25 -0
- package/agent/schedules/.gitkeep +0 -0
- package/agent/schedules/ping.ts +13 -0
- package/agent/skills/.gitkeep +0 -0
- package/agent/skills/plasm-authoring.md +8 -0
- package/agent/subagents/.gitkeep +0 -0
- package/agent/subagents/tiny/agent.ts +28 -0
- package/bin/plasm-agent.mjs +18 -0
- package/package.json +77 -0
- package/scripts/plasm-node.mjs +19 -0
- package/scripts/resolve-ts-extension.mjs +18 -0
- package/src/archive/adapters.ts +27 -0
- package/src/archive/index.ts +99 -0
- package/src/archive/local-run-archive.ts +90 -0
- package/src/archive/local-trace-archive.ts +91 -0
- package/src/archive/paths.ts +15 -0
- package/src/archive/postgres-kv-adapter.ts +72 -0
- package/src/archive/prod-archive-store.ts +143 -0
- package/src/archive/resolve-backend.ts +60 -0
- package/src/archive/run-id.ts +23 -0
- package/src/archive/types.ts +75 -0
- package/src/archive/vercel-blob-adapter.ts +21 -0
- package/src/archive/vercel-kv-adapter.ts +24 -0
- package/src/authoring/channel-dispatch.ts +44 -0
- package/src/authoring/context.ts +34 -0
- package/src/authoring/define-channel.ts +83 -0
- package/src/authoring/define-hook.ts +51 -0
- package/src/authoring/define-schedule.ts +64 -0
- package/src/authoring/define-skill.ts +38 -0
- package/src/authoring/hook-runner.ts +18 -0
- package/src/authoring/schedule-manager.ts +118 -0
- package/src/authoring/slot-loader.ts +253 -0
- package/src/authoring/subagent-loader.ts +121 -0
- package/src/catalog/loader.ts +71 -0
- package/src/cli/build.ts +54 -0
- package/src/cli/dev.ts +60 -0
- package/src/cli/info.ts +12 -0
- package/src/cli/init.ts +372 -0
- package/src/cli/link.ts +68 -0
- package/src/cli/project-root.ts +57 -0
- package/src/define-agent.ts +150 -0
- package/src/dev/client/ansi.ts +36 -0
- package/src/dev/client/http-session.ts +180 -0
- package/src/dev/client/repl.ts +92 -0
- package/src/dev/client/slash.ts +119 -0
- package/src/dev/dev-session.ts +153 -0
- package/src/dev/http.ts +29 -0
- package/src/dev/server.ts +147 -0
- package/src/dev/session-routes.ts +185 -0
- package/src/discovery/project-walker.ts +272 -0
- package/src/engine/connect-auth.ts +135 -0
- package/src/engine/create-host-transport.ts +7 -0
- package/src/engine/fixture-mock-transport.ts +54 -0
- package/src/engine/host-transport-bridge.ts +32 -0
- package/src/engine/host-transport.ts +84 -0
- package/src/engine/napi-binding.ts +265 -0
- package/src/evals/define-eval.ts +56 -0
- package/src/evals/run-eval.ts +136 -0
- package/src/gateway-model.ts +43 -0
- package/src/index.ts +296 -0
- package/src/instrumentation.ts +56 -0
- package/src/load-env.ts +63 -0
- package/src/operator/routes.ts +287 -0
- package/src/operator/types.ts +63 -0
- package/src/operator/ui-shell.ts +134 -0
- package/src/project-info.ts +229 -0
- package/src/runtime/agent-runtime.ts +469 -0
- package/src/runtime/compaction.ts +81 -0
- package/src/runtime/logical-session.ts +72 -0
- package/src/runtime/plasm-agent.ts +199 -0
- package/src/server/plasm-handler.ts +331 -0
- package/src/session-state.ts +135 -0
- package/src/state/define-state.ts +57 -0
- package/src/state/fs-state-adapter.ts +72 -0
- package/src/state/kv-state-adapter.ts +62 -0
- package/src/state/postgres-state-adapter.ts +116 -0
- package/src/stubs/capability-invoke-shape.ts +135 -0
- package/src/stubs/catalog-client.ts +170 -0
- package/src/stubs/catalog-hash.ts +11 -0
- package/src/stubs/catalog-introspection.ts +121 -0
- package/src/stubs/cgs-ts-types.ts +164 -0
- package/src/stubs/domain-parser.ts +203 -0
- package/src/stubs/generator.ts +390 -0
- package/src/stubs/input-type-to-ts.ts +233 -0
- package/src/stubs/plasm-value-emitter.ts +162 -0
- package/src/stubs/program-builder.ts +82 -0
- package/src/stubs/stub-symbols.ts +89 -0
- package/src/symbol-registry.ts +74 -0
- package/src/telemetry/plasm-spans.ts +83 -0
- package/src/tools/descriptions.ts +94 -0
- package/src/tools/format.ts +29 -0
- package/src/tools/harness-tools.ts +65 -0
- package/src/tools/plasm-tools.ts +104 -0
- package/src/workflow/world-bootstrap.ts +52 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { PlanArchiveSnapshot, RunSnapshot } from "./types.js";
|
|
5
|
+
|
|
6
|
+
function safeFilename(value: string): string {
|
|
7
|
+
if (!value || value.includes("..") || value.includes("/") || value.includes("\\")) {
|
|
8
|
+
throw new Error("invalid archive filename");
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class LocalRunArchive {
|
|
14
|
+
constructor(private readonly root: string) {}
|
|
15
|
+
|
|
16
|
+
private plansDir(): string {
|
|
17
|
+
return path.join(this.root, "plans");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private runsDir(): string {
|
|
21
|
+
return path.join(this.root, "runs");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async ensureRoot(): Promise<void> {
|
|
25
|
+
await mkdir(this.plansDir(), { recursive: true });
|
|
26
|
+
await mkdir(this.runsDir(), { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async writePlanArchive(snapshot: PlanArchiveSnapshot): Promise<void> {
|
|
30
|
+
await this.ensureRoot();
|
|
31
|
+
const file = path.join(this.plansDir(), `${safeFilename(snapshot.plan_commit_ref)}.json`);
|
|
32
|
+
await writeFile(file, JSON.stringify(snapshot, null, 2), "utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async writeRunSnapshot(snapshot: RunSnapshot): Promise<void> {
|
|
36
|
+
await this.ensureRoot();
|
|
37
|
+
const file = path.join(this.runsDir(), `${safeFilename(snapshot.run_id)}.json`);
|
|
38
|
+
await writeFile(file, JSON.stringify(snapshot, null, 2), "utf8");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async listPlans(limit = 50): Promise<PlanArchiveSnapshot[]> {
|
|
42
|
+
return this.listJsonDir<PlanArchiveSnapshot>(this.plansDir(), limit);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async listRuns(limit = 50): Promise<RunSnapshot[]> {
|
|
46
|
+
return this.listJsonDir<RunSnapshot>(this.runsDir(), limit);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getPlan(planCommitRef: string): Promise<PlanArchiveSnapshot | null> {
|
|
50
|
+
return this.readJson<PlanArchiveSnapshot>(
|
|
51
|
+
path.join(this.plansDir(), `${safeFilename(planCommitRef)}.json`),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getRun(runId: string): Promise<RunSnapshot | null> {
|
|
56
|
+
return this.readJson<RunSnapshot>(
|
|
57
|
+
path.join(this.runsDir(), `${safeFilename(runId)}.json`),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async listJsonDir<T>(dir: string, limit: number): Promise<T[]> {
|
|
62
|
+
let files: string[];
|
|
63
|
+
try {
|
|
64
|
+
files = await readdir(dir);
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const items: Array<{ at: string; value: T }> = [];
|
|
69
|
+
for (const file of files.filter((f) => f.endsWith(".json"))) {
|
|
70
|
+
const value = await this.readJson<T>(path.join(dir, file));
|
|
71
|
+
if (!value) continue;
|
|
72
|
+
const at =
|
|
73
|
+
(value as { archived_at?: string }).archived_at ??
|
|
74
|
+
(value as { plan_commit_ref?: string }).plan_commit_ref ??
|
|
75
|
+
file;
|
|
76
|
+
items.push({ at, value });
|
|
77
|
+
}
|
|
78
|
+
items.sort((a, b) => b.at.localeCompare(a.at));
|
|
79
|
+
return items.slice(0, limit).map((item) => item.value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async readJson<T>(filePath: string): Promise<T | null> {
|
|
83
|
+
try {
|
|
84
|
+
const raw = await readFile(filePath, "utf8");
|
|
85
|
+
return JSON.parse(raw) as T;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { TraceDetail, TraceRecord, TraceSummary } from "./types.js";
|
|
5
|
+
|
|
6
|
+
function safeSegment(value: string): string {
|
|
7
|
+
if (!value || value.includes("..") || value.includes("/") || value.includes("\\")) {
|
|
8
|
+
throw new Error("invalid archive path segment");
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class LocalTraceArchive {
|
|
14
|
+
constructor(private readonly root: string) {}
|
|
15
|
+
|
|
16
|
+
private traceDir(tenantId: string, traceId: string): string {
|
|
17
|
+
return path.join(this.root, "traces", safeSegment(tenantId), safeSegment(traceId));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async ensureRoot(): Promise<void> {
|
|
21
|
+
await mkdir(this.root, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async appendRecord(
|
|
25
|
+
tenantId: string,
|
|
26
|
+
traceId: string,
|
|
27
|
+
record: TraceRecord,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const dir = this.traceDir(tenantId, traceId);
|
|
30
|
+
await mkdir(dir, { recursive: true });
|
|
31
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
32
|
+
await writeFile(path.join(dir, "records.ndjson"), line, { flag: "a" });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async persistTrace(detail: TraceDetail): Promise<void> {
|
|
36
|
+
const dir = this.traceDir(detail.summary.tenant_id, detail.summary.trace_id);
|
|
37
|
+
await mkdir(dir, { recursive: true });
|
|
38
|
+
await writeFile(
|
|
39
|
+
path.join(dir, "summary.json"),
|
|
40
|
+
JSON.stringify(detail.summary, null, 2),
|
|
41
|
+
"utf8",
|
|
42
|
+
);
|
|
43
|
+
const body = detail.records.map((r) => JSON.stringify(r)).join("\n");
|
|
44
|
+
const suffix = body.length > 0 ? "\n" : "";
|
|
45
|
+
await writeFile(path.join(dir, "records.ndjson"), `${body}${suffix}`, "utf8");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async listTraces(tenantId: string, limit = 50): Promise<TraceSummary[]> {
|
|
49
|
+
const dir = path.join(this.root, "traces", safeSegment(tenantId));
|
|
50
|
+
let entries: string[];
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(dir);
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const summaries: TraceSummary[] = [];
|
|
57
|
+
for (const traceId of entries) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = await readFile(path.join(dir, traceId, "summary.json"), "utf8");
|
|
60
|
+
summaries.push(JSON.parse(raw) as TraceSummary);
|
|
61
|
+
} catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
summaries.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
66
|
+
return summaries.slice(0, limit);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getTrace(tenantId: string, traceId: string): Promise<TraceDetail | null> {
|
|
70
|
+
const dir = this.traceDir(tenantId, traceId);
|
|
71
|
+
let summaryRaw: string;
|
|
72
|
+
try {
|
|
73
|
+
summaryRaw = await readFile(path.join(dir, "summary.json"), "utf8");
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const summary = JSON.parse(summaryRaw) as TraceSummary;
|
|
78
|
+
let records: TraceRecord[] = [];
|
|
79
|
+
try {
|
|
80
|
+
const ndjson = await readFile(path.join(dir, "records.ndjson"), "utf8");
|
|
81
|
+
records = ndjson
|
|
82
|
+
.split("\n")
|
|
83
|
+
.map((line) => line.trim())
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.map((line) => JSON.parse(line) as TraceRecord);
|
|
86
|
+
} catch {
|
|
87
|
+
records = [];
|
|
88
|
+
}
|
|
89
|
+
return { summary, records };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { ArchivePaths } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/** Resolve local trace/run archive roots from env or agent defaults. */
|
|
6
|
+
export function resolveArchivePaths(agentRoot: string): ArchivePaths {
|
|
7
|
+
const archivesDir = path.join(agentRoot, ".plasm", "archives");
|
|
8
|
+
const traceRoot =
|
|
9
|
+
process.env.PLASM_TRACE_ARCHIVE_DIR?.trim() ||
|
|
10
|
+
path.join(archivesDir, "traces");
|
|
11
|
+
const runRoot =
|
|
12
|
+
process.env.PLASM_RUN_ARTIFACTS_DIR?.trim() ||
|
|
13
|
+
path.join(archivesDir, "runs");
|
|
14
|
+
return { traceRoot, runRoot };
|
|
15
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { KvArchiveIndexAdapter } from "./types.js";
|
|
2
|
+
|
|
3
|
+
function postgresUrl(): string {
|
|
4
|
+
return (
|
|
5
|
+
process.env.PLASM_ARCHIVE_POSTGRES_URL?.trim() ||
|
|
6
|
+
process.env.PLASM_STATE_POSTGRES_URL?.trim() ||
|
|
7
|
+
process.env.WORKFLOW_POSTGRES_URL?.trim() ||
|
|
8
|
+
process.env.DATABASE_URL?.trim() ||
|
|
9
|
+
""
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SCHEMA_SQL = `
|
|
14
|
+
CREATE TABLE IF NOT EXISTS plasm_agent_archive_index (
|
|
15
|
+
index_key TEXT PRIMARY KEY,
|
|
16
|
+
index_value TEXT NOT NULL,
|
|
17
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
18
|
+
);
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
type PgPool = {
|
|
22
|
+
query(sql: string, params?: unknown[]): Promise<{ rows: Array<Record<string, unknown>> }>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let sharedPool: PgPool | null = null;
|
|
26
|
+
|
|
27
|
+
async function pool(): Promise<PgPool> {
|
|
28
|
+
if (sharedPool) return sharedPool;
|
|
29
|
+
const url = postgresUrl();
|
|
30
|
+
if (!url) {
|
|
31
|
+
throw new Error("Postgres archive index requires DATABASE_URL or WORKFLOW_POSTGRES_URL");
|
|
32
|
+
}
|
|
33
|
+
const pg = await import("pg");
|
|
34
|
+
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
35
|
+
sharedPool = new Pool({ connectionString: url }) as PgPool;
|
|
36
|
+
await sharedPool.query(SCHEMA_SQL);
|
|
37
|
+
return sharedPool;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class PostgresArchiveIndexAdapter implements KvArchiveIndexAdapter {
|
|
41
|
+
async set(key: string, value: string): Promise<void> {
|
|
42
|
+
const db = await pool();
|
|
43
|
+
await db.query(
|
|
44
|
+
`INSERT INTO plasm_agent_archive_index (index_key, index_value, updated_at)
|
|
45
|
+
VALUES ($1, $2, NOW())
|
|
46
|
+
ON CONFLICT (index_key) DO UPDATE
|
|
47
|
+
SET index_value = EXCLUDED.index_value, updated_at = NOW()`,
|
|
48
|
+
[key, value],
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async get(key: string): Promise<string | null> {
|
|
53
|
+
const db = await pool();
|
|
54
|
+
const result = await db.query(
|
|
55
|
+
`SELECT index_value FROM plasm_agent_archive_index WHERE index_key = $1`,
|
|
56
|
+
[key],
|
|
57
|
+
);
|
|
58
|
+
const value = result.rows[0]?.index_value;
|
|
59
|
+
return typeof value === "string" ? value : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async list(prefix: string): Promise<string[]> {
|
|
63
|
+
const db = await pool();
|
|
64
|
+
const result = await db.query(
|
|
65
|
+
`SELECT index_key FROM plasm_agent_archive_index WHERE index_key LIKE $1`,
|
|
66
|
+
[`${prefix}%`],
|
|
67
|
+
);
|
|
68
|
+
return result.rows
|
|
69
|
+
.map((row) => row.index_key)
|
|
70
|
+
.filter((key): key is string => typeof key === "string");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { LocalArchiveStore } from "./index.js";
|
|
2
|
+
import type {
|
|
3
|
+
BlobArchiveAdapter,
|
|
4
|
+
KvArchiveIndexAdapter,
|
|
5
|
+
PlanArchiveSnapshot,
|
|
6
|
+
RunSnapshot,
|
|
7
|
+
TraceDetail,
|
|
8
|
+
TraceRecord,
|
|
9
|
+
TraceSummary,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
function blobKey(kind: "plan" | "run" | "trace", id: string): string {
|
|
13
|
+
return `plasm/archives/${kind}/${id}.json`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function indexKey(kind: "plan" | "run" | "trace", id: string): string {
|
|
17
|
+
return `plasm:index:${kind}:${id}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Durable archive store: local FS cache + Blob bodies + KV/Postgres index. */
|
|
21
|
+
export class ProdArchiveStore {
|
|
22
|
+
readonly local: LocalArchiveStore;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
agentRoot: string,
|
|
26
|
+
private readonly blob: BlobArchiveAdapter,
|
|
27
|
+
private readonly index: KvArchiveIndexAdapter,
|
|
28
|
+
) {
|
|
29
|
+
this.local = LocalArchiveStore.fromAgentRoot(agentRoot);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get paths() {
|
|
33
|
+
return this.local.paths;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async bootstrap(): Promise<void> {
|
|
37
|
+
await this.local.bootstrap();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async putJson(key: string, value: unknown): Promise<void> {
|
|
41
|
+
await this.blob.put(key, JSON.stringify(value));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async getJson<T>(key: string): Promise<T | null> {
|
|
45
|
+
const bytes = await this.blob.get(key);
|
|
46
|
+
if (!bytes) return null;
|
|
47
|
+
return JSON.parse(Buffer.from(bytes).toString("utf8")) as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async recordToolEvent(
|
|
51
|
+
tenantId: string,
|
|
52
|
+
traceId: string,
|
|
53
|
+
kind: string,
|
|
54
|
+
name: string,
|
|
55
|
+
attributes?: Record<string, string | number | boolean>,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
await this.local.recordToolEvent(tenantId, traceId, kind, name, attributes);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async finalizeTrace(detail: TraceDetail): Promise<void> {
|
|
61
|
+
await this.local.finalizeTrace(detail);
|
|
62
|
+
const key = blobKey("trace", detail.summary.trace_id);
|
|
63
|
+
await this.putJson(key, detail);
|
|
64
|
+
await this.index.set(indexKey("trace", detail.summary.trace_id), key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async writePlanArchive(snapshot: PlanArchiveSnapshot): Promise<void> {
|
|
68
|
+
await this.local.writePlanArchive(snapshot);
|
|
69
|
+
const key = blobKey("plan", snapshot.plan_commit_ref);
|
|
70
|
+
await this.putJson(key, snapshot);
|
|
71
|
+
await this.index.set(indexKey("plan", snapshot.plan_commit_ref), key);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async writeRunSnapshot(snapshot: RunSnapshot): Promise<void> {
|
|
75
|
+
await this.local.writeRunSnapshot(snapshot);
|
|
76
|
+
const key = blobKey("run", snapshot.run_id);
|
|
77
|
+
await this.putJson(key, snapshot);
|
|
78
|
+
await this.index.set(indexKey("run", snapshot.run_id), key);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async listTraces(tenantId: string, limit = 50): Promise<TraceSummary[]> {
|
|
82
|
+
const keys = await this.index.list("plasm:index:trace:");
|
|
83
|
+
const items: TraceSummary[] = [];
|
|
84
|
+
for (const idx of keys) {
|
|
85
|
+
const blobRef = await this.index.get(idx);
|
|
86
|
+
if (!blobRef) continue;
|
|
87
|
+
const detail = await this.getJson<TraceDetail>(blobRef);
|
|
88
|
+
if (detail?.summary) items.push(detail.summary);
|
|
89
|
+
}
|
|
90
|
+
if (items.length) {
|
|
91
|
+
items.sort((a, b) => b.started_at_ms - a.started_at_ms);
|
|
92
|
+
return items.slice(0, limit);
|
|
93
|
+
}
|
|
94
|
+
return this.local.listTraces(tenantId, limit);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getTrace(tenantId: string, traceId: string): Promise<TraceDetail | null> {
|
|
98
|
+
const blobRef = await this.index.get(indexKey("trace", traceId));
|
|
99
|
+
if (blobRef) {
|
|
100
|
+
const detail = await this.getJson<TraceDetail>(blobRef);
|
|
101
|
+
if (detail) return detail;
|
|
102
|
+
}
|
|
103
|
+
return this.local.getTrace(tenantId, traceId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async listPlans(limit = 50): Promise<PlanArchiveSnapshot[]> {
|
|
107
|
+
const keys = await this.index.list("plasm:index:plan:");
|
|
108
|
+
const items: PlanArchiveSnapshot[] = [];
|
|
109
|
+
for (const idx of keys) {
|
|
110
|
+
const blobRef = await this.index.get(idx);
|
|
111
|
+
if (!blobRef) continue;
|
|
112
|
+
const plan = await this.getJson<PlanArchiveSnapshot>(blobRef);
|
|
113
|
+
if (plan) items.push(plan);
|
|
114
|
+
}
|
|
115
|
+
if (items.length) {
|
|
116
|
+
items.sort((a, b) => b.archived_at.localeCompare(a.archived_at));
|
|
117
|
+
return items.slice(0, limit);
|
|
118
|
+
}
|
|
119
|
+
return this.local.listPlans(limit);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async listRuns(limit = 50): Promise<RunSnapshot[]> {
|
|
123
|
+
const keys = await this.index.list("plasm:index:run:");
|
|
124
|
+
const items: RunSnapshot[] = [];
|
|
125
|
+
for (const idx of keys) {
|
|
126
|
+
const blobRef = await this.index.get(idx);
|
|
127
|
+
if (!blobRef) continue;
|
|
128
|
+
const run = await this.getJson<RunSnapshot>(blobRef);
|
|
129
|
+
if (run) items.push(run);
|
|
130
|
+
}
|
|
131
|
+
if (items.length) {
|
|
132
|
+
items.sort((a, b) => b.archived_at.localeCompare(a.archived_at));
|
|
133
|
+
return items.slice(0, limit);
|
|
134
|
+
}
|
|
135
|
+
return this.local.listRuns(limit);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async listArchives(limit?: number) {
|
|
139
|
+
return this.local.listArchives(limit);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type { TraceRecord };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AgentWorkflowWorldDefinition } from "../define-agent.js";
|
|
2
|
+
import { LocalArchiveStore } from "./index.js";
|
|
3
|
+
import { PostgresArchiveIndexAdapter } from "./postgres-kv-adapter.js";
|
|
4
|
+
import { ProdArchiveStore } from "./prod-archive-store.js";
|
|
5
|
+
import { VercelBlobArchiveAdapter } from "./vercel-blob-adapter.js";
|
|
6
|
+
import { VercelKvArchiveIndexAdapter } from "./vercel-kv-adapter.js";
|
|
7
|
+
import type { BlobArchiveAdapter, KvArchiveIndexAdapter } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export type ArchiveBackend = "local" | "vercel" | "postgres";
|
|
10
|
+
|
|
11
|
+
export function resolveArchiveBackend(
|
|
12
|
+
world?: AgentWorkflowWorldDefinition,
|
|
13
|
+
): ArchiveBackend {
|
|
14
|
+
const explicit = process.env.PLASM_ARCHIVE_BACKEND?.trim().toLowerCase();
|
|
15
|
+
if (explicit === "local" || explicit === "vercel" || explicit === "postgres") {
|
|
16
|
+
return explicit;
|
|
17
|
+
}
|
|
18
|
+
if (
|
|
19
|
+
process.env.BLOB_READ_WRITE_TOKEN?.trim() ||
|
|
20
|
+
process.env.PLASM_RUN_ARTIFACTS_URL?.trim()
|
|
21
|
+
) {
|
|
22
|
+
if (process.env.KV_REST_API_URL?.trim() || process.env.PLASM_KV_REST_API_URL?.trim()) {
|
|
23
|
+
return "vercel";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (world?.type === "postgres") return "postgres";
|
|
27
|
+
return "local";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveBlobAdapter(): BlobArchiveAdapter | undefined {
|
|
31
|
+
if (process.env.BLOB_READ_WRITE_TOKEN?.trim()) {
|
|
32
|
+
return new VercelBlobArchiveAdapter();
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveIndexAdapter(
|
|
38
|
+
backend: ArchiveBackend,
|
|
39
|
+
): KvArchiveIndexAdapter | undefined {
|
|
40
|
+
if (backend === "vercel") {
|
|
41
|
+
return new VercelKvArchiveIndexAdapter();
|
|
42
|
+
}
|
|
43
|
+
if (backend === "postgres") {
|
|
44
|
+
return new PostgresArchiveIndexAdapter();
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createArchiveStore(
|
|
50
|
+
agentRoot: string,
|
|
51
|
+
options?: { backend?: ArchiveBackend; world?: AgentWorkflowWorldDefinition },
|
|
52
|
+
): LocalArchiveStore | ProdArchiveStore {
|
|
53
|
+
const backend = options?.backend ?? resolveArchiveBackend(options?.world);
|
|
54
|
+
const blob = resolveBlobAdapter();
|
|
55
|
+
const index = resolveIndexAdapter(backend);
|
|
56
|
+
if (backend !== "local" && blob && index) {
|
|
57
|
+
return new ProdArchiveStore(agentRoot, blob, index);
|
|
58
|
+
}
|
|
59
|
+
return LocalArchiveStore.fromAgentRoot(agentRoot);
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export interface RunIdBundle {
|
|
4
|
+
catalogCgsHash: string;
|
|
5
|
+
planCommitRef: string;
|
|
6
|
+
program: string;
|
|
7
|
+
entryId?: string;
|
|
8
|
+
requestFingerprints?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Deterministic `pr` + 64 hex run id aligned with OSS wire form. */
|
|
12
|
+
export function computeRunId(bundle: RunIdBundle): string {
|
|
13
|
+
const canonical = JSON.stringify({
|
|
14
|
+
v: 1,
|
|
15
|
+
catalog_cgs_hash: bundle.catalogCgsHash,
|
|
16
|
+
plan_commit_ref: bundle.planCommitRef,
|
|
17
|
+
program: bundle.program,
|
|
18
|
+
entry_id: bundle.entryId ?? null,
|
|
19
|
+
request_fingerprints: [...(bundle.requestFingerprints ?? [])].sort(),
|
|
20
|
+
});
|
|
21
|
+
const digest = createHash("sha256").update(canonical).digest("hex");
|
|
22
|
+
return `pr${digest}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface TraceSummary {
|
|
2
|
+
trace_id: string;
|
|
3
|
+
tenant_id: string;
|
|
4
|
+
logical_session_id?: string;
|
|
5
|
+
logical_session_ref?: string;
|
|
6
|
+
intent?: string;
|
|
7
|
+
status: "completed" | "live";
|
|
8
|
+
started_at_ms: number;
|
|
9
|
+
ended_at_ms?: number;
|
|
10
|
+
project_slug: string;
|
|
11
|
+
totals?: {
|
|
12
|
+
tool_calls?: number;
|
|
13
|
+
model_calls?: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TraceDetail {
|
|
18
|
+
summary: TraceSummary;
|
|
19
|
+
records: TraceRecord[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TraceRecord {
|
|
23
|
+
at_ms: number;
|
|
24
|
+
kind: string;
|
|
25
|
+
name: string;
|
|
26
|
+
attributes?: Record<string, string | number | boolean>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PlanArchiveSnapshot {
|
|
30
|
+
plan_commit_ref: string;
|
|
31
|
+
program: string;
|
|
32
|
+
catalog_cgs_hash: string;
|
|
33
|
+
entry_id?: string;
|
|
34
|
+
logical_session_ref?: string;
|
|
35
|
+
intent?: string;
|
|
36
|
+
comp_json?: unknown;
|
|
37
|
+
archived_at: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RunSnapshot {
|
|
41
|
+
run_id: string;
|
|
42
|
+
plan_commit_ref: string;
|
|
43
|
+
catalog_cgs_hash: string;
|
|
44
|
+
entry_id?: string;
|
|
45
|
+
logical_session_ref?: string;
|
|
46
|
+
intent?: string;
|
|
47
|
+
ok: boolean;
|
|
48
|
+
message: string;
|
|
49
|
+
results?: unknown[];
|
|
50
|
+
_meta?: {
|
|
51
|
+
plasm?: {
|
|
52
|
+
steps?: unknown[];
|
|
53
|
+
request_fingerprints?: string[];
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
archived_at: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ArchivePaths {
|
|
60
|
+
traceRoot: string;
|
|
61
|
+
runRoot: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Production Blob adapter (Vercel Blob / S3-compatible). */
|
|
65
|
+
export interface BlobArchiveAdapter {
|
|
66
|
+
put(key: string, body: string | Uint8Array): Promise<void>;
|
|
67
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Production KV / Postgres index adapter for archive listings. */
|
|
71
|
+
export interface KvArchiveIndexAdapter {
|
|
72
|
+
set(key: string, value: string): Promise<void>;
|
|
73
|
+
get(key: string): Promise<string | null>;
|
|
74
|
+
list(prefix: string): Promise<string[]>;
|
|
75
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BlobArchiveAdapter } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export class VercelBlobArchiveAdapter implements BlobArchiveAdapter {
|
|
4
|
+
async put(key: string, body: string | Uint8Array): Promise<void> {
|
|
5
|
+
const { put } = await import("@vercel/blob");
|
|
6
|
+
const payload = typeof body === "string" ? body : Buffer.from(body);
|
|
7
|
+
await put(key, payload, {
|
|
8
|
+
access: "public",
|
|
9
|
+
addRandomSuffix: false,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async get(key: string): Promise<Uint8Array | null> {
|
|
14
|
+
const { head } = await import("@vercel/blob");
|
|
15
|
+
const meta = await head(key).catch(() => null);
|
|
16
|
+
if (!meta?.url) return null;
|
|
17
|
+
const response = await fetch(meta.url);
|
|
18
|
+
if (!response.ok) return null;
|
|
19
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { KvArchiveIndexAdapter } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export class VercelKvArchiveIndexAdapter implements KvArchiveIndexAdapter {
|
|
4
|
+
private async kv() {
|
|
5
|
+
const mod = await import("@vercel/kv");
|
|
6
|
+
return mod.kv;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async set(key: string, value: string): Promise<void> {
|
|
10
|
+
const kv = await this.kv();
|
|
11
|
+
await kv.set(key, value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async get(key: string): Promise<string | null> {
|
|
15
|
+
const kv = await this.kv();
|
|
16
|
+
const value = await kv.get<string>(key);
|
|
17
|
+
return value ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async list(prefix: string): Promise<string[]> {
|
|
21
|
+
const kv = await this.kv();
|
|
22
|
+
return kv.keys(`${prefix}*`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
|
|
3
|
+
import type { AuthoringContext } from "./context.js";
|
|
4
|
+
import type { LoadedChannel } from "./slot-loader.js";
|
|
5
|
+
|
|
6
|
+
function sendJson(res: ServerResponse, status: number, payload: unknown): void {
|
|
7
|
+
const body = JSON.stringify(payload, null, 2);
|
|
8
|
+
res.statusCode = status;
|
|
9
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
10
|
+
res.end(body);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function tryHandleChannelRoute(
|
|
14
|
+
req: IncomingMessage,
|
|
15
|
+
res: ServerResponse,
|
|
16
|
+
channels: LoadedChannel[],
|
|
17
|
+
ctx: AuthoringContext,
|
|
18
|
+
): boolean {
|
|
19
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
20
|
+
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
21
|
+
|
|
22
|
+
for (const channel of channels) {
|
|
23
|
+
for (const route of channel.definition.routes) {
|
|
24
|
+
if (route.method !== method || route.path !== pathname) continue;
|
|
25
|
+
void Promise.resolve(route.handler(req, res, ctx)).catch((err: unknown) => {
|
|
26
|
+
if (!res.writableEnded) {
|
|
27
|
+
sendJson(res, 500, { error: "channel_handler_error", message: String(err) });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function listChannelRoutes(channels: LoadedChannel[]): Array<{ method: string; path: string }> {
|
|
38
|
+
return channels.flatMap((channel) =>
|
|
39
|
+
channel.definition.routes.map((route) => ({
|
|
40
|
+
method: route.method,
|
|
41
|
+
path: route.path,
|
|
42
|
+
})),
|
|
43
|
+
);
|
|
44
|
+
}
|