@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
package/src/cli/link.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function runCommand(
|
|
4
|
+
command: string,
|
|
5
|
+
args: string[],
|
|
6
|
+
cwd: string,
|
|
7
|
+
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const child = spawn(command, args, {
|
|
10
|
+
cwd,
|
|
11
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
12
|
+
env: process.env,
|
|
13
|
+
});
|
|
14
|
+
let stdout = "";
|
|
15
|
+
let stderr = "";
|
|
16
|
+
child.stdout?.on("data", (chunk) => {
|
|
17
|
+
stdout += chunk;
|
|
18
|
+
});
|
|
19
|
+
child.stderr?.on("data", (chunk) => {
|
|
20
|
+
stderr += chunk;
|
|
21
|
+
});
|
|
22
|
+
child.on("close", (code) => {
|
|
23
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function commandExists(command: string): Promise<boolean> {
|
|
29
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
30
|
+
const result = await runCommand(probe, [command], process.cwd());
|
|
31
|
+
return result.code === 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PlasmLinkResult {
|
|
35
|
+
linked: boolean;
|
|
36
|
+
envPulled: boolean;
|
|
37
|
+
messages: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runPlasmLink(projectRoot: string): Promise<PlasmLinkResult> {
|
|
41
|
+
const messages: string[] = [];
|
|
42
|
+
const hasVercel = await commandExists("vercel");
|
|
43
|
+
if (!hasVercel) {
|
|
44
|
+
messages.push(
|
|
45
|
+
"Vercel CLI not found. Install with `npm i -g vercel` or set AI_GATEWAY_API_KEY manually.",
|
|
46
|
+
);
|
|
47
|
+
return { linked: false, envPulled: false, messages };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
messages.push("Running `vercel link` (interactive)…");
|
|
51
|
+
const link = await runCommand("vercel", ["link"], projectRoot);
|
|
52
|
+
if (link.code !== 0) {
|
|
53
|
+
messages.push("vercel link failed or was cancelled.");
|
|
54
|
+
if (link.stderr.trim()) messages.push(link.stderr.trim());
|
|
55
|
+
return { linked: false, envPulled: false, messages };
|
|
56
|
+
}
|
|
57
|
+
messages.push("vercel link complete.");
|
|
58
|
+
|
|
59
|
+
messages.push("Running `vercel env pull .env.local`…");
|
|
60
|
+
const pull = await runCommand("vercel", ["env", "pull", ".env.local"], projectRoot);
|
|
61
|
+
if (pull.code !== 0) {
|
|
62
|
+
messages.push("vercel env pull failed — set AI_GATEWAY_API_KEY in .env.local manually.");
|
|
63
|
+
if (pull.stderr.trim()) messages.push(pull.stderr.trim());
|
|
64
|
+
return { linked: true, envPulled: false, messages };
|
|
65
|
+
}
|
|
66
|
+
messages.push("Pulled environment to .env.local");
|
|
67
|
+
return { linked: true, envPulled: true, messages };
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface ResolvedAgentProject {
|
|
5
|
+
/** Directory containing `agent/agent.ts`. */
|
|
6
|
+
projectRoot: string;
|
|
7
|
+
agentRoot: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function exists(p: string): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
await access(p);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Walk up from `startDir` to find `agent/agent.ts`. */
|
|
20
|
+
export async function resolveAgentProject(
|
|
21
|
+
startDir: string = process.cwd(),
|
|
22
|
+
): Promise<ResolvedAgentProject | null> {
|
|
23
|
+
let current = path.resolve(startDir);
|
|
24
|
+
for (let depth = 0; depth < 12; depth += 1) {
|
|
25
|
+
const agentRoot = path.join(current, "agent");
|
|
26
|
+
const agentTs = path.join(agentRoot, "agent.ts");
|
|
27
|
+
if (await exists(agentTs)) {
|
|
28
|
+
return { projectRoot: current, agentRoot };
|
|
29
|
+
}
|
|
30
|
+
const parent = path.dirname(current);
|
|
31
|
+
if (parent === current) break;
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function requireAgentProject(
|
|
38
|
+
startDir?: string,
|
|
39
|
+
): Promise<ResolvedAgentProject> {
|
|
40
|
+
const resolved = await resolveAgentProject(startDir);
|
|
41
|
+
if (!resolved) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"No agent project found. Run from a directory with agent/agent.ts or use `plasm-agent init`.",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function readPackageName(projectRoot: string): Promise<string | undefined> {
|
|
50
|
+
try {
|
|
51
|
+
const raw = await readFile(path.join(projectRoot, "package.json"), "utf8");
|
|
52
|
+
const parsed = JSON.parse(raw) as { name?: string };
|
|
53
|
+
return parsed.name;
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { LoadedProjectSlots } from "./authoring/slot-loader.js";
|
|
4
|
+
import { loadAuthoredSlots } from "./authoring/slot-loader.js";
|
|
5
|
+
import type { SubagentRegistry } from "./authoring/subagent-loader.js";
|
|
6
|
+
import {
|
|
7
|
+
createSubagentRegistry,
|
|
8
|
+
loadSubagents,
|
|
9
|
+
} from "./authoring/subagent-loader.js";
|
|
10
|
+
import { walkAgentProject } from "./discovery/project-walker.js";
|
|
11
|
+
import { PlasmAgent, type PlasmAgentConfig } from "./runtime/plasm-agent.js";
|
|
12
|
+
import type { AgentRuntimeConfig } from "./runtime/agent-runtime.js";
|
|
13
|
+
import { bootstrapWorkflowWorld } from "./workflow/world-bootstrap.js";
|
|
14
|
+
|
|
15
|
+
/** Context-window compaction (eve-shaped; harness wiring is phase 7+). */
|
|
16
|
+
export interface AgentCompactionConfig {
|
|
17
|
+
thresholdPercent?: number;
|
|
18
|
+
model?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Provider-specific generation knobs passed through to AI SDK calls. */
|
|
22
|
+
export interface AgentModelOptions {
|
|
23
|
+
temperature?: number;
|
|
24
|
+
maxOutputTokens?: number;
|
|
25
|
+
topP?: number;
|
|
26
|
+
topK?: number;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Workflow SDK world adapter (local dev vs durable Postgres vs Vercel-managed). */
|
|
31
|
+
export interface AgentWorkflowWorldDefinition {
|
|
32
|
+
type?: "local" | "postgres" | "vercel" | "kv";
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AgentWorkflowDefinition {
|
|
37
|
+
world?: AgentWorkflowWorldDefinition;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AgentBuildConfig {
|
|
41
|
+
/** Packages the bundler must not inline (NAPI prebuilds, native addons). */
|
|
42
|
+
externalDependencies?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AgentExperimentalConfig {
|
|
46
|
+
workflow?: AgentWorkflowDefinition;
|
|
47
|
+
/** `true` (default) = skill index + read_skill tool; `"inline"` = inject full bodies. */
|
|
48
|
+
skills?: boolean | "inline";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Authored agent runtime config (`agent/agent.ts`). Identity comes from the filesystem. */
|
|
52
|
+
export interface AgentDefinition {
|
|
53
|
+
model: string;
|
|
54
|
+
compaction?: AgentCompactionConfig;
|
|
55
|
+
modelOptions?: AgentModelOptions;
|
|
56
|
+
experimental?: AgentExperimentalConfig;
|
|
57
|
+
build?: AgentBuildConfig;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CreateAgentFromDefinitionOptions {
|
|
61
|
+
agentRoot: string;
|
|
62
|
+
tenantScope?: string;
|
|
63
|
+
maxSteps?: number;
|
|
64
|
+
telemetry?: boolean;
|
|
65
|
+
hostTransport?: AgentRuntimeConfig["hostTransport"];
|
|
66
|
+
loadedSlots?: LoadedProjectSlots;
|
|
67
|
+
subagentRegistry?: SubagentRegistry;
|
|
68
|
+
getAuthoringContext?: PlasmAgentConfig["getAuthoringContext"];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ResolvedAgentDefinition extends AgentDefinition {
|
|
72
|
+
agentRoot: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Eve-compatible helper: default-export the frozen definition from `agent/agent.ts`. */
|
|
76
|
+
export function defineAgent(definition: AgentDefinition): Readonly<AgentDefinition> {
|
|
77
|
+
return Object.freeze({ ...definition });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveAgentDefinition(
|
|
81
|
+
definition: AgentDefinition,
|
|
82
|
+
agentRoot: string,
|
|
83
|
+
): ResolvedAgentDefinition {
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
...definition,
|
|
86
|
+
agentRoot: path.resolve(agentRoot),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createPlasmAgentConfig(
|
|
91
|
+
definition: AgentDefinition,
|
|
92
|
+
options: CreateAgentFromDefinitionOptions,
|
|
93
|
+
): PlasmAgentConfig {
|
|
94
|
+
return {
|
|
95
|
+
model: definition.model,
|
|
96
|
+
agentRoot: path.resolve(options.agentRoot),
|
|
97
|
+
tenantScope: options.tenantScope,
|
|
98
|
+
maxSteps: options.maxSteps,
|
|
99
|
+
telemetry: options.telemetry,
|
|
100
|
+
hostTransport: options.hostTransport,
|
|
101
|
+
compaction: definition.compaction,
|
|
102
|
+
modelOptions: definition.modelOptions,
|
|
103
|
+
build: definition.build,
|
|
104
|
+
experimental: definition.experimental,
|
|
105
|
+
stateWorld: definition.experimental?.workflow?.world,
|
|
106
|
+
loadedSkills: options.loadedSlots?.skills.map((s) => s.definition),
|
|
107
|
+
hookRunner: options.loadedSlots?.hookRunner,
|
|
108
|
+
subagentRegistry: options.subagentRegistry,
|
|
109
|
+
getAuthoringContext: options.getAuthoringContext,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function createAgentFromDefinition(
|
|
114
|
+
definition: AgentDefinition,
|
|
115
|
+
options: CreateAgentFromDefinitionOptions,
|
|
116
|
+
): Promise<PlasmAgent> {
|
|
117
|
+
await bootstrapWorkflowWorld(definition.experimental?.workflow);
|
|
118
|
+
const agent = new PlasmAgent(createPlasmAgentConfig(definition, options));
|
|
119
|
+
await agent.bootstrap();
|
|
120
|
+
return agent;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Walk agent/, load slots + subagents, then bootstrap the root agent. */
|
|
124
|
+
export async function createAgentFromProject(
|
|
125
|
+
definition: AgentDefinition,
|
|
126
|
+
options: CreateAgentFromDefinitionOptions & { importCacheBust?: number },
|
|
127
|
+
): Promise<PlasmAgent> {
|
|
128
|
+
const agentRoot = path.resolve(options.agentRoot);
|
|
129
|
+
const discovery = await walkAgentProject(agentRoot);
|
|
130
|
+
const loadedSlots = await loadAuthoredSlots({
|
|
131
|
+
discovery,
|
|
132
|
+
importCacheBust: options.importCacheBust,
|
|
133
|
+
});
|
|
134
|
+
const { subagents, diagnostics } = await loadSubagents({
|
|
135
|
+
discovery,
|
|
136
|
+
parentSlots: loadedSlots,
|
|
137
|
+
tenantScope: options.tenantScope,
|
|
138
|
+
maxSteps: options.maxSteps,
|
|
139
|
+
telemetry: options.telemetry,
|
|
140
|
+
importCacheBust: options.importCacheBust,
|
|
141
|
+
});
|
|
142
|
+
loadedSlots.diagnostics.push(...diagnostics);
|
|
143
|
+
|
|
144
|
+
return createAgentFromDefinition(definition, {
|
|
145
|
+
...options,
|
|
146
|
+
agentRoot,
|
|
147
|
+
loadedSlots,
|
|
148
|
+
subagentRegistry: createSubagentRegistry(subagents),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const ANSI = {
|
|
2
|
+
reset: "\x1b[0m",
|
|
3
|
+
bold: "\x1b[1m",
|
|
4
|
+
dim: "\x1b[2m",
|
|
5
|
+
cyan: "\x1b[36m",
|
|
6
|
+
green: "\x1b[32m",
|
|
7
|
+
yellow: "\x1b[33m",
|
|
8
|
+
red: "\x1b[31m",
|
|
9
|
+
magenta: "\x1b[35m",
|
|
10
|
+
blue: "\x1b[34m",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export function supportsColor(): boolean {
|
|
14
|
+
if (process.env.NO_COLOR !== undefined) return false;
|
|
15
|
+
return Boolean(process.stdout.isTTY);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function paint(text: string, ...styles: string[]): string {
|
|
19
|
+
if (!supportsColor()) return text;
|
|
20
|
+
return `${styles.join("")}${text}${ANSI.reset}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function wrap(text: string, width = 78): string[] {
|
|
24
|
+
const lines: string[] = [];
|
|
25
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
26
|
+
let line = raw;
|
|
27
|
+
while (line.length > width) {
|
|
28
|
+
let breakAt = line.lastIndexOf(" ", width);
|
|
29
|
+
if (breakAt <= 0) breakAt = width;
|
|
30
|
+
lines.push(line.slice(0, breakAt).trimEnd());
|
|
31
|
+
line = line.slice(breakAt).trimStart();
|
|
32
|
+
}
|
|
33
|
+
lines.push(line);
|
|
34
|
+
}
|
|
35
|
+
return lines;
|
|
36
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { ProjectInfoPayload } from "../../project-info.js";
|
|
2
|
+
|
|
3
|
+
export type SessionEventType = "turn:start" | "turn:step" | "turn:finish" | "turn:error";
|
|
4
|
+
|
|
5
|
+
export interface ParsedSessionEvent {
|
|
6
|
+
id?: string;
|
|
7
|
+
type: SessionEventType;
|
|
8
|
+
at?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
toolsUsed?: string[];
|
|
11
|
+
text?: string | null;
|
|
12
|
+
steps?: number;
|
|
13
|
+
usage?: unknown;
|
|
14
|
+
finishReason?: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SessionTurnResponse {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
continuationToken: string;
|
|
20
|
+
status?: string;
|
|
21
|
+
text?: string;
|
|
22
|
+
steps?: number;
|
|
23
|
+
usage?: unknown;
|
|
24
|
+
error?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DevSessionRef {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
continuationToken: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SendTurnOptions {
|
|
34
|
+
/** When false, POST returns immediately; attach SSE for live step events. Default true. */
|
|
35
|
+
wait?: boolean;
|
|
36
|
+
onEvent?: (event: ParsedSessionEvent) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseSseBlock(block: string): ParsedSessionEvent | null {
|
|
40
|
+
const lines = block.split("\n");
|
|
41
|
+
let eventType: SessionEventType | undefined;
|
|
42
|
+
let data = "";
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
if (line.startsWith("event: ")) eventType = line.slice(7).trim() as SessionEventType;
|
|
45
|
+
if (line.startsWith("data: ")) data += line.slice(6);
|
|
46
|
+
}
|
|
47
|
+
if (!eventType || !data) return null;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(data) as Record<string, unknown>;
|
|
50
|
+
return {
|
|
51
|
+
id: typeof parsed.id === "string" ? parsed.id : undefined,
|
|
52
|
+
type: eventType,
|
|
53
|
+
at: typeof parsed.at === "string" ? parsed.at : undefined,
|
|
54
|
+
message: typeof parsed.message === "string" ? parsed.message : undefined,
|
|
55
|
+
toolsUsed: Array.isArray(parsed.toolsUsed)
|
|
56
|
+
? parsed.toolsUsed.filter((t): t is string => typeof t === "string")
|
|
57
|
+
: undefined,
|
|
58
|
+
text: typeof parsed.text === "string" ? parsed.text : parsed.text === null ? null : undefined,
|
|
59
|
+
steps: typeof parsed.steps === "number" ? parsed.steps : undefined,
|
|
60
|
+
usage: parsed.usage,
|
|
61
|
+
finishReason:
|
|
62
|
+
typeof parsed.finishReason === "string"
|
|
63
|
+
? parsed.finishReason
|
|
64
|
+
: parsed.finishReason === null
|
|
65
|
+
? null
|
|
66
|
+
: undefined,
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function consumeSessionStream(
|
|
74
|
+
streamUrl: string,
|
|
75
|
+
onEvent: (event: ParsedSessionEvent) => void,
|
|
76
|
+
signal?: AbortSignal,
|
|
77
|
+
): Promise<ParsedSessionEvent | null> {
|
|
78
|
+
const res = await fetch(streamUrl, { signal });
|
|
79
|
+
if (!res.ok || !res.body) {
|
|
80
|
+
throw new Error(`stream ${res.status}`);
|
|
81
|
+
}
|
|
82
|
+
const reader = res.body.getReader();
|
|
83
|
+
const decoder = new TextDecoder();
|
|
84
|
+
let buffer = "";
|
|
85
|
+
let terminal: ParsedSessionEvent | null = null;
|
|
86
|
+
while (true) {
|
|
87
|
+
const { done, value } = await reader.read();
|
|
88
|
+
if (done) break;
|
|
89
|
+
buffer += decoder.decode(value, { stream: true });
|
|
90
|
+
const blocks = buffer.split("\n\n");
|
|
91
|
+
buffer = blocks.pop() ?? "";
|
|
92
|
+
for (const block of blocks) {
|
|
93
|
+
const event = parseSseBlock(block);
|
|
94
|
+
if (!event) continue;
|
|
95
|
+
onEvent(event);
|
|
96
|
+
if (event.type === "turn:finish" || event.type === "turn:error") {
|
|
97
|
+
terminal = event;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (buffer.trim()) {
|
|
102
|
+
const event = parseSseBlock(buffer);
|
|
103
|
+
if (event) {
|
|
104
|
+
onEvent(event);
|
|
105
|
+
if (event.type === "turn:finish" || event.type === "turn:error") terminal = event;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return terminal;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class DevHttpSessionClient {
|
|
112
|
+
constructor(readonly baseUrl: string) {}
|
|
113
|
+
|
|
114
|
+
async fetchInfo(): Promise<ProjectInfoPayload> {
|
|
115
|
+
const res = await fetch(`${this.baseUrl}/plasm/v1/info`);
|
|
116
|
+
if (!res.ok) throw new Error(`info ${res.status}`);
|
|
117
|
+
return res.json() as Promise<ProjectInfoPayload>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async sendTurn(
|
|
121
|
+
message: string,
|
|
122
|
+
session: DevSessionRef | null,
|
|
123
|
+
options: SendTurnOptions = {},
|
|
124
|
+
): Promise<{ session: DevSessionRef; response: SessionTurnResponse }> {
|
|
125
|
+
const wait = options.wait ?? true;
|
|
126
|
+
const path = session
|
|
127
|
+
? `${this.baseUrl}/plasm/v1/session/${session.sessionId}`
|
|
128
|
+
: `${this.baseUrl}/plasm/v1/session`;
|
|
129
|
+
const body = session
|
|
130
|
+
? { message, continuationToken: session.continuationToken, wait }
|
|
131
|
+
: { message, wait };
|
|
132
|
+
|
|
133
|
+
if (!wait) {
|
|
134
|
+
const postRes = await fetch(path, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "content-type": "application/json" },
|
|
137
|
+
body: JSON.stringify(body),
|
|
138
|
+
});
|
|
139
|
+
const json = (await postRes.json()) as SessionTurnResponse;
|
|
140
|
+
if (!json.sessionId || !json.continuationToken) {
|
|
141
|
+
throw new Error(json.message ?? json.error ?? `session ${postRes.status}`);
|
|
142
|
+
}
|
|
143
|
+
const ref = { sessionId: json.sessionId, continuationToken: json.continuationToken };
|
|
144
|
+
if (postRes.status >= 400) {
|
|
145
|
+
return { session: ref, response: json };
|
|
146
|
+
}
|
|
147
|
+
const terminal = await consumeSessionStream(
|
|
148
|
+
`${this.baseUrl}/plasm/v1/session/${ref.sessionId}/stream`,
|
|
149
|
+
(ev) => options.onEvent?.(ev),
|
|
150
|
+
);
|
|
151
|
+
const response: SessionTurnResponse = {
|
|
152
|
+
...json,
|
|
153
|
+
text: terminal?.text ?? json.text,
|
|
154
|
+
steps: terminal?.steps ?? json.steps,
|
|
155
|
+
error: terminal?.type === "turn:error" ? "turn_failed" : json.error,
|
|
156
|
+
message: terminal?.message ?? json.message,
|
|
157
|
+
status: terminal?.type === "turn:error" ? "error" : "idle",
|
|
158
|
+
};
|
|
159
|
+
return { session: ref, response };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const res = await fetch(path, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { "content-type": "application/json" },
|
|
165
|
+
body: JSON.stringify(body),
|
|
166
|
+
});
|
|
167
|
+
const json = (await res.json()) as SessionTurnResponse;
|
|
168
|
+
if (!json.sessionId || !json.continuationToken) {
|
|
169
|
+
throw new Error(json.message ?? json.error ?? `session ${res.status}`);
|
|
170
|
+
}
|
|
171
|
+
const ref = { sessionId: json.sessionId, continuationToken: json.continuationToken };
|
|
172
|
+
if (options.onEvent) {
|
|
173
|
+
await consumeSessionStream(
|
|
174
|
+
`${this.baseUrl}/plasm/v1/session/${ref.sessionId}/stream`,
|
|
175
|
+
options.onEvent,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return { session: ref, response: json };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as readline from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
|
|
4
|
+
import { DevHttpSessionClient, type DevSessionRef } from "./http-session.js";
|
|
5
|
+
import {
|
|
6
|
+
handleSlashCommand,
|
|
7
|
+
printAssistant,
|
|
8
|
+
printBanner,
|
|
9
|
+
printError,
|
|
10
|
+
printLines,
|
|
11
|
+
printMeta,
|
|
12
|
+
printStep,
|
|
13
|
+
printUserLine,
|
|
14
|
+
type SlashResult,
|
|
15
|
+
} from "./slash.js";
|
|
16
|
+
import type { ProjectInfoPayload } from "../../project-info.js";
|
|
17
|
+
|
|
18
|
+
export interface DevTuiOptions {
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function applySlash(result: SlashResult): "quit" | "clear" | "handled" | "none" {
|
|
23
|
+
if (result.kind === "quit") return "quit";
|
|
24
|
+
if (result.kind === "clear_session") return "clear";
|
|
25
|
+
if (result.kind === "print") {
|
|
26
|
+
printLines(result.lines);
|
|
27
|
+
return "handled";
|
|
28
|
+
}
|
|
29
|
+
return "none";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runDevTui(options: DevTuiOptions): Promise<void> {
|
|
33
|
+
const client = new DevHttpSessionClient(options.baseUrl);
|
|
34
|
+
let info: ProjectInfoPayload | null = null;
|
|
35
|
+
try {
|
|
36
|
+
info = await client.fetchInfo();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
printError(`could not reach ${options.baseUrl}: ${String(err)}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
printBanner(options.baseUrl, info);
|
|
43
|
+
let session: DevSessionRef | null = null;
|
|
44
|
+
const rl = readline.createInterface({ input, output, terminal: true });
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
while (true) {
|
|
48
|
+
const line = (await rl.question("› ")).trim();
|
|
49
|
+
if (!line) continue;
|
|
50
|
+
|
|
51
|
+
if (line.startsWith("/")) {
|
|
52
|
+
const slash = handleSlashCommand(line, { info, session, baseUrl: options.baseUrl });
|
|
53
|
+
const action = applySlash(slash);
|
|
54
|
+
if (action === "quit") break;
|
|
55
|
+
if (action === "clear") {
|
|
56
|
+
session = null;
|
|
57
|
+
printMeta("new session");
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
printUserLine(line);
|
|
63
|
+
try {
|
|
64
|
+
const { session: next, response } = await client.sendTurn(line, session, {
|
|
65
|
+
wait: false,
|
|
66
|
+
onEvent: (ev) => {
|
|
67
|
+
if (ev.type === "turn:step" && ev.toolsUsed?.length) {
|
|
68
|
+
printStep(ev.toolsUsed);
|
|
69
|
+
}
|
|
70
|
+
if (ev.type === "turn:error") {
|
|
71
|
+
printError(ev.message ?? "turn failed");
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
session = next;
|
|
76
|
+
if (response.error) {
|
|
77
|
+
printError(response.message ?? response.error);
|
|
78
|
+
} else if (response.text) {
|
|
79
|
+
printAssistant(response.text);
|
|
80
|
+
}
|
|
81
|
+
if (response.steps !== undefined) {
|
|
82
|
+
printMeta(`${response.steps} step(s)`);
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
printError(String(err));
|
|
86
|
+
}
|
|
87
|
+
console.log("");
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
rl.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ProjectInfoPayload } from "../../project-info.js";
|
|
2
|
+
import { ANSI, paint, wrap } from "./ansi.js";
|
|
3
|
+
import type { DevSessionRef } from "./http-session.js";
|
|
4
|
+
|
|
5
|
+
export interface SlashContext {
|
|
6
|
+
info: ProjectInfoPayload | null;
|
|
7
|
+
session: DevSessionRef | null;
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SlashResult =
|
|
12
|
+
| { kind: "continue" }
|
|
13
|
+
| { kind: "clear_session" }
|
|
14
|
+
| { kind: "quit" }
|
|
15
|
+
| { kind: "print"; lines: string[] };
|
|
16
|
+
|
|
17
|
+
const HELP_LINES = [
|
|
18
|
+
"Slash commands:",
|
|
19
|
+
" /help this list",
|
|
20
|
+
" /info project + catalog summary",
|
|
21
|
+
" /model current agent model",
|
|
22
|
+
" /channels HTTP channel routes",
|
|
23
|
+
" /catalogs loaded catalogs",
|
|
24
|
+
" /new start a fresh session",
|
|
25
|
+
" /quit exit",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function handleSlashCommand(input: string, ctx: SlashContext): SlashResult {
|
|
29
|
+
const cmd = input.trim().toLowerCase();
|
|
30
|
+
if (cmd === "/help" || cmd === "/?") {
|
|
31
|
+
return { kind: "print", lines: HELP_LINES };
|
|
32
|
+
}
|
|
33
|
+
if (cmd === "/quit" || cmd === "/exit" || cmd === "/q") {
|
|
34
|
+
return { kind: "quit" };
|
|
35
|
+
}
|
|
36
|
+
if (cmd === "/new") {
|
|
37
|
+
return { kind: "clear_session" };
|
|
38
|
+
}
|
|
39
|
+
if (cmd === "/info") {
|
|
40
|
+
if (!ctx.info) return { kind: "print", lines: ["info unavailable — server not reachable"] };
|
|
41
|
+
const i = ctx.info;
|
|
42
|
+
const lines = [
|
|
43
|
+
`${i.framework} status=${i.status}`,
|
|
44
|
+
`project: ${i.projectRoot}`,
|
|
45
|
+
`gateway: ${i.gateway.configured ? "configured" : "missing"}`,
|
|
46
|
+
`catalogs: ${i.discovery.catalogs.length} channels: ${i.discovery.channels.length}`,
|
|
47
|
+
`skills: ${i.discovery.skills.length} subagents: ${i.loadedSlots.subagents.length}`,
|
|
48
|
+
];
|
|
49
|
+
if (ctx.session) {
|
|
50
|
+
lines.push(`session: ${ctx.session.sessionId.slice(0, 8)}…`);
|
|
51
|
+
}
|
|
52
|
+
return { kind: "print", lines };
|
|
53
|
+
}
|
|
54
|
+
if (cmd === "/model") {
|
|
55
|
+
const model = ctx.info?.dev?.model ?? "(unknown — start dev server or /info)";
|
|
56
|
+
return { kind: "print", lines: [`model: ${String(model)}`] };
|
|
57
|
+
}
|
|
58
|
+
if (cmd === "/channels") {
|
|
59
|
+
const routes = ctx.info?.dev?.routes.channels ?? [];
|
|
60
|
+
if (!routes.length) return { kind: "print", lines: ["no channels loaded"] };
|
|
61
|
+
return {
|
|
62
|
+
kind: "print",
|
|
63
|
+
lines: routes.map((r) => ` ${r.method} ${r.path}`),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (cmd === "/catalogs") {
|
|
67
|
+
const catalogs = ctx.info?.catalogs ?? [];
|
|
68
|
+
if (!catalogs.length) return { kind: "print", lines: ["no catalogs"] };
|
|
69
|
+
return {
|
|
70
|
+
kind: "print",
|
|
71
|
+
lines: catalogs.map((c) => {
|
|
72
|
+
const fresh = c.stubFresh?.fresh ? "fresh" : c.stubFresh ? "stale" : "missing";
|
|
73
|
+
return ` ${c.name} stub=${fresh}`;
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { kind: "print", lines: [`unknown command ${cmd} — try /help`] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function printBanner(baseUrl: string, info: ProjectInfoPayload | null): void {
|
|
81
|
+
const title = paint("plasm-agent dev", ANSI.bold, ANSI.cyan);
|
|
82
|
+
const url = paint(baseUrl, ANSI.dim);
|
|
83
|
+
console.log(`\n${title} ${url}`);
|
|
84
|
+
if (info?.dev?.model) {
|
|
85
|
+
console.log(paint(`model: ${String(info.dev.model)}`, ANSI.dim));
|
|
86
|
+
}
|
|
87
|
+
console.log(paint("Type a message or /help. /quit to exit.", ANSI.dim));
|
|
88
|
+
console.log("");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function printUserLine(text: string): void {
|
|
92
|
+
console.log(paint("you › ", ANSI.bold, ANSI.green) + text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function printAssistant(text: string): void {
|
|
96
|
+
const prefix = paint("agent › ", ANSI.bold, ANSI.magenta);
|
|
97
|
+
for (const line of wrap(text)) {
|
|
98
|
+
console.log(prefix + line);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function printMeta(text: string): void {
|
|
103
|
+
console.log(paint(text, ANSI.dim));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function printError(text: string): void {
|
|
107
|
+
console.log(paint(`error: ${text}`, ANSI.red));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function printStep(tools: string[]): void {
|
|
111
|
+
if (!tools.length) return;
|
|
112
|
+
console.log(paint(` · tools: ${tools.join(", ")}`, ANSI.blue));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function printLines(lines: string[]): void {
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
console.log(paint(line, ANSI.yellow));
|
|
118
|
+
}
|
|
119
|
+
}
|