支持Agent待确认动作可靠恢复
持久化精确待确认工具动作并绑定run、输入指纹与发生序号 使用OS级Agent锁串行确认、拒绝、取消、恢复与队列续跑 增加取消中状态、开发确认拒绝控件和私有Runtime文件边界 补齐并发恢复、重启核对、取消失败与前端交互回归测试 同步Agent Runtime技术方案与项目决策记录
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -431,7 +431,7 @@ pub(crate) fn confirm_game_creator_agent_runtime_task(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
run_id: String,
|
||||
next_run_id: String,
|
||||
action_id: String,
|
||||
note: String,
|
||||
) -> Result<AgentRuntimeResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
@@ -444,7 +444,30 @@ pub(crate) fn confirm_game_creator_agent_runtime_task(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
run_id.trim(),
|
||||
next_run_id.trim(),
|
||||
action_id.trim(),
|
||||
note.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn reject_game_creator_agent_runtime_task(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
run_id: String,
|
||||
action_id: String,
|
||||
note: String,
|
||||
) -> Result<AgentRuntimeResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
enforce_project_permission_policy(root, "conversation.write")?;
|
||||
enforce_project_permission_policy(root, "agent.run_status")?;
|
||||
// 该命令只由开发者显式点击“拒绝并继续”触发。
|
||||
enforce_project_permission_policy(root, "agent.resume")?;
|
||||
reject_game_creator_agent_runtime_task_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
run_id.trim(),
|
||||
action_id.trim(),
|
||||
note.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -178,6 +178,8 @@ struct AgentRuntimeState {
|
||||
#[serde(default)]
|
||||
recent_tool_calls: Vec<AgentRuntimeToolCallRecord>,
|
||||
#[serde(default)]
|
||||
pending_tool_action: Option<AgentRuntimePendingToolActionSummary>,
|
||||
#[serde(default)]
|
||||
task_queue: AgentRuntimeTaskQueueSummary,
|
||||
#[serde(default)]
|
||||
allowed_tools: Vec<String>,
|
||||
@@ -239,6 +241,23 @@ struct AgentRuntimeToolCallRecord {
|
||||
updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentRuntimePendingToolActionSummary {
|
||||
#[serde(default)]
|
||||
action_id: String,
|
||||
#[serde(default)]
|
||||
action_fingerprint: String,
|
||||
#[serde(default)]
|
||||
tool: String,
|
||||
#[serde(default)]
|
||||
input_summary: Option<String>,
|
||||
#[serde(default)]
|
||||
reason: Option<String>,
|
||||
#[serde(default)]
|
||||
requested_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentRuntimePlanStep {
|
||||
@@ -1207,6 +1226,7 @@ fn main() {
|
||||
cancel_game_creator_agent_runtime_task,
|
||||
retry_game_creator_agent_runtime_task,
|
||||
confirm_game_creator_agent_runtime_task,
|
||||
reject_game_creator_agent_runtime_task,
|
||||
read_game_creator_agent_runtime,
|
||||
read_game_creator_agent_runtimes,
|
||||
resume_game_creator_agent_runtime_tasks,
|
||||
|
||||
@@ -674,6 +674,9 @@ pub(crate) fn list_local_project_files_at(
|
||||
|
||||
let path = entry.path();
|
||||
let relative_path = relative_project_path(root, &path)?;
|
||||
if is_agent_runtime_private_control_path(&relative_path) {
|
||||
continue;
|
||||
}
|
||||
let metadata = entry.metadata().map_err(|error| {
|
||||
format!("读取文件元数据失败:{}: {error}", entry.path().display())
|
||||
})?;
|
||||
@@ -715,6 +718,7 @@ pub(crate) fn read_local_project_file_at(
|
||||
relative_path: &str,
|
||||
) -> Result<LocalProjectFileResult, String> {
|
||||
let normalized_path = normalize_relative_path(relative_path)?;
|
||||
reject_agent_runtime_private_control_path(&normalized_path)?;
|
||||
reject_sensitive_project_file_read(&normalized_path)?;
|
||||
let path = resolve_local_project_path(root, &normalized_path)?;
|
||||
let metadata = fs::metadata(&path)
|
||||
@@ -732,6 +736,19 @@ pub(crate) fn read_local_project_file_at(
|
||||
})
|
||||
}
|
||||
|
||||
fn is_agent_runtime_private_control_path(normalized_path: &str) -> bool {
|
||||
let mut parts = normalized_path.split('/');
|
||||
matches!(parts.next(), Some(part) if part.eq_ignore_ascii_case(".agent"))
|
||||
&& matches!(parts.next(), Some(part) if part.eq_ignore_ascii_case("runtime"))
|
||||
}
|
||||
|
||||
fn reject_agent_runtime_private_control_path(normalized_path: &str) -> Result<(), String> {
|
||||
if is_agent_runtime_private_control_path(normalized_path) {
|
||||
return Err("Agent Runtime 私有控制面不可通过通用文件工具访问".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn reject_sensitive_project_file_read(normalized_path: &str) -> Result<(), String> {
|
||||
for part in normalized_path.split('/') {
|
||||
let lower = part.to_ascii_lowercase();
|
||||
@@ -756,7 +773,9 @@ pub(crate) fn write_local_project_file_at(
|
||||
relative_path: &str,
|
||||
content: &str,
|
||||
) -> Result<LocalProjectFileMutationResult, String> {
|
||||
let path = resolve_local_project_path(root, relative_path)?;
|
||||
let normalized_path = normalize_relative_path(relative_path)?;
|
||||
reject_agent_runtime_private_control_path(&normalized_path)?;
|
||||
let path = resolve_local_project_path(root, &normalized_path)?;
|
||||
if path.exists() && !path.is_file() {
|
||||
return Err("只能写入文件".to_string());
|
||||
}
|
||||
@@ -768,7 +787,7 @@ pub(crate) fn write_local_project_file_at(
|
||||
.map_err(|error| format!("写入项目文件失败:{}: {error}", path.display()))?;
|
||||
|
||||
Ok(LocalProjectFileMutationResult {
|
||||
path: normalize_relative_path(relative_path)?,
|
||||
path: normalized_path,
|
||||
absolute_path: path.to_string_lossy().into_owned(),
|
||||
deleted: false,
|
||||
})
|
||||
@@ -778,10 +797,12 @@ pub(crate) fn delete_local_project_file_at(
|
||||
root: &Path,
|
||||
relative_path: &str,
|
||||
) -> Result<LocalProjectFileMutationResult, String> {
|
||||
let path = resolve_local_project_path(root, relative_path)?;
|
||||
let normalized_path = normalize_relative_path(relative_path)?;
|
||||
reject_agent_runtime_private_control_path(&normalized_path)?;
|
||||
let path = resolve_local_project_path(root, &normalized_path)?;
|
||||
if !path.exists() {
|
||||
return Ok(LocalProjectFileMutationResult {
|
||||
path: normalize_relative_path(relative_path)?,
|
||||
path: normalized_path,
|
||||
absolute_path: path.to_string_lossy().into_owned(),
|
||||
deleted: false,
|
||||
});
|
||||
@@ -793,7 +814,7 @@ pub(crate) fn delete_local_project_file_at(
|
||||
.map_err(|error| format!("删除项目文件失败:{}: {error}", path.display()))?;
|
||||
|
||||
Ok(LocalProjectFileMutationResult {
|
||||
path: normalize_relative_path(relative_path)?,
|
||||
path: normalized_path,
|
||||
absolute_path: path.to_string_lossy().into_owned(),
|
||||
deleted: true,
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -249,6 +249,7 @@ interface AgentRuntimeState {
|
||||
activePlanStepIndex?: number | null;
|
||||
observations: string[];
|
||||
recentToolCalls?: AgentRuntimeToolCallRecord[];
|
||||
pendingToolAction?: AgentRuntimePendingToolActionSummary | null;
|
||||
taskQueue?: AgentRuntimeTaskQueueSummary;
|
||||
allowedTools: string[];
|
||||
toolPolicy?: AgentRuntimeToolPolicySnapshot;
|
||||
@@ -278,6 +279,15 @@ interface AgentRuntimeToolCallRecord {
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface AgentRuntimePendingToolActionSummary {
|
||||
actionId: string;
|
||||
actionFingerprint: string;
|
||||
tool: string;
|
||||
inputSummary: string | null;
|
||||
reason: string | null;
|
||||
requestedAt: number;
|
||||
}
|
||||
|
||||
interface AgentRuntimePlanStep {
|
||||
index: number;
|
||||
title: string;
|
||||
@@ -662,6 +672,8 @@ function agentRuntimeWaitingOnFromPhase(phase: string) {
|
||||
return '工具观察结果';
|
||||
case 'waiting-for-confirmation':
|
||||
return '开发者确认 Agent 工具动作';
|
||||
case 'cancelling':
|
||||
return '当前 LLM 或工具调用返回';
|
||||
case 'response':
|
||||
return 'Agent 整理最终回复';
|
||||
case 'completed':
|
||||
@@ -684,6 +696,8 @@ function agentRuntimeNextStepFromPhase(phase: string) {
|
||||
return '等待工具观察结果';
|
||||
case 'waiting-for-confirmation':
|
||||
return '等待开发者确认工具动作';
|
||||
case 'cancelling':
|
||||
return '取消完成后可重试该任务或提交新任务';
|
||||
case 'response':
|
||||
return '等待 Agent 整理最终回复';
|
||||
case 'completed':
|
||||
@@ -785,13 +799,17 @@ function AgentRuntimeStatusPanel({
|
||||
onCancelRuntimeTask,
|
||||
onRetryRuntimeTask,
|
||||
onConfirmRuntimeTask,
|
||||
onRejectRuntimeTask,
|
||||
onRefreshRuntime,
|
||||
}: {
|
||||
runtime: AgentRuntimeState | null;
|
||||
error?: string | null;
|
||||
controlBusy?: boolean;
|
||||
onCancelRuntimeTask?: (runId: string) => void;
|
||||
onRetryRuntimeTask?: (runId: string) => void;
|
||||
onConfirmRuntimeTask?: (runId: string) => void;
|
||||
onConfirmRuntimeTask?: (runId: string, actionId: string) => void;
|
||||
onRejectRuntimeTask?: (runId: string, actionId: string) => void;
|
||||
onRefreshRuntime?: () => void;
|
||||
}) {
|
||||
if (!runtime && error) {
|
||||
return (
|
||||
@@ -800,6 +818,11 @@ function AgentRuntimeStatusPanel({
|
||||
<strong>Runtime 状态读取失败</strong>
|
||||
</header>
|
||||
<p>{error}</p>
|
||||
{onRefreshRuntime ? (
|
||||
<button type="button" disabled={controlBusy} onClick={onRefreshRuntime}>
|
||||
刷新状态
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -818,41 +841,70 @@ function AgentRuntimeStatusPanel({
|
||||
const nextStep = runtime.nextStep ?? agentRuntimeNextStepFromPhase(runtime.phase);
|
||||
const currentGoal = runtime.currentGoal ?? runtime.currentTask;
|
||||
const waitingOn = runtime.waitingOn ?? agentRuntimeWaitingOnFromPhase(runtime.phase);
|
||||
const pendingToolAction = runtime.pendingToolAction ?? null;
|
||||
const canCancel =
|
||||
Boolean(runtime.runId) &&
|
||||
agentRuntimeCanCancel(runtime.status) &&
|
||||
(agentRuntimeCanCancel(runtime.status) ||
|
||||
(runtime.phase === 'needs-reconciliation' && Boolean(pendingToolAction))) &&
|
||||
Boolean(onCancelRuntimeTask);
|
||||
const canRetry =
|
||||
Boolean(runtime.runId) &&
|
||||
agentRuntimeCanRetry(runtime.status) &&
|
||||
!pendingToolAction &&
|
||||
Boolean(onRetryRuntimeTask);
|
||||
const canConfirm =
|
||||
Boolean(runtime.runId) &&
|
||||
Boolean(pendingToolAction?.actionId) &&
|
||||
agentRuntimeCanConfirm(runtime.status) &&
|
||||
Boolean(onConfirmRuntimeTask);
|
||||
const canReject =
|
||||
Boolean(runtime.runId) &&
|
||||
Boolean(pendingToolAction?.actionId) &&
|
||||
agentRuntimeCanConfirm(runtime.status) &&
|
||||
Boolean(onRejectRuntimeTask);
|
||||
return (
|
||||
<section className="agent-runtime-status" aria-label="Agent Runtime 状态">
|
||||
<header>
|
||||
<strong>{`${runtime.status} / ${runtime.phase}`}</strong>
|
||||
<small>{runtime.sessionId}</small>
|
||||
</header>
|
||||
{onCancelRuntimeTask || onRetryRuntimeTask || onConfirmRuntimeTask ? (
|
||||
{onCancelRuntimeTask ||
|
||||
onRetryRuntimeTask ||
|
||||
onConfirmRuntimeTask ||
|
||||
onRejectRuntimeTask ||
|
||||
onRefreshRuntime ? (
|
||||
<div className="agent-runtime-actions" aria-label="Agent Runtime 操作">
|
||||
<button
|
||||
type="button"
|
||||
disabled={controlBusy || !canConfirm}
|
||||
onClick={() =>
|
||||
runtime.runId && onConfirmRuntimeTask?.(runtime.runId)
|
||||
runtime.runId &&
|
||||
pendingToolAction?.actionId &&
|
||||
onConfirmRuntimeTask?.(
|
||||
runtime.runId,
|
||||
pendingToolAction.actionId,
|
||||
)
|
||||
}
|
||||
>
|
||||
确认继续
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={controlBusy || !canReject}
|
||||
onClick={() =>
|
||||
runtime.runId &&
|
||||
pendingToolAction?.actionId &&
|
||||
onRejectRuntimeTask?.(runtime.runId, pendingToolAction.actionId)
|
||||
}
|
||||
>
|
||||
拒绝并继续
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={controlBusy || !canCancel}
|
||||
onClick={() => runtime.runId && onCancelRuntimeTask?.(runtime.runId)}
|
||||
>
|
||||
取消
|
||||
取消任务
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -861,6 +913,13 @@ function AgentRuntimeStatusPanel({
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={controlBusy || !onRefreshRuntime}
|
||||
onClick={() => onRefreshRuntime?.()}
|
||||
>
|
||||
刷新状态
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<small>{`task: ${runtime.taskId} · ${runtime.source}`}</small>
|
||||
@@ -870,6 +929,21 @@ function AgentRuntimeStatusPanel({
|
||||
<small>{runtime.currentAction}</small>
|
||||
{waitingOn ? <small>{`等待:${waitingOn}`}</small> : null}
|
||||
{nextStep ? <small>{`下一步:${nextStep}`}</small> : null}
|
||||
{pendingToolAction ? (
|
||||
<small>
|
||||
{`${
|
||||
runtime.phase === 'needs-reconciliation'
|
||||
? '待核对动作'
|
||||
: '待确认动作'
|
||||
}:${pendingToolAction.tool}${
|
||||
pendingToolAction.inputSummary
|
||||
? ` · ${pendingToolAction.inputSummary}`
|
||||
: ''
|
||||
}`}
|
||||
</small>
|
||||
) : runtime.status === 'waiting-for-confirmation' ? (
|
||||
<small>待确认动作摘要缺失,请刷新状态</small>
|
||||
) : null}
|
||||
{loopProgress ? <small>{loopProgress}</small> : null}
|
||||
{taskQueueSummary ? <small>{taskQueueSummary}</small> : null}
|
||||
{toolPolicy ? (
|
||||
@@ -3937,10 +4011,19 @@ export function WorkspaceLauncher({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAgentChatConfirmRuntimeTask(runId: string) {
|
||||
async function handleAgentChatConfirmRuntimeTask(
|
||||
runId: string,
|
||||
actionId: string,
|
||||
) {
|
||||
const projectPathForChat = validateAgentChatProjectPath();
|
||||
const agent = selectedLauncherAgentChatAgent();
|
||||
if (!projectPathForChat || !agent || !runId || agentChatBackgroundBusy) {
|
||||
if (
|
||||
!projectPathForChat ||
|
||||
!agent ||
|
||||
!runId ||
|
||||
!actionId ||
|
||||
agentChatBackgroundBusy
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const llmWarning = getCurrentAgentChatLlmWarning(agent);
|
||||
@@ -3964,7 +4047,7 @@ export function WorkspaceLauncher({
|
||||
projectPath: projectPathForChat,
|
||||
agentId: agent.id,
|
||||
runId,
|
||||
nextRunId: createAgentChatRunId('launcher-agent-confirm'),
|
||||
actionId,
|
||||
note: '开发者已确认待执行工具动作',
|
||||
},
|
||||
);
|
||||
@@ -3997,6 +4080,75 @@ export function WorkspaceLauncher({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAgentChatRejectRuntimeTask(
|
||||
runId: string,
|
||||
actionId: string,
|
||||
) {
|
||||
const projectPathForChat = validateAgentChatProjectPath();
|
||||
const agent = selectedLauncherAgentChatAgent();
|
||||
if (
|
||||
!projectPathForChat ||
|
||||
!agent ||
|
||||
!runId ||
|
||||
!actionId ||
|
||||
agentChatBackgroundBusy
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const llmWarning = getCurrentAgentChatLlmWarning(agent);
|
||||
if (llmWarning) {
|
||||
setAgentChatStatus(llmWarning);
|
||||
return;
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
setAgentChatStatus('需要在 Tauri App 内运行');
|
||||
return;
|
||||
}
|
||||
const saveVersion = agentChatLoadVersionRef.current + 1;
|
||||
agentChatLoadVersionRef.current = saveVersion;
|
||||
setAgentChatBackgroundBusy(true);
|
||||
setAgentChatStatus('正在拒绝工具动作并继续 Agent 后台任务');
|
||||
try {
|
||||
const runtime = await invoke<AgentRuntimeResult>(
|
||||
'reject_game_creator_agent_runtime_task',
|
||||
{
|
||||
projectPath: projectPathForChat,
|
||||
agentId: agent.id,
|
||||
runId,
|
||||
actionId,
|
||||
note: '开发者拒绝待执行工具动作',
|
||||
},
|
||||
);
|
||||
if (agentChatLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentChatRuntime(agentRuntimeStateFromResult(runtime));
|
||||
setAgentChatRuntimeError('');
|
||||
const conversation = await invoke<LocalConversationResult>(
|
||||
'read_local_conversation',
|
||||
{
|
||||
projectPath: projectPathForChat,
|
||||
agentId: agent.id,
|
||||
},
|
||||
);
|
||||
if (agentChatLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentChatMessages(conversation.messages);
|
||||
setAgentChatStatus(agentRuntimeStartStatus(runtime));
|
||||
} catch (error) {
|
||||
if (agentChatLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentChatStatus(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
if (agentChatLoadVersionRef.current === saveVersion) {
|
||||
setAgentChatBackgroundBusy(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const projectRows = recentWorkspaces.map((workspace) => {
|
||||
const directoryStatus = recentWorkspaceStatuses[workspace];
|
||||
const isPendingStatus = directoryStatus === undefined;
|
||||
@@ -4671,9 +4823,13 @@ export function WorkspaceLauncher({
|
||||
onRetryRuntimeTask={(runId) =>
|
||||
void handleAgentChatRetryRuntimeTask(runId)
|
||||
}
|
||||
onConfirmRuntimeTask={(runId) =>
|
||||
void handleAgentChatConfirmRuntimeTask(runId)
|
||||
onConfirmRuntimeTask={(runId, actionId) =>
|
||||
void handleAgentChatConfirmRuntimeTask(runId, actionId)
|
||||
}
|
||||
onRejectRuntimeTask={(runId, actionId) =>
|
||||
void handleAgentChatRejectRuntimeTask(runId, actionId)
|
||||
}
|
||||
onRefreshRuntime={() => void loadAgentChatConversation()}
|
||||
/>
|
||||
</section>
|
||||
<div className="launcher-agent-chat-messages" aria-label="Agent 聊天记录">
|
||||
@@ -13951,8 +14107,14 @@ export function App() {
|
||||
async function confirmSelectedAgentRuntimeTask(
|
||||
agent: AgentStatusCard,
|
||||
runId: string,
|
||||
actionId: string,
|
||||
) {
|
||||
if (!agent || !runId || agentConversationBackgroundBusyRef.current) {
|
||||
if (
|
||||
!agent ||
|
||||
!runId ||
|
||||
!actionId ||
|
||||
agentConversationBackgroundBusyRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
@@ -13981,7 +14143,7 @@ export function App() {
|
||||
projectPath: nextProjectPath,
|
||||
agentId: agent.id,
|
||||
runId,
|
||||
nextRunId: createAgentChatRunId('agent-background-confirm'),
|
||||
actionId,
|
||||
note: '开发者已确认待执行工具动作',
|
||||
},
|
||||
);
|
||||
@@ -14021,6 +14183,85 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectSelectedAgentRuntimeTask(
|
||||
agent: AgentStatusCard,
|
||||
runId: string,
|
||||
actionId: string,
|
||||
) {
|
||||
if (
|
||||
!agent ||
|
||||
!runId ||
|
||||
!actionId ||
|
||||
agentConversationBackgroundBusyRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||||
if (!invoke) {
|
||||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||||
return;
|
||||
}
|
||||
if (!nextProjectPath) {
|
||||
setAgentConversationStatus('请先初始化本地项目');
|
||||
return;
|
||||
}
|
||||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||||
if (llmWarning) {
|
||||
setAgentConversationStatus(llmWarning);
|
||||
return;
|
||||
}
|
||||
const saveVersion = agentConversationLoadVersionRef.current;
|
||||
agentConversationBackgroundBusyRef.current = true;
|
||||
setAgentConversationBackgroundBusy(true);
|
||||
setAgentConversationStatus('正在拒绝工具动作并继续 Agent 后台任务');
|
||||
try {
|
||||
const runtime = await invoke<AgentRuntimeResult>(
|
||||
'reject_game_creator_agent_runtime_task',
|
||||
{
|
||||
projectPath: nextProjectPath,
|
||||
agentId: agent.id,
|
||||
runId,
|
||||
actionId,
|
||||
note: '开发者拒绝待执行工具动作',
|
||||
},
|
||||
);
|
||||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||||
setAgentConversationRuntime(nextRuntime);
|
||||
rememberAgentRuntimeState(nextRuntime);
|
||||
setAgentConversationRuntimeError('');
|
||||
const conversation = await invoke<LocalConversationResult>(
|
||||
'read_local_conversation',
|
||||
{
|
||||
projectPath: nextProjectPath,
|
||||
agentId: agent.id,
|
||||
},
|
||||
);
|
||||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentConversationMessages(conversation.messages);
|
||||
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
|
||||
setCommandLog((current) => [
|
||||
...current,
|
||||
'agent.runtime.tool_confirmation.rejected',
|
||||
]);
|
||||
} catch (error) {
|
||||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentConversationStatus(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
} finally {
|
||||
agentConversationBackgroundBusyRef.current = false;
|
||||
setAgentConversationBackgroundBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSelectedAgentPrivateMemory(
|
||||
agent: AgentStatusCard,
|
||||
content: string,
|
||||
@@ -21489,9 +21730,27 @@ export function App() {
|
||||
? void retrySelectedAgentRuntimeTask(selectedAgent, runId)
|
||||
: undefined
|
||||
}
|
||||
onConfirmRuntimeTask={(runId) =>
|
||||
onConfirmRuntimeTask={(runId, actionId) =>
|
||||
selectedAgent
|
||||
? void confirmSelectedAgentRuntimeTask(selectedAgent, runId)
|
||||
? void confirmSelectedAgentRuntimeTask(
|
||||
selectedAgent,
|
||||
runId,
|
||||
actionId,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onRejectRuntimeTask={(runId, actionId) =>
|
||||
selectedAgent
|
||||
? void rejectSelectedAgentRuntimeTask(
|
||||
selectedAgent,
|
||||
runId,
|
||||
actionId,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onRefreshRuntime={() =>
|
||||
selectedAgent
|
||||
? void openAgentConversation(selectedAgent, true, true)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1670,7 +1670,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
const runtimeActions = screen.getByLabelText('Agent Runtime 操作');
|
||||
expect(
|
||||
(within(runtimeActions).getByRole('button', {
|
||||
name: '取消',
|
||||
name: '取消任务',
|
||||
}) as HTMLButtonElement).disabled,
|
||||
).toBe(false);
|
||||
expect(
|
||||
@@ -1735,6 +1735,52 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const cancellingRuntimeState = {
|
||||
...runningRuntimeState,
|
||||
status: 'cancelling',
|
||||
phase: 'cancelling',
|
||||
currentAction: '正在取消 Agent 后台任务',
|
||||
waitingOn: '当前 LLM 或工具调用返回',
|
||||
nextStep: '取消完成后可重试该任务或提交新任务',
|
||||
};
|
||||
await act(async () => {
|
||||
runtimeUpdateHandler?.({
|
||||
payload: {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
agentId: 'design-director',
|
||||
runId: 'launcher-agent-task-test',
|
||||
status: 'cancelling',
|
||||
phase: 'cancelling',
|
||||
runtime: {
|
||||
state: cancellingRuntimeState,
|
||||
sessionPath:
|
||||
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
|
||||
eventPath:
|
||||
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
|
||||
taskPath:
|
||||
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
|
||||
taskQueue: cancellingRuntimeState.taskQueue,
|
||||
recentEvents: runningRuntimeEvents,
|
||||
recentTasks: [runningRuntimeTask],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(await screen.findByText('cancelling / cancelling')).not.toBeNull();
|
||||
expect(screen.getByText('等待:当前 LLM 或工具调用返回')).not.toBeNull();
|
||||
const cancellingActions = screen.getByLabelText('Agent Runtime 操作');
|
||||
expect(
|
||||
(within(cancellingActions).getByRole('button', {
|
||||
name: '取消任务',
|
||||
}) as HTMLButtonElement).disabled,
|
||||
).toBe(true);
|
||||
expect(
|
||||
(within(cancellingActions).getByRole('button', {
|
||||
name: '重试',
|
||||
}) as HTMLButtonElement).disabled,
|
||||
).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
runtimeUpdateHandler?.({
|
||||
payload: {
|
||||
@@ -1784,7 +1830,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('confirms the exact pending tool action from the developer agent window', async () => {
|
||||
it('confirms or rejects the exact pending tool action from the developer agent window', async () => {
|
||||
const waitingRuntimeState = {
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
agentId: 'design-director',
|
||||
@@ -1816,6 +1862,14 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
updatedAt: 5000,
|
||||
},
|
||||
],
|
||||
pendingToolAction: {
|
||||
actionId: `action-${'b'.repeat(24)}`,
|
||||
actionFingerprint: 'b'.repeat(64),
|
||||
tool: 'file.read',
|
||||
inputSummary: 'path=game/notes.txt',
|
||||
reason: '读取角色规范依据',
|
||||
requestedAt: 5000,
|
||||
},
|
||||
taskQueue: {
|
||||
total: 1,
|
||||
pending: 0,
|
||||
@@ -1895,7 +1949,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
return runtimeResult;
|
||||
}
|
||||
if (command === 'confirm_game_creator_agent_runtime_task') {
|
||||
const runId = String(args?.nextRunId ?? 'launcher-agent-confirmed');
|
||||
const runId = waitingRuntimeState.runId;
|
||||
const taskQueue = {
|
||||
...waitingRuntimeState.taskQueue,
|
||||
running: 1,
|
||||
@@ -1908,9 +1962,10 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
...waitingRuntimeState,
|
||||
runId,
|
||||
status: 'running',
|
||||
phase: 'planning',
|
||||
currentAction: '生成 Agent 工具计划(第 1 轮)',
|
||||
waitingOn: 'Agent 输出计划或回复',
|
||||
phase: 'action',
|
||||
currentAction: '执行已确认工具 file.read',
|
||||
waitingOn: '已确认工具执行结果',
|
||||
pendingToolAction: null,
|
||||
taskQueue,
|
||||
},
|
||||
taskQueue,
|
||||
@@ -1919,7 +1974,38 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
...waitingTask,
|
||||
runId,
|
||||
status: 'running',
|
||||
phase: 'planning',
|
||||
phase: 'action',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (command === 'reject_game_creator_agent_runtime_task') {
|
||||
const runId = waitingRuntimeState.runId;
|
||||
const taskQueue = {
|
||||
...waitingRuntimeState.taskQueue,
|
||||
running: 1,
|
||||
waitingForConfirmation: 0,
|
||||
latestRunId: runId,
|
||||
};
|
||||
return {
|
||||
...runtimeResult,
|
||||
state: {
|
||||
...waitingRuntimeState,
|
||||
runId,
|
||||
status: 'running',
|
||||
phase: 'observation',
|
||||
currentAction: '开发者拒绝工具 file.read',
|
||||
waitingOn: 'Agent 根据拒绝结果修正计划',
|
||||
pendingToolAction: null,
|
||||
taskQueue,
|
||||
},
|
||||
taskQueue,
|
||||
recentTasks: [
|
||||
{
|
||||
...waitingTask,
|
||||
runId,
|
||||
status: 'running',
|
||||
phase: 'observation',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1945,12 +2031,38 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
'file.read · waiting-for-confirmation · 项目权限策略要求用户确认:file.read · 目标:path=game/notes.txt · 读取角色规范依据',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText('待确认动作:file.read · path=game/notes.txt'),
|
||||
).not.toBeNull();
|
||||
const runtimeActions = screen.getByLabelText('Agent Runtime 操作');
|
||||
const confirmButton = within(runtimeActions).getByRole('button', {
|
||||
name: '确认继续',
|
||||
}) as HTMLButtonElement;
|
||||
expect(confirmButton.disabled).toBe(false);
|
||||
fireEvent.click(confirmButton);
|
||||
expect(
|
||||
(within(runtimeActions).getByRole('button', {
|
||||
name: '拒绝并继续',
|
||||
}) as HTMLButtonElement).disabled,
|
||||
).toBe(false);
|
||||
const runtimeReadsBeforeRefresh = invoke.mock.calls.filter(
|
||||
([command]) => command === 'read_game_creator_agent_runtime',
|
||||
).length;
|
||||
fireEvent.click(
|
||||
within(runtimeActions).getByRole('button', { name: '刷新状态' }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
invoke.mock.calls.filter(
|
||||
([command]) => command === 'read_game_creator_agent_runtime',
|
||||
).length,
|
||||
).toBeGreaterThan(runtimeReadsBeforeRefresh);
|
||||
});
|
||||
const refreshedRuntimeActions = screen.getByLabelText('Agent Runtime 操作');
|
||||
const refreshedConfirmButton = within(refreshedRuntimeActions).getByRole(
|
||||
'button',
|
||||
{ name: '确认继续' },
|
||||
);
|
||||
fireEvent.click(refreshedConfirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
@@ -1959,11 +2071,45 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
agentId: 'design-director',
|
||||
runId: 'launcher-agent-waiting',
|
||||
actionId: `action-${'b'.repeat(24)}`,
|
||||
note: '开发者已确认待执行工具动作',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('running / planning')).not.toBeNull();
|
||||
expect(await screen.findByText('running / action')).not.toBeNull();
|
||||
|
||||
cleanup();
|
||||
invoke.mockClear();
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderLauncherAgentChatAt('/?agent-chat');
|
||||
fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), {
|
||||
target: { value: '/tmp/authorized-game' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '读取历史' }));
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'waiting-for-confirmation / waiting-for-confirmation',
|
||||
),
|
||||
).not.toBeNull();
|
||||
const rejectRuntimeActions = screen.getByLabelText('Agent Runtime 操作');
|
||||
fireEvent.click(
|
||||
within(rejectRuntimeActions).getByRole('button', {
|
||||
name: '拒绝并继续',
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'reject_game_creator_agent_runtime_task',
|
||||
expect.objectContaining({
|
||||
projectPath: '/tmp/authorized-game',
|
||||
agentId: 'design-director',
|
||||
runId: 'launcher-agent-waiting',
|
||||
actionId: `action-${'b'.repeat(24)}`,
|
||||
note: '开发者拒绝待执行工具动作',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('running / observation')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('shows queued developer agent background tasks when the agent is already running', async () => {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
## 2026-07-09 AI 游戏创作 App Runtime V1 增加单 Agent 后台任务
|
||||
|
||||
- 背景:开发用单 Agent 聊天已经能真实调用各 Agent 的 LLM 路由并持久化对话,但 Agent 仍主要表现为同步问答,用户无法明确投递一个任务让某个 Agent 独立运行,也无法同时启动多个 Agent 的工作。
|
||||
- 决策:在现有 `.agent/runtime` 和 `.agent/conversations` 基础上新增单 Agent 后台任务入口。Tauri 命令 `start_game_creator_agent_runtime_task` 立即写入该 Agent 的 runtime state/event/task history,追加用户任务到 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:Agent 按轮输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具并记录 `action / observation` 事件,再把已有 observation 放回下一轮 prompt,让 Agent 修正计划、继续行动或用空 actions + response 收束;当前后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复。完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加共享黑板,`agent.message` 写目标 Agent 对话,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列;策略要求确认或拒绝时不执行写入、运行或委派,只把策略结果作为 observation 回给 Agent。每个 Agent 的任务历史落在 `.agent/runtime/tasks/<agentId>.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,Runtime state 增加 `nextStep`,UI 在 Runtime 面板和主 Agent 状态卡展示当前任务、动作、下一步与最近任务。不同 Agent 使用独立 `.agent/runtime/locks/<agentId>.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。
|
||||
- 决策:在现有 `.agent/runtime` 和 `.agent/conversations` 基础上新增单 Agent 后台任务入口。Tauri 命令 `start_game_creator_agent_runtime_task` 立即写入该 Agent 的 runtime state/event/task history,追加用户任务到 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:Agent 按轮输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具并记录 `action / observation` 事件,再把已有 observation 放回下一轮 prompt,让 Agent 修正计划、继续行动或用空 actions + response 收束;当前后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复。完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加共享黑板,`agent.message` 写目标 Agent 对话,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列;策略拒绝时不执行工具并把 `blocked` observation 回给 Agent;策略要求确认时不执行工具,而是持久化精确待确认动作并暂停该 Agent 队列,待开发者确认或拒绝后在同一 run 续跑。每个 Agent 的任务历史落在 `.agent/runtime/tasks/<agentId>.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,Runtime state 增加 `nextStep`,UI 在 Runtime 面板和主 Agent 状态卡展示当前任务、动作、下一步与最近任务。不同 Agent 使用独立 `.agent/runtime/locks/<agentId>.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。
|
||||
- 2026-07-10 补充:后台 Runtime 每次追加 `.agent/runtime/events/<agentId>.jsonl` 后会通过 Tauri `game-creator-agent-runtime-update` 事件广播当前 `AgentRuntimeResult`;开发单 Agent 聊天页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都只把该事件作为实时 UI 通知并复用前端 runtime 归一化合并,事实源仍是 `.agent/runtime/agents`、`events` 和 `tasks` 文件。
|
||||
- 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `preview.start`,让 Agent 在完成写盘或静态自检后能按策略自行启动当前项目的 `127.0.0.1` 本地 HTTP 预览。该工具复用 `preview.start` 权限策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑;写入 `.agent/agent.db` 的审计类型为 `agent.runtime.preview.start`。发给 LLM 的 observation 只包含 localhost URL 和端口,不包含用户项目绝对路径。
|
||||
- 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `canvas.asset_generate`,让美术类 Agent 可在 loop 中自行请求生成首版美术素材。该工具读取 AppData / Tauri 配置中的 `editorApi`,复用 `canvas.asset_generate` 权限策略、项目写锁、External Editor API 生成和下载链路、manifest 资产登记以及 `canvas.asset_generate` 本地索引记录;另写 `agent.runtime.canvas.asset_generate` 记录到 `.agent/agent.db`,标明触发的 agent 与本地素材路径。API Key 不进入 prompt observation、manifest、agent.db 或日志;策略要求确认或拒绝时不会调用外部 API。
|
||||
@@ -4066,11 +4066,12 @@
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `toolPolicy`,从项目权限策略派生工具级 `allowedTools`、`autoTools`、`confirmTools` 和 `deniedTools`。后台 planning prompt 必须带入该快照,让 Agent 在规划阶段知道工具策略;执行阶段仍由 Runtime 白名单和项目权限 gate 决定。`blackboard.write` 继承 `memory.write` 策略,`agent.message` 继承 `conversation.write` 策略,`agent.delegate` 使用独立 `agent.delegate` 策略。
|
||||
- 2026-07-10 调整:`.agent/policy.json` 支持 `agentPolicies`,用规范 Agent id 保存单个 Agent 的 `deniedCommands / confirmCommands`。Runtime 计算有效工具策略时把项目级策略和 Agent 级策略叠加,项目级策略继续对所有 Agent 生效,Agent 级策略只能进一步拒绝或要求确认,不能放宽项目级策略;拒绝优先于确认。主聊天新增 `/agent-policy-deny Agent 命令`、`/agent-policy-allow Agent 命令`、`/agent-policy-confirm Agent 命令` 和 `/agent-policy-auto Agent 命令`,继续通过 `project.policy_write` 确认卡写入策略。
|
||||
- 2026-07-10 调整:后台 Agent 工具命中确认策略时不再当作 `blocked` observation 继续收尾,而是把当前 Runtime 写成 `status/phase = waiting-for-confirmation`,`waitingOn` 固定为等待开发者确认工具动作,`recentToolCalls`、事件流、任务记录和 `taskQueue.waitingForConfirmation` 都保留该事实;同一 Agent 的后台 drain 暂停,不继续消费后续 pending 任务。命中拒绝策略仍使用 `blocked` observation 交回 Agent 修正计划。
|
||||
- 2026-07-10 调整:Agent Runtime 后台任务支持按 Agent / runId 取消和重试。取消通过 `.agent/runtime/cancel/<agentId>/<runId>.json` 写入本地取消请求,并向任务 JSONL、事件流和 `agent.db` 追加 `cancelled` 审计;pending 任务被取消后不会被 drain 消费,running 任务会在当前 LLM 或工具调用返回后的检查点停止,不再继续执行工具或保存最终 assistant 回复。重试只能基于已有非 running / pending / waiting-for-confirmation 任务创建新的 run,并继续走 `agent.resume` 自动权限和同一 Agent 队列锁。
|
||||
- 2026-07-10 调整:Agent Runtime 后台任务支持按 Agent / runId 取消和重试。取消先通过 `.agent/runtime/cancel/<agentId>/<runId>.json` 写入本地取消请求;pending 任务被取消后不会被 drain 消费,running 任务在原 worker 仍持锁时只投影为 `cancelling`,必须等当前 LLM 或工具调用返回后的检查点真正停下,才由持锁 worker 向任务 JSONL、事件流和 `agent.db` 追加 `cancelled` 审计,不再继续执行工具或保存最终 assistant 回复。`cancelling` 期间禁止重试;重试只能基于已有非 running / pending / waiting-for-confirmation / cancelling 任务创建新的 run,并继续走 `agent.resume` 自动权限和同一 Agent 队列锁。
|
||||
- 2026-07-10 调整:Agent Runtime 后台任务的 `runId` 是同一 Agent 任务历史的身份,不允许复用覆盖。`start_game_creator_agent_runtime_task`、`agent.delegate` 和 retry 进入后台队列前会读取该 Agent 全量 task JSONL 历史;若调用方传入的规范化 runId 已存在,Runtime 自动追加 `-dup-<timestamp>-<attempt>` 生成实际 runId。任务队列、delegate observation 和 `agent.db` 审计都必须使用实际 runId,避免 `latest_game_creator_agent_runtime_tasks` 按 runId 去重时折叠掉不同任务。
|
||||
- 2026-07-10 调整:Agent Runtime 的 `memory.write scope=agent` 只能写当前 Agent 自己的私有记忆。若 action 指定其他 `agentId / targetAgentId`,Runtime 返回 `blocked` observation,不写目标 Agent 私有记忆、不写 `agent.runtime.memory.write` 审计;跨 Agent 共享稳定结论必须走 `blackboard.write`,给单个 Agent 留上下文必须走 `agent.message`。
|
||||
- 2026-07-10 调整:Agent Runtime 和本地对话使用 append-only JSONL 作为事实源时,进程内必须按目标文件路径串行追加整行。`.agent/agent.db`、`.agent/conversations/**/*.jsonl`、`.agent/runtime/events/*.jsonl`、`.agent/runtime/tasks/*.jsonl`、`.agent/activity.jsonl` 和 `.agent/output.jsonl` 统一走共享追加 helper,避免多个后台 Agent 并行完成时 JSON record 与换行交错。
|
||||
- 2026-07-10 调整:Agent Runtime 待确认工具动作支持确认后继续。开发者确认 `waiting-for-confirmation` run 时,Runtime 为新 run 写入一次性 `.agent/runtime/confirmations/<agentId>/<runId>/<commandId>.json` 票据并重新入队;票据同时保存工具名与输入 JSON 的 SHA-256 `actionFingerprint`,权限 gate 在 deny 之后、confirm 阶段只放行对应 Agent/run/command/fingerprint 一次,模型若把同一工具改成其他路径、checkpoint 或目标 Agent,旧票据立即失效并重新进入待确认。`recentToolCalls` 和确认审计只额外保存安全 `inputSummary`,例如相对路径、checkpoint id、目标 Agent 或内容字符数,不保存待写正文、消息正文、素材 prompt 或 API Key。开发窗口必须把 `onConfirmRuntimeTask` 接入真实“确认继续”按钮;该显式确认命令允许 `agent.resume` 处于 confirm,只继续服从 deny,自动重启恢复仍要求 `agent.resume` 为 auto。原 waiting run 追加 `completed/confirmed` 任务记录,避免队列长期显示等待确认;审计记录写 `agent.runtime.tool_confirmation.approved`。
|
||||
- 2026-07-10 调整:Agent Runtime 待确认工具动作改用 durable `AgentRuntimePendingToolAction`。Runtime 将精确 `action` 输入、当前 task/run、loop 轮次、action 序号、计划、已有 observations 与后续 loop 所需上下文先做敏感内容和项目绝对路径校验,再通过临时文件替换原子写入 `.agent/runtime/pending-actions/<agentId>/<runId>.json`;公共 runtime state 的 `pendingToolAction` 只暴露 `actionId / actionFingerprint / tool / inputSummary / reason / requestedAt` 安全摘要,完整输入不进入公共状态。`actionFingerprint` 绑定工具名、完整输入 JSON 与实际执行使用的 task context;`actionId` 还绑定 run、loop、action 序号和 occurrence nonce,使同一 run 内输入相同的两次动作仍是两个不同发生。确认和拒绝都必须匹配 `runId + actionId`,Runtime 会重算指纹并与私有落盘动作及公共摘要交叉校验,不一致时失败关闭。确认通过后在同一 run 直接执行持久化的原 action,把真实 observation 接回后续 Agent loop,不创建新 run,也不让模型重复生成待确认动作;拒绝不执行工具,写入 `blocked` observation 后在同一 run 继续规划。待确认账本按 `pending-confirmation / approved / executing / observed-approved / observed-rejected` 迁移:重启时 `approved` 可恢复精确动作,已持久化 observation 可直接续 loop,`executing` 表示外部副作用结果未知,Runtime 必须进入 `failed / needs-reconciliation` 并禁止自动重放,开发者核对项目状态后只能先取消原任务。waiting run、完整待确认动作和安全摘要均已落盘,App 重启不会越过该 run 去启动后续任务;等待期间同 Agent 新任务只保持 `pending`,确认、拒绝或取消结束后再由同一 drain 串行排空。`.agent/runtime/` 是 Runtime 私有控制面,通用 `file.list / file.read / file.write / file.delete` 不得列出、读取、修改或删除;checkpoint/index/diff/restore 继续整体排除该目录。每 Agent 锁包含唯一 token,旧持有者析构时只删除自己的锁;Linux 上其他仍存活进程的锁不会因超过固定时长被抢占。确认、拒绝及工具 observation 分别写入 `agent.runtime.tool_confirmation.approved`、`agent.runtime.tool_confirmation.rejected` 和 `agent.runtime.tool_observation` 审计;pending 和 confirmation 文件只在 observation/终态可靠落盘后清理,失败清理会显式报错。
|
||||
- 2026-07-10 调整:per-agent 互斥锁最终改用 OS 级文件锁,而不是依赖 JSON token、PID、超时和 `remove + create_new` 竞争所有权。Unix 使用非阻塞独占 `flock`,Windows 使用禁止共享的文件句柄;锁文件只保留诊断元数据并长期存在,进程退出会由 OS 释放所有权。确认、拒绝和取消必须先取得同一系统锁,再重新读取 runtime、latest task 和 durable pending action 后迁移状态;恢复入口也必须先拿锁,再读取 durable pending action 或 recoverable task,禁止用锁外旧快照覆盖并发结果。waiting 取消只短暂等待原 worker 释放系统锁,running 取消拿不到锁时只保留 tombstone,并由原 worker 在 LLM / 工具成功或失败返回后的检查点收束,不得根据 Runtime status 抢锁。这条最终实现取代上一条中的 token 删除和 Linux PID 存活判断描述。
|
||||
- 2026-07-10 调整:Agent Runtime 工具箱新增 `task.create`,用于让 Agent 把目标拆成新的 manifest 任务,而不只能更新 seed task。该工具默认 `confirm` 权限,写入前要求 taskId 唯一、依赖指向已有任务、列表长度受限,并写 `agent.runtime.task.create` 审计;策略要求确认或拒绝时不修改 `.agent/manifest.json`。
|
||||
- 2026-07-10 调整:Agent Runtime 新增 `agent.schedule_ready` 调度入口,默认 `confirm` 权限。命令会扫描 `.agent/manifest.json` 中依赖已完成且仍为 `pending` 的 ready task,先把任务标成 `running`,再用 taskId 作为 Agent id 投递到既有后台队列,source 记为 `agent-ready-task-scheduler`,并写 `agent.runtime.ready_task.scheduled` 审计;后续执行仍走原 per-agent 锁、任务 JSONL、LLM loop、工具策略和事件流,不新增独立 worker。默认确认策略下该命令不会静默调度。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、actionFingerprint、inputSummary、reason、summary、detail 和 updatedAt。状态面板展示最近动作与安全目标摘要时使用该字段,不解析 observation 文本;写入前继续过滤敏感上下文,不保存原始密钥、待写正文或任意未过滤输入。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user