fix(runtime): autonomous beforeToolCall + project agent-event to Console log
Two coupled user-visible defects were fixed.
1. Every tool call (shell / edit / write / read / ...) triggered a
synchronous human-approval prompt under BOTH execution modes. The
worker blocked inside pi's beforeToolCall waiting on a DB approval
row, so runs never progressed — "等待日志输出" forever.
The execution-mode contract is now enforced:
- AI CEO → fully autonomous, no framework gate.
- AI Co-Founder → still fully autonomous at the pi layer. Human
review is a model-driven decision (LLM calls an explicit
`request_approval` tool when it judges review necessary), not
a framework-level interrupt.
HumanApprovalHook collapses to a no-op `before` / `after` /
`cancelAll`. All machinery that queued `dangerous_command`
approvals from the worker-side hook is removed. No more starving
the agent on `node -e` self-call checks.
2. The Console panel only subscribed to `run-log` stdout/stderr
chunks, so pi's agent-event stream (assistant_message_delta,
tool_call_start/end, errors, turn_end) produced zero visible
output. With bug (1) fixed, runs now execute — but the user
would still see a blank Console.
runtime-bridge.handleAgentEvent now projects each meaningful
agent-event into a run-log chunk (assistant_message_delta → raw
delta, tool_call_start → `[tool:bash] {args}`, tool_call_end →
`[tool:done|error:name] {result}`, error → stderr). ConsoleView
picks these up via the existing run-log DesktopEvent pipe with
no renderer change.
tests/unit/approval-hook.test.ts rewritten to assert the new
autonomous contract (before always returns {}, no approval rows
written, cancelAll is a no-op). Tests 508/508 pass.
typecheck 0 / lint 0-0 / build:verify 0-0.
This commit is contained in:
1 parent
9b45b12508
commit
683dded501
4 files changed
+131
-577
No files matched your search
@@ -8,6 +8,55 @@ import { CostWriter } from "./cost-writer";
|
||||
import type { CredentialResolver } from "@main/credentials";
|
||||
import { getConnectorDefinition } from "@main/connectors";
|
||||
|
||||
/**
|
||||
* Projects a pi AgentEvent to a human-readable log line for the Console.
|
||||
* Returns null when the event should not surface as log output (state-only
|
||||
* events such as `turn_start` are already implied by run-status).
|
||||
*/
|
||||
function projectAgentEventToLogChunk(
|
||||
event: { type: string } & Record<string, unknown>,
|
||||
): { stream: "stdout" | "stderr"; chunk: string } | null {
|
||||
switch (event.type) {
|
||||
case "assistant_message_delta": {
|
||||
const delta = typeof event.delta === "string" ? event.delta : "";
|
||||
if (!delta) return null;
|
||||
return { stream: "stdout", chunk: delta };
|
||||
}
|
||||
case "tool_call_start": {
|
||||
const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
|
||||
const argsPreview = summariseJson(event.args, 200);
|
||||
return { stream: "stdout", chunk: `\n[tool:${toolName}] ${argsPreview}\n` };
|
||||
}
|
||||
case "tool_call_end": {
|
||||
const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
|
||||
const isError = event.isError === true;
|
||||
const resultPreview = summariseJson(event.result, 300);
|
||||
const tag = isError ? "tool:error" : "tool:done";
|
||||
return { stream: isError ? "stderr" : "stdout", chunk: `[${tag}:${toolName}] ${resultPreview}\n` };
|
||||
}
|
||||
case "error": {
|
||||
const msg = typeof event.message === "string" ? event.message : "(unknown error)";
|
||||
return { stream: "stderr", chunk: `[error] ${msg}\n` };
|
||||
}
|
||||
case "turn_end": {
|
||||
return { stream: "stdout", chunk: "\n" };
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function summariseJson(value: unknown, maxLength: number): string {
|
||||
if (value === undefined || value === null) return "";
|
||||
try {
|
||||
const json = typeof value === "string" ? value : JSON.stringify(value);
|
||||
if (json.length <= maxLength) return json;
|
||||
return `${json.slice(0, maxLength - 3)}...`;
|
||||
} catch {
|
||||
return "<unserialisable>";
|
||||
}
|
||||
}
|
||||
|
||||
export interface RuntimeBridgeDeps {
|
||||
db: AppDatabase;
|
||||
emitEvent: (event: DesktopEvent) => void;
|
||||
@@ -93,6 +142,16 @@ export class RuntimeBridge {
|
||||
event: envelope.event,
|
||||
});
|
||||
|
||||
const logChunk = projectAgentEventToLogChunk(envelope.event);
|
||||
if (logChunk) {
|
||||
this.deps.emitEvent({
|
||||
type: "run-log",
|
||||
runId: envelope.runId,
|
||||
stream: logChunk.stream,
|
||||
chunk: logChunk.chunk,
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.event.type === "message_end") {
|
||||
const ctx = this.activeRuns.get(envelope.runId);
|
||||
const messageRaw = envelope.event.message as
|
||||
|
||||
Reference in new issue
Block a user