Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,43 @@ jobs:
working-directory: ${{ matrix.package }}
run: ${{ matrix.command }}

prime-runtime-test:
name: Prime runtime tests
needs: trust
if: needs.trust.outputs.allowed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22

- name: Install uv
run: |
python3 -m pip install --user uv
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Test
run: npm run test:prime-runtime

build-check-test:
name: build-check-test
if: always() && needs.trust.outputs.allowed == 'true'
needs: [trust, build-check, test]
needs: [trust, build-check, test, prime-runtime-test]
runs-on: ubuntu-latest
steps:
- name: Verify CI results
env:
BUILD_CHECK_RESULT: ${{ needs.build-check.result }}
TEST_RESULT: ${{ needs.test.result }}
PRIME_RUNTIME_TEST_RESULT: ${{ needs.prime-runtime-test.result }}
run: |
test "$BUILD_CHECK_RESULT" = success
test "$TEST_RESULT" = success
test "$PRIME_RUNTIME_TEST_RESULT" = success
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ plans/
.prime/agent/
collect.sh
.kernel-venv/
.venv/
.worktrees/

__pycache__/
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
"build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build",
"dev": "concurrently --names \"ai,agent,coding-agent,tui\" --prefix-colors \"cyan,yellow,red,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/tui && npm run dev\"",
"dev:tsc": "cd packages/ai && npm run dev:tsc",
"check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:installer && npm run check:browser-smoke",
"check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:installer && npm run check:browser-smoke && npm run check:prime-runtime",
"check:installer": "node scripts/check-installer.mjs",
"check:browser-smoke": "node scripts/check-browser-smoke.mjs",
"check:prime-runtime": "PYTHONPATH=prime-agent-runtime/src python3 -m unittest discover -s prime-agent-runtime/test -p test_inspection.py",
"test:prime-runtime": "uv run --locked --isolated --project prime-agent-runtime --group test python -m unittest discover -s prime-agent-runtime/test -p test_*.py",
"profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui",
"profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc",
"test": "npm run test --workspaces --if-present",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added a durable recursive task graph with scoped delegation plans, inherited handoffs, trusted shared evidence, historical claim coverage, and a graph-wide token budget.
23 changes: 22 additions & 1 deletion packages/coding-agent/docs/rlm-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,31 @@ handle = await rlm("inspect the API", name="api-reviewer")
print(handle.rlm_child_id, handle.name, handle.session_dir, handle.model)
```

the call travels as a `host_request` event over the runtime's stdio protocol. `ReplKernelManager` dispatches request type `rlm.run` to the parent `AgentSession`, which starts a child through the same TypeScript agent machinery as the parent. The call returns over the same bridge immediately after task admission with a child handle; it never waits for or returns the child's answer. Results arrive only through explicit `agent_message` replies or files.
the call travels as a `host_request` event over the runtime's stdio protocol. `ReplKernelManager` dispatches request type `rlm.run` to the parent `AgentSession`, which starts a child through the same TypeScript agent machinery as the parent. The call returns over the same bridge immediately after task admission with a child handle; it never waits for or returns the child's answer. Uncontracted results arrive through explicit `agent_message` replies or files; `rlm.delegate` results and handoffs are also retained in the durable task graph.

The same bridge supports other typed host requests. Bundled Python skills such as `goal` call `rlm.host_request("goal.get", ...)`; state and policy remain in the TypeScript host.

## Durable Task Coordination

Hosts can attach an `AgentTaskGraph` to a session and use `rlm.delegate(prompt, task)` to atomically transfer exclusive claims to a child. A delegated owner inspects its bounded context with `rlm.task.current()` and records a plan before exploration:

```python
await rlm.task.plan(
"coordinator",
"The owned scope has independent queue and transport boundaries",
boundaries=["queue", "transport"],
expected_evidence=["pinned repository reads", "focused verification"],
)
```

Leaf tasks work directly. Coordinator tasks recursively delegate disjoint claims to grandchildren, wait for descendants, and synthesize their results. Every attempt has a durable handoff; agents can update it explicitly with `rlm.task.handoff({...})`, while completion, replacement, interruption, and cancellation create or finalize one atomically before claims move.

Task evidence is content-addressed and deduplicated across the run. Host integrations preflight claimed inspection scope with `assertEvidenceScopeAvailable()` and call `recordEvidence()` after trusted reads or verification, so convergence tracks only novel host-recorded evidence rather than model-authored reference strings. Evidence already recorded for a transferred claim is injected into its new owner's context. Context envelopes also include predecessor and descendant handoffs, not raw sibling transcripts. This keeps completed grandchild work visible to a supervising ancestor even if an intermediate coordinator fails before synthesis.

Delegation contracts may include a stable `contractKey` as a behavioral label, but it is not an authority boundary. Once any terminal task in the graph has covered an exclusive claim, the graph rejects another delegation of that claim regardless of parent or model-selected key. A narrower successor must identify the latest terminal `predecessorTaskId`, provide a `repeatReason`, and own only claims that predecessor's handoff marked as remaining.

Hosts may configure `maxTotalTokens` as one budget shared by the root and every descendant. Usage is already attributed to tasks, so new delegation and exploration stop when the graph-wide budget is exhausted; a configured convergence policy steers an active child into bounded finalization. The context envelope exposes used and remaining shared budget.

## Delegation Flow

```mermaid
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
"build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets && npm run bundle",
"build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && npm run copy-binary-assets",
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx rm -rf dist/prime-agent-runtime && shx cp -r ../../prime-agent-runtime dist/prime-agent-runtime && shx rm -rf dist/skills && shx cp -r skills dist/skills",
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && node scripts/copy-prime-runtime-assets.mjs && shx rm -rf dist/skills && shx cp -r skills dist/skills",
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/ && shx rm -rf dist/skills && shx cp -r skills dist/skills",
"test": "vitest --run",
"test:ci": "tsx src/core/kernel/bootstrap-cli.ts && vitest --run --exclude test/daemon-supervisor-process.test.ts",
Expand Down
32 changes: 32 additions & 0 deletions packages/coding-agent/scripts/copy-prime-runtime-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { cpSync, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
import { dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";

const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const sourceRoot = resolve(process.argv[2] ?? join(scriptDirectory, "..", "..", "..", "prime-agent-runtime"));
const destinationRoot = resolve(process.argv[3] ?? join(scriptDirectory, "..", "dist", "prime-agent-runtime"));
const excludedNames = new Set([".venv", "__pycache__"]);

function requirePath(path, kind) {
if (!existsSync(path) || (kind === "directory" ? !statSync(path).isDirectory() : !statSync(path).isFile())) {
throw new Error(`Prime runtime ${kind} is missing: ${path}`);
}
}

function shouldCopy(source) {
const segments = relative(sourceRoot, source).split(sep);
return !segments.some((segment) => excludedNames.has(segment)) && !source.endsWith(".pyc");
}

const pyproject = join(sourceRoot, "pyproject.toml");
const sourceDirectory = join(sourceRoot, "src");
requirePath(pyproject, "file");
requirePath(sourceDirectory, "directory");

rmSync(destinationRoot, { force: true, recursive: true });
mkdirSync(destinationRoot, { recursive: true });
cpSync(pyproject, join(destinationRoot, "pyproject.toml"));
cpSync(sourceDirectory, join(destinationRoot, "src"), {
filter: shouldCopy,
recursive: true,
});
21 changes: 21 additions & 0 deletions packages/coding-agent/src/core/agent-session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { assertSessionCwdExists } from "./session-cwd.js";
import { SessionImportFileNotFoundError } from "./session-import-errors.js";
import { acquireSessionLease, canonicalSessionPath, type SessionLease } from "./session-lease.js";
import { SessionManager } from "./session-manager.js";
import type { AgentTaskResumeDispatch } from "./task-graph.js";

export { SessionImportFileNotFoundError } from "./session-import-errors.js";

Expand Down Expand Up @@ -43,6 +44,8 @@ export interface AgentSessionRuntimeMetadata {
parentSessionFile?: string;
rlmChildId?: string;
rlmParentNodeId?: string;
taskId?: string;
/** Runtime restored from an already-persisted completed registry entry. */
rehydratedCompleted?: boolean;
prompt?: string;
spawnCode?: string;
Expand Down Expand Up @@ -345,6 +348,11 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
rlmParentAgent: options.parentSession.sessionName ?? options.parentSession.sessionId,
semanticParentSessionId: options.parentSession.sessionId,
semanticSpawnedByRequestId: options.spawnedByRequestId,
taskGraph: options.parentSession.taskGraph,
taskId: options.taskId,
taskAccountingTaskId: options.taskAccountingTaskId,
taskActorId: options.taskActorId,
turnCapacityPool: options.turnCapacityPool,
},
runtimeMetadata: {
kind: "subagent",
Expand All @@ -353,6 +361,7 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
parentSessionFile: options.parentSession.sessionFile,
rlmChildId: options.id,
rlmParentNodeId: options.rlmParentNodeId,
taskId: options.taskId,
prompt: options.prompt,
spawnCode: options.spawnCode,
sessionDir: options.sessionDir,
Expand All @@ -377,6 +386,18 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
return runtime;
}

async resumeTaskOwner(request: AgentTaskResumeDispatch): Promise<"admitted" | "owner_unavailable"> {
for (const runtime of this.subagentRuntimes.values()) {
if (runtime.metadata.taskId === request.taskId) {
const direct = await runtime.session.admitTaskResume(request);
if (direct === "admitted") return direct;
}
const nested = await runtime.resumeTaskOwner(request);
if (nested === "admitted") return nested;
}
return "owner_unavailable";
}

async deleteRlmSubagentRuntime(childId: string, session: AgentSession): Promise<void> {
const runtime = this.subagentRuntimes.get(childId);
if (!runtime) {
Expand Down
18 changes: 17 additions & 1 deletion packages/coding-agent/src/core/agent-session-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import type { SessionStartEvent, ToolDefinition } from "./extensions/index.js";
import { McpManager } from "./mcp/mcp-manager.js";
import { ModelRegistry } from "./model-registry.js";
import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from "./resource-loader.js";
import type { SubagentRuntimeHost } from "./rlm-runtime.js";
import type { RlmSubagentCapacityPool, SubagentRuntimeHost } from "./rlm-runtime.js";
import { type CreateAgentSessionResult, createAgentSession } from "./sdk.js";
import { semanticEdgeLedgerPath } from "./semantic-edges.js";
import type { SessionManager } from "./session-manager.js";
import { SettingsManager } from "./settings-manager.js";
import type { AgentTaskGraph } from "./task-graph.js";
import { installAgentTelemetry, isTelemetryEnabled } from "./telemetry.js";

export interface AgentSessionRuntimeDiagnostic {
Expand Down Expand Up @@ -65,6 +66,16 @@ export interface AgentSessionCreationOptions {
rlmParentAgent?: string;
semanticParentSessionId?: string;
semanticSpawnedByRequestId?: string;
/** Shared durable task graph for this run. */
taskGraph?: AgentTaskGraph;
/** Task owned by this session inside taskGraph. */
taskId?: string;
/** Task charged for model usage when this session does not own a task. */
taskAccountingTaskId?: string;
/** Immutable actor identity authorized for task mutations in this session. */
taskActorId?: string;
/** Shared capacity acquired for every child turn, including retained follow-ups. */
turnCapacityPool?: RlmSubagentCapacityPool;
subagentRuntimeHost?: SubagentRuntimeHost;
rlmHeartbeatController?: AgentRlmHeartbeatController;
prewarmIpythonKernel?: boolean;
Expand Down Expand Up @@ -260,6 +271,11 @@ export async function createAgentSessionFromServices(
rlmParentAgent: options.rlmParentAgent,
semanticParentSessionId: options.semanticParentSessionId,
semanticSpawnedByRequestId: options.semanticSpawnedByRequestId,
taskGraph: options.taskGraph,
taskId: options.taskId,
taskAccountingTaskId: options.taskAccountingTaskId,
taskActorId: options.taskActorId,
turnCapacityPool: options.turnCapacityPool,
subagentRuntimeHost: options.subagentRuntimeHost,
rlmHeartbeatController: options.rlmHeartbeatController,
sessionStartEvent: options.sessionStartEvent,
Expand Down
Loading