conductor-oss / conductor-oss/javascript-sdk
AgentRuntime.run() returns status RUNNING for terminal executions — getResult() reads status once without waiting
- 主要言語
- TypeScript
- スター
- 58
- フォーク
- 20
- 平均マージ
- 1日 13時間
- マージ済み PR(30日)
- 7
説明
## 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 blocked`
- `Suite 8: Guardrails › max_retries escalation — always-fail → FAILED`
```ts
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:
```console
$ 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()`:
```js
const events = [];
for await (const event of agentStream) { events.push(event); }
const result = await agentStream.getResult();
```
`getResult()` then does a **single, unguarded** status read:
```js
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:
```js
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.
コントリビューションガイド
このリポジトリのコントリビューションガイドは索引されていません
調査の方向性
AgentStream.getResult() と AgentRuntime.run() から始め、ストリーミングを使用しない _pollForCompletion() パスと動作を比較します。Suite 8 の guardrail ケースで問題を再現し、ストリーミング実行がサーバーの終端状態を待機し、RUNNING ではなく FAILED または COMPLETED を返すことを確認します。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- typescript
- 領域
- api, testing
- issue の種類
- バグ
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 活発さ
- 静か
- 明瞭さ
- 明確に書かれている
- 初心者へのやさしさ
- 74/100