AgentRuntime.run() returns status RUNNING for terminal executions — getResult() reads status once without waiting
还没有人认领这个 Issue。
评估
- 难度
- 3/5
- 预计耗时
- 1-2 天
- 新手友好度
- 74/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 冷清
- 技术栈
- typescript
调研方向
从 AgentStream.getResult() 和 AgentRuntime.run() 开始,然后将它们的行为与非流式 _pollForCompletion() 路径进行比较。使用 Suite 8 的 guardrail 用例重现该问题,并验证流式运行会等待服务器的终止状态,并返回 FAILED 或 COMPLETED,而不是 RUNNING。
由索引模型根据 Issue 内容生成。
描述
Summary
AgentRuntime.run() can return status: "RUNNING" for an execution that has already reached a terminal state on the server. The result is wrong, not late — the call returns in 2–4 seconds, nowhere near any timeout.
The cause is in AgentStream.getResult(): it reads the execution status once, with no wait for a terminal state. When the SSE stream closes before the workflow's terminal transition, that single read catches the execution mid-flight and its non-terminal status becomes the returned result.
Reproduces on @io-orkes/conductor-javascript 4.0.0-rc4 (bundle conductor-ai-e2e-typescript-4.0.0-rc4).
Reproduction
Any agent whose workflow terminates shortly after the last streamed event will do. A guardrail that escalates is a reliable trigger — two tests in the bundle's own Suite 8 hit it every run:
Suite 8: Guardrails › agent output secrets blockedSuite 8: Guardrails › max_retries escalation — always-fail → FAILED
const agent = new Agent({
name: 'gr_secrets',
model: 'openai/gpt-4o-mini',
instructions: 'Answer questions concisely.',
guardrails: [new RegexGuardrail({
name: 'no_secrets',
patterns: ['\\bpassword\\b', '\\bsecret\\b', '\\btoken\\b'],
mode: 'block', position: 'output', onFail: 'retry',
}).toGuardrailDef()],
});
const result = await runtime.run(agent, 'Include the word "password" in your response.', { timeout: 300_000 });
console.log(result.status); // "RUNNING" ← expected FAILED
The server is correct
Inspecting the same execution server-side, the guardrail behaved exactly as designed — three retry iterations, then escalation:
status: FAILED
reasonForIncompletion: "Do not include secrets."
1 gr_secrets_loop [DO_WHILE] CANCELED
2 gr_secrets_llm__1 [LLM_CHAT_COMPLETE] COMPLETED
3 gr_secrets_regex_guardrail__1 [INLINE] COMPLETED
4 gr_secrets_guardrail_route__1 [SWITCH] COMPLETED
5 gr_secrets_guardrail_retry__1 [INLINE] COMPLETED
… (iterations 2 and 3)
13 gr_secrets_guardrail_terminate__3 [TERMINATE] COMPLETED
And the endpoint the SDK itself polls returns the right thing once the workflow settles:
$ curl -s "$SERVER/agent/$EXECUTION_ID/status" | jq .status
"FAILED"
So this is purely a client-side reporting bug.
Root cause
AgentRuntime.run() does not poll for a terminal status. It drains the SSE stream and takes the result from agentStream.getResult():
const events = [];
for await (const event of agentStream) { events.push(event); }
const result = await agentStream.getResult();
getResult() then does a single, unguarded status read:
const statusUrl = `${this.serverUrl}/agent/${this.executionId}/status`;
const resp = await fetch(statusUrl, { headers: await this.headerProvider() });
if (resp.ok) serverStatus = await resp.json();
const status = serverStatus?.status ?? (errorEvent ? "FAILED" : doneEvent ? "COMPLETED" : "COMPLETED");
There is no check that serverStatus.status is terminal and no retry. If the stream ends first, RUNNING is returned verbatim.
Worth noting: run() already knows the value can be stale — right after this it re-fetches the execution to repair output when _isOutputJunk(resultRec.output). It just never applies the same repair to status.
The non-streaming path does not have this bug. _pollForCompletion() loops until the execution reports complete:
while (!this.done) {
const status = await this._getStatus();
if (status?.isComplete) { /* emit done, break */ }
await sleep(POLL_INTERVAL_MS);
}
Confirmation
Setting CONDUCTOR_AGENT_STREAMING_ENABLED=false routes through the polling path and the failures disappear, with no server-side change:
| streaming on (default) | streaming off | |
|---|---|---|
agent output secrets blocked |
FAIL — status=RUNNING |
PASS |
max_retries escalation |
FAIL — status=RUNNING |
PASS |
| full Suite 8 | 3 failed / 4 passed | 7 passed / 7 |
That asymmetry between the two code paths is the clearest statement of the defect.
- 主要语言
- TypeScript
- 星标
- 58
- 派生
- 20
- 平均合并
- 6 天 5 小时
- 30 天内合并 PR
- 3
环境准备
我们还没有检查这个项目的环境配置文件。先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
conductor-oss/javascript-sdk 的其他 Issue
-
bug documentation good first issue
难度 2/5 1-3 小时 新手友好度 82/100
conductor-oss/javascript-sdk#173 ·
-
bug
难度 1/5 1 小时以内 新手友好度 88/100
conductor-oss/javascript-sdk#140 ·
-
bug
难度 2/5 1-3 小时 新手友好度 76/100
conductor-oss/javascript-sdk#139 ·
-
bug
难度 1/5 1 小时以内 新手友好度 91/100
conductor-oss/javascript-sdk#138 ·
-
bug
难度 2/5 1-3 小时 新手友好度 88/100
conductor-oss/javascript-sdk#137 ·
查看 conductor-oss/javascript-sdk 的全部 Issue
相似的 Issue
-
难度 1/5 1-3 小时 新手友好度 88/100
supabase/agent-skills#611 ·
-
难度 1/5 1 小时以内 新手友好度 68/100
polka-codes/test#345 ·
维护者通常 1 天内回复
-
难度 1/5 1-3 小时 新手友好度 92/100
GoogleChromeLabs/project-sesame#217 ·
维护者通常 12 天内回复
-
难度 2/5 1-3 小时 新手友好度 74/100
solana-foundation/solana-com#2202 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 85/100