@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,57 @@
|
|
|
1
|
+
import type { AgentWorkflowWorldDefinition } from "../define-agent.js";
|
|
2
|
+
import type { SymbolRegistrySnapshot } from "../symbol-registry.js";
|
|
3
|
+
import type { AgentSessionState, SessionStore } from "../session-state.js";
|
|
4
|
+
import { FsStateAdapter } from "./fs-state-adapter.js";
|
|
5
|
+
import { KvStateAdapter } from "./kv-state-adapter.js";
|
|
6
|
+
import { PostgresStateAdapter } from "./postgres-state-adapter.js";
|
|
7
|
+
|
|
8
|
+
export type StateBackend = "fs" | "kv" | "postgres";
|
|
9
|
+
|
|
10
|
+
export interface AgentStateStore extends SessionStore {
|
|
11
|
+
backend(): StateBackend;
|
|
12
|
+
getSymbolRegistry(tenantId: string): Promise<SymbolRegistrySnapshot | null>;
|
|
13
|
+
putSymbolRegistry(tenantId: string, snapshot: SymbolRegistrySnapshot): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveStateBackend(
|
|
17
|
+
world?: AgentWorkflowWorldDefinition,
|
|
18
|
+
): StateBackend {
|
|
19
|
+
const explicit = process.env.PLASM_STATE_BACKEND?.trim().toLowerCase();
|
|
20
|
+
if (explicit === "fs" || explicit === "kv" || explicit === "postgres") {
|
|
21
|
+
return explicit;
|
|
22
|
+
}
|
|
23
|
+
if (process.env.KV_REST_API_URL?.trim() || process.env.PLASM_KV_REST_API_URL?.trim()) {
|
|
24
|
+
return "kv";
|
|
25
|
+
}
|
|
26
|
+
if (
|
|
27
|
+
process.env.WORKFLOW_POSTGRES_URL?.trim() ||
|
|
28
|
+
process.env.DATABASE_URL?.trim() ||
|
|
29
|
+
process.env.PLASM_STATE_POSTGRES_URL?.trim()
|
|
30
|
+
) {
|
|
31
|
+
if (world?.type === "postgres") return "postgres";
|
|
32
|
+
if (!world?.type && process.env.PLASM_STATE_BACKEND?.trim() === "postgres") {
|
|
33
|
+
return "postgres";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (world?.type === "kv") return "kv";
|
|
37
|
+
if (world?.type === "postgres") return "postgres";
|
|
38
|
+
return "fs";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createAgentStateStore(options: {
|
|
42
|
+
agentRoot: string;
|
|
43
|
+
tenantScope?: string;
|
|
44
|
+
backend?: StateBackend;
|
|
45
|
+
world?: AgentWorkflowWorldDefinition;
|
|
46
|
+
}): AgentStateStore {
|
|
47
|
+
const backend = options.backend ?? resolveStateBackend(options.world);
|
|
48
|
+
const tenantScope = options.tenantScope ?? "local";
|
|
49
|
+
switch (backend) {
|
|
50
|
+
case "kv":
|
|
51
|
+
return new KvStateAdapter(options.agentRoot, tenantScope);
|
|
52
|
+
case "postgres":
|
|
53
|
+
return new PostgresStateAdapter(tenantScope);
|
|
54
|
+
default:
|
|
55
|
+
return new FsStateAdapter(options.agentRoot, tenantScope);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { SymbolRegistrySnapshot } from "../symbol-registry.js";
|
|
5
|
+
import {
|
|
6
|
+
LocalSessionStore,
|
|
7
|
+
type AgentSessionState,
|
|
8
|
+
} from "../session-state.js";
|
|
9
|
+
import type { AgentStateStore, StateBackend } from "./define-state.js";
|
|
10
|
+
|
|
11
|
+
function intentKey(intent: string): string {
|
|
12
|
+
return Buffer.from(intent, "utf8").toString("base64url");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class FsStateAdapter implements AgentStateStore {
|
|
16
|
+
private readonly sessions: LocalSessionStore;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly agentRoot: string,
|
|
20
|
+
private readonly tenantScope: string,
|
|
21
|
+
) {
|
|
22
|
+
this.sessions = new LocalSessionStore(agentRoot);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
backend(): StateBackend {
|
|
26
|
+
return "fs";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async get(intent: string): Promise<AgentSessionState | null> {
|
|
30
|
+
return this.sessions.get(intent);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async put(state: AgentSessionState): Promise<void> {
|
|
34
|
+
await this.sessions.put(state);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async listIntents(): Promise<string[]> {
|
|
38
|
+
return this.sessions.listIntents();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private symbolsPath(): string {
|
|
42
|
+
return path.join(this.agentRoot, ".plasm", "symbols.json");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getSymbolRegistry(tenantId: string): Promise<SymbolRegistrySnapshot | null> {
|
|
46
|
+
void tenantId;
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readFile(this.symbolsPath(), "utf8");
|
|
49
|
+
return JSON.parse(raw) as SymbolRegistrySnapshot;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async putSymbolRegistry(
|
|
56
|
+
tenantId: string,
|
|
57
|
+
snapshot: SymbolRegistrySnapshot,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
void tenantId;
|
|
60
|
+
const file = this.symbolsPath();
|
|
61
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
62
|
+
await writeFile(file, JSON.stringify(snapshot, null, 2), "utf8");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function sessionKvKey(tenantId: string, intent: string): string {
|
|
67
|
+
return `plasm:${tenantId}:session:${intentKey(intent)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function symbolsKvKey(tenantId: string): string {
|
|
71
|
+
return `plasm:${tenantId}:symbols`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SymbolRegistrySnapshot } from "../symbol-registry.js";
|
|
2
|
+
import type { AgentSessionState } from "../session-state.js";
|
|
3
|
+
import type { AgentStateStore, StateBackend } from "./define-state.js";
|
|
4
|
+
import { sessionKvKey, symbolsKvKey } from "./fs-state-adapter.js";
|
|
5
|
+
|
|
6
|
+
type KvClient = {
|
|
7
|
+
get<T>(key: string): Promise<T | null>;
|
|
8
|
+
set(key: string, value: unknown): Promise<unknown>;
|
|
9
|
+
keys(pattern: string): Promise<string[]>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function loadKv(): Promise<KvClient> {
|
|
13
|
+
const mod = await import("@vercel/kv");
|
|
14
|
+
return mod.kv as KvClient;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class KvStateAdapter implements AgentStateStore {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly agentRoot: string,
|
|
20
|
+
private readonly tenantScope: string,
|
|
21
|
+
) {
|
|
22
|
+
void this.agentRoot;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
backend(): StateBackend {
|
|
26
|
+
return "kv";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async get(intent: string): Promise<AgentSessionState | null> {
|
|
30
|
+
const kv = await loadKv();
|
|
31
|
+
return kv.get<AgentSessionState>(sessionKvKey(this.tenantScope, intent));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async put(state: AgentSessionState): Promise<void> {
|
|
35
|
+
const kv = await loadKv();
|
|
36
|
+
await kv.set(sessionKvKey(this.tenantScope, state.intent), state);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async listIntents(): Promise<string[]> {
|
|
40
|
+
const kv = await loadKv();
|
|
41
|
+
const keys = await kv.keys(`plasm:${this.tenantScope}:session:*`);
|
|
42
|
+
const intents: string[] = [];
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
const state = await kv.get<AgentSessionState>(key);
|
|
45
|
+
if (state?.intent) intents.push(state.intent);
|
|
46
|
+
}
|
|
47
|
+
return intents;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getSymbolRegistry(tenantId: string): Promise<SymbolRegistrySnapshot | null> {
|
|
51
|
+
const kv = await loadKv();
|
|
52
|
+
return kv.get<SymbolRegistrySnapshot>(symbolsKvKey(tenantId));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async putSymbolRegistry(
|
|
56
|
+
tenantId: string,
|
|
57
|
+
snapshot: SymbolRegistrySnapshot,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const kv = await loadKv();
|
|
60
|
+
await kv.set(symbolsKvKey(tenantId), snapshot);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { SymbolRegistrySnapshot } from "../symbol-registry.js";
|
|
2
|
+
import type { AgentSessionState } from "../session-state.js";
|
|
3
|
+
import type { AgentStateStore, StateBackend } from "./define-state.js";
|
|
4
|
+
|
|
5
|
+
function postgresUrl(): string {
|
|
6
|
+
return (
|
|
7
|
+
process.env.PLASM_STATE_POSTGRES_URL?.trim() ||
|
|
8
|
+
process.env.WORKFLOW_POSTGRES_URL?.trim() ||
|
|
9
|
+
process.env.DATABASE_URL?.trim() ||
|
|
10
|
+
""
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SCHEMA_SQL = `
|
|
15
|
+
CREATE TABLE IF NOT EXISTS plasm_agent_state (
|
|
16
|
+
tenant_id TEXT NOT NULL,
|
|
17
|
+
kind TEXT NOT NULL,
|
|
18
|
+
state_key TEXT NOT NULL,
|
|
19
|
+
payload JSONB NOT NULL,
|
|
20
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
21
|
+
PRIMARY KEY (tenant_id, kind, state_key)
|
|
22
|
+
);
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
type PgPool = {
|
|
26
|
+
query(sql: string, params?: unknown[]): Promise<{ rows: Array<Record<string, unknown>> }>;
|
|
27
|
+
end(): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let sharedPool: PgPool | null = null;
|
|
31
|
+
|
|
32
|
+
async function pool(): Promise<PgPool> {
|
|
33
|
+
if (sharedPool) return sharedPool;
|
|
34
|
+
const url = postgresUrl();
|
|
35
|
+
if (!url) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Postgres state backend requires PLASM_STATE_POSTGRES_URL, WORKFLOW_POSTGRES_URL, or DATABASE_URL",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const pg = await import("pg");
|
|
41
|
+
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
42
|
+
sharedPool = new Pool({ connectionString: url }) as PgPool;
|
|
43
|
+
await sharedPool.query(SCHEMA_SQL);
|
|
44
|
+
return sharedPool;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function intentKey(intent: string): string {
|
|
48
|
+
return Buffer.from(intent, "utf8").toString("base64url");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class PostgresStateAdapter implements AgentStateStore {
|
|
52
|
+
constructor(private readonly tenantScope: string) {}
|
|
53
|
+
|
|
54
|
+
backend(): StateBackend {
|
|
55
|
+
return "postgres";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async get(intent: string): Promise<AgentSessionState | null> {
|
|
59
|
+
const db = await pool();
|
|
60
|
+
const result = await db.query(
|
|
61
|
+
`SELECT payload FROM plasm_agent_state
|
|
62
|
+
WHERE tenant_id = $1 AND kind = 'session' AND state_key = $2`,
|
|
63
|
+
[this.tenantScope, intentKey(intent)],
|
|
64
|
+
);
|
|
65
|
+
const row = result.rows[0]?.payload;
|
|
66
|
+
return row ? (row as AgentSessionState) : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async put(state: AgentSessionState): Promise<void> {
|
|
70
|
+
const db = await pool();
|
|
71
|
+
await db.query(
|
|
72
|
+
`INSERT INTO plasm_agent_state (tenant_id, kind, state_key, payload, updated_at)
|
|
73
|
+
VALUES ($1, 'session', $2, $3::jsonb, NOW())
|
|
74
|
+
ON CONFLICT (tenant_id, kind, state_key)
|
|
75
|
+
DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()`,
|
|
76
|
+
[this.tenantScope, intentKey(state.intent), JSON.stringify(state)],
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async listIntents(): Promise<string[]> {
|
|
81
|
+
const db = await pool();
|
|
82
|
+
const result = await db.query(
|
|
83
|
+
`SELECT payload FROM plasm_agent_state
|
|
84
|
+
WHERE tenant_id = $1 AND kind = 'session'`,
|
|
85
|
+
[this.tenantScope],
|
|
86
|
+
);
|
|
87
|
+
return result.rows
|
|
88
|
+
.map((row) => (row.payload as AgentSessionState | undefined)?.intent)
|
|
89
|
+
.filter((intent): intent is string => Boolean(intent));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getSymbolRegistry(tenantId: string): Promise<SymbolRegistrySnapshot | null> {
|
|
93
|
+
const db = await pool();
|
|
94
|
+
const result = await db.query(
|
|
95
|
+
`SELECT payload FROM plasm_agent_state
|
|
96
|
+
WHERE tenant_id = $1 AND kind = 'symbols' AND state_key = 'registry'`,
|
|
97
|
+
[tenantId],
|
|
98
|
+
);
|
|
99
|
+
const row = result.rows[0]?.payload;
|
|
100
|
+
return row ? (row as SymbolRegistrySnapshot) : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async putSymbolRegistry(
|
|
104
|
+
tenantId: string,
|
|
105
|
+
snapshot: SymbolRegistrySnapshot,
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
const db = await pool();
|
|
108
|
+
await db.query(
|
|
109
|
+
`INSERT INTO plasm_agent_state (tenant_id, kind, state_key, payload, updated_at)
|
|
110
|
+
VALUES ($1, 'symbols', 'registry', $2::jsonb, NOW())
|
|
111
|
+
ON CONFLICT (tenant_id, kind, state_key)
|
|
112
|
+
DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()`,
|
|
113
|
+
[tenantId, JSON.stringify(snapshot)],
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CapabilityIntrospectionJson,
|
|
3
|
+
InputFieldSchemaJson,
|
|
4
|
+
InputTypeJson,
|
|
5
|
+
} from "./catalog-introspection.js";
|
|
6
|
+
|
|
7
|
+
export type CapabilityInvokeShape =
|
|
8
|
+
| "RootQuery"
|
|
9
|
+
| "ScopedQuery"
|
|
10
|
+
| "GetById"
|
|
11
|
+
| "SearchText"
|
|
12
|
+
| "SearchFiltered"
|
|
13
|
+
| "RootCreate"
|
|
14
|
+
| "ScopedUpdate"
|
|
15
|
+
| "ScopedAction"
|
|
16
|
+
| "ScopedDelete"
|
|
17
|
+
| "MethodObject"
|
|
18
|
+
| "MethodUnion";
|
|
19
|
+
|
|
20
|
+
export function inputTypeRoot(input: InputTypeJson | undefined): InputTypeJson | undefined {
|
|
21
|
+
return input;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function objectFieldsFromCap(
|
|
25
|
+
cap: CapabilityIntrospectionJson,
|
|
26
|
+
): InputFieldSchemaJson[] {
|
|
27
|
+
const schema = cap.input_schema?.input_type;
|
|
28
|
+
if (!schema || schema.type !== "object") return [];
|
|
29
|
+
return schema.fields ?? [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasScopeOrFilterParams(fields: InputFieldSchemaJson[]): boolean {
|
|
33
|
+
return fields.some((f) => {
|
|
34
|
+
const role = f.role ?? "filter";
|
|
35
|
+
return role === "scope" || role === "filter";
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function searchParam(fields: InputFieldSchemaJson[]): InputFieldSchemaJson | undefined {
|
|
40
|
+
return fields.find((f) => (f.role ?? "filter") === "search") ?? fields[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nonSearchParams(fields: InputFieldSchemaJson[]): InputFieldSchemaJson[] {
|
|
44
|
+
const search = searchParam(fields);
|
|
45
|
+
return fields.filter((f) => f !== search);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function classifyInvokeShape(cap: CapabilityIntrospectionJson): CapabilityInvokeShape {
|
|
49
|
+
const kind = cap.kind.toLowerCase();
|
|
50
|
+
const inputType = cap.input_schema?.input_type;
|
|
51
|
+
|
|
52
|
+
if (inputType?.type === "union") {
|
|
53
|
+
return "MethodUnion";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const fields = objectFieldsFromCap(cap);
|
|
57
|
+
|
|
58
|
+
switch (kind) {
|
|
59
|
+
case "query":
|
|
60
|
+
return hasScopeOrFilterParams(fields) ? "ScopedQuery" : "RootQuery";
|
|
61
|
+
case "get":
|
|
62
|
+
return "GetById";
|
|
63
|
+
case "search": {
|
|
64
|
+
const extra = nonSearchParams(fields);
|
|
65
|
+
return extra.length > 0 ? "SearchFiltered" : "SearchText";
|
|
66
|
+
}
|
|
67
|
+
case "create":
|
|
68
|
+
return "RootCreate";
|
|
69
|
+
case "update":
|
|
70
|
+
return "ScopedUpdate";
|
|
71
|
+
case "action":
|
|
72
|
+
return "ScopedAction";
|
|
73
|
+
case "delete":
|
|
74
|
+
return "ScopedDelete";
|
|
75
|
+
default:
|
|
76
|
+
return "MethodObject";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function capabilityReturnsVoid(cap: CapabilityIntrospectionJson): boolean {
|
|
81
|
+
if (cap.output_schema?.type === "side_effect") return true;
|
|
82
|
+
if (cap.kind.toLowerCase() === "delete" && cap.provides.length === 0) return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function capabilityReturnsScalar(cap: CapabilityIntrospectionJson): boolean {
|
|
87
|
+
const kind = cap.kind.toLowerCase();
|
|
88
|
+
return kind === "get";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function capabilityNeedsInput(
|
|
92
|
+
cap: CapabilityIntrospectionJson,
|
|
93
|
+
shape: CapabilityInvokeShape,
|
|
94
|
+
idField?: string,
|
|
95
|
+
): boolean {
|
|
96
|
+
if (shape === "RootQuery") return false;
|
|
97
|
+
if (shape === "GetById" || shape === "ScopedUpdate" || shape === "ScopedAction" || shape === "ScopedDelete") {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const fields = objectFieldsFromCap(cap);
|
|
101
|
+
if (fields.length > 0) return true;
|
|
102
|
+
if (shape === "ScopedQuery" || shape === "SearchText" || shape === "SearchFiltered") {
|
|
103
|
+
return fields.some((f) => f.required);
|
|
104
|
+
}
|
|
105
|
+
void idField;
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Body fields for dotted-arg emission (excludes scoped receiver id). */
|
|
110
|
+
export function invokeBodyFields(
|
|
111
|
+
cap: CapabilityIntrospectionJson,
|
|
112
|
+
shape: CapabilityInvokeShape,
|
|
113
|
+
entityIdField: string,
|
|
114
|
+
): InputFieldSchemaJson[] {
|
|
115
|
+
const fields = objectFieldsFromCap(cap);
|
|
116
|
+
switch (shape) {
|
|
117
|
+
case "GetById":
|
|
118
|
+
case "ScopedUpdate":
|
|
119
|
+
case "ScopedAction":
|
|
120
|
+
case "ScopedDelete":
|
|
121
|
+
return fields.filter((f) => f.name !== entityIdField);
|
|
122
|
+
case "SearchText":
|
|
123
|
+
case "SearchFiltered": {
|
|
124
|
+
const search = searchParam(fields);
|
|
125
|
+
return fields.filter((f) => f !== search);
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
return fields;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function searchFieldName(cap: CapabilityIntrospectionJson): string | undefined {
|
|
133
|
+
const fields = objectFieldsFromCap(cap);
|
|
134
|
+
return searchParam(fields)?.name;
|
|
135
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { HostTransportFn } from "../engine/napi-binding.js";
|
|
2
|
+
import { createProductionHostTransport } from "../engine/create-host-transport.js";
|
|
3
|
+
import {
|
|
4
|
+
createEngine,
|
|
5
|
+
type DryRunResult,
|
|
6
|
+
type PlasmEngine,
|
|
7
|
+
} from "../engine/napi-binding.js";
|
|
8
|
+
import { createDefaultHostTransport } from "../engine/host-transport.js";
|
|
9
|
+
import { createFixtureMockTransport } from "../engine/fixture-mock-transport.js";
|
|
10
|
+
import type { ProgramBuilder, ProgramBuilderOptions } from "./program-builder.js";
|
|
11
|
+
import { createProgramBuilder } from "./program-builder.js";
|
|
12
|
+
|
|
13
|
+
export interface CatalogClientOptions extends ProgramBuilderOptions {
|
|
14
|
+
engine?: PlasmEngine;
|
|
15
|
+
intent?: string;
|
|
16
|
+
transport?: HostTransportFn;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StubInvokeOptions {
|
|
20
|
+
transport?: HostTransportFn;
|
|
21
|
+
engine?: PlasmEngine;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sessionInit = new WeakMap<ProgramBuilder, Promise<void>>();
|
|
25
|
+
|
|
26
|
+
/** Load catalog + register deterministic `e#` symbols for program execution (not agent teaching). */
|
|
27
|
+
export async function ensureStubSession(builder: ProgramBuilder): Promise<void> {
|
|
28
|
+
const existing = sessionInit.get(builder);
|
|
29
|
+
if (existing) return existing;
|
|
30
|
+
|
|
31
|
+
const task = (async () => {
|
|
32
|
+
const catalogRoot = builder.catalogRoot;
|
|
33
|
+
const engine = builder.engine ?? createEngine();
|
|
34
|
+
if (catalogRoot) {
|
|
35
|
+
await engine.loadCatalog({
|
|
36
|
+
rootDir: catalogRoot,
|
|
37
|
+
manifest: { entryId: builder.entryId },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const entities = [...(builder.stubEntities ?? [])].sort((a, b) => a.localeCompare(b));
|
|
41
|
+
if (!entities.length) return;
|
|
42
|
+
await engine.synthesizeTeaching(
|
|
43
|
+
"",
|
|
44
|
+
entities.map((entity) => ({ api: builder.entryId, entity })),
|
|
45
|
+
);
|
|
46
|
+
})();
|
|
47
|
+
sessionInit.set(builder, task);
|
|
48
|
+
return task;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @deprecated Use {@link ensureStubSession}. */
|
|
52
|
+
export async function ensureCatalogLoaded(builder: ProgramBuilder): Promise<void> {
|
|
53
|
+
return ensureStubSession(builder);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createCatalogClient(options: CatalogClientOptions): ProgramBuilder {
|
|
57
|
+
return createProgramBuilder({
|
|
58
|
+
entryId: options.entryId,
|
|
59
|
+
cgsHash: options.cgsHash,
|
|
60
|
+
engine: options.engine ?? createEngine(),
|
|
61
|
+
logicalSessionRef: options.logicalSessionRef,
|
|
62
|
+
agentSessionId: options.agentSessionId,
|
|
63
|
+
catalogRoot: options.catalogRoot,
|
|
64
|
+
stubEntities: options.stubEntities,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ExecuteRowsResult<TRow> {
|
|
69
|
+
planCommitRef: string;
|
|
70
|
+
summary: string;
|
|
71
|
+
rows: TRow[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Plasm string literal from a TypeScript string (safe quoting). */
|
|
75
|
+
export function plasmLiteral(value: string): string {
|
|
76
|
+
return JSON.stringify(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Plasm numeric literal from a TypeScript number. */
|
|
80
|
+
export function plasmNumber(value: number | undefined): string {
|
|
81
|
+
if (value === undefined || Number.isNaN(value)) return "0";
|
|
82
|
+
return String(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Plasm boolean literal. */
|
|
86
|
+
export function plasmBoolean(value: boolean | undefined): string {
|
|
87
|
+
return value ? "true" : "false";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface DottedArgSpec {
|
|
91
|
+
key: string;
|
|
92
|
+
value: unknown;
|
|
93
|
+
kind?: "literal" | "number" | "boolean" | "select";
|
|
94
|
+
optional?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Build `key=value` dotted arg list for Plasm invoke (omits undefined optional keys). */
|
|
98
|
+
export function buildDottedArgs(specs: DottedArgSpec[]): string {
|
|
99
|
+
const parts: string[] = [];
|
|
100
|
+
for (const { key, value, kind = "literal", optional } of specs) {
|
|
101
|
+
if (optional && value === undefined) continue;
|
|
102
|
+
const emit =
|
|
103
|
+
kind === "number"
|
|
104
|
+
? plasmNumber(value as number)
|
|
105
|
+
: kind === "boolean"
|
|
106
|
+
? plasmBoolean(value as boolean)
|
|
107
|
+
: plasmLiteral(String(value ?? ""));
|
|
108
|
+
parts.push(`${key}=${emit}`);
|
|
109
|
+
}
|
|
110
|
+
return parts.join(", ");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseRowsJson<TRow>(rowsJson: string | undefined): TRow[] {
|
|
114
|
+
if (!rowsJson?.trim()) return [];
|
|
115
|
+
const parsed = JSON.parse(rowsJson) as unknown;
|
|
116
|
+
if (Array.isArray(parsed)) return parsed as TRow[];
|
|
117
|
+
if (parsed && typeof parsed === "object" && Array.isArray((parsed as { results?: unknown }).results)) {
|
|
118
|
+
return (parsed as { results: TRow[] }).results;
|
|
119
|
+
}
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function extractRowsFromLive(live: {
|
|
124
|
+
rowsJson?: string;
|
|
125
|
+
message?: string;
|
|
126
|
+
}): unknown[] {
|
|
127
|
+
if (live.rowsJson) {
|
|
128
|
+
return parseRowsJson(live.rowsJson);
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Dry-run then live-execute a program; parse entity rows from NAPI output. */
|
|
134
|
+
export async function executeRows<TRow>(
|
|
135
|
+
builder: ProgramBuilder,
|
|
136
|
+
program: string,
|
|
137
|
+
options?: StubInvokeOptions,
|
|
138
|
+
): Promise<ExecuteRowsResult<TRow>> {
|
|
139
|
+
await ensureStubSession(builder);
|
|
140
|
+
const bound = builder.program(program);
|
|
141
|
+
const dry: DryRunResult = await bound.dryRun();
|
|
142
|
+
const transport =
|
|
143
|
+
options?.transport ??
|
|
144
|
+
(process.env.PLASM_STUB_USE_MOCK_TRANSPORT === "1"
|
|
145
|
+
? createFixtureMockTransport()
|
|
146
|
+
: createProductionHostTransport());
|
|
147
|
+
|
|
148
|
+
if (typeof bound.run !== "function") {
|
|
149
|
+
throw new Error("executeRows: program builder missing run()");
|
|
150
|
+
}
|
|
151
|
+
const live = (await bound.run(dry.planCommitRef, transport)) as {
|
|
152
|
+
rowsJson?: string;
|
|
153
|
+
message?: string;
|
|
154
|
+
};
|
|
155
|
+
const rows = extractRowsFromLive(live) as TRow[];
|
|
156
|
+
return {
|
|
157
|
+
planCommitRef: dry.planCommitRef,
|
|
158
|
+
summary: dry.summary,
|
|
159
|
+
rows,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Dry-run only — plan review without live HTTP. */
|
|
164
|
+
export async function dryRunProgram(
|
|
165
|
+
builder: ProgramBuilder,
|
|
166
|
+
program: string,
|
|
167
|
+
): Promise<DryRunResult> {
|
|
168
|
+
await ensureStubSession(builder);
|
|
169
|
+
return builder.program(program).dryRun();
|
|
170
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { computeCatalogCgsHash } from "./generator.js";
|
|
5
|
+
|
|
6
|
+
/** Live CGS digest from authored `domain.yaml` + `mappings.yaml` on disk. */
|
|
7
|
+
export async function resolveCatalogLiveHash(catalogDir: string): Promise<string> {
|
|
8
|
+
const domainYaml = await readFile(path.join(catalogDir, "domain.yaml"), "utf8");
|
|
9
|
+
const mappingsYaml = await readFile(path.join(catalogDir, "mappings.yaml"), "utf8");
|
|
10
|
+
return computeCatalogCgsHash(domainYaml, mappingsYaml);
|
|
11
|
+
}
|