优化策划agent (#340)
Project CI / Repository checks (push) Successful in 2m53s
Project CI / Frontend tests (push) Successful in 3m55s
Project CI / Backend tests (push) Successful in 6m50s
Project CI / Native shell tests (push) Successful in 17m42s

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/340
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #340.
This commit is contained in:
2026-09-13 19:32:12 +08:00
committed by 孔令弘
parent 73b4eec8b7
commit 340d8241a5
14 changed files with 170 additions and 54 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.12",
"version": "0.1.27",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
+1 -1
View File
@@ -1725,7 +1725,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.12"
version = "0.1.27"
dependencies = [
"agent-runtime-core",
"axum",
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.12"
version = "0.1.27"
edition = "2021"
publish = false
@@ -229,15 +229,18 @@ fn prepare_design_decision(
}
if approved {
let phase = approve_design_phase(session, request_id)?;
let suffix = if phase == "consultant" {
""
} else {
"请开始该阶段工作。"
};
append_design_user(
session,
id,
format!("用户已批准上一阶段,现在进入 {phase} 阶段。{suffix}"),
if phase == "consultant" {
format!(
"用户已批准上一阶段,现在进入 {phase} 阶段。开始本轮工作前,先调用 get_workflow_status 确认 Runtime 当前阶段;在工具返回前,不要开始顾问工作或断言阶段状态。"
)
} else {
format!(
"用户已批准上一阶段,现在进入 {phase} 阶段。开始本轮工作前,先调用 get_workflow_status 确认 Runtime 当前阶段;在工具返回前,不要开始新阶段工作、写入文件、提交审批或断言阶段状态。"
)
},
);
begin_design_turn(session, id);
} else {
@@ -473,7 +476,7 @@ fn design_debug(root: &Path, kind: &str, data: Value) {
type Entry = (PathBuf, Value);
static QUEUE: OnceLock<std::sync::mpsc::SyncSender<Entry>> = OnceLock::new();
let sender = QUEUE.get_or_init(|| {
let (sender, receiver) = std::sync::mpsc::sync_channel::<Entry>(16);
let (sender, receiver) = std::sync::mpsc::sync_channel::<Entry>(256);
let _ = std::thread::Builder::new()
.name("design-debug".into())
.spawn(move || {
@@ -546,8 +549,26 @@ async fn request_design_provider(
None,
));
let result = if llm.stream {
let mut stream_sequence = 0_u64;
client
.stream_run(request.clone(), |delta| {
stream_sequence = stream_sequence.saturating_add(1);
design_debug(
root,
"stream",
json!({
"turnId": turn_id,
"requestIndex": session.turn.as_ref().map(|turn| turn.request_index),
"attempt": attempt,
"sequence": stream_sequence,
"occurredAtUnixNanos": unix_timestamp_nanos().to_string(),
"model": llm.model,
"deltaChars": delta.delta_text.chars().count(),
"accumulatedChars": delta.accumulated_text.chars().count(),
"deltaText": delta.delta_text,
"finishReason": delta.finish_reason,
}),
);
emit(design_event(
root,
&turn_id,
@@ -1501,7 +1501,7 @@ const GAME_CREATOR_APP_CONFIG_SCHEMA_VERSION: &str = "game-creator-config.v2";
const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://dev.genarrative.world/gpt/v1";
const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-6-astra";
const DEFAULT_GAME_CREATOR_LLM_API_KIND: &str = "openai_responses";
const DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT: &str = "max";
const DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT: &str = "high";
const DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS: u64 = 128_000;
const DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT: u64 = 64_000;
const DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT: u64 = 12_000;
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Genarrative AI Game Creator",
"version": "0.1.12",
"version": "0.1.27",
"identifier": "world.genarrative.ai-game-creator",
"build": {
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
+46 -12
View File
@@ -817,7 +817,10 @@ export function App({
null,
);
planningV2SessionRef.current = planningV2Session;
const [planningV2TransientReply, setPlanningV2TransientReply] = useState('');
const [planningV2TransientReply, setPlanningV2TransientReplyVisible] =
useState('');
const planningV2TransientReplyTargetRef = useRef('');
const planningV2VisibleReplyRef = useRef('');
const [planningV2Reasoning, setPlanningV2Reasoning] = useState('');
const planningV2TurnRef = useRef<{
projectPath: string;
@@ -838,6 +841,37 @@ export function App({
>(),
);
function setPlanningV2TransientReplyTarget(next: string) {
planningV2TransientReplyTargetRef.current = next;
if (!next) {
planningV2VisibleReplyRef.current = '';
setPlanningV2TransientReplyVisible('');
}
}
useEffect(() => {
const timer = window.setInterval(() => {
const target = planningV2TransientReplyTargetRef.current;
setPlanningV2TransientReplyVisible((current) => {
if (!target) {
planningV2VisibleReplyRef.current = '';
return '';
}
const prefix = target.startsWith(current) ? current : '';
if (prefix === target) {
planningV2VisibleReplyRef.current = target;
return target;
}
const remaining = target.length - prefix.length;
const step = remaining > 160 ? 4 : remaining > 48 ? 2 : 1;
const next = target.slice(0, prefix.length + step);
planningV2VisibleReplyRef.current = next;
return next;
});
}, 50);
return () => window.clearInterval(timer);
}, []);
function applyPlanningV2CommandResult(
result: PlanningSessionCommandResultV2,
clientTurnId?: string,
@@ -1006,7 +1040,7 @@ export function App({
};
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
setPlanningV2TransientReply('');
setPlanningV2TransientReplyTarget('');
try {
const view = await invoke<DesignView>('continue_design_agent_session', {
projectPath: nextProjectPath,
@@ -1029,7 +1063,7 @@ export function App({
setPlanGddError(message);
} finally {
designAgentTurnRef.current = null;
setPlanningV2TransientReply('');
setPlanningV2TransientReplyTarget('');
setChatAgentBusy(false);
}
}
@@ -1520,7 +1554,7 @@ export function App({
setProjectSupervisorResponseStream(null);
setProjectSupervisorRuntimeError('');
setPlanningV2Session(null);
setPlanningV2TransientReply('');
setPlanningV2TransientReplyTarget('');
setPlanningV2Active(planningStartMode);
planningV2ActiveRef.current = planningStartMode;
designAgentLaneRef.current = planningStartMode;
@@ -1914,7 +1948,7 @@ export function App({
return;
}
if (payload.status === 'started') {
setPlanningV2TransientReply(
setPlanningV2TransientReplyTarget(
payload.accumulatedText || '正在生成策划方案…',
);
setProjectSupervisorRuntimeError('');
@@ -1923,7 +1957,7 @@ export function App({
payload.status === 'delta' &&
(payload.accumulatedText || payload.deltaText)
) {
setPlanningV2TransientReply(
setPlanningV2TransientReplyTarget(
payload.accumulatedText || payload.deltaText,
);
setProjectSupervisorRuntimeError('');
@@ -1971,13 +2005,13 @@ export function App({
return;
}
if (payload.kind === 'text' && payload.text != null) {
setPlanningV2TransientReply(payload.text);
setPlanningV2TransientReplyTarget(payload.text);
}
if (payload.reasoningText != null) {
setPlanningV2Reasoning(payload.reasoningText);
}
if (payload.kind === 'tool' && payload.text) {
setPlanningV2TransientReply(payload.text);
setPlanningV2TransientReplyTarget(payload.text);
}
if (payload.view) {
applyDesignView(payload.view, payload.projectPath);
@@ -6037,7 +6071,7 @@ export function App({
};
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
setPlanningV2TransientReply('');
setPlanningV2TransientReplyTarget('');
try {
const result = currentSessionId
? await invoke<PlanningSessionCommandResultV2>(
@@ -6090,7 +6124,7 @@ export function App({
]);
} finally {
planningV2TurnRef.current = null;
setPlanningV2TransientReply('');
setPlanningV2TransientReplyTarget('');
setChatAgentBusy(false);
}
}
@@ -11790,7 +11824,7 @@ export function App({
projectPath: nextProjectPath,
clientTurnId,
};
setPlanningV2TransientReply('');
setPlanningV2TransientReplyTarget('');
setChatAgentBusy(true);
setPlanGddDecisionBusy(true);
void invoke<DesignView>('decide_design_phase', {
@@ -11831,7 +11865,7 @@ export function App({
return;
}
designAgentTurnRef.current = null;
setPlanningV2TransientReply('');
setPlanningV2TransientReplyTarget('');
setChatAgentBusy(false);
setPlanGddDecisionBusy(false);
});
@@ -25,23 +25,20 @@ type DesignAgentSurfaceProps = {
onRetry: () => void;
};
export function DesignAgentSurface({
type DesignAgentControlsProps = Pick<
DesignAgentSurfaceProps,
'view' | 'busy' | 'error' | 'onApprove' | 'onClarify' | 'onRetry'
>;
export function DesignAgentPhaseStatus({
view,
busy,
error,
onApprove,
onClarify,
onRetry,
}: DesignAgentSurfaceProps) {
const [clarifyText, setClarifyText] = useState('');
busy,
}: DesignAgentControlsProps) {
const phase = view?.session.currentPhase ?? 'concept';
const pending = view?.session.pendingApproval;
const clarification = view?.session.pendingClarification;
useLayoutEffect(() => {
setClarifyText('');
}, [clarification?.requestId]);
return (
<section className="design-agent-controls" aria-label="策划阶段控制">
<section className="design-agent-controls" aria-label="策划阶段状态">
<div className="design-agent-controls__header">
<div>
<span></span>
@@ -54,6 +51,42 @@ export function DesignAgentSurface({
) : null}
</div>
{error ? <p className="design-agent-controls__error">{error}</p> : null}
{view?.canRetry ? (
<button
className="design-agent-retry"
type="button"
disabled={busy}
onClick={onRetry}
>
<RotateCcw size={15} aria-hidden="true" />
</button>
) : null}
</section>
);
}
export function DesignAgentPendingActions({
view,
busy,
onApprove,
onClarify,
}: DesignAgentControlsProps) {
const [clarifyText, setClarifyText] = useState('');
const phase = view?.session.currentPhase ?? 'concept';
const pending = view?.session.pendingApproval;
const clarification = view?.session.pendingClarification;
useLayoutEffect(() => {
setClarifyText('');
}, [clarification?.requestId]);
if (!pending || phase === 'consultant') {
if (!clarification) return null;
}
return (
<section
className="design-agent-pending-actions"
aria-label="策划待处理事项"
>
{pending && phase !== 'consultant' ? (
<div className="design-agent-approval">
<p></p>
@@ -110,17 +143,26 @@ export function DesignAgentSurface({
</button>
</div>
) : null}
{view?.canRetry ? (
<button
className="design-agent-retry"
type="button"
disabled={busy}
onClick={onRetry}
>
<RotateCcw size={15} aria-hidden="true" />
</button>
) : null}
</section>
);
}
export function DesignAgentSurface({
view,
busy,
error,
onApprove,
onClarify,
onRetry,
}: DesignAgentSurfaceProps) {
return (
<DesignAgentPhaseStatus
view={view}
busy={busy}
error={error}
onApprove={onApprove}
onClarify={onClarify}
onRetry={onRetry}
/>
);
}
@@ -31,7 +31,10 @@ import {
ConversationModelSelect,
type ConversationModelSelectHandle,
} from './ConversationModelSelect';
import { DesignAgentSurface } from './DesignAgentSurface';
import {
DesignAgentPendingActions,
DesignAgentPhaseStatus,
} from './DesignAgentSurface';
import { PlanGddSurface } from './GddApprovalCard';
import {
pendingCommandDetail,
@@ -212,7 +215,7 @@ export function ProjectSupervisorView({
>
<div className="project-supervisor-conversation">
{designView || onDesignApprove ? (
<DesignAgentSurface
<DesignAgentPhaseStatus
view={designView}
busy={runtimePanelProps.controlBusy || planGddDecisionBusy}
error={planGddError}
@@ -393,6 +396,16 @@ export function ProjectSupervisorView({
</button>
</div>
) : null}
{designView || onDesignApprove ? (
<DesignAgentPendingActions
view={designView}
busy={runtimePanelProps.controlBusy || planGddDecisionBusy}
error={planGddError}
onApprove={onDesignApprove ?? (() => undefined)}
onClarify={onDesignClarify ?? (() => undefined)}
onRetry={onDesignRetry ?? (() => undefined)}
/>
) : null}
<form
className={`project-supervisor-composer${
directCodex ? ' is-direct-codex' : ''
@@ -52,7 +52,7 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
baseUrl: '',
model: '',
apiKind: 'openai_responses',
reasoningEffort: 'max',
reasoningEffort: 'high',
stream: true,
webSearchEnabled: true,
contextWindowTokens: 128000,
@@ -9754,6 +9754,12 @@ button.design-workspace-tree__entry:hover,
padding: 0;
}
.design-agent-pending-actions {
display: grid;
gap: 10px;
margin-top: 2px;
}
.design-agent-approval p,
.design-agent-clarify p {
margin: 0;
@@ -798,7 +798,7 @@ function createProjectSupervisorRuntimeHarness({
baseUrl: '',
model: 'quality',
apiKind: 'openai_responses',
reasoningEffort: 'max',
reasoningEffort: 'high',
stream: true,
webSearchEnabled: true,
contextWindowTokens: 128000,
@@ -1000,7 +1000,7 @@ export function registerPublishedRuntimeSettingsTests() {
expect(screen.queryByLabelText('LLM API Key')).toBeNull();
expect(screen.queryByLabelText('LLM Base URL')).toBeNull();
expect(screen.queryByLabelText('LLM 模型')).toBeNull();
expect(screen.getByLabelText('推理档')).toHaveProperty('value', 'max');
expect(screen.getByLabelText('推理档')).toHaveProperty('value', 'high');
expect(screen.getByLabelText('联网检索')).toHaveProperty('checked', true);
fireEvent.click(screen.getByRole('button', { name: /Agent 分工/ }));
expect(screen.getByText('所有角色使用统一账号服务')).not.toBeNull();
@@ -1031,7 +1031,7 @@ export function registerPublishedRuntimeSettingsTests() {
baseUrl: '',
model: '',
apiKind: 'openai_responses',
reasoningEffort: 'max',
reasoningEffort: 'high',
stream: true,
webSearchEnabled: true,
contextWindowTokens: 128000,
+1 -1
View File
@@ -95,7 +95,7 @@
},
"apps/ai-game-creator-shell": {
"name": "@genarrative/ai-game-creator-shell",
"version": "0.1.12",
"version": "0.1.27",
"dependencies": {
"@cubone/react-file-manager": "^1.35.0",
"@genarrative/image-canvas-core": "0.1.0",