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
@@ -12,8 +12,9 @@ export class MainImageToolSession {
constructor(
private readonly channel: RuntimeProtocolBinaryChannel,
private readonly binding: ImageToolBinding,
signal?: AbortSignal
private readonly binding: ImageToolBinding | undefined,
signal?: AbortSignal,
private readonly storyGraph?: import('../agent/knowledge-mcp-gateway').StoryGraphRemoteBinding
) {
this.unsubscribe = channel.onClose(() => this.close())
const abort = (): void => this.close()
@@ -45,9 +46,17 @@ export class MainImageToolSession {
let reply: ReturnType<typeof remoteImageToolReplySchema.parse>
try {
this.wait.signal.throwIfAborted()
if (this.binding.context.workMode !== 'execute') throw new Error('Image tools are unavailable in Ask mode')
const result = await this.binding.call(call.input, call.callId, this.wait.signal)
reply = remoteImageToolReplySchema.parse({ callId: call.callId, result })
if ('name' in call) {
if (!this.storyGraph) throw new Error('Story Graph capability unavailable')
const storyGraphResult = call.name === 'story_graph_list'
? { available: await this.storyGraph.available() }
: await this.storyGraph.call(call.name, call.input, this.wait.signal)
reply = remoteImageToolReplySchema.parse({ callId: call.callId, storyGraphResult })
} else {
if (!this.binding || this.binding.context.workMode !== 'execute') throw new Error('Image tools are unavailable in Ask mode')
const result = await this.binding.call(call.input, call.callId, this.wait.signal)
reply = remoteImageToolReplySchema.parse({ callId: call.callId, result })
}
} catch (error) {
reply = { callId: call.callId, error: (error instanceof Error ? error.message : 'Image tool failed').slice(0, 4_000) }
}
@@ -525,9 +525,17 @@ describe('ProtocolRemoteRuntimeChannel', () => {
expect(describe).toHaveBeenCalledTimes(2)
expect(fixture.client.blobBinaries[1]!.closed).toBe(true)
expect(fixture.client.requests.at(-1)!.params).not.toHaveProperty('imageTool', expect.anything())
const graph = { available: vi.fn(async () => true), call: vi.fn() }
await fixture.channel.preparePrompt(prepare('graph-ask', 'ask'), undefined, wait.signal, graph)
expect(fixture.client.requests.at(-1)!.params).toMatchObject({ imageTool: { storyGraph: true } })
expect((fixture.client.requests.at(-1)!.params as { imageTool: object }).imageTool).not.toHaveProperty('description')
graph.available.mockResolvedValue(false)
await fixture.channel.preparePrompt(prepare('graph-disabled', 'ask'), undefined, wait.signal, graph)
expect(fixture.client.blobBinaries[2]!.closed).toBe(true)
expect(fixture.client.requests.at(-1)!.params).not.toHaveProperty('imageTool', expect.anything())
fixture.client.responder = () => { throw new Error('preparation response lost') }
await expect(fixture.channel.preparePrompt(prepare('operation-4'), binding, wait.signal)).rejects.toThrow()
expect(fixture.client.blobBinaries[2]!.closed).toBe(true)
expect(fixture.client.blobBinaries[3]!.closed).toBe(true)
} finally { await fixture.channel.close() }
})
it('sends question replies on the authenticated control channel and rejects foreign bindings', async () => {
@@ -588,21 +588,23 @@ export class ProtocolRemoteRuntimeChannel
async preparePrompt(
preparation: z.input<typeof remotePromptOperationPreparationSchema>,
imageToolBinding?: ImageToolBinding,
waitSignal?: AbortSignal
waitSignal?: AbortSignal,
storyGraphBinding?: import('../agent/knowledge-mcp-gateway').StoryGraphRemoteBinding
): Promise<
z.infer<typeof remotePromptOperationAcceptanceSchema>
> {
this.#imageTool?.close()
this.#imageTool = undefined
const description = preparation.workMode === 'execute' ? await imageToolBinding?.describe() : undefined
const storyGraph = await storyGraphBinding?.available()
waitSignal?.throwIfAborted()
let imageTool: z.infer<typeof remotePromptOperationPreparationSchema>['imageTool']
if (description && imageToolBinding) {
if ((description && imageToolBinding) || storyGraph) {
this.#assertCurrent()
const binary = this.#state.client.allocateBinaryChannel?.({ kind: 'blob' })
if (!binary) throw new Error('Remote image tool transport is unavailable')
imageTool = { channelId: binary.channelId, channelEpoch: binary.channelEpoch, description }
this.#imageTool = new MainImageToolSession(binary, imageToolBinding, waitSignal)
imageTool = { channelId: binary.channelId, channelEpoch: binary.channelEpoch, ...(description ? { description } : {}), ...(storyGraph ? { storyGraph: true } : {}) }
this.#imageTool = new MainImageToolSession(binary, imageToolBinding, waitSignal, storyGraphBinding)
}
try {
const parsed = remotePromptOperationPreparationSchema.parse({ ...preparation, imageTool })