feat: let agents read supervisor story graphs

Expose saved decisions, timelines and source evidence through read-only Story Graph tools across Model, Harness and local/remote OpenCode and Continue. Supervisor enablement and runtime assignments govern discovery and calls; reads do not invoke models or modify stored evidence. Remote use requires matching Desktop and Agent changes.

Make Continue discussion show the complete editable message with a separated destination and shared action layout. Sending uses the visible draft without reattaching removed context.

Validation: existing focused MCP, runtime, controlled Linux Host, discussion component and Electron layout checks plus typechecks; staged diff check passed. Cross-scope fact reuse, historical reconstruction and full package acceptance remain deferred.

Release note: Agents can consult saved supervisor decisions and their sources when continuing work. Continue discussion now lets you review and edit the complete message before sending.
This commit is contained in:
mesalogo
2026-09-28 12:38:00 +08:00
parent 83a3ea261e
commit 66f77b83c2
61 changed files with 1527 additions and 150 deletions
@@ -634,6 +634,36 @@ describe('RuntimeAcpBackend', () => {
expect(fixture.lifecycle).not.toContain('bridge-listen')
} finally { await fixture.backend.dispose() }
})
it('binds Story Graph Ask discovery and reads to authenticated blob frames', async () => {
const fixture = harness()
const client = new Client({ name: 'graph-backend-test', version: '1' })
try {
await open(fixture)
const accepted = await invoke(fixture, 'runtime/preparePrompt', fixture.preparation({
imageTool: { channelId: 'graph-channel', channelEpoch: '1', storyGraph: true }
})) as { imageToolUrl: string }
await client.connect(new StreamableHTTPClientTransport(new URL(accepted.imageToolUrl)))
const listing = client.listTools()
await vi.waitFor(() => expect(fixture.blobSink).toHaveBeenCalledOnce())
const reply = async (index: number, storyGraphResult: object) => {
const frame = fixture.blobSink.mock.calls[index]![0]
const call = remoteImageToolCallSchema.parse(decodeRemoteImageToolMessage(frame.payload))
const payload = encodeRemoteImageToolMessage({ callId: call.callId, storyGraphResult })
await fixture.backend.onBlobFrame({ header: { ...frame.header, direction: 'main-to-agent', payloadLength: payload.byteLength }, payload },
{ ...fixture.context, channelId: 'graph-channel' })
return call
}
expect(await reply(0, { available: true })).toMatchObject({ name: 'story_graph_list' })
expect((await listing).tools).toHaveLength(3)
const reading = client.callTool({ name: 'story_graph_search', arguments: { query: 'Decision' } })
await vi.waitFor(() => expect(fixture.blobSink).toHaveBeenCalledTimes(2))
expect(await reply(1, { items: [], coverage: { status: 'unknown' } })).toMatchObject({ name: 'story_graph_search', input: { query: 'Decision' } })
expect(await reading).not.toHaveProperty('isError', true)
await invoke(fixture, 'runtime/completePrompt', { bindingId: 'binding-1', operationId: 'request-1', requestId: 'request-1' })
await expect(fetch(accepted.imageToolUrl)).rejects.toThrow()
} finally { await client.close(); await fixture.backend.dispose() }
})
it.each([false, true])('allows mode changes only on a shared Session (shared=%s)', async (shared) => {
const fixture = harness({ agentOwned: true, shareOwnedProcesses: shared, workMode: 'execute' })
try {