feat: slash system / input service / agent scope
This commit is contained in:
@@ -9,6 +9,7 @@ export type {
|
||||
ApiProxy, SessionsApi, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
||||
WorkspaceApi, WorkspaceId, WorkspaceView,
|
||||
CommandsApi, CommandDescriptor, CommandExecuteResult, SkillsApi, SkillEntry,
|
||||
} from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
export type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'
|
||||
export type {
|
||||
|
||||
@@ -347,10 +347,11 @@ class FxInbox<F> implements StreamConn<F> {
|
||||
* @returns an ApiProxy backed entirely by in-memory state — no host process, no network.
|
||||
*/
|
||||
export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
||||
// The resident fixture sessions all carry history, so none of them is blank.
|
||||
const sessions: SessionSummary[] = options.empty ? [] : [
|
||||
{ sessionId: sid('fx-alpha'), updatedAt: Date.now(), running: true, cwd: '/tmp/fixture' },
|
||||
{ sessionId: sid('fx-beta'), updatedAt: Date.now() - 60_000, running: false, parentSessionId: sid('fx-alpha'), cwd: '/tmp/fixture' },
|
||||
{ sessionId: sid('fx-gamma'), updatedAt: Date.now() - 120_000, running: false, cwd: '/tmp/fixture' },
|
||||
{ sessionId: sid('fx-alpha'), updatedAt: Date.now(), running: true, blank: false, cwd: '/tmp/fixture' },
|
||||
{ sessionId: sid('fx-beta'), updatedAt: Date.now() - 60_000, running: false, blank: false, parentSessionId: sid('fx-alpha'), cwd: '/tmp/fixture' },
|
||||
{ sessionId: sid('fx-gamma'), updatedAt: Date.now() - 120_000, running: false, blank: false, cwd: '/tmp/fixture' },
|
||||
]
|
||||
const logs = new Map<SessionId, SessionEvent[]>([[sid('fx-alpha'), buildAlphaLog()]])
|
||||
const nextTurn = new Map<SessionId, number>([[sid('fx-alpha'), 60]])
|
||||
@@ -582,12 +583,13 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
||||
}
|
||||
}
|
||||
const created: SessionSummary = {
|
||||
sessionId: requestedId ?? sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, cwd,
|
||||
sessionId: requestedId ?? sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: true, cwd,
|
||||
}
|
||||
sessions.push(created)
|
||||
attachedSessions += 1
|
||||
const emitSession = (): void => {
|
||||
emitHost({ type: 'host/session-added', sessionId: created.sessionId, cwd })
|
||||
// Mirrors the host: the frame fires at creation, so blank is constantly true.
|
||||
emitHost({ type: 'host/session-added', sessionId: created.sessionId, blank: true, cwd })
|
||||
}
|
||||
if (workspace !== undefined && options.failWorkspaceAttach) {
|
||||
emitSession()
|
||||
@@ -628,6 +630,8 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
||||
})
|
||||
}
|
||||
summary.updatedAt = Date.now()
|
||||
// First accepted prompt appends events: the summary stops being blank.
|
||||
summary.blank = false
|
||||
const userText = content.map(b => (b.type === 'text' ? b.text : '')).join('')
|
||||
if (mode === 'steer' && replays.has(id)) {
|
||||
// Steering: insert a steering message into the current turn; the replay continues.
|
||||
@@ -738,6 +742,70 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
|
||||
return ok(request, { workspace: { ...workspace } })
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
// The catalog mirrors one session's effective view (every fixture
|
||||
// session has an agent, like the real host).
|
||||
list: (request) => {
|
||||
const summary = summaryOf(request.payload.sessionId)
|
||||
if (summary === undefined) {
|
||||
return err(request, {
|
||||
code: 'session-not-found',
|
||||
message: `no session ${request.payload.sessionId}`,
|
||||
details: { sessionId: request.payload.sessionId },
|
||||
})
|
||||
}
|
||||
return ok(request, {
|
||||
commands: [
|
||||
{ name: 'compact', description: 'fixture:压缩当前会话上下文' },
|
||||
{ name: 'echo', description: 'fixture:回显参数', input: { hint: 'text to echo' } },
|
||||
{ name: 'goal-fixture', description: 'fixture:目标样本命令', input: { hint: '<objective>' } },
|
||||
],
|
||||
})
|
||||
},
|
||||
execute: (request) => {
|
||||
const summary = summaryOf(request.payload.sessionId)
|
||||
if (summary === undefined) {
|
||||
return err(request, {
|
||||
code: 'session-not-found',
|
||||
message: `no session ${request.payload.sessionId}`,
|
||||
details: { sessionId: request.payload.sessionId },
|
||||
})
|
||||
}
|
||||
const line = request.payload.line.trim()
|
||||
const match = /^\/(\S+)(?:\s+(.*))?$/.exec(line)
|
||||
const name = match?.[1]
|
||||
if (name === 'compact' || name === 'echo') {
|
||||
return ok(request, {
|
||||
matched: true as const,
|
||||
result: { kind: 'success' as const, text: name === 'echo' ? (match?.[2] ?? '') : 'fixture:已压缩(假动作)' },
|
||||
})
|
||||
}
|
||||
if (name === 'goal-fixture') {
|
||||
return ok(request, {
|
||||
matched: true as const,
|
||||
result: { kind: 'success' as const, text: `fixture:goal 已设置(${request.payload.sessionId})` },
|
||||
})
|
||||
}
|
||||
return ok(request, { matched: false as const })
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
list: (request) => {
|
||||
const summary = summaryOf(request.payload.sessionId)
|
||||
if (summary === undefined) {
|
||||
return err(request, {
|
||||
code: 'session-not-found',
|
||||
message: `no session ${request.payload.sessionId}`,
|
||||
details: { sessionId: request.payload.sessionId },
|
||||
})
|
||||
}
|
||||
return ok(request, {
|
||||
skills: [
|
||||
{ name: 'fixture-demo', description: 'fixture 技能样本', whenToUse: '仅供 UI 目录渲染验收' },
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
events: {
|
||||
async *mux(_request, signal) {
|
||||
const conn = new FxInbox<MuxFrame>()
|
||||
@@ -855,6 +923,10 @@ export class FixtureApiClient extends AbstractApiClient {
|
||||
case 'workspace.create': return this.api.workspace.create(request)
|
||||
case 'workspace.rename': return this.api.workspace.rename(request)
|
||||
case 'workspace.insertSessionBefore': return this.api.workspace.insertSessionBefore(request)
|
||||
case 'command.list': return this.api.commands.list(request)
|
||||
// The in-memory execute never blocks, so a never-aborting signal is faithful here.
|
||||
case 'command.execute': return this.api.commands.execute(request, new AbortController().signal)
|
||||
case 'skill.list': return this.api.skills.list(request)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export type {
|
||||
ApiProxy, SessionsApi, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
||||
ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView,
|
||||
CommandsApi, CommandDescriptor, CommandExecuteResult, SkillsApi, SkillEntry,
|
||||
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
||||
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
|
||||
IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
|
||||
|
||||
Reference in New Issue
Block a user