conductor-oss / conductor-oss/javascript-sdk
AgentRuntime.run() returns status RUNNING for terminal executions — getResult() reads status once without waiting
- Lingua principale
- TypeScript
- Stelle
- 58
- Fork
- 20
- Merge medio
- 1g 13h
- PR unite (30g)
- 7
Descrizione
## 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.
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Direzione di ricerca
Start at AgentStream.getResult() and AgentRuntime.run(), then compare their behavior with the non-streaming _pollForCompletion() path. Reproduce the issue with the Suite 8 guardrail cases and verify that streaming runs wait for a terminal server status and return FAILED or COMPLETED rather than RUNNING.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- typescript
- Ambito
- api, testing
- Tipo di issue
- Bug
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Stato di attività
- Tranquilla
- Chiarezza
- Specificata chiaramente
- Idoneità per principianti
- 74/100