feat(agent): automation-first sessions with step plan, option choice and inline approval

- agent.plan emits plan_updated; system prompt asks for a plan first and updates while executing
- agent.propose_options registers a decision and suspends the run (waitForFact, no worker slot);
  the answer or timeout (recommended/stop) resumes it as the tool result
- console-session runs get inline approvals for plan-bound capabilities: approve replays the call
  with the approval, reject fails it with the reason and records it on the decision
- proposeFirst blocks execute capabilities until an option is chosen; E12 covers all paths

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 21:07:01 +08:00
1 parent 74c2ed45aa
commit cf2c4ff0e6
14 files changed
+1360 -26

No files matched your search

+23 -4
View File
@@ -22,8 +22,8 @@ import {
type AgentRuntimePort,
type CreateRunSpec,
emptyUsage,
type InjectedEventBody,
type RunOutcome,
type SubagentEventBody,
} from "../runtime/port.ts";
import { type ResolvedAgentSettings, resolveAgentSettings } from "../settings.ts";
import { compactionFromSettings, type PiModelResolver, resolveModelFromSettings } from "./model.ts";
@@ -58,6 +58,8 @@ export const DEFAULT_COMPACTION_INSTRUCTIONS =
"保留:本次目标、约束与授权范围、证据引用(对象 ID/记录)、已完成的动作及其结果(已提交≠已入库)、剩余事项。不要保留任何凭证。";
const LANE = "main";
/** 可挂起重放的能力:首次执行时写入的调用级 memo(重放时据此区分) */
const STARTED_MEMO = "umt.started";
const SUMMARY_LIMIT = 600;
/** Pi(pi-agent-core AgentHarness)实现的 AgentRuntimePort */
@@ -152,15 +154,32 @@ class PiAgentRun implements AgentRunHandle {
const { descriptor } = entry;
const registry = this.options.registry;
const actor = this.spec.actor;
const gate = this.spec.replayGate;
// 可挂起重放(见 CreateRunSpec.replayGate):对 harness 声明 safe,执行时用 memo 判断是否重放
const gated = descriptor.replay === "never" && !!gate;
return {
name: entry.toolName,
label: descriptor.title,
description: `${descriptor.description}\n[能力 ${descriptor.id} v${descriptor.version};类别 ${descriptor.kind};风险 ${descriptor.riskLevel}]`,
parameters: registry.inputJsonSchema(descriptor.id) as unknown as TSchema,
replay: descriptor.replay,
replay: gated ? "safe" : descriptor.replay,
...(descriptor.kind === "execute" ? { executionMode: "sequential" as const } : {}),
execute: async (toolCallId, params, _onUpdate, _toolCtx, invocation, context) => {
const signal = context.abortSignal ?? this.abortController.signal;
const invocationId = invocation.invocationId ?? toolCallId;
if (gated) {
const replayed = (await invocation.getMemo(STARTED_MEMO)) === true;
if (!replayed) await invocation.setMemo(STARTED_MEMO, true);
else if (!gate(descriptor.id, invocationId))
throw new Error(
JSON.stringify({
error: {
code: "interrupted",
message: "执行被中断,外部结果未知;先用查询工具核对实际状态,不要直接重复提交",
},
}),
);
}
try {
const output = await registry.invoke(descriptor.id, params, {
actorId: actor.actorId,
@@ -169,7 +188,7 @@ class PiAgentRun implements AgentRunHandle {
...(actor.allowedKinds ? { allowedKinds: actor.allowedKinds } : {}),
signal,
source: "agent",
invocationId: invocation.invocationId ?? toolCallId,
invocationId,
agentRunId: this.runId,
});
return {
@@ -303,7 +322,7 @@ class PiAgentRun implements AgentRunHandle {
await this.lane.abort(BACKGROUND_CONTEXT).catch(() => undefined);
}
appendEvent(body: SubagentEventBody): boolean {
appendEvent(body: InjectedEventBody): boolean {
if (this.closed) return false;
this.emit(body);
return true;