Merge branch 'fix-spawn-contract-docs' into integration/audit-fixes
12345678910117 unmodified linesExpand all1281291301311301321331341351361371381391401411421431441451003 unmodified linesExpand allfrom __future__ import annotations
import inspectimport jsonimport osimport reimport subprocessimport sysimport tempfileimport unittest117 unmodified linesExpand all "Call contract: installed Python skills use await <skill_import>(...)", reloaded.overview(), ) overview = reloaded.overview() self.assertIn("handle = await rlm.spawn('sub-task', name='worker', role='<role>')", overview) self.assertIn("handle = await rlm.spawn('sub-task', name='worker', model='<model>')", overview) match = re.search(r"rlm\.spawn\(([^)]+)\)", overview) self.assertIsNotNone(match) kwargs = re.findall(r"([a-zA-Z_]\w*)\s*=", match.group(1)) sig = inspect.signature(callable_rlm.spawn) for kw in kwargs: self.assertIn(kw, sig.parameters) for param_name, param in sig.parameters.items(): if param.default is inspect.Parameter.empty and param_name != "prompt": self.assertIn(param_name, kwargs) self.assertIn("never the child's answer", overview) self.assertIn("receiver_role='parent'", overview) self.assertIn("await rlm.list_subagents()", overview) self.assertIn("receiver_role='child'", overview)1003 unmodified linesExpand all1- Fixed stale `rlm.spawn` call contract documentation in the model-facing harness digest and refinement prompts to match the supported signature.123 unmodified linesExpand all12412512612712812913013113213379 unmodified lines123 unmodified linesExpand all
| Keybinding id | Default | Description ||--------|---------|-------------|| `app.tools.expand` | `ctrl+o` | Cycle overview → thinking and file diffs → all output || `app.messages.expand` | `ctrl+p` | Expand or collapse agent-to-agent messages, independently of tools || `app.edits.expand` | `ctrl+j` | Expand or collapse edit diffs, independently of tools || `app.message.followUp` | `alt+enter` | Queue follow-up message || `app.message.navigateOlder` | `alt+up` | Select the next older pending message || `app.message.navigateNewer` | `alt+down` | Select the next newer pending message or restore the draft || `app.message.moveEarlier` | `ctrl+alt+up` | Move the selected pending message one place earlier in its queue |79 unmodified lines1380 unmodified linesExpand all13811382138313841385138613871388138932 unmodified lines14221423142414251426142714281429143014 unmodified lines14451446144714481449145014511452145318 unmodified lines14721473147414751476147714781479148018 unmodified lines14991500150115021503150415051506150715 unmodified lines15231524152515261527152815291530153114 unmodified lines154615471548154915501551155215531554237 unmodified linesExpand all179217931794179517961797179817991800180118021008 unmodified linesExpand all1380 unmodified linesExpand all contextUsage: TrayUsage | undefined; }; uiServices: { getContextUsage(): TrayUsage | undefined }; isAgentStreaming?(): boolean; getCurrentModel?(): { id: string; provider: string; reasoning?: boolean } | undefined; getTrayContextLabel(): string | undefined; }; const getTrayContextLabel = (InteractiveMode.prototype as unknown as TrayLabelHarness).getTrayContextLabel;
32 unmodified lines } satisfies GoalState, contextUsage: undefined, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("Pursuing goal (1m 05s)"); });
14 unmodified lines } satisfies GoalState, contextUsage: { contextWindow: 100_000, tokens: 75_000, percent: 75 }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("Pursuing goal (1m 05s) · 75k (75%)"); });
18 unmodified lines }, }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("50k/75k cached (75%) · ttl 4m"); });
18 unmodified lines }, }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("50k/75k cached (75%) · ttl 4m"); });
15 unmodified lines heartbeat: createHeartbeat("active"), contextUsage: { contextWindow: 100_000, tokens: 75_000, percent: 75 }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("Pursuing goal (1m 05s) · 1 heartbeat · 75k (75%)"); });
14 unmodified lines } satisfies GoalState, contextUsage: { contextWindow: 100_000, tokens: null, percent: null }, }; fakeThis.uiServices = { getContextUsage: () => undefined }; fakeThis.getCurrentModel = () => undefined;
expect(getTrayContextLabel.call(fakeThis)).toBe("Pursuing goal (1m 05s)"); });});237 unmodified linesExpand all const fakeThis: any = { toolOutputExpanded: false, agentMessagesExpanded: false, editDiffsExpanded: false, pendingBashComponents: [], activeBashComponent: undefined, sideQuestionComponent: undefined, customHeader: undefined, builtInHeader: { setExpanded: vi.fn() }, chatContainer: { children: chatChildren }, ui: {1008 unmodified linesExpand all1 unmodified line2345678910111267891071 unmodified lines82838485929386878889901 unmodified lineimport * as os from "node:os";import * as path from "node:path";import { afterEach, describe, expect, it } from "vitest";import { ENV_AGENT_DIR } from "../src/config.js";import { type AppKeybinding, KEYBINDINGS, type KeybindingsConfig, KeybindingsManager, type KeyId,} from "../src/core/keybindings.js";import { type AppKeybinding, type KeybindingsConfig, KeybindingsManager, type KeyId } from "../src/core/keybindings.js";import { runMigrations } from "../src/migrations.js";
describe("keybindings migration", () => { const tempDirs: string[] = [];71 unmodified lines keybindings: ["tui.editor.cursorUp", "app.tools.expand"], }); });
it("does not define an independent message expansion action", () => { expect(KEYBINDINGS).not.toHaveProperty("app.messages.expand"); it("keeps the independent message and edit-diff expansion actions", () => { expect(new KeybindingsManager({}).getKeys("app.messages.expand")).toEqual(["ctrl+p"]); expect(new KeybindingsManager({}).getKeys("app.edits.expand")).toEqual(["ctrl+j"]); });});784 unmodified linesExpand all785786787788789789790791792793318 unmodified linesExpand all784 unmodified linesExpand all const request = completeSimpleMock.mock.calls[0][1]; const userPrompt = request.messages[0].content[0].text; expect(userPrompt).toContain("Requested refinement scope: local"); expect(userPrompt).toContain("Global entries in the overview are read-only context"); expect(request.systemPrompt).toContain('handle = await rlm.spawn("sub-task", name="worker", role="<role>")'); expect(request.systemPrompt).toContain('handle = await rlm.spawn("sub-task", name="worker", model="<model>")'); expect(request.systemPrompt).toContain("never the child's answer"); expect(request.systemPrompt).toContain('receiver_role="parent"'); expect(request.systemPrompt).toContain("await rlm.list_subagents()"); expect(request.systemPrompt).toContain('receiver_role="child"');318 unmodified linesExpand all12345678341 unmodified linesExpand all350351352353350354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383342 unmodified linesExpand allimport { spawnSync } from "node:child_process";import { existsSync } from "node:fs";import { homedir } from "node:os";import { join, resolve } from "node:path";import { describe, expect, test } from "vitest";import { buildRlmPrompt } from "../src/core/prompts/index.js";import { formatHarnessStateForPrompt, type HarnessState } from "../src/core/refinement/index.js";import type { Skill } from "../src/core/skills.js";341 unmodified linesExpand all expect(prompt).toContain("When to call `await refine.run()`"); expect(prompt).toContain("Call contract: read each installed Python skill's SKILL.md"); expect(prompt).toContain("Continual harness skill entries are Python REPL skills"); expect(prompt).toContain("Spawn a continual harness subagent spec by composing a concise task prompt"); expect(prompt).toContain("handle = await rlm.spawn('sub-task', name='worker', role='<role>')"); expect(prompt).toContain("handle = await rlm.spawn('sub-task', name='worker', model='<model>')"); const spawnMatch = prompt.match(/rlm\.spawn\(([^)]+)\)/); expect(spawnMatch).not.toBeNull(); const documentedArgs = [...spawnMatch![1].matchAll(/([a-zA-Z_]\w*)\s*=/g)].map((m) => m[1]); const python = [ process.env.PRIME_AGENT_KERNEL_PYTHON, resolve(import.meta.dirname, "../../..", "prime-agent-runtime", ".venv", "bin", "python"), join(homedir(), ".prime", "agent", "kernel-venv", "bin", "python"), ].find((p) => p && existsSync(p)); expect(python).toBeDefined(); const sigCheck = spawnSync( python as string, [ "-c", "import inspect, json, rlm; sig = inspect.signature(rlm.spawn); print(json.dumps({'params': list(sig.parameters.keys()), 'required': [k for k, v in sig.parameters.items() if v.default is inspect.Parameter.empty and k != 'prompt']}))", ], { encoding: "utf8" }, ); expect(sigCheck.status).toBe(0); const { params, required } = JSON.parse(sigCheck.stdout) as { params: string[]; required: string[] }; for (const arg of documentedArgs) { expect(params).toContain(arg); } for (const req of required) { expect(documentedArgs).toContain(req); } expect(prompt).toContain("admission returns immediately"); expect(prompt).toContain("never the child's answer"); expect(prompt).toContain("receiver_role='parent'"); expect(prompt).toContain("await rlm.list_subagents()");342 unmodified linesExpand all