Merge branch 'master' into codex/upgrade-spacetimedb-2.8.3
This commit is contained in:
@@ -1694,7 +1694,12 @@ pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_persis
|
||||
&config_path_for_request,
|
||||
)
|
||||
.map_err(platform_llm::LlmError::InvalidConfig)?;
|
||||
client.run(request).await
|
||||
request_game_creator_agent_runtime_provider_llm(
|
||||
&client,
|
||||
&llm_for_request,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => unreachable!("agent mode is normalized"),
|
||||
}
|
||||
@@ -1704,6 +1709,18 @@ pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_persis
|
||||
.await
|
||||
}
|
||||
|
||||
async fn request_game_creator_agent_runtime_provider_llm(
|
||||
client: &platform_llm::LlmClient,
|
||||
llm: &GameCreatorLlmConfig,
|
||||
request: platform_llm::LlmRunRequest,
|
||||
) -> Result<platform_llm::LlmRunResponse, platform_llm::LlmError> {
|
||||
if llm.stream {
|
||||
client.stream_run(request, |_| {}).await
|
||||
} else {
|
||||
client.run(request).await
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_transient_retries(
|
||||
root: &Path,
|
||||
provider_snapshot: &AgentRuntimeProviderRequestSnapshot,
|
||||
@@ -1748,9 +1765,8 @@ pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_transi
|
||||
request_game_creator_agent_codex_cli(request.clone()).await
|
||||
}
|
||||
GAME_CREATOR_AGENT_MODE_PROVIDER => {
|
||||
client
|
||||
.expect("provider mode constructs an HTTP client")
|
||||
.run(request.clone())
|
||||
let client = client.expect("provider mode constructs an HTTP client");
|
||||
request_game_creator_agent_runtime_provider_llm(&client, llm, request.clone())
|
||||
.await
|
||||
}
|
||||
_ => unreachable!("agent mode is normalized"),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2505,6 +2505,120 @@ async fn background_agent_runtime_executes_native_function_tool_plan() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_executes_streamed_native_function_tool_plan() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-stream-native-tool", "流式原生工具项目")
|
||||
.expect("project init");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let arguments = serde_json::json!({
|
||||
"reason": "读取项目索引",
|
||||
"input": {}
|
||||
})
|
||||
.to_string();
|
||||
let function_name = native_runtime_function_name("project.index").expect("index function");
|
||||
let base_url = spawn_mock_llm_stream_responses_with_capture(
|
||||
vec![
|
||||
native_anthropic_tool_plan_response(
|
||||
"call-stream-native-index",
|
||||
&function_name,
|
||||
&arguments,
|
||||
),
|
||||
native_anthropic_tool_plan_response(
|
||||
"call-stream-native-final",
|
||||
AGENT_RUNTIME_RESPOND_FUNCTION_NAME,
|
||||
&serde_json::json!({
|
||||
"response": "流式原生工具调用已完成聚合。STREAM_NATIVE_TOOL_OK"
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
native_anthropic_text_stream_response(
|
||||
"最终回复已通过独立流式收束请求生成。STREAM_NATIVE_FINAL_OK",
|
||||
),
|
||||
],
|
||||
Some(sender),
|
||||
);
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentMode": "provider",
|
||||
"agentLlm": {{
|
||||
"design-director": {{
|
||||
"apiKey": "stream-native-tool-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "stream-native-tool-model",
|
||||
"apiKind": "anthropic",
|
||||
"stream": true,
|
||||
"webSearchEnabled": false,
|
||||
"maxRetries": 0
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
let run_id = "design-stream-native-function-tool-run";
|
||||
|
||||
start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"用流式原生工具读取项目索引",
|
||||
run_id,
|
||||
)
|
||||
.expect("start streamed native tool task");
|
||||
|
||||
let request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("streamed native tool request");
|
||||
assert!(request.contains("POST /v1/messages HTTP/1.1"));
|
||||
assert_eq!(
|
||||
mock_http_request_json(&request)["stream"],
|
||||
Value::Bool(true)
|
||||
);
|
||||
assert!(request.contains(&function_name));
|
||||
let respond_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("streamed native respond_to_user request");
|
||||
assert!(respond_request.contains(AGENT_RUNTIME_RESPOND_FUNCTION_NAME));
|
||||
assert_eq!(
|
||||
mock_http_request_json(&respond_request)["stream"],
|
||||
Value::Bool(true)
|
||||
);
|
||||
let final_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("streamed native final-reply request");
|
||||
assert!(!final_request.contains(AGENT_RUNTIME_RESPOND_FUNCTION_NAME));
|
||||
assert_eq!(
|
||||
mock_http_request_json(&final_request)["stream"],
|
||||
Value::Bool(true)
|
||||
);
|
||||
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
|
||||
|
||||
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
|
||||
assert_eq!(runtime.status, "idle");
|
||||
assert_eq!(runtime.phase, "completed");
|
||||
assert_eq!(runtime.recent_tool_calls.len(), 1);
|
||||
assert_eq!(runtime.recent_tool_calls[0].tool, "project.index");
|
||||
assert_eq!(runtime.recent_tool_calls[0].status, "ok");
|
||||
assert_eq!(
|
||||
runtime.last_response.as_deref(),
|
||||
Some("最终回复已通过独立流式收束请求生成。STREAM_NATIVE_FINAL_OK")
|
||||
);
|
||||
let protocol_records = read_agent_db_records_for_test(&root)
|
||||
.into_iter()
|
||||
.filter(|record| {
|
||||
record["recordType"] == "agent.runtime.tool_plan.protocol" && record["runId"] == run_id
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(protocol_records.len(), 2);
|
||||
assert_eq!(protocol_records[0]["protocol"], "native_runtime_tools");
|
||||
assert_eq!(protocol_records[0]["functionCallCount"], 1);
|
||||
assert_eq!(protocol_records[0]["functionNames"][0], function_name);
|
||||
assert_eq!(
|
||||
protocol_records[1]["functionNames"][0],
|
||||
AGENT_RUNTIME_RESPOND_FUNCTION_NAME
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manual_context_compaction_is_private_and_hydrates_runtime_usage() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -335,7 +335,7 @@ async fn response_stream_disabled_keeps_direct_planning_reply_to_one_request() {
|
||||
let direct_response = "非流配置直接采用 planning response,且只发起一次请求。";
|
||||
let mock = spawn_response_stream_mock_llm_server(
|
||||
"openai_responses",
|
||||
final_tool_plan_response(direct_response),
|
||||
ResponseStreamMockPlanningResponse::NonStream(final_tool_plan_response(direct_response)),
|
||||
None,
|
||||
);
|
||||
let base_url = mock.base_url.clone();
|
||||
@@ -426,7 +426,7 @@ async fn response_stream_private_process_output_is_never_published_or_committed_
|
||||
let raw_provider_response = format!("{first_delta}{second_delta}");
|
||||
let mock = spawn_response_stream_mock_llm_server(
|
||||
"openai_responses",
|
||||
final_tool_plan_response(&planning_fallback),
|
||||
ResponseStreamMockPlanningResponse::Stream(final_tool_plan_response(&planning_fallback)),
|
||||
Some(ResponseStreamMockFinalResponse::Deltas(
|
||||
first_delta.clone(),
|
||||
second_delta.clone(),
|
||||
@@ -593,7 +593,7 @@ async fn response_stream_private_process_output_is_never_published_or_committed_
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert_eq!(
|
||||
mock_http_request_json(&requests[0])["stream"],
|
||||
Value::Bool(false)
|
||||
Value::Bool(true)
|
||||
);
|
||||
assert_eq!(
|
||||
mock_http_request_json(&requests[1])["stream"],
|
||||
@@ -629,7 +629,7 @@ async fn response_stream_final_disconnect_with_retry_disabled_fails_without_comm
|
||||
let planning_fallback = "final stream 失败后只提交这条 planning fallback。";
|
||||
let mock = spawn_response_stream_mock_llm_server(
|
||||
"openai_responses",
|
||||
final_tool_plan_response(planning_fallback),
|
||||
ResponseStreamMockPlanningResponse::Stream(final_tool_plan_response(planning_fallback)),
|
||||
Some(ResponseStreamMockFinalResponse::Disconnect),
|
||||
);
|
||||
let base_url = mock.base_url.clone();
|
||||
@@ -718,7 +718,7 @@ async fn response_stream_final_disconnect_with_retry_disabled_fails_without_comm
|
||||
);
|
||||
assert_eq!(
|
||||
mock_http_request_json(&requests[0])["stream"],
|
||||
Value::Bool(false)
|
||||
Value::Bool(true)
|
||||
);
|
||||
assert_eq!(
|
||||
mock_http_request_json(&requests[1])["stream"],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 【技术说明】AGC 接第三方 Provider 的兼容性缺陷
|
||||
|
||||
- 首次记录:2026-08-19
|
||||
- 最新核对:2026-08-25,当前实现仍保留本文所述 Provider 分发约束
|
||||
- 结论:**这不是单一策划链路的问题**。各创作流程共用同一套 Provider 分发;第三方端点必须满足当前 `agentMode`、`apiKind` 和工具调用协议约束。缺陷 4 已修复,其余限制仍按本文处理。
|
||||
- 最新核对:2026-08-27,当前实现仍保留本文所述 Provider 分发约束
|
||||
- 结论:**这不是单一策划链路的问题**。各创作流程共用同一套 Provider 分发;第三方端点必须满足当前 `agentMode`、`apiKind` 和工具调用协议约束。缺陷 4 已修复;`llm.stream=true` 时 Provider tool-plan 现在按配置发送流式请求并在后端聚合完整响应,前端展示合同不变。其余限制仍按本文处理。
|
||||
|
||||
---
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
| 2 | `codex_app_server` 模式把第三方端点喂给 codex | apiKind≠openai_responses 时秒挂;否则 413 + 工具误用,180 秒超时后留下待核对的孤儿请求 | 模式前提未被约束 |
|
||||
| 3 | `provider` 模式下 `tool_choice=required` 与 DeepSeek 思考模式互斥 | 首个 tool-plan 请求 400,整个 runtime 起不来 | 参数空间缺一个值 |
|
||||
| 4 | 普通 action 批次带 plan update 时,两条预检规则互斥 | 「更新计划 + 委派专业 Agent」同一轮返回就报「批次成员身份或顺序不匹配」 | **本分支回归**(已修) |
|
||||
| 5 | `llm.stream` 只记录配置,不驱动 Provider tool-plan 传输 | 要求 `stream=true` 的网关第一发 tool-plan 得到 HTTP 400,整轮不可用 | 传输配置失效(已修) |
|
||||
|
||||
缺陷 1~3 叠加的结果:**当前代码里没有任何一组配置能让 DeepSeek 跑起来**。缺陷 4 与 provider 无关,换成 `gpt-5.6-terra` 打通 LLM 链路后才暴露出来。
|
||||
|
||||
@@ -291,3 +292,26 @@ let expected_member_plan_update = batch
|
||||
- DeepSeek 网关 413 的具体阈值,以及 `provider` 模式下 AGC 自组的请求体是否也会触顶。
|
||||
|
||||
---
|
||||
|
||||
## 9. 缺陷 5:`llm.stream` 未作用于 Provider tool-plan(已修)
|
||||
|
||||
### 现象
|
||||
|
||||
`agentMode=provider`、`llm.stream=true` 时,审计与重试指纹记录 `stream=true`,但首个 tool-plan 仍调用 `LlmClient::run()`,请求体实际为 `stream=false`。只接受流式请求的 OpenAI 兼容网关返回 HTTP 400 `Stream must be set to true`;由于这是本地请求构造错误,重试同一请求无法恢复。
|
||||
|
||||
### 修复边界
|
||||
|
||||
Provider 的持久化重试分发与常规重试分发统一按 `llm.stream` 选择 `stream_run()` / `run()`。`stream_run()` 负责聚合文本、工具调用与终态,tool-plan 仍在响应完整后按现有协议解析、校验和交接;不把半截 tool-call 参数发布给前端,也不改变最终回复的 response-stream 合同。
|
||||
|
||||
### 回归
|
||||
|
||||
- `response_stream_uses_distinct_streamed_final_reply_for_responses_and_chat`:覆盖 Responses / Chat 两种 wire 的 tool-plan 与 final-reply 请求均发送 `stream=true`。
|
||||
- `response_stream_disabled_keeps_direct_planning_reply_to_one_request`:覆盖 `llm.stream=false` 时 tool-plan 仍发送 `stream=false` 且保持单请求直接收束。
|
||||
- `background_agent_runtime_executes_streamed_native_function_tool_plan`:覆盖 Anthropic tool-use 分片在 Shell Runtime 中聚合为原生工具动作,并完成 tool-plan 协议审计与动作执行。
|
||||
- `platform-llm` 既有 Chat / Responses 流式工具调用聚合用例继续覆盖分片工具参数装配。
|
||||
|
||||
### 升级边界
|
||||
|
||||
升级前遗留的 durable retry sidecar 若是在旧实现(审计记录 `stream=true`、实际发送 `stream=false`)期间创建,升级恢复后会按当前配置真实发送流式请求。该行为修正了配置与 wire 行为的一致性,但不保证与升级前已发出的失败请求字节一致;排查跨版本恢复时以 raw failure log 的请求快照为准。
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user