Add native agentic runner (kind: "agent") on pi agent-core + ai#18
Conversation
Introduce PiAgentRunner: a vendor-neutral AgentRunner that drives a real multi-turn tool-calling loop in-process via @earendil-works/pi-agent-core and pi-ai. Unlike CliRunner (opaque subprocess, requires codex/kiro on PATH) and HttpRunner (a single chat completion that cannot edit files), this runner owns the inner loop: the model reads, lists, writes, edits, and runs shell commands through a minimal toolset (read_file, list_dir, write_file, edit_file, bash) rooted at req.cwd (the task worktree). Any provider with an API key becomes a first-class file-editing actor. Pairs with the captureDiff seam: because the agent edits the worktree on disk, the engine's git-diff capture reviews the real changes, so the model's self-reported diff is no longer trusted. - New runner kind "agent" in RunnerProfileSchema + runnerFactory. - Profile options: provider, apiKeyEnv, thinkingLevel, maxTurns, timeoutMs, tools allowlist, maxOutputChars. - Cooperative cancellation (req.signal), wall-clock timeout, and a turn cap all converge on agent.abort(); token usage is summed into meta in the shape observability normalizeUsage() already understands. - Heavy collaborators (model, execution env, streamFn, api key) are injectable so the real loop + real tools are exercised offline. Tests (test/piAgentRunner.test.ts, 7): drive the real Agent loop + real tools against pi's faux provider over a temp dir \u2014 file edit, file create, tool-error recovery, maxTurns abort, tool allowlist, pre-aborted cancellation, and factory selection. Full suite green; typecheck clean.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughAdds ChangesPiAgentRunner multi-turn implementation with activity streaming
Sequence Diagram(s)sequenceDiagram
participant Caller
participant PiAgentRunner
participant Agent
participant ExecutionEnv
Caller->>PiAgentRunner: run(req)
alt req.signal already aborted
PiAgentRunner-->>Caller: RunResult (cancelled: true)
else
PiAgentRunner->>ExecutionEnv: create from req.cwd
PiAgentRunner->>PiAgentRunner: resolveModel (register providers once)
PiAgentRunner->>PiAgentRunner: wire compaction transformContext hook
PiAgentRunner->>Agent: construct with tools and beforeToolCall safety
PiAgentRunner->>PiAgentRunner: subscribe to events, register timeout/abort listener
loop multi-turn until maxTurns or abort or signal
Agent->>ExecutionEnv: tool call
PiAgentRunner->>PiAgentRunner: beforeToolCall checks path/bash pattern
alt blocked by confinement or regex
PiAgentRunner->>PiAgentRunner: increment blockedToolCalls
else allowed
ExecutionEnv-->>Agent: tool result
end
req->>PiAgentRunner: onEvent (turn_start / tool_start / tool_end)
PiAgentRunner->>PiAgentRunner: count turn, accumulate tokens
end
PiAgentRunner->>Agent: abort (maxTurns or timeout or signal)
Agent-->>PiAgentRunner: final transcript
PiAgentRunner->>PiAgentRunner: extract text, clamp, detect quota
PiAgentRunner-->>Caller: RunResult
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
test/piAgentRunner.test.ts (1)
113-120: ⚡ Quick winAllowlist test should exercise a disallowed tool call.
This case currently never calls
write_file, so it can pass even if allowlist enforcement breaks. Script awrite_filetool call and assert the tool is rejected (and file is not created).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/piAgentRunner.test.ts` around lines 113 - 120, The allowlist test in the "respects a tool allowlist (write_file disabled)" case does not actually exercise the disallowed tool, making it unable to catch allowlist enforcement breakage. Modify the faux response in this test to script a write_file tool call attempt (instead of "no tools used"), then add assertions to verify that the write_file tool is rejected by the runner and that no file is created on disk. This will properly validate that the allowlist is enforced when write_file is excluded from the tools array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/runners/piAgentRunner.ts`:
- Around line 61-67: The bash tool is currently included in PI_AGENT_TOOL_NAMES
and enabled by default at lines 147-148, which creates a security risk for
prompt injection and data exfiltration. Remove bash from the PI_AGENT_TOOL_NAMES
constant at lines 61-67 to exclude it from default-enabled tools. Then update
the logic at lines 147-148 that enables all tools when tools is unset to only
include the file tools (read_file, list_dir, write_file, edit_file), allowing
bash to be added only when explicitly requested in the profile configuration.
The bash tool definition at lines 220-233 remains unchanged but will no longer
be included by default.
- Around line 301-306: The turns counter in the turn_start event handler is
incremented before checking against maxTurns, allowing one extra turn to execute
before the abort triggers. Move the limit check to occur before incrementing
turns, or change the condition from `turns > this.opts.maxTurns` to `turns >=
this.opts.maxTurns` and perform the check prior to the increment operation. This
ensures the maxTurns budget is enforced strictly without allowing an overshoot.
---
Nitpick comments:
In `@test/piAgentRunner.test.ts`:
- Around line 113-120: The allowlist test in the "respects a tool allowlist
(write_file disabled)" case does not actually exercise the disallowed tool,
making it unable to catch allowlist enforcement breakage. Modify the faux
response in this test to script a write_file tool call attempt (instead of "no
tools used"), then add assertions to verify that the write_file tool is rejected
by the runner and that no file is created on disk. This will properly validate
that the allowlist is enforced when write_file is excluded from the tools array.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6232505c-2e14-485f-aece-bd22f22594be
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
package.jsonsrc/runners/agentRunner.tssrc/runners/piAgentRunner.tssrc/runners/runnerFactory.tstest/piAgentRunner.test.ts
Enforce a tool-call permission policy via pi's beforeToolCall hook: - confineToCwd (default on) blocks the file tools (read/list/write/edit) from touching paths that escape the worktree root, whether via absolute paths or .. traversal. A blocked call becomes an error tool result the model sees and can recover from, and is counted in meta.blockedToolCalls. - denyBashPattern (optional regex) blocks matching bash commands, e.g. network egress like curl/wget. confineToCwd guards the FILE tools only; bash stays inherently unconfined, so the docs point at denyBashPattern + OS-level containment for real isolation. Tests (+5): parent-escape write blocked, absolute-path read blocked, in-worktree paths allowed, denyBashPattern blocks curl, and confineToCwd: false permits escapes \u2014 all against the real Agent loop via the faux provider. typecheck clean; suite green.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/runners/piAgentRunner.ts (1)
73-107: 💤 Low valueReDoS risk in
denyBashPatternis config-gated but unmitigated.The Zod refinement at lines 93-100 validates that
denyBashPatterncompiles as a regex, but does not guard against patterns that cause exponential backtracking (e.g.,(a+)+$). Since this value comes from the runner profile (config/trusted source) rather than runtime user input, the attack surface is limited to profile authors. Still, consider adding a timeout or using a safe-regex library if untrusted configs are possible.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/runners/piAgentRunner.ts` around lines 73 - 107, The denyBashPattern field validation in SafetySchema only checks if the pattern compiles as a valid regex but does not guard against patterns that could cause ReDoS (Regular Expression Denial of Service) through exponential backtracking. Enhance the .refine() method for the denyBashPattern field to include additional validation that detects and rejects potentially dangerous regex patterns. This can be done by using a safe-regex library or by implementing pattern analysis logic that identifies problematic constructs like nested quantifiers. Update the error message to clarify that the pattern must be both valid and safe from ReDoS issues.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/runners/piAgentRunner.ts`:
- Around line 73-107: The denyBashPattern field validation in SafetySchema only
checks if the pattern compiles as a valid regex but does not guard against
patterns that could cause ReDoS (Regular Expression Denial of Service) through
exponential backtracking. Enhance the .refine() method for the denyBashPattern
field to include additional validation that detects and rejects potentially
dangerous regex patterns. This can be done by using a safe-regex library or by
implementing pattern analysis logic that identifies problematic constructs like
nested quantifiers. Update the error message to clarify that the pattern must be
both valid and safe from ReDoS issues.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 64db2483-7cfe-4d7d-a868-c949662339d8
📒 Files selected for processing (2)
src/runners/piAgentRunner.tstest/piAgentRunner.test.ts
Three pi-harness Tier 3 features for the native agent runner. G) Sub-step activity streaming. RunRequest gains an onEvent sink + a vendor-neutral RunnerActivity type; PiAgentRunner forwards turn_start / tool_start / tool_end from pi's agent events. The role layer (runnerRoles) enriches each activity with role + runner identity and threads onEvent into every runner.run() call; createRoles exposes onActivity; session wires it to store.recordEvent(runner_activity), which the server's store tap already fans to the SSE hub - so a monitor sees the actor's tool calls live with no server.ts change. D) Context compaction. PiAgentRunner gains a compaction option and wires pi's transformContext + convertToLlm: when the transcript grows past budget it summarizes the older middle via pi's generateSummary and keeps the prompt + recent tail. findCompactionCut snaps the retained tail back to a turn-start assistant so a toolResult is never orphaned. Exported for direct testing. E) Steering. RunRequest.steering hands the caller a steer(text) bound to the live loop; PiAgentRunner injects it via agent.steer so a human can nudge a running task (taking effect after the current turn) instead of only cancelling. meta.steerCount reports injections. Tests: runner-level activity emission + role enrichment/threading; compaction cut + summarize/keep + no-trigger + too-short (vs faux); mid- run steering proven to reach the next turn's context. Full suite 197 green; typecheck clean.
- bash is no longer default-enabled: profiles must explicitly allowlist it via the tools option. Default-on tools are read/list/write/edit only, shrinking the prompt-injection / exfiltration surface. - maxTurns is now checked before incrementing the turn counter, so the cap is enforced strictly instead of overshooting by one turn. - Tests updated: denyBashPattern test opts into bash; added a test asserting bash is excluded by default. Addresses CodeRabbit review on PR #18. Co-authored-by: Inzimam Ul Haq <27832433+inhaq@users.noreply.github.com>
The onActivity (and the identical onRunnerCall) sink dropped the Promise from store.recordEvent, so a rejected persistence could surface as an unhandled rejection and destabilize the run under strict settings. Make the fire-and-forget intent explicit with void and catch failures, logging via opts.log. Addresses CodeRabbit review on PR #20. Co-authored-by: Inzimam Ul Haq <27832433+inhaq@users.noreply.github.com>
Restacks Tier 3 onto the updated base (bash opt-in + maxTurns-before- increment from PR #18 review). Resolves the piAgentRunner.ts turn_start handler conflict by enforcing the turn cap before incrementing while keeping the Tier 3 streaming emits (turn_start / tool_start / tool_end). Co-authored-by: Inzimam Ul Haq <27832433+inhaq@users.noreply.github.com>
Tier 3 agent runner: streaming, compaction, steering
# Conflicts: # src/runners/agentRunner.ts # src/runners/runnerFactory.ts
This pull request was created by @kiro-agent on behalf of @inhaq 👻
Comment with /kiro fix to address specific feedback or /kiro all to address everything.
Learn about Kiro Web
Adds
PiAgentRunner— a third runner mechanism that gives loopwright a real inner agentic loop, built on@earendil-works/pi-agent-core+pi-ai. This is the strategic follow-on to #17.Why
Until now the engine outsourced the inner loop:
CliRunnershells out tocodex/kiro— the loop is opaque and the binary must be installed on PATH.HttpRunnerdoes a single/chat/completionscall and cannot edit files — it just returns a diff as JSON.PiAgentRunnerowns the loop in-process: the model reads, lists, writes, edits, and runs shell commands through tools rooted atreq.cwd(the task's git worktree). Any provider with an API key becomes a first-class file-editing actor — no external CLI required.It pairs with the
captureDiffseam from #17: because the agent edits the worktree on disk, the engine reviews the realgit diff, so the model's self-reported diff is no longer trusted.What's in it
"agent"wired intoRunnerProfileSchemaandrunnerFactory(the engine, state machine, scheduler, and roles are untouched — it's purely a new backend behind the existingAgentRunnerinterface).NodeExecutionEnv:read_file,list_dir,write_file,edit_file(unique-substring replace),bash(output redacted + truncated).provider,apiKeyEnv,thinkingLevel,maxTurns,timeoutMs,toolsallowlist,maxOutputChars.req.signal), a wall-clock timeout, and a turn cap all converge onagent.abort(); token usage is summed intometa.usagein the shapeobservability/events.tsnormalizeUsage()already understands; quota/rate-limit errors setquotaExhausted.Testing
test/piAgentRunner.test.ts(7 tests) drives the real piAgentloop and the real tools against pi'sfauxprovider over a temp directory — no network or API key:edit_fileand surfaces the final assistant text;write_file;edit_file) without crashing the run;maxTurns;createRunnerforkind: "agent".npm run typecheckclean; full suite green (195 tests).Dependencies
Adds
@earendil-works/pi-agent-coreand@earendil-works/pi-ai(both MIT). These are the first runtime deps beyondzod; they're what provide the agent loop, the unified multi-provider transport, and the Node execution env.Notes / follow-ups
desktop/src/settings.ts) isn't wired to exposekind: "agent"yet — a small UI follow-up.tool_execution_*events into the SSE hub, and addbeforeToolCallpermission gating (no writes outside the worktree / no network).Summary by CodeRabbit