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:
1 parent
74c2ed45aa
commit
cf2c4ff0e6
14 files changed
+1360
-26
No files matched your search
@@ -14,6 +14,7 @@ import type { ModuleApp, ServerContext } from "../types.ts";
|
||||
import { planBinding } from "./authorize.ts";
|
||||
import { cancelTask, retryTask, taskToApi } from "./capabilities.ts";
|
||||
import { route } from "./http.ts";
|
||||
import { afterApprovalDecided, answerOptions } from "./interaction.ts";
|
||||
import { type InvokeLimiter, sharedInvokeLimiter, UpstreamBusyError, UpstreamTimeoutError } from "./invoke-limits.ts";
|
||||
import { AgentRunError, abortRun, appendMessage, startRun } from "./service.ts";
|
||||
import { descendants, descendantUsage } from "./subagents.ts";
|
||||
@@ -56,7 +57,6 @@ export function runToApi(r: AgentRunRow) {
|
||||
startedAt: toIso(r.started_at),
|
||||
finishedAt: toIso(r.finished_at),
|
||||
createdBy: r.created_by,
|
||||
mode: r.mode,
|
||||
dedupeKey: r.dedupe_key,
|
||||
depth: r.depth,
|
||||
spawnReason: r.spawn_reason,
|
||||
@@ -73,7 +73,6 @@ export function runToNode(r: AgentRunRow) {
|
||||
parentRunId: r.parent_run_id,
|
||||
goal: r.goal,
|
||||
status: r.status,
|
||||
mode: r.mode,
|
||||
depth: r.depth,
|
||||
spawnReason: r.spawn_reason,
|
||||
toolCallCount: r.tool_call_count,
|
||||
@@ -196,13 +195,21 @@ export function registerAgentRoutes(
|
||||
route(
|
||||
app,
|
||||
Api.agentRuns.create,
|
||||
async ({ body, actorId }) => {
|
||||
async ({ body, actorId, c }) => {
|
||||
// 运行模式已取消:用户会话一律按授权执行;旧客户端传 mode=plan_only 按"先看方案再执行"处理
|
||||
const proposeFirst = body.proposeFirst || body.mode === "plan_only";
|
||||
if (body.mode) c.header("Deprecation", "true");
|
||||
const r = await startRun(ctx, {
|
||||
goal: body.goal,
|
||||
mode: body.mode,
|
||||
mode: "execute",
|
||||
capabilityAllowlist: body.capabilityAllowlist,
|
||||
trigger: "user",
|
||||
createdBy: actorId,
|
||||
session: {
|
||||
// 控制台管理员会话发起:需审批的动作在步骤处出审批卡(只有管理员会话能批准)
|
||||
interactive: c.get("principal")?.kind === "admin_session",
|
||||
proposeFirst,
|
||||
},
|
||||
}).catch(mapErr);
|
||||
return runToApi(r.run);
|
||||
},
|
||||
@@ -213,6 +220,19 @@ export function registerAgentRoutes(
|
||||
runToApi(await appendMessage(ctx, params.id, body.text, actorId).catch(mapErr)),
|
||||
);
|
||||
|
||||
route(app, Api.agentRuns.choose, async ({ params, body, actorId }) => {
|
||||
const run = await db().selectFrom("agent_run").selectAll().where("id", "=", params.id).executeTakeFirst();
|
||||
if (!run) throw new ApiException("not_found", "Agent 运行不存在");
|
||||
if (body.optionId === null && !body.reason?.trim())
|
||||
throw new ApiException("validation_failed", "都不要时写一句原因,Agent 按它调整方案");
|
||||
const r = await answerOptions(ctx, run.id, body, actorId);
|
||||
if (r === "not_found") throw new ApiException("not_found", "这个会话没有这项方案选择");
|
||||
if (r === "bad_option") throw new ApiException("validation_failed", "没有这个方案");
|
||||
if (r === "closed") throw new ApiException("conflict", "这项方案选择已处理");
|
||||
const fresh = await db().selectFrom("agent_run").selectAll().where("id", "=", run.id).executeTakeFirstOrThrow();
|
||||
return runToApi(fresh);
|
||||
});
|
||||
|
||||
route(app, Api.agentRuns.abort, async ({ params, body, actorId }) =>
|
||||
runToApi(await abortRun(ctx, params.id, body.reason, actorId).catch(mapErr)),
|
||||
);
|
||||
@@ -353,6 +373,8 @@ export function registerAgentRoutes(
|
||||
.executeTakeFirstOrThrow();
|
||||
});
|
||||
await audit(ctx, actorId, `approval.${body.status}`, r.id, { planType: r.plan_type, planId: r.plan_id });
|
||||
// 会话内审批:写关联待处理事项的结果(拒绝带理由),唤醒挂起在这份审批上的会话
|
||||
await afterApprovalDecided(ctx, r, actorId);
|
||||
return camel(r);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user