Merge updated DeepSeek onboarding base
# Conflicts: # packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
@@ -1276,6 +1276,75 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
}
|
||||
},
|
||||
|
||||
async fork(request) {
|
||||
const { sessionId, atSeq } = request.payload
|
||||
const found = await agentFor(sessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
const source = found.agent.session
|
||||
const events = source.events
|
||||
// An in-log anchor belongs to the turn containing it and must never
|
||||
// clip backward to an earlier completed turn. Omitted and past-end
|
||||
// anchors retain the last-completed-turn shortcut.
|
||||
const lastSeq = events.at(-1)?.seq ?? -1
|
||||
const anchoredBoundary = atSeq === undefined
|
||||
? undefined
|
||||
: events.find(e => e.type === 'turn/end' && e.seq >= atSeq)
|
||||
const boundary = anchoredBoundary
|
||||
?? (atSeq === undefined || atSeq > lastSeq
|
||||
? events.findLast(e => e.type === 'turn/end')
|
||||
: undefined)
|
||||
if (boundary === undefined) {
|
||||
return err(request, {
|
||||
code: 'fork-unavailable',
|
||||
message: atSeq !== undefined && atSeq <= lastSeq
|
||||
? `session "${sessionId}" has not completed the turn containing event ${String(atSeq)}`
|
||||
: `session "${sessionId}" has no completed turn to fork from`,
|
||||
details: { sessionId },
|
||||
})
|
||||
}
|
||||
// Extend the cut through trailing out-of-band appends (session/title,
|
||||
// injections) up to the next turn/start: they are standalone events, so
|
||||
// the seed stays balanced, and the child inherits a title generated
|
||||
// right after the boundary turn.
|
||||
let cut = boundary.seq + 1
|
||||
while (cut < events.length && events[cut]?.type !== 'turn/start') cut++
|
||||
const childId = `session-${randomUUID()}` as SessionId
|
||||
try {
|
||||
await ctx.agents.create({
|
||||
sessionId: childId,
|
||||
seed: events.slice(0, cut),
|
||||
meta: {
|
||||
...source.header.cwd === undefined ? {} : { cwd: source.header.cwd },
|
||||
parentSession: source.id,
|
||||
seedLength: cut,
|
||||
},
|
||||
agentOptions,
|
||||
setup: installTarget,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
return err(request, {
|
||||
code: 'internal',
|
||||
message: `failed to fork session "${sessionId}": ${String(error)}`,
|
||||
details: {},
|
||||
})
|
||||
}
|
||||
// Keep the child in the source's Workspace so the list nests it under
|
||||
// its parent; the child is already published if the attach fails.
|
||||
const workspace = ctx.workspace.list().find(w => w.sessionIds.includes(source.id))
|
||||
if (workspace !== undefined) {
|
||||
try {
|
||||
await workspace.attachSession(childId)
|
||||
} catch (error: unknown) {
|
||||
return err(request, {
|
||||
code: 'workspace-attach-failed',
|
||||
message: `session "${childId}" was forked but could not attach to workspace "${workspace.id}": ${String(error)}`,
|
||||
details: { sessionId: childId, workspaceId: workspace.id },
|
||||
})
|
||||
}
|
||||
}
|
||||
return ok(request, { sessionId: childId })
|
||||
},
|
||||
|
||||
async prompt(request) {
|
||||
const { sessionId, mode, content } = request.payload
|
||||
const found = await agentFor(sessionId)
|
||||
|
||||
@@ -23,6 +23,11 @@ export const askUserQuestionItemSchema = z.object({
|
||||
detail: z.string().optional(),
|
||||
options: z.array(z.object({ label: z.string(), description: z.string().optional() })).optional(),
|
||||
multiSelect: z.boolean().optional(),
|
||||
// Presentation intent: a tagged union on the wire, so an unknown tag is a
|
||||
// rejected frame rather than a silently generic render.
|
||||
intent: z.discriminatedUnion('kind', [
|
||||
z.object({ kind: z.literal('plan-review'), approve: z.string() }),
|
||||
]).optional(),
|
||||
}) satisfies z.ZodType<Wire<AskUserQuestionItem>>
|
||||
|
||||
/** Unified message envelope carried by transient queue frames. */
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface RpcMethodMap {
|
||||
'session.models': SessionsApi['models']
|
||||
'session.selectModel': SessionsApi['selectModel']
|
||||
'session.rename': SessionsApi['rename']
|
||||
'session.fork': SessionsApi['fork']
|
||||
'session.prompt': SessionsApi['prompt']
|
||||
'session.updateQueue': SessionsApi['updateQueue']
|
||||
'session.cancel': SessionsApi['cancel']
|
||||
|
||||
@@ -55,6 +55,7 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
|
||||
z.object({ code: z.literal('settings-conflict'), message: z.string(), details: z.object({ ns: z.string(), expected: z.number(), actual: z.number() }) }),
|
||||
z.object({ code: z.literal('credential-rejected'), message: z.string(), details: z.object({ ref: z.string() }) }),
|
||||
z.object({ code: z.literal('title-invalid'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
|
||||
z.object({ code: z.literal('fork-unavailable'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
|
||||
z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }),
|
||||
]) as unknown as z.ZodType<RpcError>
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ export interface RpcErrorDetailsMap {
|
||||
/** A credential write was refused (read-only shadowing layer or storage failure); the message is the seam's own text. */
|
||||
'credential-rejected': { ref: string }
|
||||
'title-invalid': { sessionId: SessionId }
|
||||
'fork-unavailable': { sessionId: SessionId }
|
||||
'internal': {}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,17 @@ export const sessionRenameValueSchema = z.object({
|
||||
seq: z.number().int().nonnegative(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.rename'>>>
|
||||
|
||||
/** session.fork request payload (atSeq anchors the completed-turn cut). */
|
||||
export const sessionForkRequestSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
atSeq: z.number().int().nonnegative().optional(),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'session.fork'>>>
|
||||
|
||||
/** session.fork response value (the child session id). */
|
||||
export const sessionForkValueSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.fork'>>>
|
||||
|
||||
/** session.history request payload (beforeSeq/maxMessages page backwards from the window tail). */
|
||||
export const sessionHistoryRequestSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
|
||||
@@ -238,6 +238,21 @@ export interface SessionsApi {
|
||||
* one — carried for future rendering; the state change is the feedback). A usage/state error is an
|
||||
* RPC error with code command-error; an unrecognized name is an RPC error with code unknown-command.
|
||||
*/
|
||||
/**
|
||||
* Forks a new session from a completed-turn prefix of the source. `atSeq`
|
||||
* anchors the cut: the boundary is the first `turn/end` at or after it
|
||||
* (a message's fork button passes the message seq, so the fork includes
|
||||
* that whole turn); a boundary past the log end, or an omitted `atSeq`,
|
||||
* falls back to the source's last completed turn. An in-log anchor whose
|
||||
* turn is still open fails with `fork-unavailable` instead of clipping to
|
||||
* an earlier turn. The child inherits the source cwd, latest logged model
|
||||
* target, workspace attachment, and `parentSessionId` lineage; the seed
|
||||
* prefix carries the source title.
|
||||
*/
|
||||
fork(request: RpcRequest<{ sessionId: SessionId; atSeq?: number }>):
|
||||
Promise<RpcResponse<{ sessionId: SessionId }>>
|
||||
|
||||
/** 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[] }>):
|
||||
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import {
|
||||
sessionCancelValueSchema,
|
||||
sessionCreateValueSchema,
|
||||
sessionForkValueSchema,
|
||||
sessionHistoryValueSchema,
|
||||
sessionListValueSchema,
|
||||
sessionModelsValueSchema,
|
||||
@@ -76,6 +77,7 @@ export interface IApiClient {
|
||||
models(payload: RequestPayload<'session.models'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.models'>>>
|
||||
selectModel(payload: RequestPayload<'session.selectModel'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.selectModel'>>>
|
||||
rename(payload: RequestPayload<'session.rename'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.rename'>>>
|
||||
fork(payload: RequestPayload<'session.fork'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.fork'>>>
|
||||
prompt(payload: RequestPayload<'session.prompt'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.prompt'>>>
|
||||
updateQueue(payload: RequestPayload<'session.updateQueue'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.updateQueue'>>>
|
||||
cancel(payload: RequestPayload<'session.cancel'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.cancel'>>>
|
||||
@@ -143,6 +145,7 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
|
||||
'session.models': sessionModelsValueSchema,
|
||||
'session.selectModel': sessionSelectModelValueSchema,
|
||||
'session.rename': sessionRenameValueSchema,
|
||||
'session.fork': sessionForkValueSchema,
|
||||
'session.prompt': sessionPromptValueSchema,
|
||||
'session.updateQueue': sessionUpdateQueueValueSchema,
|
||||
'session.cancel': sessionCancelValueSchema,
|
||||
@@ -365,6 +368,7 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
models: (payload, signal) => this.callUnary('session.models', payload, signal),
|
||||
selectModel: (payload, signal) => this.callUnary('session.selectModel', payload, signal),
|
||||
rename: (payload, signal) => this.callUnary('session.rename', payload, signal),
|
||||
fork: (payload, signal) => this.callUnary('session.fork', payload, signal),
|
||||
prompt: (payload, signal) => this.callUnary('session.prompt', payload, signal),
|
||||
updateQueue: (payload, signal) => this.callUnary('session.updateQueue', payload, signal),
|
||||
cancel: (payload, signal) => this.callUnary('session.cancel', payload, signal),
|
||||
|
||||
@@ -17,6 +17,7 @@ import { clientRequestSchema, clientResponseSchema } from '../api/rpc.schema.ts'
|
||||
import {
|
||||
sessionCancelRequestSchema,
|
||||
sessionCreateRequestSchema,
|
||||
sessionForkRequestSchema,
|
||||
sessionHistoryRequestSchema,
|
||||
sessionListRequestSchema,
|
||||
sessionModelsRequestSchema,
|
||||
@@ -78,6 +79,7 @@ const UNARY_ROUTES: UnaryRoutes = {
|
||||
'session.models': { schema: sessionModelsRequestSchema, invoke: (api, r) => api.sessions.models(r) },
|
||||
'session.selectModel': { schema: sessionSelectModelRequestSchema, invoke: (api, r) => api.sessions.selectModel(r) },
|
||||
'session.rename': { schema: sessionRenameRequestSchema, invoke: (api, r) => api.sessions.rename(r) },
|
||||
'session.fork': { schema: sessionForkRequestSchema, invoke: (api, r) => api.sessions.fork(r) },
|
||||
'session.prompt': { schema: sessionPromptRequestSchema, invoke: (api, r) => api.sessions.prompt(r) },
|
||||
'session.updateQueue': { schema: sessionUpdateQueueRequestSchema, invoke: (api, r) => api.sessions.updateQueue(r) },
|
||||
'session.cancel': { schema: sessionCancelRequestSchema, invoke: (api, r) => api.sessions.cancel(r) },
|
||||
|
||||
Reference in New Issue
Block a user