fix(web): bind prompts to plan selection
This commit is contained in:
@@ -10,7 +10,7 @@ The layering/protocol decisions are recorded in the [GUI layering and RPC protoc
|
||||
|
||||
The mux stream projects the latest log-backed title as a validated `session/title` control frame after each attached-session subscription baseline and immediately after the corresponding live raw title event. This projection does not add titles to `session.list`; cold sessions remain metadata-only there until opening or resuming attaches their logs.
|
||||
|
||||
Plan mode uses two unary methods instead of deriving current state from a history page: `session.planMode` returns the committed state plus any boundary-pending selection, and `session.setPlanMode` records a selection and returns the same authoritative shape. A present `pending` target must differ from `active`; a net-zero service cleanup intent projects as `{ active }`, and the wire schema rejects equal values. Both methods return `null` when the optional host service is absent; `null` is capability absence, while `{ active: false }` is a supported inactive session. Committed changes still arrive through the raw logged `plan/mode` session event.
|
||||
Plan mode uses two unary methods instead of deriving current state from a history page: `session.planMode` returns the committed state plus any boundary-pending selection, and `session.setPlanMode` records a selection and returns the same authoritative shape. A present `pending` target must differ from `active`; a net-zero service cleanup intent projects as `{ active }`, and the wire schema rejects equal values. Both methods return `null` when the optional host service is absent; `null` is capability absence, while `{ active: false }` is a supported inactive session. Committed changes still arrive through the raw logged `plan/mode` session event. `session.prompt` may carry a `planMode` target so the host records that selection immediately before accepting the prompt; it fails when the capability is absent and restores the prior target after a synchronous prompt rejection.
|
||||
|
||||
## Carrier layer (`/client` + root)
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ export const sessionPromptRequestSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
mode: z.union([z.literal('queue'), z.literal('steer')]),
|
||||
content: z.array(contentBlockSchema),
|
||||
planMode: z.boolean().optional(),
|
||||
}) as unknown as z.ZodType<RequestPayload<'session.prompt'>>
|
||||
|
||||
/** session.prompt response value. */
|
||||
|
||||
@@ -74,8 +74,17 @@ export interface SessionsApi {
|
||||
history(request: RpcRequest<{ sessionId: SessionId; beforeSeq?: number; maxMessages?: number }>):
|
||||
Promise<RpcResponse<{ events: HistoryEntry[]; hasMore: boolean }>>
|
||||
|
||||
/** Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer. */
|
||||
prompt(request: RpcRequest<{ sessionId: SessionId; mode: 'queue' | 'steer'; content: ContentBlock[] }>):
|
||||
/**
|
||||
* Sends a message. `content` is core's ContentBlock[] verbatim and `mode`
|
||||
* maps 1:1 — queue→send, steer→steer. An optional `planMode` target is
|
||||
* admitted atomically with the prompt.
|
||||
*/
|
||||
prompt(request: RpcRequest<{
|
||||
sessionId: SessionId
|
||||
mode: 'queue' | 'steer'
|
||||
content: ContentBlock[]
|
||||
planMode?: boolean
|
||||
}>):
|
||||
Promise<RpcResponse<{ accepted: true }>>
|
||||
|
||||
/** Stops: clears both FIFOs + aborts the current step (1:1 with agent.cancel). */
|
||||
|
||||
@@ -84,7 +84,12 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
it('covers create/prompt/cancel/plan/describe passthrough', async () => {
|
||||
const c = client()
|
||||
expect((await c.sessions.create({})).result.ok).toBe(true)
|
||||
expect((await c.sessions.prompt({ sessionId: 's' as never, mode: 'queue', content: [{ type: 'text', text: 'x' }] })).result.ok).toBe(true)
|
||||
expect((await c.sessions.prompt({
|
||||
sessionId: 's' as never,
|
||||
mode: 'queue',
|
||||
content: [{ type: 'text', text: 'x' }],
|
||||
planMode: true,
|
||||
})).result.ok).toBe(true)
|
||||
expect((await c.sessions.cancel({ sessionId: 's' as never })).result.ok).toBe(true)
|
||||
expect((await c.sessions.planMode({ sessionId: 's' as never })).result).toEqual({ ok: true, value: null })
|
||||
expect((await c.sessions.setPlanMode({ sessionId: 's' as never, active: true })).result).toEqual({ ok: true, value: null })
|
||||
|
||||
@@ -101,9 +101,14 @@ describe('sessions domain schemas', () => {
|
||||
expect(sessionHistoryRequestSchema.parse({ sessionId: 's1', beforeSeq: 3, maxMessages: 5 }).beforeSeq).toBe(3)
|
||||
expect(() => sessionHistoryRequestSchema.parse({ sessionId: 's1', maxMessages: 0 })).toThrow()
|
||||
expect(sessionHistoryValueSchema.parse({ events: [], hasMore: false }).hasMore).toBe(false)
|
||||
const prompt = sessionPromptRequestSchema.parse({ sessionId: 's1', mode: 'queue', content: [{ type: 'text', text: 'hi' }] })
|
||||
expect(prompt.mode).toBe('queue')
|
||||
const prompt = sessionPromptRequestSchema.parse({
|
||||
sessionId: 's1', mode: 'queue', content: [{ type: 'text', text: 'hi' }], planMode: true,
|
||||
})
|
||||
expect(prompt).toMatchObject({ mode: 'queue', planMode: true })
|
||||
expect(() => sessionPromptRequestSchema.parse({ sessionId: 's1', mode: 'inject', content: [] })).toThrow()
|
||||
expect(() => sessionPromptRequestSchema.parse({
|
||||
sessionId: 's1', mode: 'queue', content: [], planMode: 'plan',
|
||||
})).toThrow()
|
||||
expect(sessionPromptValueSchema.parse({ accepted: true }).accepted).toBe(true)
|
||||
expect(sessionCancelRequestSchema.parse({ sessionId: 's1' }).sessionId).toBe('s1')
|
||||
expect(sessionCancelValueSchema.parse({ accepted: true }).accepted).toBe(true)
|
||||
|
||||
Reference in New Issue
Block a user