Merge branch 'stack/agent-profiles-1-seam' into stack/agent-profiles-3-wire

# Conflicts:
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
This commit is contained in:
Yichen Jiang
2026-08-09 21:35:37 +08:00
267 changed files with 12094 additions and 4835 deletions

View File

@@ -38,7 +38,6 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
await waitFor(() => {
expect(document.querySelector('[data-sample="bash"]')).not.toBeNull()
}, { timeout: 10_000 })
// Resolve the resident approval so the ordinary composer bar (which owns
// ContextMeter) resumes without replacing the session shell. This minimal
// boot graph intentionally does not mount the separate question UI plugin.

View File

@@ -12,6 +12,7 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
import type { ReplayEntry, ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import { conversationContextKey } from '@deepseek-ai/dsh-client-runtime/client'
import {
launchWebScaffold,
watchConsole,
@@ -160,6 +161,14 @@ function toolResultText(event: Extract<SessionEvent, { type: 'tool/result' }>):
.join('')
}
function messageKey(event: SessionEvent<'user/message'>): string {
return conversationContextKey('input-message', String(event.data.id))
}
function assistantKey(event: SessionEvent<'assistant/message'>): string {
return conversationContextKey('assistant-step', `${event.data.turn}:${event.data.step}`)
}
describe('web e2e: continuous conversation grown through the composer', () => {
let browser: Browser
let page: Page
@@ -222,6 +231,11 @@ describe('web e2e: continuous conversation grown through the composer', () => {
const settled = scaffold.whenTurnSettled(60_000)
await page.getByRole('button', { name: 'Send message', exact: true }).click()
await page.getByText(spec.userMarker, { exact: false }).last().waitFor({ timeout: 15_000 })
await expect.poll(() => sessionEvents.slice(eventStart).some(event => (
event.type === 'user/message'
&& event.data.source.kind === 'user'
&& userText(event).includes(spec.userMarker)
)), { timeout: 15_000 }).toBe(true)
const echoedUser = sessionEvents.slice(eventStart).find(
(event): event is SessionEvent<'user/message'> => (
event.type === 'user/message'
@@ -230,7 +244,7 @@ describe('web e2e: continuous conversation grown through the composer', () => {
),
)
if (echoedUser === undefined) throw new Error(`turn ${String(spec.index)} has no user echo event`)
const userRow = page.locator(`[data-chat-anchor-key="node:${String(echoedUser.seq)}"]`)
const userRow = page.locator(`[data-chat-anchor-key="${messageKey(echoedUser)}"]`)
await expect.poll(() => userRow.count(), { timeout: 10_000 }).toBe(1)
expect(await userRow.getAttribute('data-chat-flow-kind')).toBe('user')
expect(await userRow.textContent()).toContain(spec.userMarker)
@@ -274,9 +288,9 @@ describe('web e2e: continuous conversation grown through the composer', () => {
expect(turnEnds[0]?.data).toEqual({ turn: spec.index, reason: { kind: 'completed' } })
expect(chunks).toHaveLength(spec.deltas.length + (spec.callId === undefined ? 4 : 9))
const assistantRow = page.locator(`[data-chat-anchor-key="node:${String(finalAssistants[0]!.seq)}"]`)
const assistantRow = page.locator(`[data-chat-anchor-key="${assistantKey(finalAssistants[0]!)}"]`)
await expect.poll(() => assistantRow.count(), { timeout: 10_000 }).toBe(1)
expect(await assistantRow.getAttribute('data-chat-flow-kind')).toBe('assistant')
expect(await assistantRow.getAttribute('data-chat-flow-kind')).toBe('assistant-step')
expect(await assistantRow.textContent()).toContain(spec.doneMarker)
const calls = turnEvents.filter((event): event is SessionEvent<'tool/call'> => event.type === 'tool/call')

View File

@@ -10,6 +10,7 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
import type { ReplayEntry, ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import { conversationContextKey } from '@deepseek-ai/dsh-client-runtime/client'
import { createChatScrollFixture } from './chat-scroll-fixture.ts'
import {
launchWebScaffold,
@@ -115,6 +116,18 @@ function requiredEvent<T extends SessionEvent['type']>(
return event
}
function messageKey(event: SessionEvent<'user/message'>): string {
return conversationContextKey('input-message', String(event.data.id))
}
function assistantKey(event: SessionEvent<'assistant/message'>): string {
return conversationContextKey('assistant-step', `${event.data.turn}:${event.data.step}`)
}
function turnTailKey(turn: number): string {
return conversationContextKey('turn-tail', String(turn))
}
describe('web e2e: long Chat interaction contract', () => {
let browser: Browser
let page: Page
@@ -176,8 +189,10 @@ describe('web e2e: long Chat interaction contract', () => {
const expectedUserText = textContent(branchUserEvent.data.content)
await wheelUntilMounted(page, `[data-chat-call-id="${TARGET_CALL_2}"]`, -1_100)
const toolUserRow = page.locator(`[data-chat-anchor-key="node:${String(toolUserEvent.seq)}"]`)
const toolAssistantRow = page.locator(`[data-chat-anchor-key="node:${String(toolAssistantEvent.seq)}"]`)
const toolUserKey = messageKey(toolUserEvent)
const toolAssistantKey = assistantKey(toolAssistantEvent)
const toolUserRow = page.locator(`[data-chat-anchor-key="${toolUserKey}"]`)
const toolAssistantRow = page.locator(`[data-chat-anchor-key="${toolAssistantKey}"]`)
const call1 = page.locator(`[data-chat-call-id="${TARGET_CALL_1}"]`)
const call2 = page.locator(`[data-chat-call-id="${TARGET_CALL_2}"]`)
@@ -186,28 +201,27 @@ describe('web e2e: long Chat interaction contract', () => {
expect(await call1.count()).toBe(1)
expect(await call2.count()).toBe(1)
expect(await toolUserRow.getAttribute('data-chat-flow-kind')).toBe('user')
expect(await toolAssistantRow.getAttribute('data-chat-flow-kind')).toBe('assistant')
expect(await toolAssistantRow.getAttribute('data-chat-flow-kind')).toBe('assistant-step')
expect(await toolUserRow.textContent()).toContain(toolUserMarker)
expect(await toolAssistantRow.textContent()).toContain(toolAssistantMarker)
expect(await call1.textContent()).toContain(toolMarker1)
expect(await call2.textContent()).toContain(toolMarker2)
const expectedOrder = [
`node:${String(toolUserEvent.seq)}`,
`call:${TARGET_CALL_1}`,
`call:${TARGET_CALL_2}`,
`node:${String(toolAssistantEvent.seq)}`,
toolUserKey,
conversationContextKey('tool-call', TARGET_CALL_1),
conversationContextKey('tool-call', TARGET_CALL_2),
toolAssistantKey,
]
const actualOrder = await page.locator('[data-chat-anchor-key]').evaluateAll((rows, keys) => (
rows.map(row => (row as HTMLElement).dataset.chatAnchorKey)
.filter((key): key is string => key !== undefined && keys.includes(key))
), expectedOrder)
expect(actualOrder).toEqual(expectedOrder)
const groupKeys = await Promise.all([call1, call2].map(row => row.evaluate(element => (
element.closest<HTMLElement>('[data-chat-flow-kind="tool-group"]')?.dataset.chatFlowKey ?? null
const toolKinds = await Promise.all([call1, call2].map(row => row.evaluate(element => (
element.closest<HTMLElement>('[data-chat-flow-kind]')?.dataset.chatFlowKind ?? null
))))
expect(groupKeys[0]).not.toBeNull()
expect(groupKeys[1]).toBe(groupKeys[0])
expect(toolKinds).toEqual(['tool-call', 'tool-call'])
const summary1 = call1.locator('[data-sample="bash"]')
const summary2 = call2.locator('[data-sample="bash"]')
@@ -219,9 +233,12 @@ describe('web e2e: long Chat interaction contract', () => {
expect(await summary1.getAttribute('aria-expanded')).toBe('false')
await call2.getByText(`${toolMarker2} output line 12`, { exact: true }).waitFor({ timeout: 10_000 })
await wheelUntilMounted(page, `[data-chat-anchor-key="node:${String(branchUserEvent.seq)}"]`, -1_100)
const userRow = page.locator(`[data-chat-anchor-key="node:${String(branchUserEvent.seq)}"]`)
const assistantRow = page.locator(`[data-chat-anchor-key="node:${String(branchAssistantEvent.seq)}"]`)
const branchUserKey = messageKey(branchUserEvent)
const branchAssistantKey = assistantKey(branchAssistantEvent)
await wheelUntilMounted(page, `[data-chat-anchor-key="${branchUserKey}"]`, -1_100)
const userRow = page.locator(`[data-chat-anchor-key="${branchUserKey}"]`)
const assistantRow = page.locator(`[data-chat-anchor-key="${branchAssistantKey}"]`)
const turnTailRow = page.locator(`[data-chat-anchor-key="${turnTailKey(BRANCH_TURN)}"]`)
expect(await userRow.textContent()).toContain(branchUserMarker)
expect(await assistantRow.textContent()).toContain(branchAssistantMarker)
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
@@ -230,8 +247,8 @@ describe('web e2e: long Chat interaction contract', () => {
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText()), { timeout: 5_000 })
.toBe(expectedUserText)
await assistantRow.hover()
await assistantRow.getByRole('button', { name: 'Branch into a new conversation', exact: true }).click()
await turnTailRow.hover()
await turnTailRow.getByRole('button', { name: 'Branch into a new conversation', exact: true }).click()
await expect.poll(
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SESSION_ID)),
{ timeout: 15_000 },

View File

@@ -2,7 +2,7 @@
// Assembled search-card snapshot: boots the real built workspace client bundles
// through AppWebEntry's ModuleLoader path against the keyless
// FixtureApiClient transport (no API key, no model round), opens the fixture
// session, and pins the search card the `grep` turn (fixture turn 66) renders in
// session, and pins the search card the `grep` turn (fixture turn 67) renders in
// the assembled application. The built-boot smoke proves the graph boots but
// carries no behavior assertions by contract; this is the assembled-output check
// that a broken SearchRow registration or a dropped card would fail — the
@@ -51,7 +51,7 @@ describe('assembled search card', () => {
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
fireEvent.click(await within(tree).findByText('Fixture 历史会话'))
// Wait for chat content to reach the fixture's later turns (the bash sample
// is turn 65, the grep card turn 66).
// is turn 66, the grep card turn 67).
await waitFor(() => {
expect(document.querySelector('[data-sample="bash"]')).not.toBeNull()
}, { timeout: 10_000 })

View File

@@ -92,11 +92,15 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
return taken
}
const commandId = 'cmd-seeded-manual-compact'
const compactionId = 'compact-seeded-manual-compact'
at({
type: 'command/run',
data: { commandId, name: 'compact', args: '', source: { kind: 'user' } },
})
const startSeq = at({ type: 'compact/start', data: { turn: null } })
const startSeq = at({
type: 'compact/start',
data: { compactionId, sourceCommandId: commandId, turn: null },
})
// Load-bearing exactness: the projections subtract this count verbatim, so
// it must equal what the host's fold prices for these nodes. The estimator
// prices message CONTENT only, so a minimal wrapper per storage shape is
@@ -125,6 +129,8 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
const summarySeq = at({
type: 'compact/summary',
data: {
compactionId,
sourceCommandId: commandId,
summary: [{
type: 'text',
text: '## Cold resume compact summary\n\n- The exact summary remains available.',
@@ -143,12 +149,17 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
type: 'text',
text: '<context_checkpoint>Model-only compact checkpoint.</context_checkpoint>',
}],
source: { kind: 'plugin', plugin: 'compact' },
source: {
kind: 'plugin', plugin: 'compact', compactionId, sourceCommandId: commandId,
},
},
surfaceOp: { op: 'replace', start: first, end: last },
sourceEventSeqs: [startSeq, summarySeq, ...surfaceSeqs],
})
at({ type: 'compact/end', data: { turn: null } })
at({
type: 'compact/end',
data: { compactionId, sourceCommandId: commandId, turn: null },
})
at({
type: 'command/done',
data: {

View File

@@ -109,6 +109,7 @@ describe.skipIf(MODE === 'record')('web e2e: user-explicit skill invocation thro
{ timeout: 10_000 },
).toBe(1)
const settled = scaffold.whenTurnSettled()
await composer.fill(`/${SKILL_NAME} ${ARGS_TEXT}`)
await composer.press('Enter')
@@ -135,6 +136,7 @@ describe.skipIf(MODE === 'record')('web e2e: user-explicit skill invocation thro
// The injection started a turn; the replay adapter answers it.
await page.getByText('USER_INVOKE_REPLY', { exact: false }).first().waitFor({ timeout: 20_000 })
await settled
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)

View File

@@ -14,10 +14,10 @@
{"type":"assistant/chunk","seq":204,"time":1785013633104,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":205,"time":1785013633108,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to write a single `run_code` program that:\n1. Runs bash to echo \"CODE_ROUND_OK\"\n2. Tries to read a file \"missing.txt\" and catches the error\n3. Returns an object with both outcomes\n4. They also want me to reply \"DONE\" and stop after\n\nLet me write this program."},{"type":"tool-call","id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":8082,"outputTokens":235,"cacheReadTokens":384,"reasoningTokens":75}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204],"surfaceOp":"append"}
{"type":"tool/call","seq":206,"time":1785013633108,"data":{"turn":1,"step":1,"callId":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}}
{"type":"tool/code-dispatch-start","seq":207,"time":1785013633173,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"}}}
{"type":"tool/code-dispatch","seq":208,"time":1785013633196,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"},"isError":false,"content":[{"type":"text","text":"CODE_ROUND_OK\n"}]}}
{"type":"tool/code-dispatch-start","seq":209,"time":1785013633197,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"}}}
{"type":"tool/code-dispatch","seq":210,"time":1785013633198,"data":{"parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"},"isError":true,"content":[{"type":"text","text":"Error: cannot read \"{{cwd}}/workspace/missing.txt\": not found"}]}}
{"type":"tool/code-dispatch-start","seq":207,"time":1785013633173,"data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"}}}
{"type":"tool/code-dispatch","seq":208,"time":1785013633196,"data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"},"isError":false,"content":[{"type":"text","text":"CODE_ROUND_OK\n"}]}}
{"type":"tool/code-dispatch-start","seq":209,"time":1785013633197,"data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"}}}
{"type":"tool/code-dispatch","seq":210,"time":1785013633198,"data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"},"isError":true,"content":[{"type":"text","text":"Error: cannot read \"{{cwd}}/workspace/missing.txt\": not found"}]}}
{"type":"tool/result","seq":211,"time":1785013633201,"data":{"turn":1,"step":1,"callId":"call_00_6VNoF1gDSerTBKoCfYSH3765","content":[{"type":"text","text":"{\n \"bash\": \"CODE_ROUND_OK\",\n \"readError\": {\n \"toolName\": \"read\",\n \"message\": \"cannot read \\\"{{cwd}}/workspace/missing.txt\\\": not found\"\n }\n}"}],"isError":false},"sourceEventSeqs":[206],"surfaceOp":"append"}
{"type":"step/end","seq":212,"time":1785013633204,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":213,"time":1785013633207,"data":{"turn":1,"step":2}}

View File

@@ -2,7 +2,7 @@
// Assembled todo snapshot: boots the real built `packages/client/*/lib/
// client.js` bundles through AppWebEntry's ModuleLoader path against the
// keyless FixtureApiClient transport, opens the fixture session, and pins the
// two surfaces the fixture's parallel plan (turn 71, two items `in_progress`)
// two surfaces the fixture's parallel plan (turn 72, two items `in_progress`)
// reaches — the `todo_write` tool row and the dock's plan strip.
//
// The row is pinned as three separate fields on purpose. `summary=` is the