From 7cadc6cc849006a05803ea54dc387f93a82dce7d Mon Sep 17 00:00:00 2001 From: Travis Nesland Date: Fri, 12 Jun 2026 16:39:16 -0400 Subject: [PATCH] feat(runtime): add local runtime package --- bun.lock | 14 ++ package.json | 6 +- packages/agentic-runtime-local/README.md | 27 ++++ packages/agentic-runtime-local/package.json | 44 ++++++ .../agentic-runtime-local/src/index.test.ts | 103 ++++++++++++ packages/agentic-runtime-local/src/index.ts | 148 ++++++++++++++++++ .../agentic-runtime-local/tsconfig.build.json | 20 +++ packages/agentic-runtime-local/tsconfig.json | 11 ++ 8 files changed, 370 insertions(+), 3 deletions(-) create mode 100644 packages/agentic-runtime-local/README.md create mode 100644 packages/agentic-runtime-local/package.json create mode 100644 packages/agentic-runtime-local/src/index.test.ts create mode 100644 packages/agentic-runtime-local/src/index.ts create mode 100644 packages/agentic-runtime-local/tsconfig.build.json create mode 100644 packages/agentic-runtime-local/tsconfig.json diff --git a/bun.lock b/bun.lock index dd2fbc9..d55e339 100644 --- a/bun.lock +++ b/bun.lock @@ -18,12 +18,26 @@ "typescript": "^5.7.0", }, }, + "packages/agentic-runtime-local": { + "name": "@tnezdev/agentic-runtime-local", + "version": "0.5.0", + "devDependencies": { + "@tnezdev/agentic": "^0.5.0", + "@types/bun": "^1.3.11", + "typescript": "^5.7.0", + }, + "peerDependencies": { + "@tnezdev/agentic": "^0.5.0", + }, + }, }, "packages": { "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260426.1", "", {}, "sha512-cBYeQaWwv/jFV8ualmwp6wIxmAf0rDe2DPPQwPbslKmPHqgv861YpAvm45r05K40QboZgxNQVIPgNkmtHqZeJQ=="], "@tnezdev/agentic": ["@tnezdev/agentic@workspace:packages/agentic"], + "@tnezdev/agentic-runtime-local": ["@tnezdev/agentic-runtime-local@workspace:packages/agentic-runtime-local"], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], diff --git a/package.json b/package.json index 7af2192..9e38f39 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "packages/*" ], "scripts": { - "build": "bun run --cwd packages/agentic build", - "typecheck": "bun run --cwd packages/agentic typecheck", - "test": "bun run --cwd packages/agentic test" + "build": "bun run --cwd packages/agentic build && bun run --cwd packages/agentic-runtime-local build", + "typecheck": "bun run --cwd packages/agentic typecheck && bun run --cwd packages/agentic-runtime-local typecheck", + "test": "bun run --cwd packages/agentic test && bun run --cwd packages/agentic-runtime-local test" } } diff --git a/packages/agentic-runtime-local/README.md b/packages/agentic-runtime-local/README.md new file mode 100644 index 0000000..53a6b3e --- /dev/null +++ b/packages/agentic-runtime-local/README.md @@ -0,0 +1,27 @@ +# @tnezdev/agentic-runtime-local + +Local runtime package for Agentic workspaces. + +The public runtime target is `local`: + +```bash +agentic runtime add local +agentic runtime init local +agentic runtime status local +``` + +This package is intentionally thin. It exports the Agentic runtime manifest, +creates local runtime glue, and provides placeholder `run` behavior while real +execution is split into later runtime work. + +Pi may power this runtime under the hood in the future. In this project, Pi +means https://pi.dev/, not Raspberry Pi. `pi` is not a public runtime target. + +## Initial Scope + +- `init` creates `.agentic/runtime/local/runtime.json` and a local `targets/` directory. +- `run` returns a placeholder result without executing a harness. +- `status` reports whether the local runtime glue has been initialized. + +Non-goals for this skeleton: daemon/service mode, scheduling, Cloudflare code, +and a session primitive. diff --git a/packages/agentic-runtime-local/package.json b/packages/agentic-runtime-local/package.json new file mode 100644 index 0000000..35bd32f --- /dev/null +++ b/packages/agentic-runtime-local/package.json @@ -0,0 +1,44 @@ +{ + "name": "@tnezdev/agentic-runtime-local", + "version": "0.5.0", + "description": "Local runtime package for Agentic workspaces", + "license": "MIT", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/tnezdev/agentic.git", + "directory": "packages/agentic-runtime-local" + }, + "publishConfig": { + "access": "public" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "dist/**/*.map", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit -p tsconfig.json", + "test": "bun test", + "prepack": "bun run build" + }, + "dependencies": {}, + "peerDependencies": { + "@tnezdev/agentic": "^0.5.0" + }, + "devDependencies": { + "@tnezdev/agentic": "^0.5.0", + "@types/bun": "^1.3.11", + "typescript": "^5.7.0" + } +} diff --git a/packages/agentic-runtime-local/src/index.test.ts b/packages/agentic-runtime-local/src/index.test.ts new file mode 100644 index 0000000..913e675 --- /dev/null +++ b/packages/agentic-runtime-local/src/index.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect, beforeEach, afterEach } from "bun:test" +import { mkdtemp, readFile, rm } from "node:fs/promises" +import { join } from "node:path" +import { tmpdir } from "node:os" +import type { RuntimeContext } from "@tnezdev/agentic/runtime" +import { runtime } from "./index.js" + +describe("local runtime package", () => { + let tmpDir: string + let ctx: RuntimeContext + + beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), "agentic-runtime-local-test-")) + ctx = { + cwd: tmpDir, + workspace_root: tmpDir, + runtime_name: "local", + runtime_package: "@tnezdev/agentic-runtime-local", + json: true, + env: {}, + config: { + adapter: "filesystem", + memory: { + dir: ".agentic/memory", + defaultTier: "L1", + dreamDepth: 3, + }, + workflow: { + graphsDir: ".agentic/workflows", + runsDir: ".agentic/runs", + }, + wake: {}, + runtime: { + targets: {}, + }, + }, + runtime_config: {}, + agentic: {} as RuntimeContext["agentic"], + } + }) + + afterEach(async () => { + await rm(tmpDir, { recursive: true }) + }) + + it("exports a valid local runtime manifest", () => { + expect(runtime.kind).toBe("agentic-runtime") + expect(runtime.api_version).toBe(1) + expect(runtime.name).toBe("local") + expect(runtime.package_name).toBe("@tnezdev/agentic-runtime-local") + expect(runtime.capabilities).toEqual(["init", "run", "status"]) + expect(runtime.commands.init).toBeFunction() + expect(runtime.commands.run).toBeFunction() + expect(runtime.commands.status).toBeFunction() + }) + + it("initializes local runtime glue idempotently", async () => { + const first = await runtime.commands.init!(ctx, { args: [], flags: {} }) + const second = await runtime.commands.init!(ctx, { args: [], flags: {} }) + + expect(first?.summary).toContain("Initialized") + expect(second?.summary).toContain("already initialized") + + const state = JSON.parse( + await readFile(join(tmpDir, ".agentic", "runtime", "local", "runtime.json"), "utf-8"), + ) + expect(state).toEqual({ + version: 1, + runtime: "local", + package_name: "@tnezdev/agentic-runtime-local", + targets_dir: "targets", + }) + }) + + it("reports status before and after init", async () => { + const before = await runtime.commands.status!(ctx, { args: [], flags: {} }) + expect(before?.data).toMatchObject({ initialized: false }) + + await runtime.commands.init!(ctx, { args: [], flags: {} }) + const after = await runtime.commands.status!(ctx, { args: [], flags: {} }) + + expect(after?.summary).toContain("initialized") + expect(after?.data).toMatchObject({ + initialized: true, + targets_dir_exists: true, + }) + }) + + it("returns a placeholder run result", async () => { + const result = await runtime.commands.run!(ctx, { + target: "examples/second-brain", + args: ["--dry-run"], + flags: {}, + }) + + expect(result?.summary).toContain("not implemented yet") + expect(result?.data).toMatchObject({ + target: "examples/second-brain", + args: ["--dry-run"], + initialized: false, + }) + }) +}) diff --git a/packages/agentic-runtime-local/src/index.ts b/packages/agentic-runtime-local/src/index.ts new file mode 100644 index 0000000..b711c0c --- /dev/null +++ b/packages/agentic-runtime-local/src/index.ts @@ -0,0 +1,148 @@ +import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises" +import { join, relative } from "node:path" +import type { + AgenticRuntimePackage, + RuntimeCommandResult, + RuntimeContext, + RuntimeInitArgs, + RuntimeRunArgs, + RuntimeStatusArgs, +} from "@tnezdev/agentic/runtime" + +const RUNTIME_NAME = "local" +const PACKAGE_NAME = "@tnezdev/agentic-runtime-local" +const STATE_VERSION = 1 +const RUNTIME_DIR = join(".agentic", "runtime", RUNTIME_NAME) +const TARGETS_DIR = "targets" +const STATE_FILE = "runtime.json" + +type LocalRuntimeState = { + version: typeof STATE_VERSION + runtime: typeof RUNTIME_NAME + package_name: typeof PACKAGE_NAME + targets_dir: typeof TARGETS_DIR +} + +function runtimeDir(ctx: RuntimeContext): string { + return join(ctx.workspace_root, RUNTIME_DIR) +} + +function targetsDir(ctx: RuntimeContext): string { + return join(runtimeDir(ctx), TARGETS_DIR) +} + +function statePath(ctx: RuntimeContext): string { + return join(runtimeDir(ctx), STATE_FILE) +} + +function workspaceRelative(ctx: RuntimeContext, path: string): string { + return relative(ctx.workspace_root, path) || "." +} + +async function pathExists(path: string): Promise { + try { + await access(path) + return true + } catch { + return false + } +} + +async function dirExists(path: string): Promise { + try { + return (await stat(path)).isDirectory() + } catch { + return false + } +} + +async function readState(ctx: RuntimeContext): Promise { + try { + return JSON.parse(await readFile(statePath(ctx), "utf-8")) as LocalRuntimeState + } catch { + return undefined + } +} + +async function initLocalRuntime( + ctx: RuntimeContext, + _args: RuntimeInitArgs, +): Promise { + const dir = runtimeDir(ctx) + const targetDir = targetsDir(ctx) + const path = statePath(ctx) + await mkdir(targetDir, { recursive: true }) + + const existing = await readState(ctx) + const created = existing === undefined + if (created) { + const state: LocalRuntimeState = { + version: STATE_VERSION, + runtime: RUNTIME_NAME, + package_name: PACKAGE_NAME, + targets_dir: TARGETS_DIR, + } + await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, "utf-8") + } + + return { + summary: created + ? "Initialized local Agentic runtime glue." + : "Local Agentic runtime glue is already initialized.", + data: { + initialized: true, + created, + config_dir: workspaceRelative(ctx, dir), + state_path: workspaceRelative(ctx, path), + targets_dir: workspaceRelative(ctx, targetDir), + }, + } +} + +async function runLocalRuntime( + ctx: RuntimeContext, + args: RuntimeRunArgs, +): Promise { + return { + summary: "Local runtime execution is not implemented yet.", + data: { + target: args.target ?? null, + args: args.args, + initialized: await pathExists(statePath(ctx)), + }, + } +} + +async function statusLocalRuntime( + ctx: RuntimeContext, + _args: RuntimeStatusArgs, +): Promise { + const initialized = await pathExists(statePath(ctx)) + return { + summary: initialized + ? "Local Agentic runtime glue is initialized." + : "Local runtime package is installed; run `agentic runtime init local` to create local glue.", + data: { + initialized, + state_path: workspaceRelative(ctx, statePath(ctx)), + targets_dir: workspaceRelative(ctx, targetsDir(ctx)), + targets_dir_exists: await dirExists(targetsDir(ctx)), + }, + } +} + +export const runtime: AgenticRuntimePackage = { + kind: "agentic-runtime", + api_version: 1, + name: RUNTIME_NAME, + package_name: PACKAGE_NAME, + description: "Run Agentic workspaces on the local machine.", + capabilities: ["init", "run", "status"], + commands: { + init: initLocalRuntime, + run: runLocalRuntime, + status: statusLocalRuntime, + }, +} + +export default runtime diff --git a/packages/agentic-runtime-local/tsconfig.build.json b/packages/agentic-runtime-local/tsconfig.build.json new file mode 100644 index 0000000..0ff6a1a --- /dev/null +++ b/packages/agentic-runtime-local/tsconfig.build.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "allowImportingTsExtensions": false, + "paths": { + "@tnezdev/agentic/runtime": ["../agentic/dist/runtime.d.ts"], + "@tnezdev/agentic": ["../agentic/dist/index.d.ts"] + } + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/agentic-runtime-local/tsconfig.json b/packages/agentic-runtime-local/tsconfig.json new file mode 100644 index 0000000..111b794 --- /dev/null +++ b/packages/agentic-runtime-local/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@tnezdev/agentic/runtime": ["../agentic/src/runtime.ts"], + "@tnezdev/agentic": ["../agentic/src/index.ts"] + } + }, + "include": ["src/**/*"] +}