@trama-dev/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 trama contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @trama-dev/runtime
2
+
3
+ The runtime behind [trama](https://github.com/NaNhkNaN/trama) — where the program is the orchestration.
4
+
5
+ This package has two surfaces:
6
+
7
+ **Client** — what trama-generated programs import. 9 functions, nothing else.
8
+
9
+ ```typescript
10
+ import { ctx, agent, tools } from "@trama-dev/runtime";
11
+
12
+ const data = await tools.read("input.csv");
13
+ const report = await agent.ask(`Analyze this:\n${data}`);
14
+ await tools.write("report.md", report);
15
+ await ctx.done();
16
+ ```
17
+
18
+ **Runner** — what the CLI and programmatic callers use to execute programs.
19
+
20
+ ```typescript
21
+ import { runProgram, createProject } from "@trama-dev/runtime/runner";
22
+ ```
23
+
24
+ See the [main README](https://github.com/NaNhkNaN/trama) for the full picture.
@@ -0,0 +1,3 @@
1
+ import type { Agent } from "./types.js";
2
+ import type { PiAdapter } from "./pi-adapter.js";
3
+ export declare function createAgent(adapter: PiAdapter): Agent;
package/dist/agent.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Minimal shape validation.
3
+ * Checks all schema keys exist and values match declared primitive types.
4
+ * The part after ":" is a description and is ignored during validation.
5
+ */
6
+ function validateShape(result, schema) {
7
+ if (typeof result !== "object" || result === null)
8
+ return "response is not an object";
9
+ const obj = result;
10
+ for (const [key, typeDesc] of Object.entries(schema)) {
11
+ if (!(key in obj))
12
+ return `missing key: ${key}`;
13
+ const expectedType = typeDesc.split(":")[0].trim();
14
+ const actual = typeof obj[key];
15
+ if (expectedType === "string" && actual !== "string")
16
+ return `${key}: expected string, got ${actual}`;
17
+ if (expectedType === "number" && actual !== "number")
18
+ return `${key}: expected number, got ${actual}`;
19
+ if (expectedType === "boolean" && actual !== "boolean")
20
+ return `${key}: expected boolean, got ${actual}`;
21
+ }
22
+ return null;
23
+ }
24
+ export function createAgent(adapter) {
25
+ return {
26
+ async ask(prompt, options) {
27
+ return adapter.ask(prompt, options);
28
+ },
29
+ async generate(input) {
30
+ const schemaInstruction = `Respond with ONLY a JSON object matching this shape:\n${JSON.stringify(input.schema, null, 2)}\nNo markdown. No explanation. Just the JSON.`;
31
+ const fullPrompt = `${input.prompt}\n\n${schemaInstruction}`;
32
+ const attempt = async (p) => {
33
+ const response = await adapter.ask(p, { system: input.system });
34
+ const cleaned = response
35
+ .trim()
36
+ .replace(/^```json\s*/i, "")
37
+ .replace(/^```\s*/, "")
38
+ .replace(/\s*```$/, "")
39
+ .trim();
40
+ const parsed = JSON.parse(cleaned);
41
+ const error = validateShape(parsed, input.schema);
42
+ if (error)
43
+ throw new Error(`Schema validation failed: ${error}`);
44
+ return parsed;
45
+ };
46
+ try {
47
+ return await attempt(fullPrompt);
48
+ }
49
+ catch (firstError) {
50
+ const retryPrompt = `Your previous response failed validation: ${firstError}.\nTry again.\n\n${fullPrompt}`;
51
+ return await attempt(retryPrompt);
52
+ }
53
+ }
54
+ };
55
+ }
@@ -0,0 +1,4 @@
1
+ import type { Ctx, Agent, Tools } from "./types.js";
2
+ export declare const ctx: Ctx;
3
+ export declare const agent: Agent;
4
+ export declare const tools: Tools;
package/dist/client.js ADDED
@@ -0,0 +1,53 @@
1
+ // @trama-dev/runtime client — this is what program.ts imports.
2
+ // Thin HTTP wrapper that calls the runner's IPC server.
3
+ const PORT = process.env.TRAMA_PORT;
4
+ if (!PORT) {
5
+ throw new Error("@trama-dev/runtime must be executed via `trama run`. " +
6
+ "Direct execution is not supported (TRAMA_PORT env var missing).");
7
+ }
8
+ if (!process.env.TRAMA_INPUT) {
9
+ throw new Error("@trama-dev/runtime: TRAMA_INPUT env var missing. " +
10
+ "This program must be launched by the trama runner.");
11
+ }
12
+ const BASE = `http://127.0.0.1:${PORT}`;
13
+ async function call(endpoint, body) {
14
+ const res = await fetch(`${BASE}${endpoint}`, {
15
+ method: "POST",
16
+ headers: { "Content-Type": "application/json" },
17
+ body: JSON.stringify(body),
18
+ });
19
+ if (!res.ok)
20
+ throw new Error(`trama runtime error: ${await res.text()}`);
21
+ return res.json();
22
+ }
23
+ let doneCalled = false;
24
+ export const ctx = {
25
+ input: JSON.parse(process.env.TRAMA_INPUT),
26
+ state: JSON.parse(process.env.TRAMA_STATE ?? "{}"),
27
+ iteration: parseInt(process.env.TRAMA_ITERATION ?? "0"),
28
+ maxIterations: parseInt(process.env.TRAMA_MAX_ITERATIONS ?? "100"),
29
+ async log(msg, data) {
30
+ await call("/ctx/log", { message: msg, data });
31
+ },
32
+ async checkpoint() {
33
+ await call("/ctx/checkpoint", { state: ctx.state });
34
+ ctx.iteration += 1;
35
+ },
36
+ async done(result) {
37
+ if (doneCalled)
38
+ return;
39
+ await call("/ctx/done", { state: ctx.state, result });
40
+ doneCalled = true;
41
+ ctx.iteration += 1;
42
+ },
43
+ };
44
+ export const agent = {
45
+ ask: (prompt, opts) => call("/agent/ask", { prompt, ...opts }).then(r => r.result),
46
+ generate: (input) => call("/agent/generate", input).then(r => r.result),
47
+ };
48
+ export const tools = {
49
+ read: (path) => call("/tools/read", { path }).then(r => r.result),
50
+ write: (path, content) => call("/tools/write", { path, content }).then(r => r.result),
51
+ shell: (cmd, opts) => call("/tools/shell", { command: cmd, ...opts }),
52
+ fetch: (url, opts) => call("/tools/fetch", { url, ...opts }),
53
+ };
@@ -0,0 +1,4 @@
1
+ import type { Ctx } from "./types.js";
2
+ export declare function createContext(projectDir: string, initialState: Record<string, unknown>, options?: {
3
+ maxIterations?: number;
4
+ }): Ctx;
@@ -0,0 +1,79 @@
1
+ import { readFileSync, writeFileSync, appendFileSync } from "fs";
2
+ import { join } from "path";
3
+ /**
4
+ * Recursively check that a value is strictly JSON-serializable.
5
+ * Throws TypeError with the path to the offending value.
6
+ *
7
+ * Uses a stack (ancestor chain) instead of a flat set so that shared-but-
8
+ * non-circular structures like { a: shared, b: shared } pass validation.
9
+ */
10
+ function assertSerializable(value, path, ancestors = new Set()) {
11
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
12
+ return;
13
+ }
14
+ if (typeof value === "number") {
15
+ if (!Number.isFinite(value)) {
16
+ throw new TypeError(`${path} is not JSON-serializable (${value})`);
17
+ }
18
+ return;
19
+ }
20
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
21
+ throw new TypeError(`${path} is not JSON-serializable (${typeof value})`);
22
+ }
23
+ if (ancestors.has(value)) {
24
+ throw new TypeError(`${path} contains a circular reference`);
25
+ }
26
+ ancestors.add(value);
27
+ if (Array.isArray(value)) {
28
+ for (let i = 0; i < value.length; i++) {
29
+ assertSerializable(value[i], `${path}[${i}]`, ancestors);
30
+ }
31
+ ancestors.delete(value);
32
+ return;
33
+ }
34
+ if (Object.getPrototypeOf(value) !== Object.prototype) {
35
+ const name = value.constructor?.name ?? "unknown";
36
+ throw new TypeError(`${path} is not a plain object (${name})`);
37
+ }
38
+ for (const [k, v] of Object.entries(value)) {
39
+ assertSerializable(v, `${path}.${k}`, ancestors);
40
+ }
41
+ ancestors.delete(value);
42
+ }
43
+ export function createContext(projectDir, initialState, options) {
44
+ const logsPath = join(projectDir, "logs", "latest.jsonl");
45
+ const statePath = join(projectDir, "state.json");
46
+ const meta = JSON.parse(readFileSync(join(projectDir, "meta.json"), "utf-8"));
47
+ let doneCalled = false;
48
+ let doneLogged = false;
49
+ return {
50
+ input: meta.input,
51
+ state: { ...initialState },
52
+ iteration: typeof initialState.__trama_iteration === "number"
53
+ && Number.isFinite(initialState.__trama_iteration)
54
+ ? initialState.__trama_iteration : 0,
55
+ maxIterations: options?.maxIterations ?? 100,
56
+ async log(message, data) {
57
+ const entry = { ts: Date.now(), message, data: data ?? null };
58
+ appendFileSync(logsPath, JSON.stringify(entry) + "\n");
59
+ console.log(`[trama] ${message}`);
60
+ },
61
+ async checkpoint() {
62
+ assertSerializable(this.state, "ctx.state");
63
+ const nextIteration = this.iteration + 1;
64
+ this.state.__trama_iteration = nextIteration;
65
+ writeFileSync(statePath, JSON.stringify(this.state, null, 2));
66
+ this.iteration = nextIteration;
67
+ },
68
+ async done(result) {
69
+ if (doneCalled)
70
+ return;
71
+ if (result && !doneLogged) {
72
+ await this.log("done", result);
73
+ doneLogged = true;
74
+ }
75
+ await this.checkpoint();
76
+ doneCalled = true;
77
+ }
78
+ };
79
+ }
@@ -0,0 +1,8 @@
1
+ export { runProgram } from "./runner.js";
2
+ export declare function validateProjectName(name: string): void;
3
+ export declare function createProject(name: string, prompt: string, options?: {
4
+ model?: string;
5
+ }): Promise<void>;
6
+ export declare function updateProject(name: string, prompt: string): Promise<void>;
7
+ export declare function listProjects(): Promise<void>;
8
+ export declare function showLogs(name: string): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,215 @@
1
+ // @trama-dev/runtime runner surface — exported via "@trama-dev/runtime/runner"
2
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, copyFileSync, appendFileSync, statSync, rmSync, cpSync, mkdtempSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir, tmpdir } from "os";
5
+ import { PiAdapter } from "./pi-adapter.js";
6
+ import { loadConfig, loadState, smokeRunAndRepair, copyToHistory, RUNTIME_TYPES, PI_VERSION } from "./runner.js";
7
+ export { runProgram } from "./runner.js";
8
+ // --- Example programs (included in system prompt during create/update) ---
9
+ const EXAMPLE_PROGRAMS = `// Example A: Simple (single action)
10
+ import { ctx, tools } from "@trama-dev/runtime";
11
+
12
+ const content = "# Hello World\\nGenerated at " + new Date().toISOString();
13
+ await tools.write("hello.md", content);
14
+ await ctx.log("wrote hello.md");
15
+ await ctx.done();
16
+
17
+ // Example B: Medium (LLM + tools)
18
+ import { ctx, agent, tools } from "@trama-dev/runtime";
19
+
20
+ const data = await tools.read(ctx.input.args.file as string);
21
+ const summary = await agent.ask("Summarize this data concisely:\\n" + data);
22
+ await tools.write("summary.md", summary);
23
+ await ctx.done({ summaryLength: summary.length });
24
+
25
+ // Example C: Complex (autoresearch loop)
26
+ import { ctx, agent, tools } from "@trama-dev/runtime";
27
+
28
+ let code = (ctx.state.code as string | undefined) ?? await tools.read("target.ts");
29
+ let bestMetric = (ctx.state.bestMetric as number | undefined) ?? Infinity;
30
+
31
+ for (let i = ctx.iteration; i < ctx.maxIterations; i++) {
32
+ const proposal = await agent.generate<{ reasoning: string; newCode: string }>({
33
+ prompt: "Analyze this code and propose ONE specific improvement:\\n" + code,
34
+ schema: { reasoning: "string: why this change helps", newCode: "string: complete improved code" },
35
+ });
36
+
37
+ await tools.write("candidate.ts", proposal.newCode);
38
+ const bench = await tools.shell("npx tsx benchmark.ts candidate.ts");
39
+
40
+ if (bench.exitCode !== 0) {
41
+ await ctx.log("benchmark failed", { stderr: bench.stderr });
42
+ continue;
43
+ }
44
+
45
+ const metric = parseFloat(bench.stdout.trim());
46
+ if (metric < bestMetric) {
47
+ code = proposal.newCode;
48
+ bestMetric = metric;
49
+ await ctx.log("improved", { iteration: i, metric, reasoning: proposal.reasoning });
50
+ ctx.state = { code, bestMetric };
51
+ await ctx.checkpoint();
52
+ }
53
+ }
54
+
55
+ await tools.write("target.ts", code);
56
+ await ctx.done({ finalMetric: bestMetric });`;
57
+ function getSystemPrompt() {
58
+ return (`You are generating a TypeScript program for the trama runtime.\n\n` +
59
+ `Runtime API:\n${RUNTIME_TYPES}\n\n` +
60
+ `Examples:\n${EXAMPLE_PROGRAMS}`);
61
+ }
62
+ function projectsDir() {
63
+ return join(homedir(), ".trama", "projects");
64
+ }
65
+ export function validateProjectName(name) {
66
+ if (!name || /[\/\\]/.test(name) || name === "." || name === "..") {
67
+ throw new Error(`Invalid project name: "${name}". Must be a simple directory name without path separators.`);
68
+ }
69
+ }
70
+ function resolveProject(name) {
71
+ validateProjectName(name);
72
+ const dir = join(projectsDir(), name);
73
+ if (!existsSync(dir)) {
74
+ throw new Error(`Project "${name}" not found. Run \`trama list\` to see available projects.`);
75
+ }
76
+ return dir;
77
+ }
78
+ // --- Public API ---
79
+ export async function createProject(name, prompt, options) {
80
+ validateProjectName(name);
81
+ const projectDir = join(projectsDir(), name);
82
+ let keepProjectOnFailure = false;
83
+ if (existsSync(projectDir)) {
84
+ throw new Error(`Project "${name}" already exists.`);
85
+ }
86
+ mkdirSync(projectDir, { recursive: true });
87
+ try {
88
+ mkdirSync(join(projectDir, "history"), { recursive: true });
89
+ mkdirSync(join(projectDir, "logs"), { recursive: true });
90
+ writeFileSync(join(projectDir, "package.json"), JSON.stringify({ type: "module" }, null, 2));
91
+ writeFileSync(join(projectDir, ".gitignore"), "node_modules/\nstate.json\nlogs/\nhistory/\n");
92
+ const meta = {
93
+ input: { prompt, args: {} },
94
+ createdAt: new Date().toISOString(),
95
+ piVersion: PI_VERSION,
96
+ };
97
+ writeFileSync(join(projectDir, "meta.json"), JSON.stringify(meta, null, 2));
98
+ const config = loadConfig();
99
+ if (options?.model)
100
+ config.model = options.model;
101
+ const adapter = new PiAdapter(config, projectDir);
102
+ console.log(`Generating program for "${name}"...`);
103
+ const program = await adapter.ask(`${prompt}\n\nOutput ONLY the TypeScript program. No markdown fences. No explanation.`, { system: getSystemPrompt() });
104
+ writeFileSync(join(projectDir, "program.ts"), program);
105
+ keepProjectOnFailure = true;
106
+ console.log("Validating program...");
107
+ await smokeRunAndRepair(projectDir, adapter, "create");
108
+ copyFileSync(join(projectDir, "program.ts"), join(projectDir, "history", "0001.ts"));
109
+ appendFileSync(join(projectDir, "history", "index.jsonl"), JSON.stringify({ version: 1, reason: "create", timestamp: new Date().toISOString(), prompt }) + "\n");
110
+ console.log(`Created ${name}. Run with: trama run ${name}`);
111
+ }
112
+ catch (err) {
113
+ if (!keepProjectOnFailure) {
114
+ rmSync(projectDir, { recursive: true, force: true });
115
+ }
116
+ throw err;
117
+ }
118
+ }
119
+ export async function updateProject(name, prompt) {
120
+ const projectDir = resolveProject(name);
121
+ const programPath = join(projectDir, "program.ts");
122
+ const originalSource = readFileSync(programPath, "utf-8");
123
+ const state = loadState(projectDir);
124
+ const meta = JSON.parse(readFileSync(join(projectDir, "meta.json"), "utf-8"));
125
+ const historyIndexPath = join(projectDir, "history", "index.jsonl");
126
+ const originalHistory = existsSync(historyIndexPath) ? readFileSync(historyIndexPath, "utf-8") : "";
127
+ const config = loadConfig();
128
+ const adapter = new PiAdapter(config, projectDir);
129
+ const systemPrompt = getSystemPrompt() +
130
+ `\n\nOriginal prompt: ${meta.input.prompt}` +
131
+ `\n\nCurrent program.ts:\n${originalSource}` +
132
+ (Object.keys(state).length > 0 ? `\n\nCurrent state:\n${JSON.stringify(state, null, 2)}` : "");
133
+ console.log(`Updating "${name}"...`);
134
+ const updated = await adapter.ask(`${prompt}\n\nOutput the complete updated program.ts. No partial patches. No markdown fences. No explanation.`, { system: systemPrompt });
135
+ const backupRoot = mkdtempSync(join(tmpdir(), "trama-update-backup-"));
136
+ const backupDir = join(backupRoot, "project");
137
+ cpSync(projectDir, backupDir, { recursive: true });
138
+ writeFileSync(programPath, updated);
139
+ console.log("Validating updated program...");
140
+ try {
141
+ await smokeRunAndRepair(projectDir, adapter, "update");
142
+ }
143
+ catch (err) {
144
+ const failedHistory = existsSync(historyIndexPath) ? readFileSync(historyIndexPath, "utf-8") : "";
145
+ rmSync(projectDir, { recursive: true, force: true });
146
+ cpSync(backupDir, projectDir, { recursive: true });
147
+ if (failedHistory.startsWith(originalHistory)) {
148
+ const repairEntries = failedHistory.slice(originalHistory.length);
149
+ if (repairEntries.length > 0) {
150
+ appendFileSync(join(projectDir, "history", "index.jsonl"), repairEntries);
151
+ }
152
+ }
153
+ rmSync(backupRoot, { recursive: true, force: true });
154
+ throw new Error(`Update validation failed — project files have been restored to the previous version.\n` +
155
+ `${err instanceof Error ? err.message : err}`);
156
+ }
157
+ rmSync(backupRoot, { recursive: true, force: true });
158
+ copyToHistory(projectDir, "update", prompt);
159
+ console.log(`Updated ${name}. Run with: trama run ${name}`);
160
+ }
161
+ export async function listProjects() {
162
+ const dir = projectsDir();
163
+ if (!existsSync(dir)) {
164
+ console.log("No projects found.");
165
+ return;
166
+ }
167
+ const entries = readdirSync(dir, { withFileTypes: true })
168
+ .filter(e => e.isDirectory());
169
+ if (entries.length === 0) {
170
+ console.log("No projects found.");
171
+ return;
172
+ }
173
+ for (const entry of entries) {
174
+ const projectDir = join(dir, entry.name);
175
+ let prompt = "";
176
+ let modified = "";
177
+ try {
178
+ const meta = JSON.parse(readFileSync(join(projectDir, "meta.json"), "utf-8"));
179
+ prompt = meta.input?.prompt ?? "";
180
+ if (prompt.length > 60)
181
+ prompt = prompt.slice(0, 57) + "...";
182
+ }
183
+ catch { /* no meta */ }
184
+ try {
185
+ const stat = statSync(join(projectDir, "program.ts"));
186
+ modified = stat.mtime.toISOString().slice(0, 19).replace("T", " ");
187
+ }
188
+ catch { /* no program.ts */ }
189
+ console.log(` ${entry.name} ${prompt} ${modified}`);
190
+ }
191
+ }
192
+ export async function showLogs(name) {
193
+ const projectDir = resolveProject(name);
194
+ const logsPath = join(projectDir, "logs", "latest.jsonl");
195
+ if (!existsSync(logsPath)) {
196
+ console.log("No logs found.");
197
+ return;
198
+ }
199
+ const content = readFileSync(logsPath, "utf-8").trim();
200
+ if (!content) {
201
+ console.log("No log entries.");
202
+ return;
203
+ }
204
+ for (const line of content.split("\n")) {
205
+ try {
206
+ const entry = JSON.parse(line);
207
+ const time = new Date(entry.ts).toISOString().slice(11, 19);
208
+ const data = entry.data ? ` ${JSON.stringify(entry.data)}` : "";
209
+ console.log(`[${time}] ${entry.message}${data}`);
210
+ }
211
+ catch {
212
+ console.log(line);
213
+ }
214
+ }
215
+ }
@@ -0,0 +1,12 @@
1
+ import type { PiAdapterConfig, RepairInput } from "./types.js";
2
+ export declare class PiAdapter {
3
+ private config;
4
+ private cwd;
5
+ constructor(config: PiAdapterConfig, cwd: string);
6
+ private createSession;
7
+ private extractText;
8
+ ask(prompt: string, options?: {
9
+ system?: string;
10
+ }): Promise<string>;
11
+ repair(input: RepairInput): Promise<string>;
12
+ }
@@ -0,0 +1,59 @@
1
+ import { createAgentSession, SessionManager, DefaultResourceLoader, } from "@mariozechner/pi-coding-agent";
2
+ import { getModel } from "@mariozechner/pi-ai";
3
+ export class PiAdapter {
4
+ config;
5
+ cwd;
6
+ constructor(config, cwd) {
7
+ this.config = config;
8
+ this.cwd = cwd;
9
+ }
10
+ async createSession(systemPrompt) {
11
+ const resourceLoader = new DefaultResourceLoader({
12
+ cwd: this.cwd,
13
+ noExtensions: true,
14
+ noSkills: true,
15
+ ...(systemPrompt && { appendSystemPrompt: systemPrompt }),
16
+ });
17
+ await resourceLoader.reload();
18
+ const { session } = await createAgentSession({
19
+ cwd: this.cwd,
20
+ model: getModel(this.config.provider, this.config.model),
21
+ sessionManager: SessionManager.inMemory(),
22
+ resourceLoader,
23
+ });
24
+ return session;
25
+ }
26
+ extractText(session) {
27
+ const messages = session.state.messages;
28
+ for (let i = messages.length - 1; i >= 0; i--) {
29
+ const msg = messages[i];
30
+ if ("role" in msg && msg.role === "assistant") {
31
+ return msg.content
32
+ .filter(b => b.type === "text" && b.text)
33
+ .map(b => b.text)
34
+ .join("");
35
+ }
36
+ }
37
+ return "";
38
+ }
39
+ async ask(prompt, options) {
40
+ const session = await this.createSession(options?.system);
41
+ try {
42
+ await session.prompt(prompt);
43
+ const text = this.extractText(session);
44
+ if (!text)
45
+ throw new Error("Empty response from LLM");
46
+ return text;
47
+ }
48
+ finally {
49
+ session.dispose();
50
+ }
51
+ }
52
+ async repair(input) {
53
+ return this.ask(`Fix this program so it runs.\n\n` +
54
+ `Runtime types:\n${input.runtimeTypes}\n\n` +
55
+ `Broken program:\n${input.programSource}\n\n` +
56
+ `Error:\n${input.error}\n\n` +
57
+ `Output ONLY the fixed program. No explanation.`, { system: "You are a TypeScript repair tool." });
58
+ }
59
+ }
@@ -0,0 +1,15 @@
1
+ import type { RunOptions, TramaConfig } from "./types.js";
2
+ import { PiAdapter } from "./pi-adapter.js";
3
+ export declare const PI_VERSION: string;
4
+ export declare const RUNTIME_TYPES = "// @trama-dev/runtime - Complete API\n\nexport interface Ctx {\n input: { prompt: string; args: Record<string, unknown> };\n state: Record<string, unknown>;\n iteration: number;\n maxIterations: number;\n log(message: string, data?: Record<string, unknown>): Promise<void>;\n checkpoint(): Promise<void>;\n done(result?: Record<string, unknown>): Promise<void>;\n}\n\nexport interface Agent {\n ask(prompt: string, options?: { system?: string }): Promise<string>;\n generate<T>(input: {\n prompt: string;\n schema: Record<string, string>;\n system?: string;\n }): Promise<T>;\n}\n\nexport interface ShellResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface Tools {\n read(path: string): Promise<string>;\n write(path: string, content: string): Promise<void>;\n shell(command: string, options?: { cwd?: string; timeout?: number }): Promise<ShellResult>;\n fetch(url: string, options?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n }): Promise<{ status: number; body: string; headers: Record<string, string> }>;\n}\n\nexport declare const ctx: Ctx;\nexport declare const agent: Agent;\nexport declare const tools: Tools;";
5
+ export declare function loadConfig(): TramaConfig;
6
+ export declare function loadState(projectDir: string): Record<string, unknown>;
7
+ /**
8
+ * Copy current program.ts to history/NNNN.ts and append to index.jsonl.
9
+ *
10
+ * WARNING: Do NOT use this for create-time repair attempts.
11
+ * Create-time repairs go to index.jsonl only (reason: "create-repair").
12
+ */
13
+ export declare function copyToHistory(projectDir: string, reason: string, detail?: string): void;
14
+ export declare function smokeRunAndRepair(projectDir: string, adapter: PiAdapter, reason: "create" | "update"): Promise<void>;
15
+ export declare function runProgram(options: RunOptions): Promise<void>;
package/dist/runner.js ADDED
@@ -0,0 +1,470 @@
1
+ import { createServer } from "http";
2
+ import { spawn } from "child_process";
3
+ import { readFileSync, writeFileSync, appendFileSync, readdirSync, copyFileSync, mkdirSync, existsSync, symlinkSync, rmSync, lstatSync, readlinkSync, } from "fs";
4
+ import { join, dirname } from "path";
5
+ import { homedir } from "os";
6
+ import { fileURLToPath } from "url";
7
+ import { PiAdapter } from "./pi-adapter.js";
8
+ import { createContext } from "./context.js";
9
+ import { createAgent } from "./agent.js";
10
+ import { createTools } from "./tools.js";
11
+ // --- Module resolution for child processes ---
12
+ /**
13
+ * Find the node_modules directory that contains @trama-dev/runtime.
14
+ * Works for both global install and monorepo development.
15
+ */
16
+ function findRuntimeNodeModules() {
17
+ let dir = dirname(fileURLToPath(import.meta.url));
18
+ while (dir !== dirname(dir)) {
19
+ const candidate = join(dir, "node_modules");
20
+ if (existsSync(join(candidate, "@trama-dev", "runtime"))) {
21
+ return candidate;
22
+ }
23
+ dir = dirname(dir);
24
+ }
25
+ throw new Error("Could not locate node_modules containing @trama-dev/runtime");
26
+ }
27
+ const TRAMA_NODE_MODULES = findRuntimeNodeModules();
28
+ const TSX_CLI = join(TRAMA_NODE_MODULES, "tsx", "dist", "cli.mjs");
29
+ export const PI_VERSION = JSON.parse(readFileSync(join(TRAMA_NODE_MODULES, "@mariozechner", "pi-coding-agent", "package.json"), "utf-8")).version;
30
+ /**
31
+ * Ensure the project directory has a node_modules symlink to @trama-dev/runtime.
32
+ * Only replaces the path if it is a symlink (ours) or doesn't exist.
33
+ * Refuses to touch a real directory to avoid destroying user content.
34
+ */
35
+ function ensureRuntimeLink(projectDir) {
36
+ const linkDir = join(projectDir, "node_modules", "@trama-dev");
37
+ const linkPath = join(linkDir, "runtime");
38
+ const target = join(TRAMA_NODE_MODULES, "@trama-dev", "runtime");
39
+ try {
40
+ const stat = lstatSync(linkPath);
41
+ if (stat.isSymbolicLink()) {
42
+ // Ours — check if already correct
43
+ if (readlinkSync(linkPath) === target)
44
+ return;
45
+ rmSync(linkPath);
46
+ }
47
+ else {
48
+ // Real directory/file — not ours, refuse to destroy it
49
+ throw new Error(`${linkPath} exists and is not a symlink. ` +
50
+ `trama needs this path for module resolution. ` +
51
+ `Remove it manually or rename it to proceed.`);
52
+ }
53
+ }
54
+ catch (err) {
55
+ // Re-throw if it's our own error (not ENOENT)
56
+ if (err instanceof Error && !("code" in err))
57
+ throw err;
58
+ // ENOENT — path doesn't exist, proceed to create
59
+ }
60
+ mkdirSync(linkDir, { recursive: true });
61
+ symlinkSync(target, linkPath, "dir");
62
+ }
63
+ /**
64
+ * Ensure project has ESM package.json and .gitignore.
65
+ * If package.json exists but lacks "type": "module", patches it in.
66
+ */
67
+ function ensureProjectScaffold(projectDir) {
68
+ const pkgPath = join(projectDir, "package.json");
69
+ if (existsSync(pkgPath)) {
70
+ try {
71
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
72
+ if (pkg.type !== "module") {
73
+ pkg.type = "module";
74
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
75
+ }
76
+ }
77
+ catch (err) {
78
+ throw new Error(`Malformed package.json at ${pkgPath}. ` +
79
+ `trama will not overwrite it automatically; fix or remove it and try again.\n` +
80
+ `Parse error: ${err instanceof Error ? err.message : err}`);
81
+ }
82
+ }
83
+ else {
84
+ writeFileSync(pkgPath, JSON.stringify({ type: "module" }, null, 2));
85
+ }
86
+ const giPath = join(projectDir, ".gitignore");
87
+ const requiredEntries = ["node_modules/", "state.json", "logs/", "history/"];
88
+ if (existsSync(giPath)) {
89
+ const existing = readFileSync(giPath, "utf-8");
90
+ const missing = requiredEntries.filter(e => !existing.split("\n").some(line => line.trim() === e));
91
+ if (missing.length > 0) {
92
+ const suffix = existing.endsWith("\n") ? "" : "\n";
93
+ writeFileSync(giPath, existing + suffix + missing.join("\n") + "\n");
94
+ }
95
+ }
96
+ else {
97
+ writeFileSync(giPath, requiredEntries.join("\n") + "\n");
98
+ }
99
+ }
100
+ // --- Runtime type definitions (included in system prompts) ---
101
+ export const RUNTIME_TYPES = `// @trama-dev/runtime - Complete API
102
+
103
+ export interface Ctx {
104
+ input: { prompt: string; args: Record<string, unknown> };
105
+ state: Record<string, unknown>;
106
+ iteration: number;
107
+ maxIterations: number;
108
+ log(message: string, data?: Record<string, unknown>): Promise<void>;
109
+ checkpoint(): Promise<void>;
110
+ done(result?: Record<string, unknown>): Promise<void>;
111
+ }
112
+
113
+ export interface Agent {
114
+ ask(prompt: string, options?: { system?: string }): Promise<string>;
115
+ generate<T>(input: {
116
+ prompt: string;
117
+ schema: Record<string, string>;
118
+ system?: string;
119
+ }): Promise<T>;
120
+ }
121
+
122
+ export interface ShellResult {
123
+ exitCode: number;
124
+ stdout: string;
125
+ stderr: string;
126
+ }
127
+
128
+ export interface Tools {
129
+ read(path: string): Promise<string>;
130
+ write(path: string, content: string): Promise<void>;
131
+ shell(command: string, options?: { cwd?: string; timeout?: number }): Promise<ShellResult>;
132
+ fetch(url: string, options?: {
133
+ method?: string;
134
+ headers?: Record<string, string>;
135
+ body?: string;
136
+ }): Promise<{ status: number; body: string; headers: Record<string, string> }>;
137
+ }
138
+
139
+ export declare const ctx: Ctx;
140
+ export declare const agent: Agent;
141
+ export declare const tools: Tools;`;
142
+ // --- Config & state loaders ---
143
+ export function loadConfig() {
144
+ const configPath = join(homedir(), ".trama", "config.json");
145
+ const defaults = {
146
+ provider: "anthropic",
147
+ model: "claude-sonnet-4-20250514",
148
+ maxRepairAttempts: 3,
149
+ defaultTimeout: 300_000,
150
+ defaultMaxIterations: 100,
151
+ };
152
+ try {
153
+ return { ...defaults, ...JSON.parse(readFileSync(configPath, "utf-8")) };
154
+ }
155
+ catch {
156
+ return defaults;
157
+ }
158
+ }
159
+ export function loadState(projectDir) {
160
+ const statePath = join(projectDir, "state.json");
161
+ if (!existsSync(statePath))
162
+ return {};
163
+ const raw = readFileSync(statePath, "utf-8");
164
+ try {
165
+ return JSON.parse(raw);
166
+ }
167
+ catch (err) {
168
+ throw new Error(`Corrupt state.json in ${projectDir} — cannot safely continue.\n` +
169
+ `Parse error: ${err instanceof Error ? err.message : err}\n` +
170
+ `Back up or delete state.json to reset state.`);
171
+ }
172
+ }
173
+ // --- IPC Server ---
174
+ function createIPCServer(ctx, agentImpl, toolsImpl) {
175
+ return createServer((req, res) => {
176
+ if (req.method !== "POST") {
177
+ res.writeHead(405);
178
+ res.end("Method not allowed");
179
+ return;
180
+ }
181
+ let body = "";
182
+ req.on("data", (chunk) => { body += chunk; });
183
+ req.on("end", async () => {
184
+ try {
185
+ const data = JSON.parse(body);
186
+ let result;
187
+ switch (req.url) {
188
+ case "/agent/ask":
189
+ result = await agentImpl.ask(data.prompt, { system: data.system });
190
+ res.writeHead(200, { "Content-Type": "application/json" });
191
+ res.end(JSON.stringify({ result }));
192
+ break;
193
+ case "/agent/generate":
194
+ result = await agentImpl.generate(data);
195
+ res.writeHead(200, { "Content-Type": "application/json" });
196
+ res.end(JSON.stringify({ result }));
197
+ break;
198
+ case "/tools/read":
199
+ result = await toolsImpl.read(data.path);
200
+ res.writeHead(200, { "Content-Type": "application/json" });
201
+ res.end(JSON.stringify({ result }));
202
+ break;
203
+ case "/tools/write":
204
+ await toolsImpl.write(data.path, data.content);
205
+ res.writeHead(200, { "Content-Type": "application/json" });
206
+ res.end(JSON.stringify({ result: null }));
207
+ break;
208
+ case "/tools/shell":
209
+ result = await toolsImpl.shell(data.command, { cwd: data.cwd, timeout: data.timeout });
210
+ res.writeHead(200, { "Content-Type": "application/json" });
211
+ res.end(JSON.stringify(result));
212
+ break;
213
+ case "/tools/fetch":
214
+ result = await toolsImpl.fetch(data.url, {
215
+ method: data.method, headers: data.headers, body: data.body,
216
+ });
217
+ res.writeHead(200, { "Content-Type": "application/json" });
218
+ res.end(JSON.stringify(result));
219
+ break;
220
+ case "/ctx/checkpoint":
221
+ ctx.state = data.state;
222
+ await ctx.checkpoint();
223
+ res.writeHead(200, { "Content-Type": "application/json" });
224
+ res.end(JSON.stringify({ result: null }));
225
+ break;
226
+ case "/ctx/log":
227
+ await ctx.log(data.message, data.data);
228
+ res.writeHead(200, { "Content-Type": "application/json" });
229
+ res.end(JSON.stringify({ result: null }));
230
+ break;
231
+ case "/ctx/done":
232
+ ctx.state = data.state;
233
+ await ctx.done(data.result);
234
+ res.writeHead(200, { "Content-Type": "application/json" });
235
+ res.end(JSON.stringify({ result: null }));
236
+ break;
237
+ default:
238
+ res.writeHead(404);
239
+ res.end("Not found");
240
+ }
241
+ }
242
+ catch (err) {
243
+ res.writeHead(500);
244
+ res.end(String(err));
245
+ }
246
+ });
247
+ });
248
+ }
249
+ function startServer(server) {
250
+ return new Promise((resolve, reject) => {
251
+ server.listen(0, "127.0.0.1", () => {
252
+ const addr = server.address();
253
+ if (addr && typeof addr === "object") {
254
+ resolve(addr.port);
255
+ }
256
+ else {
257
+ reject(new Error("Failed to get server port"));
258
+ }
259
+ });
260
+ server.on("error", reject);
261
+ });
262
+ }
263
+ function closeServer(server) {
264
+ return new Promise((resolve, reject) => {
265
+ server.close(err => (err ? reject(err) : resolve()));
266
+ });
267
+ }
268
+ /** Kill orphan shell processes, drop all HTTP connections, then close. */
269
+ function forceCloseServer(server, toolsImpl) {
270
+ toolsImpl.killActiveShells();
271
+ server.closeAllConnections();
272
+ return closeServer(server);
273
+ }
274
+ // --- Child process management ---
275
+ /**
276
+ * Collect stdout/stderr as strings and wait for exit.
277
+ * Enforces wall-clock timeout via explicit setTimeout + kill.
278
+ */
279
+ function waitForChild(child, timeout, sinks) {
280
+ return new Promise((resolve, reject) => {
281
+ let stdout = "";
282
+ let stderr = "";
283
+ let timedOut = false;
284
+ let exited = false;
285
+ child.stdout?.on("data", (chunk) => {
286
+ const text = chunk.toString();
287
+ stdout += text;
288
+ sinks?.stdout?.(text);
289
+ });
290
+ child.stderr?.on("data", (chunk) => {
291
+ const text = chunk.toString();
292
+ stderr += text;
293
+ sinks?.stderr?.(text);
294
+ });
295
+ const timer = setTimeout(() => {
296
+ timedOut = true;
297
+ child.kill("SIGTERM");
298
+ setTimeout(() => {
299
+ if (!exited)
300
+ child.kill("SIGKILL");
301
+ }, 5000);
302
+ }, timeout);
303
+ child.on("error", (err) => {
304
+ clearTimeout(timer);
305
+ reject(err);
306
+ });
307
+ child.on("close", (code) => {
308
+ exited = true;
309
+ clearTimeout(timer);
310
+ if (timedOut) {
311
+ resolve({ exitCode: 1, stdout, stderr: stderr + "\n[trama] program killed: timeout" });
312
+ }
313
+ else {
314
+ resolve({ exitCode: code ?? 1, stdout, stderr });
315
+ }
316
+ });
317
+ });
318
+ }
319
+ // --- History management ---
320
+ /**
321
+ * Copy current program.ts to history/NNNN.ts and append to index.jsonl.
322
+ *
323
+ * WARNING: Do NOT use this for create-time repair attempts.
324
+ * Create-time repairs go to index.jsonl only (reason: "create-repair").
325
+ */
326
+ export function copyToHistory(projectDir, reason, detail) {
327
+ const historyDir = join(projectDir, "history");
328
+ const existing = readdirSync(historyDir).filter(f => /^\d{4}\.ts$/.test(f));
329
+ const nums = existing.map(f => parseInt(f.replace(".ts", ""), 10));
330
+ const nextNum = nums.length > 0 ? Math.max(...nums) + 1 : 1;
331
+ const next = String(nextNum).padStart(4, "0");
332
+ copyFileSync(join(projectDir, "program.ts"), join(historyDir, `${next}.ts`));
333
+ const entry = { version: nextNum, reason, timestamp: new Date().toISOString() };
334
+ if (reason === "repair" && detail)
335
+ entry.error = detail;
336
+ if (reason === "update" && detail)
337
+ entry.prompt = detail;
338
+ appendFileSync(join(historyDir, "index.jsonl"), JSON.stringify(entry) + "\n");
339
+ }
340
+ // --- Smoke run (used by create and update) ---
341
+ export async function smokeRunAndRepair(projectDir, adapter, reason) {
342
+ mkdirSync(join(projectDir, "history"), { recursive: true });
343
+ mkdirSync(join(projectDir, "logs"), { recursive: true });
344
+ ensureProjectScaffold(projectDir);
345
+ ensureRuntimeLink(projectDir);
346
+ const maxAttempts = 3;
347
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
348
+ const ctx = createContext(projectDir, loadState(projectDir));
349
+ const agentImpl = createAgent(adapter);
350
+ const toolsImpl = createTools(projectDir);
351
+ writeFileSync(join(projectDir, "logs", "latest.jsonl"), "");
352
+ const server = createIPCServer(ctx, agentImpl, toolsImpl);
353
+ const port = await startServer(server);
354
+ try {
355
+ const meta = JSON.parse(readFileSync(join(projectDir, "meta.json"), "utf-8"));
356
+ const child = spawn(process.execPath, [TSX_CLI, join(projectDir, "program.ts")], {
357
+ cwd: projectDir,
358
+ env: {
359
+ ...process.env,
360
+ TRAMA_PORT: String(port),
361
+ TRAMA_INPUT: JSON.stringify(meta.input),
362
+ TRAMA_STATE: JSON.stringify(ctx.state),
363
+ TRAMA_ITERATION: String(ctx.iteration),
364
+ TRAMA_MAX_ITERATIONS: String(ctx.maxIterations),
365
+ },
366
+ stdio: ["pipe", "pipe", "pipe"],
367
+ });
368
+ const result = await waitForChild(child, 5000, {
369
+ stdout: chunk => { process.stdout.write(chunk); },
370
+ });
371
+ if (result.exitCode === 0) {
372
+ await closeServer(server);
373
+ return;
374
+ }
375
+ throw new Error(`Smoke run failed (exit ${result.exitCode})\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
376
+ }
377
+ catch (error) {
378
+ if (attempt === maxAttempts) {
379
+ await forceCloseServer(server, toolsImpl);
380
+ throw new Error(`Smoke run failed after ${maxAttempts} repair attempts: ${error}`);
381
+ }
382
+ appendFileSync(join(projectDir, "history", "index.jsonl"), JSON.stringify({
383
+ reason: `${reason}-repair`,
384
+ timestamp: new Date().toISOString(),
385
+ error: String(error),
386
+ }) + "\n");
387
+ const source = readFileSync(join(projectDir, "program.ts"), "utf-8");
388
+ const fixed = await adapter.repair({
389
+ programSource: source,
390
+ error: String(error),
391
+ runtimeTypes: RUNTIME_TYPES,
392
+ });
393
+ writeFileSync(join(projectDir, "program.ts"), fixed);
394
+ await forceCloseServer(server, toolsImpl);
395
+ }
396
+ }
397
+ }
398
+ // --- Core execution ---
399
+ export async function runProgram(options) {
400
+ const { projectDir } = options;
401
+ if (!existsSync(join(projectDir, "meta.json"))) {
402
+ throw new Error(`Project not found at ${projectDir}. ` +
403
+ `Run \`trama list\` to see available projects.`);
404
+ }
405
+ const config = loadConfig();
406
+ const maxRepairAttempts = options.maxRepairAttempts ?? config.maxRepairAttempts;
407
+ const timeout = options.timeout ?? config.defaultTimeout;
408
+ const maxIterations = config.defaultMaxIterations;
409
+ if (!Number.isFinite(timeout) || timeout <= 0) {
410
+ throw new Error(`Invalid timeout: ${timeout}. Must be a positive number.`);
411
+ }
412
+ if (!Number.isInteger(maxRepairAttempts) || maxRepairAttempts < 0) {
413
+ throw new Error(`Invalid maxRepairAttempts: ${maxRepairAttempts}. Must be a non-negative integer.`);
414
+ }
415
+ mkdirSync(join(projectDir, "history"), { recursive: true });
416
+ mkdirSync(join(projectDir, "logs"), { recursive: true });
417
+ ensureProjectScaffold(projectDir);
418
+ ensureRuntimeLink(projectDir);
419
+ const adapter = new PiAdapter(config, projectDir);
420
+ for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {
421
+ const ctx = createContext(projectDir, loadState(projectDir), { maxIterations });
422
+ const agentImpl = createAgent(adapter);
423
+ const toolsImpl = createTools(projectDir);
424
+ const logsPath = join(projectDir, "logs", "latest.jsonl");
425
+ writeFileSync(logsPath, "");
426
+ const server = createIPCServer(ctx, agentImpl, toolsImpl);
427
+ const port = await startServer(server);
428
+ try {
429
+ const meta = JSON.parse(readFileSync(join(projectDir, "meta.json"), "utf-8"));
430
+ const child = spawn(process.execPath, [TSX_CLI, join(projectDir, "program.ts")], {
431
+ cwd: projectDir,
432
+ env: {
433
+ ...process.env,
434
+ TRAMA_PORT: String(port),
435
+ TRAMA_INPUT: JSON.stringify(meta.input),
436
+ TRAMA_STATE: JSON.stringify(ctx.state),
437
+ TRAMA_ITERATION: String(ctx.iteration),
438
+ TRAMA_MAX_ITERATIONS: String(ctx.maxIterations),
439
+ },
440
+ stdio: ["pipe", "pipe", "pipe"],
441
+ });
442
+ const result = await waitForChild(child, timeout, {
443
+ stdout: chunk => { process.stdout.write(chunk); },
444
+ });
445
+ if (result.exitCode === 0) {
446
+ await closeServer(server);
447
+ return;
448
+ }
449
+ throw new Error(`program.ts exited with code ${result.exitCode}\n` +
450
+ `stdout: ${result.stdout}\n` +
451
+ `stderr: ${result.stderr}`);
452
+ }
453
+ catch (error) {
454
+ if (attempt === maxRepairAttempts) {
455
+ await forceCloseServer(server, toolsImpl);
456
+ throw new Error(`Failed after ${maxRepairAttempts} repair attempts: ${error}`);
457
+ }
458
+ await ctx.log(`Repair attempt ${attempt + 1}/${maxRepairAttempts}`, { error: String(error) });
459
+ const source = readFileSync(join(projectDir, "program.ts"), "utf-8");
460
+ const fixed = await adapter.repair({
461
+ programSource: source,
462
+ error: String(error),
463
+ runtimeTypes: RUNTIME_TYPES,
464
+ });
465
+ writeFileSync(join(projectDir, "program.ts"), fixed);
466
+ copyToHistory(projectDir, "repair", String(error));
467
+ await forceCloseServer(server, toolsImpl);
468
+ }
469
+ }
470
+ }
@@ -0,0 +1,6 @@
1
+ import type { Tools } from "./types.js";
2
+ export interface ToolsWithCleanup extends Tools {
3
+ /** Kill all in-flight shell processes. Called by runner on timeout/error. */
4
+ killActiveShells(): void;
5
+ }
6
+ export declare function createTools(projectDir: string): ToolsWithCleanup;
package/dist/tools.js ADDED
@@ -0,0 +1,84 @@
1
+ import { spawn } from "child_process";
2
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import { resolve, dirname, sep } from "path";
4
+ export function createTools(projectDir) {
5
+ const normalizedProjectDir = resolve(projectDir);
6
+ const activeShells = new Set();
7
+ /**
8
+ * Path traversal guard. Not a security sandbox — just a footgun guard.
9
+ * Uses normalizedProjectDir + sep to prevent /tmp/foo-bar matching /tmp/foo.
10
+ */
11
+ const resolvePath = (p) => {
12
+ const resolved = resolve(normalizedProjectDir, p);
13
+ if (resolved !== normalizedProjectDir && !resolved.startsWith(normalizedProjectDir + sep)) {
14
+ throw new Error(`Path escapes project directory: ${p}`);
15
+ }
16
+ return resolved;
17
+ };
18
+ return {
19
+ killActiveShells() {
20
+ for (const proc of activeShells) {
21
+ proc.kill("SIGKILL");
22
+ }
23
+ activeShells.clear();
24
+ },
25
+ async read(path) {
26
+ return readFileSync(resolvePath(path), "utf-8");
27
+ },
28
+ async write(path, content) {
29
+ const target = resolvePath(path);
30
+ mkdirSync(dirname(target), { recursive: true });
31
+ writeFileSync(target, content);
32
+ },
33
+ async shell(command, options = {}) {
34
+ const timeout = options.timeout ?? 30_000;
35
+ const cwd = options.cwd ? resolvePath(options.cwd) : normalizedProjectDir;
36
+ return new Promise((resolve) => {
37
+ const child = spawn("sh", ["-c", command], {
38
+ cwd,
39
+ stdio: ["pipe", "pipe", "pipe"],
40
+ });
41
+ activeShells.add(child);
42
+ let stdout = "";
43
+ let stderr = "";
44
+ let timedOut = false;
45
+ let exited = false;
46
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
47
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
48
+ child.on("error", (err) => {
49
+ activeShells.delete(child);
50
+ resolve({ exitCode: 1, stdout, stderr: err.message });
51
+ });
52
+ const timer = setTimeout(() => {
53
+ timedOut = true;
54
+ child.kill("SIGTERM");
55
+ setTimeout(() => {
56
+ if (!exited)
57
+ child.kill("SIGKILL");
58
+ }, 5000);
59
+ }, timeout);
60
+ child.on("close", (code) => {
61
+ exited = true;
62
+ activeShells.delete(child);
63
+ clearTimeout(timer);
64
+ resolve({
65
+ exitCode: timedOut ? 1 : (code ?? 1),
66
+ stdout,
67
+ stderr: timedOut ? stderr + "\ntimeout" : stderr,
68
+ });
69
+ });
70
+ });
71
+ },
72
+ async fetch(url, options = {}) {
73
+ const response = await globalThis.fetch(url, {
74
+ method: options.method ?? "GET",
75
+ headers: options.headers,
76
+ body: options.body,
77
+ });
78
+ const body = await response.text();
79
+ const headers = {};
80
+ response.headers.forEach((v, k) => { headers[k] = v; });
81
+ return { status: response.status, body, headers };
82
+ }
83
+ };
84
+ }
@@ -0,0 +1,99 @@
1
+ export interface Ctx {
2
+ /** Original user prompt and any arguments */
3
+ input: {
4
+ prompt: string;
5
+ args: Record<string, unknown>;
6
+ };
7
+ /**
8
+ * Persistent state. Must be strictly JSON-serializable.
9
+ * Local working copy — only checkpoint() and done() persist to disk.
10
+ * Reserved keys: any key starting with `__trama_` is reserved.
11
+ */
12
+ state: Record<string, unknown>;
13
+ /**
14
+ * Current iteration counter (starts at 0).
15
+ * Advances only when checkpoint() or done() is called.
16
+ */
17
+ iteration: number;
18
+ /** Max iterations hint. Not enforced by runtime. */
19
+ maxIterations: number;
20
+ /** Write a structured log entry */
21
+ log(message: string, data?: Record<string, unknown>): Promise<void>;
22
+ /** Persist current ctx.state to disk */
23
+ checkpoint(): Promise<void>;
24
+ /**
25
+ * Signal program completion. Persists state and logs result.
26
+ * Does NOT terminate execution — program should return naturally.
27
+ * Idempotent: second call is a no-op.
28
+ */
29
+ done(result?: Record<string, unknown>): Promise<void>;
30
+ }
31
+ export interface Agent {
32
+ /**
33
+ * LLM call via pi-coding-agent. Returns plain text.
34
+ * The underlying pi agent may use its built-in tools autonomously.
35
+ */
36
+ ask(prompt: string, options?: {
37
+ system?: string;
38
+ }): Promise<string>;
39
+ /**
40
+ * Structured LLM call. Returns typed object matching the provided schema.
41
+ * Supported schema value prefixes: "string", "number", "boolean".
42
+ * Retries once on parse/validation failure.
43
+ */
44
+ generate<T>(input: {
45
+ prompt: string;
46
+ schema: Record<string, string>;
47
+ system?: string;
48
+ }): Promise<T>;
49
+ }
50
+ export interface ShellResult {
51
+ exitCode: number;
52
+ stdout: string;
53
+ stderr: string;
54
+ }
55
+ export interface Tools {
56
+ /** Read a file. Path is relative to project dir. */
57
+ read(path: string): Promise<string>;
58
+ /** Write a file. Path is relative to project dir. */
59
+ write(path: string, content: string): Promise<void>;
60
+ /** Execute a shell command. Returns structured result. */
61
+ shell(command: string, options?: {
62
+ cwd?: string;
63
+ timeout?: number;
64
+ }): Promise<ShellResult>;
65
+ /** HTTP fetch. Returns response body as string. */
66
+ fetch(url: string, options?: {
67
+ method?: string;
68
+ headers?: Record<string, string>;
69
+ body?: string;
70
+ }): Promise<{
71
+ status: number;
72
+ body: string;
73
+ headers: Record<string, string>;
74
+ }>;
75
+ }
76
+ export interface PiAdapterConfig {
77
+ provider: string;
78
+ model: string;
79
+ }
80
+ export interface RunOptions {
81
+ projectDir: string;
82
+ maxRepairAttempts?: number;
83
+ timeout?: number;
84
+ }
85
+ export interface RepairInput {
86
+ programSource: string;
87
+ error: string;
88
+ runtimeTypes: string;
89
+ }
90
+ export interface ChildResult {
91
+ exitCode: number;
92
+ stdout: string;
93
+ stderr: string;
94
+ }
95
+ export interface TramaConfig extends PiAdapterConfig {
96
+ maxRepairAttempts: number;
97
+ defaultTimeout: number;
98
+ defaultMaxIterations: number;
99
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // @trama-dev/runtime - All shared types
2
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@trama-dev/runtime",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/client.d.ts",
8
+ "import": "./dist/client.js"
9
+ },
10
+ "./runner": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "description": "Runtime and client library for trama — an agentic runtime for agent-authored TypeScript programs",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/NaNhkNaN/trama.git",
26
+ "directory": "packages/runtime"
27
+ },
28
+ "homepage": "https://github.com/NaNhkNaN/trama",
29
+ "bugs": "https://github.com/NaNhkNaN/trama/issues",
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "dependencies": {
36
+ "@mariozechner/pi-coding-agent": "0.63.1",
37
+ "@mariozechner/pi-ai": "0.63.1",
38
+ "tsx": "^4.0.0"
39
+ }
40
+ }