Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b53f7f7
Simplify harness ingress and async run::start path.
ytallo May 20, 2026
160e87b
Simplify turn_state trigger adapters with Zod boundary parsing.
ytallo May 20, 2026
59c79f5
Colocate turn-orchestrator registration with handler modules.
ytallo May 20, 2026
1bffc94
Remove isStepableRecordWrite wrapper and inline parseStepableWrite in…
ytallo May 20, 2026
11c95b1
Split stepOnStepableWrite from the raw event adapter so parse narrowi…
ytallo May 20, 2026
49f37e6
Refactor turn-orchestrator to streamline state handling and improve t…
ytallo May 21, 2026
0cb0c82
Enqueue turn wakes on the turn-step FIFO queue and gate durable steps.
ytallo May 22, 2026
c983544
Split turn-orchestrator into per-state queue handlers.
ytallo May 22, 2026
98765d4
Merge origin/main into feat/harness-trigger-run-start-simplify
ytallo May 22, 2026
4467fab
style: apply biome formatting to register and steering test
ytallo May 23, 2026
829b60e
refactor: rename message_end to message_complete and update related t…
ytallo May 24, 2026
360ed05
chore: remove pnpm-lock.yaml file
ytallo May 24, 2026
2f912dc
refactor: consolidate event translation logic into createAgentEventTr…
ytallo May 24, 2026
624dac1
delete: remove harness-flow.html file
ytallo May 24, 2026
cdf44e1
chore: ignore local harness-node build output
ytallo May 25, 2026
92d13c6
style: apply biome 2.4.10 formatting to turn-orchestrator files
ytallo May 25, 2026
b93ad52
docs(harness): document harness::trigger flat run::start ingress
ytallo May 25, 2026
608742e
Merge remote-tracking branch 'origin/main' into feat/harness-trigger-…
ytallo May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ harness/config.yaml
.worktrees/
CLAUDE.md
data/
docs/superpowers/
docs/superpowers/

# Local scratch (untracked, not part of any branch)
harness-node/
2 changes: 1 addition & 1 deletion acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ connection at startup and translates each event:
|---|---|
| `message_update { llm_event: text_delta }` | `agent_message_chunk` |
| `message_update { llm_event: thinking_delta }` | `agent_thought_chunk` |
| `message_end` (assistant role, full text) | `agent_message_chunk` (one shot) |
| `message_complete` (assistant role, full text) | `agent_message_chunk` (one shot) |
| `tool_execution_start` | `tool_call` (status: `in_progress`) |
| `tool_execution_end` | `tool_call_update` (status: `completed`/`failed`) |
| other | dropped |
Expand Down
25 changes: 11 additions & 14 deletions acp/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,14 +800,11 @@ fn translate_agent_event(event: &Value) -> Option<Vec<Value>> {
"content": { "type": "text", "text": delta },
})])
}
// turn-orchestrator (current head) does not emit message_update
// text deltas — provider-router consumes the streaming response
// internally and returns the fully-assembled assistant message,
// surfaced as a single message_end event. Translate those to one
// agent_message_chunk per text content block so Zed renders the
// full reply. message_start and tool-result message_end variants
// are dropped to avoid duplication.
"message_end" => {
// Batch/non-delta clients receive the fully-assembled assistant
// message as a single message_complete event. Translate those to
// one agent_message_chunk per text content block so Zed renders the
// full reply.
"message_complete" => {
let message = event.get("message")?;
if message.get("role").and_then(|v| v.as_str()) != Some("assistant") {
return None;
Expand Down Expand Up @@ -1042,14 +1039,14 @@ mod tests {

#[test]
fn translate_unknown_event_drops_silently() {
assert!(translate_agent_event(&json!({ "type": "agent_start" })).is_none());
assert!(translate_agent_event(&json!({ "type": "turn_start" })).is_none());
assert!(translate_agent_event(&json!({ "type": "not_a_real_event" })).is_none());
assert!(translate_agent_event(&json!({ "type": "message_start" })).is_none());
}

#[test]
fn translate_message_end_assistant_emits_chunk() {
fn translate_message_complete_assistant_emits_chunk() {
let ev = json!({
"type": "message_end",
"type": "message_complete",
"message": {
"role": "assistant",
"content": [{ "type": "text", "text": "hi there" }],
Expand All @@ -1064,9 +1061,9 @@ mod tests {
}

#[test]
fn translate_message_end_user_dropped() {
fn translate_message_complete_user_dropped() {
let ev = json!({
"type": "message_end",
"type": "message_complete",
"message": {
"role": "user",
"content": [{ "type": "text", "text": "x" }],
Expand Down
14 changes: 5 additions & 9 deletions console/web/src/lib/backend/real.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
AgentMessage,
SessionEventEnvelope,
} from '@/types/iii-agent-event'
import { createTurnStateTranslator, translateAgentEvent } from './translate'
import { createAgentEventTranslator } from './translate'
import type {
ChatBackend,
ChatStreamOptions,
Expand Down Expand Up @@ -75,7 +75,7 @@ async function* realStream(
})
subscribed = true

const turnStateTranslator = createTurnStateTranslator()
const { translate } = createAgentEventTranslator()

client
.call<Record<string, unknown> | null>('turn::get_state', {
Expand All @@ -101,7 +101,6 @@ async function* realStream(
let kickoffError: Error | null = null
client
.call('harness::trigger', {
function_id: 'run::start',
session_id: sessionId,
message_id: messageId,
payload: {
Expand All @@ -123,7 +122,7 @@ async function* realStream(
.catch((err) => {
kickoffError = err instanceof Error ? err : new Error(String(err))
if (import.meta.env.DEV) {
console.warn('[real-backend] harness::trigger run::start failed', err)
console.warn('[real-backend] harness::trigger failed', err)
}
wake()
})
Expand All @@ -134,7 +133,7 @@ async function* realStream(
const err = kickoffError as Error
yield {
kind: 'assistant-token',
token: `harness::trigger run::start failed — ${err.message}`,
token: `harness::trigger failed — ${err.message}`,
}
yield { kind: 'assistant-end' }
return
Expand All @@ -148,10 +147,7 @@ async function* realStream(
if (kickoffError) continue
const event = queue.shift()
if (!event) continue
const streamEvents =
event.type === 'turn_state_changed'
? turnStateTranslator(event, sessionId)
: translateAgentEvent(event, sessionId)
const streamEvents = translate(event, sessionId)
for (const streamEvent of streamEvents) {
yield streamEvent
}
Expand Down
104 changes: 72 additions & 32 deletions console/web/src/lib/backend/translate.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest'
import type { AgentEvent } from '@/types/iii-agent-event'
import { createTurnStateTranslator, translateAgentEvent } from './translate'
import { createAgentEventTranslator } from './translate'

describe('createAgentEventTranslator — message_complete', () => {
const { translate } = createAgentEventTranslator()

describe('translateAgentEvent — message_end stop_reason surfacing', () => {
const baseAssistant = {
role: 'assistant' as const,
content: [{ type: 'text' as const, text: 'partial reply…' }],
Expand All @@ -11,34 +13,54 @@ describe('translateAgentEvent — message_end stop_reason surfacing', () => {
timestamp: 0,
}

it('emits ONLY assistant-end for a clean stop_reason="end"', () => {
it('emits ONLY assistant-end for a clean stop_reason="end" when body was streamed', () => {
const event: AgentEvent = {
type: 'message_end',
type: 'message_complete',
message: { ...baseAssistant, stop_reason: 'end' },
body_streamed: true,
}
expect(translateAgentEvent(event)).toEqual([{ kind: 'assistant-end' }])
expect(translate(event)).toEqual([{ kind: 'assistant-end' }])
})

it('emits assistant-token blocks and assistant-end for a non-streamed batch message', () => {
const event: AgentEvent = {
type: 'message_complete',
message: {
...baseAssistant,
stop_reason: 'end',
content: [{ type: 'text', text: 'hello batch' }],
},
body_streamed: false,
}
expect(translate(event)).toEqual([
{ kind: 'assistant-token', token: 'hello batch' },
{ kind: 'assistant-end' },
])
})

it('emits assistant-end + stop-reason notice when the turn hit max_tokens (stop_reason="length")', () => {
const event: AgentEvent = {
type: 'message_end',
type: 'message_complete',
message: { ...baseAssistant, stop_reason: 'length' },
body_streamed: true,
}
const out = translateAgentEvent(event)
const out = translate(event)
expect(out[0]).toEqual({ kind: 'assistant-end' })
expect(out[1]).toMatchObject({ kind: 'stop-reason', reason: 'length' })
})

it('emits assistant-end + stop-reason notice carrying error_message when stop_reason="error"', () => {
const event: AgentEvent = {
type: 'message_end',
type: 'message_complete',
message: {
...baseAssistant,
stop_reason: 'error',
error_message: 'lmstudio stream closed mid-response after ~3214 output tokens',
error_message:
'lmstudio stream closed mid-response after ~3214 output tokens',
},
body_streamed: true,
}
const out = translateAgentEvent(event)
const out = translate(event)
expect(out[0]).toEqual({ kind: 'assistant-end' })
expect(out[1]).toEqual({
kind: 'stop-reason',
Expand All @@ -49,45 +71,50 @@ describe('translateAgentEvent — message_end stop_reason surfacing', () => {

it('emits assistant-end + stop-reason on abort', () => {
const event: AgentEvent = {
type: 'message_end',
type: 'message_complete',
message: { ...baseAssistant, stop_reason: 'aborted' },
body_streamed: true,
}
const out = translateAgentEvent(event)
const out = translate(event)
expect(out).toHaveLength(2)
expect((out[1] as { kind: string; reason: string }).reason).toBe('aborted')
})

it('does NOT emit a stop-reason notice for function_call (turn will continue)', () => {
const event: AgentEvent = {
type: 'message_end',
type: 'message_complete',
message: { ...baseAssistant, stop_reason: 'function_call' },
body_streamed: true,
}
expect(translateAgentEvent(event)).toEqual([{ kind: 'assistant-end' }])
expect(translate(event)).toEqual([{ kind: 'assistant-end' }])
})

it('returns [] for non-assistant message_end (user/function_result messages)', () => {
it('returns [] for non-assistant message_complete (user/function_result messages)', () => {
const event: AgentEvent = {
type: 'message_end',
type: 'message_complete',
message: {
role: 'user',
content: [{ type: 'text', text: 'hi' }],
timestamp: 0,
},
}
expect(translateAgentEvent(event)).toEqual([])
expect(translate(event)).toEqual([])
})

it('omits the error_message field when none was provided', () => {
const event: AgentEvent = {
type: 'message_end',
type: 'message_complete',
message: { ...baseAssistant, stop_reason: 'length' },
body_streamed: true,
}
const out = translateAgentEvent(event)
const out = translate(event)
expect(out[1]).toEqual({ kind: 'stop-reason', reason: 'length' })
})
})

describe('translateAgentEvent — compaction_done', () => {
describe('createAgentEventTranslator — compaction_done', () => {
const { translate } = createAgentEventTranslator()

it('translates compaction_done to a single compaction StreamEvent carrying the summary + tokens_before', () => {
const event: AgentEvent = {
type: 'compaction_done',
Expand All @@ -97,7 +124,7 @@ describe('translateAgentEvent — compaction_done', () => {
compaction_entry_id: 'entry-c-1',
tail_start_id: 'entry-t-1',
}
expect(translateAgentEvent(event, 'sess-1')).toEqual([
expect(translate(event, 'sess-1')).toEqual([
{
kind: 'compaction',
mode: 'async',
Expand All @@ -118,7 +145,7 @@ describe('translateAgentEvent — compaction_done', () => {
compaction_entry_id: 'e',
tail_start_id: null,
}
const out = translateAgentEvent(event, 'sess-x')
const out = translate(event, 'sess-x')
expect(out).toHaveLength(1)
expect(
(out[0] as { kind: 'compaction'; mode: 'sync' | 'async' }).mode,
Expand All @@ -134,16 +161,17 @@ describe('translateAgentEvent — compaction_done', () => {
compaction_entry_id: 'e',
tail_start_id: null,
}
const out = translateAgentEvent(event, 'sess-y')
const out = translate(event, 'sess-y')
expect(
(out[0] as { kind: 'compaction'; tailStartId: string | null }).tailStartId,
(out[0] as { kind: 'compaction'; tailStartId: string | null })
.tailStartId,
).toBeNull()
})
})

describe('createTurnStateTranslator', () => {
describe('createAgentEventTranslator — turn_state_changed', () => {
it('emits fcall-start { pendingApproval: true } when a new entry appears', () => {
const translate = createTurnStateTranslator()
const { translate } = createAgentEventTranslator()
const event: AgentEvent = {
type: 'turn_state_changed',
event_type: 'state:updated',
Expand Down Expand Up @@ -175,7 +203,7 @@ describe('createTurnStateTranslator', () => {
})

it('emits nothing when the awaiting_approval list is unchanged', () => {
const translate = createTurnStateTranslator()
const { translate } = createAgentEventTranslator()
const same = {
state: 'function_awaiting_approval',
awaiting_approval: [
Expand Down Expand Up @@ -204,7 +232,7 @@ describe('createTurnStateTranslator', () => {
})

it('emits nothing when state leaves function_awaiting_approval (the orchestrator emits the matching function_execution_end)', () => {
const translate = createTurnStateTranslator()
const { translate } = createAgentEventTranslator()
translate(
{
type: 'turn_state_changed',
Expand All @@ -227,7 +255,11 @@ describe('createTurnStateTranslator', () => {
old_value: {
state: 'function_awaiting_approval',
awaiting_approval: [
{ function_call_id: 'fc-1', function_id: 'shell::shell', args: {} },
{
function_call_id: 'fc-1',
function_id: 'shell::shell',
args: {},
},
],
},
},
Expand All @@ -237,20 +269,28 @@ describe('createTurnStateTranslator', () => {
})

it('partitions mirrors by sessionId so two chats do not interfere', () => {
const translate = createTurnStateTranslator()
const { translate } = createAgentEventTranslator()
const pending = {
state: 'function_awaiting_approval',
awaiting_approval: [
{ function_call_id: 'fc-1', function_id: 'shell::shell', args: {} },
],
}
translate(
{ type: 'turn_state_changed', event_type: 'state:created', new_value: pending },
{
type: 'turn_state_changed',
event_type: 'state:created',
new_value: pending,
},
'sess-a',
)
expect(
translate(
{ type: 'turn_state_changed', event_type: 'state:created', new_value: pending },
{
type: 'turn_state_changed',
event_type: 'state:created',
new_value: pending,
},
'sess-b',
),
).toHaveLength(1)
Expand Down
Loading
Loading