fix(gui): address goal UI review feedback
This commit is contained in:
@@ -60,6 +60,8 @@ export const goalEditRequestSchema = z.object({
|
||||
ref: goalRefSchema,
|
||||
objective: z.string().min(1).optional(),
|
||||
maxGoalRounds: z.number().int().positive().optional(),
|
||||
}).refine(value => value.objective !== undefined || value.maxGoalRounds !== undefined, {
|
||||
message: 'goal.edit requires objective or maxGoalRounds',
|
||||
}) as unknown as z.ZodType<Wire<RequestPayload<'goal.edit'>>>
|
||||
|
||||
/** goal.edit response value. */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { RpcRequest, RpcResponse } from './rpc.ts'
|
||||
|
||||
/** Identifies one goal across its durable revisions. */
|
||||
@@ -60,29 +61,29 @@ export interface EditGoalRequest {
|
||||
/** Goal-domain unary methods. */
|
||||
export interface GoalsApi {
|
||||
/** Read the current goal for one session. Returns null when no goal is current. */
|
||||
get(request: RpcRequest<{ sessionId: string }>): Promise<RpcResponse<{ goal: GoalView | null }>>
|
||||
get(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ goal: GoalView | null }>>
|
||||
|
||||
/** Create and arm a goal. */
|
||||
create(request: RpcRequest<{ sessionId: string; objective: string; maxGoalRounds?: number }>):
|
||||
create(request: RpcRequest<{ sessionId: SessionId; objective: string; maxGoalRounds?: number }>):
|
||||
Promise<RpcResponse<{ goal: GoalView }>>
|
||||
|
||||
/** Edit objective and/or round cap without changing phase. */
|
||||
edit(request: RpcRequest<{ sessionId: string; ref: GoalRef; objective?: string; maxGoalRounds?: number }>):
|
||||
edit(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef; objective?: string; maxGoalRounds?: number }>):
|
||||
Promise<RpcResponse<{ goal: GoalView }>>
|
||||
|
||||
/** Pause an active goal and disarm automatic continuation. */
|
||||
pause(request: RpcRequest<{ sessionId: string; ref: GoalRef }>):
|
||||
pause(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
|
||||
Promise<RpcResponse<{ goal: GoalView }>>
|
||||
|
||||
/** Resume and arm a stopped goal. */
|
||||
resume(request: RpcRequest<{ sessionId: string; ref: GoalRef }>):
|
||||
resume(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
|
||||
Promise<RpcResponse<{ goal: GoalView }>>
|
||||
|
||||
/** Mark a current non-complete goal complete and disarm it. */
|
||||
complete(request: RpcRequest<{ sessionId: string; ref: GoalRef }>):
|
||||
complete(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
|
||||
Promise<RpcResponse<{ goal: GoalView }>>
|
||||
|
||||
/** Clear the current goal while retaining a durable tombstone and history. */
|
||||
clear(request: RpcRequest<{ sessionId: string; ref: GoalRef }>):
|
||||
clear(request: RpcRequest<{ sessionId: SessionId; ref: GoalRef }>):
|
||||
Promise<RpcResponse<{ cleared: true }>>
|
||||
}
|
||||
|
||||
@@ -425,6 +425,13 @@ describe('goals unary surface', () => {
|
||||
const response = await client(scriptedApi()).goals.create({ sessionId: sid('s1'), objective: '' })
|
||||
expect(response.result.ok).toBe(false)
|
||||
if (!response.result.ok) expect(response.result.error.code).toBe('bad-request')
|
||||
|
||||
let editCalls = 0
|
||||
const api = scriptedApi({ goals: { edit: (r) => { editCalls++; return ok(r, { goal: view }) } } })
|
||||
const emptyEdit = await client(api).goals.edit({ sessionId: sid('s1'), ref })
|
||||
expect(emptyEdit.result.ok).toBe(false)
|
||||
if (!emptyEdit.result.ok) expect(emptyEdit.result.error.code).toBe('bad-request')
|
||||
expect(editCalls).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { hostDescribeRequestSchema, hostDescribeValueSchema } from '../src/api/h
|
||||
import { hostFrameSchema, muxFrameSchema, askUserQuestionItemSchema } from '../src/api/events.schema.ts'
|
||||
import { approvalRequestIdSchema, approvalResponsePayloadSchema } from '../src/api/approvals.schema.ts'
|
||||
import { askUserQuestionAnswerSchema, questionResponsePayloadSchema } from '../src/api/questions.schema.ts'
|
||||
import { goalEditRequestSchema } from '../src/api/goals.schema.ts'
|
||||
|
||||
describe('RpcId', () => {
|
||||
it('brands a raw string at zero runtime cost', () => {
|
||||
@@ -126,6 +127,15 @@ describe('host domain schemas', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('goals domain schemas', () => {
|
||||
it('requires at least one replacement field for goal.edit', () => {
|
||||
const ref = { id: 'g1', revision: 1 }
|
||||
expect(goalEditRequestSchema.parse({ sessionId: 's1', ref, objective: 'updated' }).objective).toBe('updated')
|
||||
expect(goalEditRequestSchema.parse({ sessionId: 's1', ref, maxGoalRounds: 3 }).maxGoalRounds).toBe(3)
|
||||
expect(() => goalEditRequestSchema.parse({ sessionId: 's1', ref })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('events frame schemas', () => {
|
||||
it('accepts every mux frame branch', () => {
|
||||
const frames = [
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {} from '@deepseek-ai/dsh-commands'
|
||||
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
|
||||
import type { ApiProxy, HistoryEntry, HostFrame, MuxFrame, SessionSummary, ToolEventView } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import type { GoalView } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import type { GoalView as CoreGoalView } from '@deepseek-ai/dsh-goal'
|
||||
import type { ClientResponse, RpcError, RpcReceipt, RpcRequest, RpcResponse } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
||||
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
||||
|
||||
@@ -222,7 +223,7 @@ function backscanArgs(events: readonly SessionEvent[], callId: string): { name:
|
||||
}
|
||||
|
||||
/** Project a server-side GoalView into the wire GoalView shape. */
|
||||
function goalView(g: import('@deepseek-ai/dsh-goal').GoalView): GoalView {
|
||||
function goalView(g: CoreGoalView): GoalView {
|
||||
return {
|
||||
id: g.id,
|
||||
revision: g.revision,
|
||||
@@ -296,6 +297,20 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve a session, apply one goal mutation, and map domain failures to the wire result. */
|
||||
async function mutateGoal(
|
||||
request: RpcRequest<{ sessionId: SessionId }>,
|
||||
mutation: (agent: Agent) => CoreGoalView,
|
||||
): Promise<RpcResponse<{ goal: GoalView }>> {
|
||||
const found = await agentFor(request.payload.sessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
try {
|
||||
return ok(request, { goal: goalView(mutation(found.agent)) })
|
||||
} catch (error: unknown) {
|
||||
return err(request, { code: 'internal', message: String(error), details: {} })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sessions: {
|
||||
// Attached sessions summarize from memory; persisted-but-unattached (cold)
|
||||
@@ -482,81 +497,43 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
goals: {
|
||||
async get(request) {
|
||||
const { sessionId } = request.payload
|
||||
const found = await agentFor(sessionId as SessionId)
|
||||
const found = await agentFor(sessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
const goal = ctx.goals.get(found.agent)
|
||||
return ok(request, { goal: goal ? goalView(goal) : null })
|
||||
},
|
||||
|
||||
async create(request) {
|
||||
const { sessionId, objective, maxGoalRounds } = request.payload
|
||||
const found = await agentFor(sessionId as SessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
try {
|
||||
const goal = ctx.goals.create(found.agent, {
|
||||
objective,
|
||||
...(maxGoalRounds !== undefined ? { maxGoalRounds } : {}),
|
||||
})
|
||||
return ok(request, { goal: goalView(goal) })
|
||||
} catch (error: unknown) {
|
||||
return err(request, { code: 'internal', message: String(error), details: {} })
|
||||
}
|
||||
const { objective, maxGoalRounds } = request.payload
|
||||
return mutateGoal(request, agent => ctx.goals.create(agent, {
|
||||
objective,
|
||||
...(maxGoalRounds !== undefined ? { maxGoalRounds } : {}),
|
||||
}))
|
||||
},
|
||||
|
||||
async edit(request) {
|
||||
const { sessionId, ref, objective, maxGoalRounds } = request.payload
|
||||
const found = await agentFor(sessionId as SessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
try {
|
||||
const goal = ctx.goals.edit(found.agent, ref, {
|
||||
...(objective !== undefined ? { objective } : {}),
|
||||
...(maxGoalRounds !== undefined ? { maxGoalRounds } : {}),
|
||||
})
|
||||
return ok(request, { goal: goalView(goal) })
|
||||
} catch (error: unknown) {
|
||||
return err(request, { code: 'internal', message: String(error), details: {} })
|
||||
}
|
||||
const { ref, objective, maxGoalRounds } = request.payload
|
||||
return mutateGoal(request, agent => ctx.goals.edit(agent, ref, {
|
||||
...(objective !== undefined ? { objective } : {}),
|
||||
...(maxGoalRounds !== undefined ? { maxGoalRounds } : {}),
|
||||
}))
|
||||
},
|
||||
|
||||
async pause(request) {
|
||||
const { sessionId, ref } = request.payload
|
||||
const found = await agentFor(sessionId as SessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
try {
|
||||
const goal = ctx.goals.pause(found.agent, ref)
|
||||
return ok(request, { goal: goalView(goal) })
|
||||
} catch (error: unknown) {
|
||||
return err(request, { code: 'internal', message: String(error), details: {} })
|
||||
}
|
||||
return mutateGoal(request, agent => ctx.goals.pause(agent, request.payload.ref))
|
||||
},
|
||||
|
||||
async resume(request) {
|
||||
const { sessionId, ref } = request.payload
|
||||
const found = await agentFor(sessionId as SessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
try {
|
||||
const goal = ctx.goals.resume(found.agent, ref)
|
||||
return ok(request, { goal: goalView(goal) })
|
||||
} catch (error: unknown) {
|
||||
return err(request, { code: 'internal', message: String(error), details: {} })
|
||||
}
|
||||
return mutateGoal(request, agent => ctx.goals.resume(agent, request.payload.ref))
|
||||
},
|
||||
|
||||
async complete(request) {
|
||||
const { sessionId, ref } = request.payload
|
||||
const found = await agentFor(sessionId as SessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
try {
|
||||
const goal = ctx.goals.complete(found.agent, ref)
|
||||
return ok(request, { goal: goalView(goal) })
|
||||
} catch (error: unknown) {
|
||||
return err(request, { code: 'internal', message: String(error), details: {} })
|
||||
}
|
||||
return mutateGoal(request, agent => ctx.goals.complete(agent, request.payload.ref))
|
||||
},
|
||||
|
||||
async clear(request) {
|
||||
const { sessionId, ref } = request.payload
|
||||
const found = await agentFor(sessionId as SessionId)
|
||||
const found = await agentFor(sessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
try {
|
||||
ctx.goals.clear(found.agent, ref)
|
||||
|
||||
@@ -325,7 +325,7 @@ describe('goals RPC surface', () => {
|
||||
it('an unservable session id is an RPC error on every goal method', async () => {
|
||||
const test = await harness()
|
||||
const api = createApiProxy(test.ctx, { provider: 'p', model: 'm', cwd: '/tmp' })
|
||||
const missing = 'no-such-session'
|
||||
const missing = SessionId('no-such-session')
|
||||
const ref = { id: 'goal-x' as GoalView['id'], revision: 1 }
|
||||
|
||||
const attempts = [
|
||||
|
||||
Reference in New Issue
Block a user