mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 08:02:37 +00:00
fix: keep structured session status current
This commit is contained in:
+37
-1
@@ -3,7 +3,10 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
||||
import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope'
|
||||
import type { AgentSessionMutationEnvelope } from '../../../shared/agent-session-wire'
|
||||
import type {
|
||||
AgentSessionMutationEnvelope,
|
||||
AgentSessionStatusEvent
|
||||
} from '../../../shared/agent-session-wire'
|
||||
import { AgentSessionRecordStore } from '../../runtime/agent-session-record-store'
|
||||
import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter'
|
||||
import { AgentSessionOptionRejectedError } from './structured-agent-session-option-error'
|
||||
@@ -212,6 +215,39 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('structured session options and close', () => {
|
||||
it('publishes an acknowledged model without waiting for journal traffic', async () => {
|
||||
const body = {
|
||||
kind: 'message' as const,
|
||||
role: 'user' as const,
|
||||
blocks: [{ type: 'text' as const, text: 'first task' }]
|
||||
}
|
||||
await host.send(CALLER, {
|
||||
envelope: envelope('agentSession.send', { body }),
|
||||
body
|
||||
})
|
||||
const events: AgentSessionStatusEvent[] = []
|
||||
host.subscribeStatus({ id: 'session-list', emit: (event) => events.push(event) })
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: 'snapshot',
|
||||
sessions: [expect.objectContaining({ status: 'idle', model: DEFAULT_MODEL })]
|
||||
}
|
||||
])
|
||||
const fields = { key: 'model', value: PICKED_MODEL }
|
||||
|
||||
await host.setOption(CALLER, {
|
||||
envelope: envelope('agentSession.setOption', fields),
|
||||
...fields
|
||||
})
|
||||
|
||||
expect(events.slice(1)).toEqual([
|
||||
{
|
||||
type: 'status',
|
||||
session: expect.objectContaining({ sessionId: SESSION, model: PICKED_MODEL })
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('settles a pre-mutation rejection so a fresh retry can succeed', async () => {
|
||||
optionFailure = new AgentSessionOptionRejectedError('model list unavailable')
|
||||
const fields = { key: 'model', value: PICKED_MODEL }
|
||||
|
||||
@@ -23,5 +23,6 @@ export async function performSetOption(
|
||||
throw error
|
||||
}
|
||||
await ctx.persistOptions(applied ?? { [input.key]: input.value })
|
||||
ctx.publish()
|
||||
return { ok: true, value: { ...input, ...(applied ? { options: { ...applied } } : {}) } }
|
||||
}
|
||||
|
||||
@@ -86,16 +86,16 @@ describe('structured agent session status projection', () => {
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: 'look at the sidebar' }]
|
||||
})
|
||||
const said = item('said', 2, {
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text: 'Reading the card first.' }]
|
||||
})
|
||||
const running = item('running', 3, {
|
||||
const running = item('running', 2, {
|
||||
kind: 'status',
|
||||
text: 'Working',
|
||||
turnLifecycle: { turnId: 'turn-1', state: 'running' }
|
||||
})
|
||||
const said = item('said', 3, {
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text: 'Reading the card first.' }]
|
||||
})
|
||||
const tool = item('tool', 4, {
|
||||
kind: 'tool-call',
|
||||
name: 'Read',
|
||||
@@ -103,7 +103,7 @@ describe('structured agent session status projection', () => {
|
||||
state: 'running'
|
||||
})
|
||||
|
||||
expect(projectStructuredAgentSessionStatusSummary([ask, said, running, tool])).toEqual({
|
||||
expect(projectStructuredAgentSessionStatusSummary([ask, running, said, tool])).toEqual({
|
||||
status: 'working',
|
||||
latestPrompt: 'look at the sidebar',
|
||||
toolName: 'Read',
|
||||
@@ -112,6 +112,30 @@ describe('structured agent session status projection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('clears the previous answer as soon as the next prompt is persisted', () => {
|
||||
const firstAsk = item('first-ask', 1, {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: 'first task' }]
|
||||
})
|
||||
const previousAnswer = item('previous-answer', 2, {
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text: 'The first task is done.' }]
|
||||
})
|
||||
const nextAsk = item('next-ask', 3, {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: 'second task' }]
|
||||
})
|
||||
expect(projectStructuredAgentSessionStatusSummary([firstAsk, previousAnswer, nextAsk])).toEqual(
|
||||
{
|
||||
status: 'idle',
|
||||
latestPrompt: 'second task'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('reports no tool line once the turn settles, even with an abandoned running call', () => {
|
||||
const ask = item('ask', 1, {
|
||||
kind: 'message',
|
||||
|
||||
@@ -192,13 +192,16 @@ export function latestStructuredAgentSessionPrompt(
|
||||
return ''
|
||||
}
|
||||
|
||||
/** The newest assistant prose. Tool-only assistant items are skipped — they carry
|
||||
* no words, and stopping at one would blank a row that has something to say. */
|
||||
/** The newest assistant prose in the latest user turn. Tool-only assistant items
|
||||
* are skipped; the user boundary clears prose from the preceding turn. */
|
||||
export function latestStructuredAgentSessionAssistantMessage(
|
||||
items: readonly AgentJournalRenderItem[]
|
||||
): string {
|
||||
for (let index = items.length - 1; index >= 0; index -= 1) {
|
||||
const body = items[index]?.body
|
||||
if (body?.kind === 'message' && body.role === 'user') {
|
||||
return ''
|
||||
}
|
||||
if (body?.kind === 'message' && body.role === 'assistant') {
|
||||
const prose = messageProse(body.blocks)
|
||||
if (prose.trim()) {
|
||||
|
||||
Reference in New Issue
Block a user