mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
Merge pull request #6351 from deepseek-harness/fix/sdk-desktop-alignment
fix(sdk): 补齐桌面工具、上下文和原生问答
This commit is contained in:
commit
b88a1c2d14
133 files changed
+4548
-237
No files matched your search
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-10-19-sdk-native-user-questions.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-19-sdk-native-user-questions.md
|
||||
/agent-note-sdk-native-user-questions:
|
||||
en: b506991bd07d87dc
|
||||
zh: 9f3e1ecb42163d72
|
||||
/agent-note-sdk-native-user-questions/problem:
|
||||
en: b3dbcdb4b5bce2d1
|
||||
zh: 60df882dc253fcf5
|
||||
/agent-note-sdk-native-user-questions/decision:
|
||||
en: 0f7b71f0ef58f5f8
|
||||
zh: 07bb12acad27d2fc
|
||||
/agent-note-sdk-native-user-questions/consequences:
|
||||
en: 46e3a450c6dce54e
|
||||
zh: 9f99efc6f8d2ab8d
|
||||
/agent-note-sdk-native-user-questions/alternatives-considered:
|
||||
en: 9861066ae00e7bde
|
||||
zh: 0dec00f4e3c692e0
|
||||
/agent-note-sdk-native-user-questions/verification:
|
||||
en: c16fb364fe8ec13b
|
||||
zh: fa23c162348ba682
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: SDK native user questions
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-19-sdk-native-user-questions.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The SDK can expose the shared `ask_user_question` tool without a Web GUI, but an out-of-process host cannot answer the native user-questions event through the SDK. Publishing the tool alone leaves calls without an answerer. The SDK must carry the interaction while the application chooses its user interface or simulated-user policy.
|
||||
|
||||
## Decision
|
||||
|
||||
The TypeScript and Python clients accept a question callback and advertise it during initialization. The SDK server adapts the existing user-questions seam to a bidirectional RPC only for its exact owned root Agents. Clients without the capability keep native in-process answerers. Tool definitions and the service's live-root and delegation checks remain shared with the desktop composition.
|
||||
|
||||
Question identity is independent of the JSON-RPC envelope so cancellation can identify outstanding host work. The runtime validates a complete answer batch, aborts pending requests when the tool or server stops, and ignores late responses. The host owns answer collection; the SDK neither chooses answers nor treats missing input as approval. The [protocol reference](../../../../packages/sdk/protocol/README.md#native-user-questions) owns wire details.
|
||||
|
||||
## Consequences
|
||||
|
||||
A GUI and runtime patch module are unnecessary. A host can use the same interface for a terminal, browser, or simulated user. The Python SDK keeps its synchronous API by running callbacks outside the reader thread; callbacks must observe cancellation and release their own resources. Late callbacks cannot publish into a later runtime process. Question waits are process-local; completed tool results retain ordinary Session persistence.
|
||||
|
||||
A Web Session controller is not an interaction provider for the SDK. Reminder restoration uses the SDK's own Session owner and initialized model options; it does not claim a second write handle through a GUI controller.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Requiring a Web GUI or mounting its controller couples headless execution to another lifecycle owner. Asking each host to inject a runtime patch repeats the transport bridge outside the maintained SDK. Removing the shared question tool would reduce the model's available capabilities rather than supply its missing answer path.
|
||||
|
||||
## Verification
|
||||
|
||||
Server tests cover ownership, native fallback, answer validation, cancellation, and transport failure. Client tests cover concurrent bidirectional transport and cancellation, including callback advertisement and answer delivery with either explicit startup or a direct `initialize()` call. Initialization starts the runtime before constructing the handshake capabilities so both entry paths advertise the installed callback. Python tests also cover reentrant requests and a late callback after process replacement. Real shipped-profile tests drive the built-in question in standard and PTC modes and close a runtime while the host is answering. The [server tests](../../../../packages/sdk/server/tests/user-questions.spec.ts), [client tests](../../../../packages/sdk/client/tests/user-questions.spec.ts), [Python tests](../../../../python/sdk/tests/test_user_questions.py), and [profile tests](../../../../apps/cli/tests/profiles/sdk/user-questions.e2e.ts) retain this evidence.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: SDK 原生用户问答
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-19-sdk-native-user-questions.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
SDK 可以在没有 Web GUI 时暴露共享的 `ask_user_question` 工具,但进程外宿主无法通过 SDK 回答原生用户问答事件。仅发布工具会使调用缺少回答方。SDK 必须承载交互,应用则自行选择用户界面或模拟用户策略。
|
||||
|
||||
## 决策
|
||||
|
||||
TypeScript 与 Python 客户端接收问答回调,并在初始化时声明。SDK server 仅为其确切拥有的根 Agent,把现有用户问答 seam 适配为双向 RPC。未声明能力的客户端保留原生进程内回答方。工具定义以及服务对存活根 Agent 和委派关系的检查仍与桌面端组合共享。
|
||||
|
||||
问题标识独立于 JSON-RPC 帧,使取消能够定位待决的宿主工作。运行时校验完整答案批次,在工具或服务器停止时中止待决请求,并忽略晚到响应。宿主拥有答案收集过程;SDK 不选择答案,也不把缺失输入当作批准。[协议参考](../../../../packages/sdk/protocol/README.zh.md#native-user-questions)拥有具体协议说明。
|
||||
|
||||
## 影响
|
||||
|
||||
此流程不要求 GUI 或运行时补丁模块。宿主可以用同一接口接入终端、浏览器或模拟用户。Python SDK 在读取线程之外执行回调,以保留同步 API;回调必须响应取消并释放自己的资源。晚到回调无法向后续新运行时进程发送结果。问题等待仅属于当前进程;已完成工具结果仍通过普通 Session 路径持久化。
|
||||
|
||||
Web Session controller 不能充当 SDK 的交互提供方。提醒恢复使用 SDK 自身的 Session 所有者和初始化模型选项,不通过 GUI 控制器争用第二个写入 handle。
|
||||
|
||||
## 已考虑的替代方案
|
||||
|
||||
要求 Web GUI 或挂载其控制器,会将 headless 执行与另一个生命周期所有者耦合。要求各宿主注入运行时补丁,会在受维护的 SDK 外重复实现传输适配。移除共享问答工具只会减少模型可用能力,无法提供缺失的回答通道。
|
||||
|
||||
## 验证
|
||||
|
||||
服务器测试覆盖所有权、原生回退、答案校验、取消和传输失败。客户端测试覆盖并发双向传输和取消,包括显式启动和直接调用 `initialize()` 两条路径的回调声明与答案投递。初始化在构造握手能力之前启动运行时,使两条入口均声明已安装的回调。Python 测试还覆盖回调内再次请求,以及进程替换后的晚到回调。真实发行 profile 测试在标准模式和 PTC 模式调用内建问答,并在宿主回答期间关闭运行时。[服务器测试](../../../../packages/sdk/server/tests/user-questions.spec.ts)、[客户端测试](../../../../packages/sdk/client/tests/user-questions.spec.ts)、[Python 测试](../../../../python/sdk/tests/test_user_questions.py)和 [profile 测试](../../../../apps/cli/tests/profiles/sdk/user-questions.e2e.ts)保留这些依据。
|
||||
@@ -26,6 +26,9 @@
|
||||
/agent-note-frontend-large-session-performance-budgets/calibration/trajectory-readiness-polling:
|
||||
en: bdcfefd7358e335b
|
||||
zh: f718f66ca5944203
|
||||
/agent-note-frontend-large-session-performance-budgets/calibration/history-selector-invalidation:
|
||||
en: 2d2f78f4cf6aad8a
|
||||
zh: aec6af5f7c66de16
|
||||
/agent-note-frontend-large-session-performance-budgets/alternatives-considered:
|
||||
en: 4d2a4a64ccd14507
|
||||
zh: 8a4aaf4d4abea13d
|
||||
|
||||
@@ -86,6 +86,22 @@ A controlled macOS arm64 diagnostic on Node 24.19.0, Chromium 149.0.7827.55 and
|
||||
|
||||
Complete local 240-turn runs produce Trajectory samples of 157.795/146.737/148.571 ms before the observer change and 130.184/191.247/157.543 ms afterward, with medians 148.571 and 157.543 ms. Both runs pass every fixed endpoint budget; no local speedup or hosted calibration is claimed. The readiness regression rejects a hidden searchbox or record despite a visible unrelated table row; restoring the original selector waits makes that test fail.
|
||||
|
||||
### History selector invalidation
|
||||
|
||||
The older-history control is the immediate sibling before the `display: contents` flow anchor. Leading spacing therefore uses `.column > .older + [data-slot="conversation.chat.flow"]`, preserving the earlier selector's specificity and visible-row filtering. An ancestor `.column:has(> .older)` combined with the descendant empty-row `:has()` makes unrelated draft mutations invalidate the history subtree. A temporary Chromium invalidation trace identified this exact spacing selector and style recalculations covering 13,591–13,641 elements. The trace is causal evidence, not a latency measurement: tracing itself adds substantial overhead.
|
||||
|
||||
On Linux x64, Node 24.19.0 and Chromium 149.0.7827.55, separate three-sample runs of the unchanged browser benchmark measured these medians using the original and sibling selectors. Both retained all 25,409 DOM nodes and every trusted input overlapped the streaming reply.
|
||||
|
||||
| Endpoint | Original selector | Sibling selector |
|
||||
|---|---:|---:|
|
||||
| Draft typing | 517.39 ms | 378.50 ms |
|
||||
| First reply | 1556.89 ms | 801.58 ms |
|
||||
| Stream main-thread task | 2540.59 ms | 1774.75 ms |
|
||||
|
||||
These local runs both failed the independent open budget: their open medians were 1192.85 and 1666.99 ms. A separate six-sample diagnostic alternated the original and sibling selectors in the browser CSSOM before timing, using the same built artifacts. Its original/sibling open wall medians were 2598.99/1349.22 ms, while Chromium task medians were 325.67/305.90 ms. This finds no browser-computation regression in those samples, but does not explain the remaining wait or certify the whole benchmark. The alternating diagnostic measured typing medians of 350.18/362.82 ms, so the earlier typing improvement is not uniform across runs; first-reply medians were 1498.48/815.85 ms and stream-task medians 2449.69/1915.73 ms. The diagnostic changes are not shipped; workload, overlap assertions, budgets and benchmark source remain unchanged.
|
||||
|
||||
Post-GC browser heap was 80.70/80.71 MiB. GNU time reported maximum RSS of 535.78/561.80 MiB for each benchmark command and its children; this is not input-phase memory or an aggregate browser-memory measurement. Moving the composer's selection reveal into an animation frame was rejected: profiling showed the style flush moved to another geometry read, without improving typing. The product fix removes the invalidation source instead.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Use the Node fold as paint evidence.** Rejected because it never performs DOM mutation, layout, or browser scheduling. The focused reconnect case likewise makes no GUI speed claim.
|
||||
|
||||
@@ -86,6 +86,22 @@ Trajectory 激活在动画帧上采样可见搜索框与 `[data-trajectory-scrol
|
||||
|
||||
完整本地 240 轮运行在修改观察器前得到 Trajectory 样本 157.795/146.737/148.571 ms,修改后为 130.184/191.247/157.543 ms,中位数分别为 148.571 和 157.543 ms。两次运行均通过所有固定终点预算;不声称本地提速或托管环境校准。即使存在可见的无关表格行,就绪回归测试仍拒绝隐藏的搜索框或记录;恢复原选择器等待会使该测试失败。
|
||||
|
||||
### 历史区域选择器失效
|
||||
|
||||
更早历史控件紧邻 `display: contents` 流锚点之前。因此首行间距使用 `.column > .older + [data-slot="conversation.chat.flow"]`,保持原选择器的优先级与可见行过滤。祖先 `.column:has(> .older)` 与后代空行 `:has()` 组合,会让无关的草稿修改使历史子树样式失效。临时 Chromium 失效跟踪定位到这条具体的间距选择器,样式重算覆盖 13,591–13,641 个元素。跟踪用于定位原因,不用于测量延迟:跟踪本身会增加大量开销。
|
||||
|
||||
在 Linux x64、Node 24.19.0 与 Chromium 149.0.7827.55 上,使用原选择器与兄弟选择器分别执行未修改的浏览器基准,每组取三个样本的中位数。两组都保留全部 25,409 个 DOM 节点,且每个受信任的输入都与流式回复重叠。
|
||||
|
||||
| 终点 | 原选择器 | 兄弟选择器 |
|
||||
|---|---:|---:|
|
||||
| 草稿键入 | 517.39 ms | 378.50 ms |
|
||||
| 首段回复 | 1556.89 ms | 801.58 ms |
|
||||
| 流式主线程任务 | 2540.59 ms | 1774.75 ms |
|
||||
|
||||
这两次本地运行都未通过独立的打开预算:打开中位数分别为 1192.85 与 1666.99 ms。另一项六样本诊断使用同一份构建产物,在计时前通过浏览器 CSSOM 交替使用原选择器与兄弟选择器。原版/修改版的打开壁钟中位数为 2598.99/1349.22 ms,Chromium 任务中位数为 325.67/305.90 ms。这些样本未观察到浏览器计算回归,但没有解释剩余等待,也不代表完整基准已通过。交替诊断的键入中位数为 350.18/362.82 ms,因此前述键入改善并非每次运行都能观察到;首段回复中位数为 1498.48/815.85 ms,流式任务中位数为 2449.69/1915.73 ms。诊断改动不随产品提交;负载、重叠断言、预算与基准源码保持不变。
|
||||
|
||||
强制 GC 后的浏览器 heap 为 80.70/80.71 MiB。GNU time 为两次基准命令及其子进程报告的最大 RSS 为 535.78/561.80 MiB;这不是输入阶段内存,也不是浏览器总内存测量。曾尝试将输入框的选区显示推迟到动画帧,但分析显示样式刷新转移到另一处几何读取,没有改善键入,因此未采用。产品修复直接移除样式失效的来源。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**用 Node 折叠作为绘制证据。** 拒绝,因为它不执行 DOM 修改、布局或浏览器调度。聚焦重连用例同样不声称 GUI 提速。
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Frame one text or tool response from the local Messages endpoint. */
|
||||
export function messagesResponse(content: Record<string, unknown>, stopReason: 'end_turn' | 'max_tokens' | 'tool_use'): string {
|
||||
return [
|
||||
{ type: 'message_start', message: { id: 'sdk-smoke-response', model: 'deepseek-v4-pro', usage: { input_tokens: 3, output_tokens: 0 } } },
|
||||
{ type: 'content_block_start', index: 0, content_block: content },
|
||||
{ type: 'content_block_stop', index: 0 },
|
||||
{ type: 'message_delta', delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
|
||||
{ type: 'message_stop' },
|
||||
].map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join('')
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Freeze only the isolated runtime's wall clock before reminder activation starts. */
|
||||
export function apply(ctx) {
|
||||
const original = Date.now
|
||||
const now = Number(process.env.DSH_TEST_SCHEDULE_NOW)
|
||||
ctx.effect(() => {
|
||||
Date.now = () => now
|
||||
return () => { Date.now = original }
|
||||
})
|
||||
ctx.provide('sdkTestClock', { ready: true })
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { execa } from 'execa'
|
||||
import { describe, expect, it, onTestFinished } from 'vitest'
|
||||
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
|
||||
|
||||
import { messagesResponse } from './fixtures/messages-response.ts'
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
|
||||
const launch = resolveExampleLaunch({
|
||||
srcBin: fileURLToPath(new URL('../../../src/bin.ts', import.meta.url)),
|
||||
@@ -17,16 +19,6 @@ const launch = resolveExampleLaunch({
|
||||
})
|
||||
const decompress = promisify(zstdDecompress)
|
||||
|
||||
/** Frame one text or tool response from the local Messages endpoint. */
|
||||
function messagesResponse(content: Record<string, unknown>, stopReason: 'end_turn' | 'max_tokens' | 'tool_use'): string {
|
||||
return [
|
||||
{ type: 'message_start', message: { id: 'sdk-smoke-response', model: 'deepseek-v4-pro', usage: { input_tokens: 3, output_tokens: 0 } } },
|
||||
{ type: 'content_block_start', index: 0, content_block: content },
|
||||
{ type: 'content_block_stop', index: 0 },
|
||||
{ type: 'message_delta', delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
|
||||
{ type: 'message_stop' },
|
||||
].map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join('')
|
||||
}
|
||||
|
||||
function waitForLine(
|
||||
lines: string[],
|
||||
@@ -66,7 +58,11 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
{ label: 'reports max-token turns with mapping enabled through env', envValue: 'true', editorEnabled: false },
|
||||
{ label: 'reports max-token turns with mapping disabled through env', envValue: 'false', editorEnabled: false },
|
||||
{ label: 'allows an explicit patch to enable str_replace_editor', envValue: undefined, editorEnabled: true },
|
||||
])('$label', async ({ envValue, editorEnabled }) => {
|
||||
{ label: 'uses desktop tools through the PTC environment switch', envValue: undefined, editorEnabled: false, ptc: true },
|
||||
])('$label', async (scenario) => {
|
||||
const { envValue, editorEnabled } = scenario
|
||||
const ptc = 'ptc' in scenario && scenario.ptc
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-smoke-'))
|
||||
const editorPatch = join(root, 'editor.patch.yml')
|
||||
if (editorEnabled) await writeFile(editorPatch, [
|
||||
@@ -103,6 +99,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
DSH_HOME: join(root, '.dsh'),
|
||||
DSH_PERMISSION_MODE: 'danger-full-access',
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
DSH_TOOLS_MODE: ptc ? 'ptc' : 'native',
|
||||
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
||||
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
|
||||
...(envValue === undefined ? {} : { DSH_MAX_TOKENS_AS_SUCCESS: envValue }),
|
||||
@@ -176,9 +173,23 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
const toolNames = tools.map(tool => tool.name)
|
||||
expect(modelRequests[0]?.output_config).toEqual({ effort: 'max' })
|
||||
expect(modelRequests[0]?.max_tokens).toBe(1234)
|
||||
expect(toolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch', 'web_search']))
|
||||
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
|
||||
expect(toolNames).not.toContain('list_subagent_models')
|
||||
const expectedTools = ['read', 'write', 'edit', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
|
||||
'present', 'ask_user_question', 'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update']
|
||||
const requestText = JSON.stringify(modelRequests[0])
|
||||
if (ptc) {
|
||||
expect(toolNames).toEqual(['run_code'])
|
||||
for (const tool of expectedTools) expect(requestText.includes(`${tool}:`), tool).toBe(true)
|
||||
} else {
|
||||
expect(toolNames).toEqual(expect.arrayContaining(expectedTools))
|
||||
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
|
||||
expect(toolNames).not.toContain('list_subagent_models')
|
||||
}
|
||||
for (const text of ['Tokens prefixed with @', 'Prefer showing the primary results',
|
||||
'Time sampled while preparing']) {
|
||||
expect(requestText.includes(text), text).toBe(true)
|
||||
}
|
||||
expect(requestText).not.toContain('Current DeepSeek Harness implementation checkout:')
|
||||
expect(requestText).not.toContain('Current DeepSeek Harness Web GUI URL:')
|
||||
|
||||
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
|
||||
const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/** Reminder activation must share SDK ownership and initialized model limits after process restart. */
|
||||
import { createServer } from 'node:http'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
|
||||
import { clearAmbientProxyEnv } from '../../../../../scripts/test-proxy-environment.ts'
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { messagesResponse } from './fixtures/messages-response.ts'
|
||||
|
||||
it('delivers an overdue reminder with the SDK route and lets the client resume that same Session', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'sdk-schedule-resume-'))
|
||||
const patch = join(root, 'clock.patch.yml')
|
||||
const fixture = fileURLToPath(new URL('./fixtures/schedule-clock.mjs', import.meta.url))
|
||||
await writeFile(patch, [
|
||||
'- insert:', ' - id: sdk-test-clock', ` name: ${JSON.stringify(fixture)}`,
|
||||
'- id: sdk-jsonrpc-server', ' inject: [sdkAppStartup, loader, sdkTestClock]', '',
|
||||
].join('\n'))
|
||||
const requests: Record<string, unknown>[] = []
|
||||
const restored: Record<string, unknown>[] = []
|
||||
let restarting = false
|
||||
const server = createServer((incoming, response) => {
|
||||
let body = ''
|
||||
incoming.setEncoding('utf8')
|
||||
incoming.on('data', (chunk: string) => { body += chunk })
|
||||
incoming.on('end', () => {
|
||||
const request = JSON.parse(body) as Record<string, unknown>
|
||||
requests.push(request)
|
||||
if (restarting) restored.push(request)
|
||||
const content = requests.length === 1 ? {
|
||||
type: 'tool_use', id: 'create-reminder', name: 'schedule_create',
|
||||
input: { title: 'Check SDK', prompt: 'Report the SDK reminder', after_seconds: 86400 },
|
||||
} : { type: 'text', text: 'ready' }
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
response.end(messagesResponse(content, requests.length === 1 ? 'tool_use' : 'end_turn'))
|
||||
})
|
||||
})
|
||||
const clients: DeepSeekHarness[] = []
|
||||
onTestFinished(async () => {
|
||||
try { await Promise.all(clients.map(client => client.close())) } finally {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => { if (error === undefined) resolve(); else reject(error) })
|
||||
})
|
||||
} finally { await rm(root, { recursive: true, force: true }) }
|
||||
}
|
||||
})
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('model fixture has no port')
|
||||
const env = { ...process.env, DSH_TOOLS_MODE: 'native', DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
|
||||
DEEPSEEK_API_KEY: 'keyless-schedule-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
|
||||
clearAmbientProxyEnv(env)
|
||||
const first = new DeepSeekHarness({ cwd: root, dshHome: join(root, '.dsh'), patches: [patch],
|
||||
model: 'deepseek-v4-pro', maxTokens: 5678, initializeTimeoutMs: 30_000,
|
||||
env: { ...env, DSH_TEST_SCHEDULE_NOW: String(Date.parse('2030-01-01T00:00:00Z')) } })
|
||||
clients.push(first)
|
||||
expect((await first.session('saved').run('Set a reminder')).finalResponse).toBe('ready')
|
||||
expect(requests).toHaveLength(2)
|
||||
await first.close()
|
||||
restarting = true
|
||||
const second = new DeepSeekHarness({ cwd: root, dshHome: join(root, '.dsh'), patches: [patch],
|
||||
model: 'deepseek-v4-flash', maxTokens: 1234, initializeTimeoutMs: 30_000,
|
||||
env: { ...env, DSH_TEST_SCHEDULE_NOW: String(Date.parse('2030-01-03T00:00:00Z')) } })
|
||||
clients.push(second)
|
||||
await second.start()
|
||||
await vi.waitFor(() => { expect(restored).toHaveLength(1) }, { timeout: 30_000 })
|
||||
expect(restored[0]).toMatchObject({ model: 'deepseek-v4-flash', max_tokens: 1234 })
|
||||
const session = await second.resumeSession('saved')
|
||||
expect((await session.run('Continue after the reminder')).finalResponse).toBe('ready')
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
/** Keyless SDK questions through the shipped profile, native tools, and both tool-call modes. */
|
||||
import { createServer } from 'node:http'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { DeepSeekHarness, type SdkUserQuestionRequest } from '@deepseek-ai/dsh-sdk-client'
|
||||
import { clearAmbientProxyEnv } from '../../../../../scripts/test-proxy-environment.ts'
|
||||
import { expect, it, onTestFinished } from 'vitest'
|
||||
import { messagesResponse } from './fixtures/messages-response.ts'
|
||||
|
||||
it.each([
|
||||
{ mode: 'native', cancel: false }, { mode: 'ptc', cancel: false }, { mode: 'native', cancel: true },
|
||||
])('handles a built-in question through $mode (cancel: $cancel)', async ({ mode, cancel }) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'sdk-question-profile-'))
|
||||
const requests: Record<string, unknown>[] = []
|
||||
const questions = [{ id: 'format', question: 'Which format?', options: [{ label: 'CSV' }, { label: 'JSON' }] }]
|
||||
const server = createServer((incoming, response) => {
|
||||
let body = ''
|
||||
incoming.setEncoding('utf8')
|
||||
incoming.on('data', (chunk: string) => { body += chunk })
|
||||
incoming.on('end', () => {
|
||||
requests.push(JSON.parse(body) as Record<string, unknown>)
|
||||
const tool = mode === 'ptc' ? {
|
||||
type: 'tool_use', id: 'question-call', name: 'run_code', input: {
|
||||
code: `return await tools.ask_user_question(${JSON.stringify({ questions })});`, description: 'Ask which output format to use',
|
||||
},
|
||||
} : { type: 'tool_use', id: 'question-call', name: 'ask_user_question', input: { questions } }
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
response.end(messagesResponse(requests.length === 1 ? tool : { type: 'text', text: 'answer received' },
|
||||
requests.length === 1 ? 'tool_use' : 'end_turn'))
|
||||
})
|
||||
})
|
||||
const state: { harness?: DeepSeekHarness } = {}
|
||||
onTestFinished(async () => {
|
||||
try { await state.harness?.close() } finally {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error === undefined) resolve()
|
||||
else reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
finally { await rm(root, { recursive: true, force: true }) }
|
||||
}
|
||||
})
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('model fixture has no port')
|
||||
const env = { ...process.env, DSH_TOOLS_MODE: mode, DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
|
||||
DEEPSEEK_API_KEY: 'keyless-question-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
|
||||
clearAmbientProxyEnv(env)
|
||||
const seen: SdkUserQuestionRequest[] = []
|
||||
const entered = Promise.withResolvers<AbortSignal>()
|
||||
const harness = state.harness = new DeepSeekHarness({
|
||||
cwd: root, dshHome: join(root, '.dsh'), env, model: 'deepseek-v4-pro', maxTokens: 1234, initializeTimeoutMs: 30_000,
|
||||
onUserQuestion: async (request, { signal }) => {
|
||||
expect(signal.aborted).toBe(false)
|
||||
seen.push(request)
|
||||
entered.resolve(signal)
|
||||
if (cancel) return new Promise((_resolve, reject) => {
|
||||
signal.addEventListener('abort', () => { reject(new Error('question cancelled')) }, { once: true })
|
||||
})
|
||||
return { answers: [{ id: 'format', selected: ['CSV'] }] }
|
||||
},
|
||||
})
|
||||
const pending = harness.session('question-session').run('Ask which output format to use')
|
||||
if (cancel) {
|
||||
const settled = pending.catch(() => undefined)
|
||||
const signal = await entered.promise
|
||||
await harness.close()
|
||||
await settled
|
||||
expect(signal.aborted).toBe(true)
|
||||
expect(requests).toHaveLength(1)
|
||||
return
|
||||
}
|
||||
const result = await pending
|
||||
expect(seen).toEqual([{ requestId: expect.any(String) as string, sessionId: 'question-session', questions }])
|
||||
expect(result.finalResponse).toBe('answer received')
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(requests[1]?.max_tokens).toBe(1234)
|
||||
expect(JSON.stringify(requests[1]?.messages)).toContain('CSV')
|
||||
expect(result.events.some(event => event.type === 'tool/result' && event.data.error !== undefined)).toBe(false)
|
||||
})
|
||||
@@ -1 +1 @@
|
||||
Prefer showing the primary results within your final response alongside a brief explanation. Use  when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present when a separate file card helps the user open the complete deliverable, including images, Office documents, spreadsheets, and slide decks. Each presented file adds a card below the reply, with preview and native-open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
|
||||
Prefer showing the primary results within your final response alongside a brief explanation. Use  when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
|
||||
@@ -2799,8 +2799,8 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-schedule`
|
||||
|
||||
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionController` · `sessionPersistence`
|
||||
- `source`: [`packages/schedule/schedule/src/index.ts:72`](../packages/schedule/schedule/src/index.ts)
|
||||
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionPersistence`
|
||||
- `source`: [`packages/schedule/schedule/src/index.ts:83`](../packages/schedule/schedule/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the Host Schedule domain. */
|
||||
@@ -4714,7 +4714,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deliverables` | `systemPrompt` · `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-native` | — | [`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-goal` | — | [`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts) |
|
||||
@@ -4764,6 +4764,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
| `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) |
|
||||
|
||||
@@ -2801,8 +2801,8 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-schedule`
|
||||
|
||||
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionController` · `sessionPersistence`
|
||||
- `source`: [`packages/schedule/schedule/src/index.ts:72`](../packages/schedule/schedule/src/index.ts)
|
||||
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionPersistence`
|
||||
- `source`: [`packages/schedule/schedule/src/index.ts:83`](../packages/schedule/schedule/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Configuration for the Host Schedule domain. */
|
||||
@@ -4716,7 +4716,7 @@ export interface Config {
|
||||
| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deliverables` | `systemPrompt` · `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker-native` | — | [`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-client-ui-goal` | — | [`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts) |
|
||||
@@ -4766,6 +4766,7 @@ export interface Config {
|
||||
| `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) |
|
||||
|
||||
@@ -62,6 +62,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:412`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
|
||||
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
|
||||
| `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
@@ -81,7 +82,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:412`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
|
||||
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
|
||||
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
|
||||
| `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
@@ -83,7 +84,7 @@
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
|
||||
|
||||
+17
-11
@@ -240,6 +240,7 @@ flowchart TD
|
||||
pkg_deepseek_account_platform["deepseek-account-platform"]
|
||||
end
|
||||
subgraph group_deliverables["packages/deliverables"]
|
||||
pkg_deliverable_file_references["deliverable-file-references"]
|
||||
pkg_tool_present["tool-present"]
|
||||
pkg_workspace_changes["workspace-changes"]
|
||||
end
|
||||
@@ -502,6 +503,7 @@ flowchart TD
|
||||
pkg_attachment_local --> pkg_home_paths
|
||||
pkg_hmr --> pkg_app_boot
|
||||
pkg_hmr --> pkg_cmdline
|
||||
pkg_deliverable_file_references --> pkg_system_prompt
|
||||
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
|
||||
pkg_experimental_webworker_runtime --> pkg_client_connection
|
||||
pkg_experimental_webworker_runtime --> pkg_client_modules
|
||||
@@ -1361,6 +1363,7 @@ flowchart TD
|
||||
pkg_sdk_protocol --> pkg_llm
|
||||
pkg_sdk_protocol --> pkg_session
|
||||
pkg_sdk_protocol --> pkg_subagent
|
||||
pkg_sdk_protocol --> pkg_user_questions
|
||||
pkg_workflow_ptc --> pkg_agent
|
||||
pkg_workflow_ptc --> pkg_llm
|
||||
pkg_workflow_ptc --> pkg_ptc_runtime
|
||||
@@ -1392,15 +1395,6 @@ flowchart TD
|
||||
pkg_sdk_client --> pkg_llm
|
||||
pkg_sdk_client --> pkg_sdk_protocol
|
||||
pkg_sdk_client --> pkg_session
|
||||
pkg_sdk_jsonrpc_server --> pkg_agent
|
||||
pkg_sdk_jsonrpc_server --> pkg_attachment
|
||||
pkg_sdk_jsonrpc_server --> pkg_llm
|
||||
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
|
||||
pkg_sdk_jsonrpc_server --> pkg_scope
|
||||
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
|
||||
pkg_sdk_jsonrpc_server --> pkg_session
|
||||
pkg_sdk_jsonrpc_server --> pkg_subagent
|
||||
pkg_sdk_jsonrpc_server --> pkg_working_directory
|
||||
pkg_client_test_runtime --> pkg_api_gateway
|
||||
pkg_client_test_runtime --> pkg_api_session_controller
|
||||
pkg_client_test_runtime --> pkg_api_workspace_controller
|
||||
@@ -1430,6 +1424,17 @@ flowchart TD
|
||||
pkg_tool_schedule --> pkg_schedule
|
||||
pkg_tool_schedule --> pkg_subagent
|
||||
pkg_tool_schedule --> pkg_tools
|
||||
pkg_sdk_jsonrpc_server --> pkg_agent
|
||||
pkg_sdk_jsonrpc_server --> pkg_attachment
|
||||
pkg_sdk_jsonrpc_server --> pkg_llm
|
||||
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
|
||||
pkg_sdk_jsonrpc_server --> pkg_schedule
|
||||
pkg_sdk_jsonrpc_server --> pkg_scope
|
||||
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
|
||||
pkg_sdk_jsonrpc_server --> pkg_session
|
||||
pkg_sdk_jsonrpc_server --> pkg_subagent
|
||||
pkg_sdk_jsonrpc_server --> pkg_user_questions
|
||||
pkg_sdk_jsonrpc_server --> pkg_working_directory
|
||||
```
|
||||
|
||||
<!-- BEGIN GENERATED module-graph:packages -->
|
||||
@@ -1597,6 +1602,7 @@ flowchart TD
|
||||
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
|
||||
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
|
||||
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
|
||||
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
|
||||
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
|
||||
@@ -1772,13 +1778,13 @@ flowchart TD
|
||||
| [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | `experimental` | [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) |
|
||||
<!-- END GENERATED module-graph:packages -->
|
||||
+17
-11
@@ -242,6 +242,7 @@ flowchart TD
|
||||
pkg_deepseek_account_platform["deepseek-account-platform"]
|
||||
end
|
||||
subgraph group_deliverables["packages/deliverables"]
|
||||
pkg_deliverable_file_references["deliverable-file-references"]
|
||||
pkg_tool_present["tool-present"]
|
||||
pkg_workspace_changes["workspace-changes"]
|
||||
end
|
||||
@@ -504,6 +505,7 @@ flowchart TD
|
||||
pkg_attachment_local --> pkg_home_paths
|
||||
pkg_hmr --> pkg_app_boot
|
||||
pkg_hmr --> pkg_cmdline
|
||||
pkg_deliverable_file_references --> pkg_system_prompt
|
||||
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
|
||||
pkg_experimental_webworker_runtime --> pkg_client_connection
|
||||
pkg_experimental_webworker_runtime --> pkg_client_modules
|
||||
@@ -1363,6 +1365,7 @@ flowchart TD
|
||||
pkg_sdk_protocol --> pkg_llm
|
||||
pkg_sdk_protocol --> pkg_session
|
||||
pkg_sdk_protocol --> pkg_subagent
|
||||
pkg_sdk_protocol --> pkg_user_questions
|
||||
pkg_workflow_ptc --> pkg_agent
|
||||
pkg_workflow_ptc --> pkg_llm
|
||||
pkg_workflow_ptc --> pkg_ptc_runtime
|
||||
@@ -1394,15 +1397,6 @@ flowchart TD
|
||||
pkg_sdk_client --> pkg_llm
|
||||
pkg_sdk_client --> pkg_sdk_protocol
|
||||
pkg_sdk_client --> pkg_session
|
||||
pkg_sdk_jsonrpc_server --> pkg_agent
|
||||
pkg_sdk_jsonrpc_server --> pkg_attachment
|
||||
pkg_sdk_jsonrpc_server --> pkg_llm
|
||||
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
|
||||
pkg_sdk_jsonrpc_server --> pkg_scope
|
||||
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
|
||||
pkg_sdk_jsonrpc_server --> pkg_session
|
||||
pkg_sdk_jsonrpc_server --> pkg_subagent
|
||||
pkg_sdk_jsonrpc_server --> pkg_working_directory
|
||||
pkg_client_test_runtime --> pkg_api_gateway
|
||||
pkg_client_test_runtime --> pkg_api_session_controller
|
||||
pkg_client_test_runtime --> pkg_api_workspace_controller
|
||||
@@ -1432,6 +1426,17 @@ flowchart TD
|
||||
pkg_tool_schedule --> pkg_schedule
|
||||
pkg_tool_schedule --> pkg_subagent
|
||||
pkg_tool_schedule --> pkg_tools
|
||||
pkg_sdk_jsonrpc_server --> pkg_agent
|
||||
pkg_sdk_jsonrpc_server --> pkg_attachment
|
||||
pkg_sdk_jsonrpc_server --> pkg_llm
|
||||
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
|
||||
pkg_sdk_jsonrpc_server --> pkg_schedule
|
||||
pkg_sdk_jsonrpc_server --> pkg_scope
|
||||
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
|
||||
pkg_sdk_jsonrpc_server --> pkg_session
|
||||
pkg_sdk_jsonrpc_server --> pkg_subagent
|
||||
pkg_sdk_jsonrpc_server --> pkg_user_questions
|
||||
pkg_sdk_jsonrpc_server --> pkg_working_directory
|
||||
```
|
||||
|
||||
<!-- BEGIN GENERATED module-graph:packages -->
|
||||
@@ -1599,6 +1604,7 @@ flowchart TD
|
||||
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
|
||||
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
|
||||
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
|
||||
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
|
||||
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
|
||||
@@ -1774,13 +1780,13 @@ flowchart TD
|
||||
| [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | `experimental` | [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) |
|
||||
<!-- END GENERATED module-graph:packages -->
|
||||
@@ -36,5 +36,5 @@
|
||||
en: 077aa6573d9ede1a
|
||||
zh: 0acea592a6e3630a
|
||||
/host-wide-schedule/host-delivery:
|
||||
en: b6d522d24b9b97ba
|
||||
zh: 84df8ea09bedf51c
|
||||
en: d5fc5b853529b139
|
||||
zh: 4cc5eba14f9660e6
|
||||
@@ -431,7 +431,7 @@ New tasks initialize an empty history. Older task rows without history expose on
|
||||
|
||||
## Host delivery
|
||||
|
||||
The Host reads persisted tasks at startup and sets its timer to the earliest active target. Inactive tasks remain queryable but never schedule another delivery. At delivery it resolves the original Session through the Session controller, including cold Sessions. After restoration it samples the wall clock again and excludes members that are no longer due after a rollback; those tasks retain their timer obligation. Neither a Goal nor a separate Run completion record participates in dispatch.
|
||||
The Host reads persisted tasks at startup and sets its timer to the earliest active target. Inactive tasks remain queryable but never schedule another delivery. At delivery it resolves the original Session, including cold Sessions, through `schedule/resolve-agent`. The embedding owns activation: the SDK handler waits for initialization and retains its Session handle and model options; without a handler, the Web Session controller restores the Session. The composition waits for its activation provider before starting Schedule. After restoration it samples the wall clock again and excludes members that are no longer due after a rollback; those tasks retain their timer obligation. Neither a Goal nor a separate Run completion record participates in dispatch.
|
||||
|
||||
Plugin-sourced `followup()` synchronously appends the message to the Session inbox. After a successful Session flush, one task-record put appends a saved delivery with its instruction snapshot and stores the latest `ScheduleDeliveryReceipt` together with the one-shot's `inactive` status or the recurring task's next target and status. Recurring tasks in the same batch share the receipt's `messageId`; each receipt retains that task's delivered occurrence time. Delivery never steers or cancels the current turn and does not wait for model completion.
|
||||
|
||||
@@ -547,4 +547,24 @@ Durable task set changed; clients refetch global task and Session-active catalog
|
||||
```
|
||||
|
||||
Source: [`packages/schedule/schedule/src/types.ts`](../../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
<a id="scheduleresolve-agent--waterfall"></a>
|
||||
|
||||
#### `schedule/resolve-agent` — waterfall
|
||||
|
||||
Restore a reminder's Session through its embedding's owning controller. Call next() to use the Web Session controller.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Restore a reminder's Session through its embedding's owning controller.
|
||||
* Call next() to use the Web Session controller.
|
||||
* @param sessionId - Original Session receiving the reminder.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'schedule/resolve-agent'(sessionId: SessionId, next: () => Promise<Agent>): Promise<Agent>
|
||||
```
|
||||
|
||||
Types: [Agent](core.md) · [SessionId](core.md)
|
||||
|
||||
Source: [`packages/schedule/schedule/src/index.ts`](../../packages/schedule/schedule/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
@@ -431,7 +431,7 @@ type ScheduleDeliveryHistoryResult =
|
||||
|
||||
## 宿主投递
|
||||
|
||||
宿主启动时读取持久任务,并将定时器设为最早的活动目标。未运行任务仍可查询,但不会再次安排投递。投递时通过 Session controller 获取原 Session,包括尚未加载的 Session。恢复后重新采样实际时间,并排除时钟回拨后尚未到期的成员;这些任务仍保留定时义务。投递不涉及 Goal 或独立的 Run 完成记录。
|
||||
宿主启动时读取持久任务,并将定时器设为最早的活动目标。未运行任务仍可查询,但不会再次安排投递。投递通过 `schedule/resolve-agent` 解析原 Session,包括尚未激活的会话。嵌入方拥有激活过程:SDK 处理方等待初始化,并保留其 Session handle 和模型选项;没有处理方时由 Web Session controller 恢复会话。组合等待其激活提供方就绪后再启动 Schedule。恢复后重新采样实际时间,并排除时钟回拨后尚未到期的成员;这些任务仍保留定时义务。投递不涉及 Goal 或独立的 Run 完成记录。
|
||||
|
||||
插件来源的 `followup()` 同步将消息追加到 Session 收件箱。Session flush 成功后,一次任务记录 put 追加带有指令快照的发送记录,并同时存储最新的 `ScheduleDeliveryReceipt`,以及一次性任务的 `inactive` 状态或重复任务的下一目标和状态。同一批次的重复任务共享回执的 `messageId`;每条回执保留该任务已投递的发生时点。投递不会中途引导或取消当前轮次,也不等待模型完成。
|
||||
|
||||
@@ -547,4 +547,24 @@ Durable task set changed; clients refetch global task and Session-active catalog
|
||||
```
|
||||
|
||||
Source: [`packages/schedule/schedule/src/types.ts`](../../packages/schedule/schedule/src/types.ts)
|
||||
|
||||
<a id="scheduleresolve-agent--waterfall"></a>
|
||||
|
||||
#### `schedule/resolve-agent` — waterfall
|
||||
|
||||
Restore a reminder's Session through its embedding's owning controller. Call next() to use the Web Session controller.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Restore a reminder's Session through its embedding's owning controller.
|
||||
* Call next() to use the Web Session controller.
|
||||
* @param sessionId - Original Session receiving the reminder.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'schedule/resolve-agent'(sessionId: SessionId, next: () => Promise<Agent>): Promise<Agent>
|
||||
```
|
||||
|
||||
Types: [Agent](core.zh.md) · [SessionId](core.zh.md)
|
||||
|
||||
Source: [`packages/schedule/schedule/src/index.ts`](../../packages/schedule/schedule/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/sdk-desktop-context/guide.md
|
||||
/:
|
||||
en: d3cffa64b9c50498
|
||||
zh: 515c03ce9ae6d031
|
||||
/sdk-desktop-tools-and-context:
|
||||
en: d202616bf6e03fa4
|
||||
zh: cc276adda4b952be
|
||||
/sdk-desktop-tools-and-context/change:
|
||||
en: 85ac2825c52e2503
|
||||
zh: fd6fdbdca131c778
|
||||
/sdk-desktop-tools-and-context/migration:
|
||||
en: 9caef4f8b153622a
|
||||
zh: 1b4901f5b18b72fd
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: The existing SDK profile includes native desktop tools and model context.
|
||||
---
|
||||
|
||||
# SDK desktop tools and context
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
The existing `sdk` profile now includes `present`, `ask_user_question`, schedule tools, file-reference guidance and time context. It does not start a Web server. Standard tool calling remains the default; `DSH_TOOLS_MODE=ptc` selects programmatic tool calling in the same profile. `sdk-minimal` is unchanged.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Keep selecting `sdk`. Provide `onUserQuestion` (TypeScript) or `on_user_question` (Python) to answer built-in questions over the native protocol, or retain an in-process `user-questions/request` answerer; without one, the tool reports `NO_PROVIDER`.
|
||||
2. Working directory and model still come from SDK initialization. The SDK profile does not mount the Web-specific checkout and GUI development context.
|
||||
3. Custom compositions that need the previous model-visible tool/context roster can disable the `present`, `tool-ask-user`, `tool-schedule`, `file-reference-local`, `deliverable-file-references`, and `time-context` rows. Review any pinned prompts and tool schemas before upgrading.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: 现有 SDK profile 包含原生桌面工具和模型上下文。
|
||||
---
|
||||
|
||||
# SDK 桌面工具与上下文
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
现有 `sdk` profile 现在包含 `present`、`ask_user_question`、定时任务工具、文件引用说明和时间上下文。它不启动 Web server。默认仍使用标准工具调用;`DSH_TOOLS_MODE=ptc` 在同一 profile 中选择程序化工具调用。`sdk-minimal` 保持不变。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 继续选择 `sdk`。通过 `onUserQuestion`(TypeScript)或 `on_user_question`(Python)使用原生协议回答内建问题,或保留进程内的 `user-questions/request` 回答方;没有回答方时,工具报告 `NO_PROVIDER`。
|
||||
2. 工作目录和模型仍来自 SDK 初始化。SDK profile 不挂载 Web 专用的 checkout 和 GUI 开发上下文。
|
||||
3. 需要保留之前模型可见工具/上下文组合的自定义配置,可以禁用 `present`、`tool-ask-user`、`tool-schedule`、`file-reference-local`、`deliverable-file-references` 和 `time-context` 配置项。升级前请检查固定的提示词和工具 schema。
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 4e53164159b5ce41
|
||||
zh: 89582dc0294d0776
|
||||
/deepseek-ai-dsh-sdk-app/use-this-package:
|
||||
en: 63573087d1db9aff
|
||||
zh: 638612ac3f0da2c0
|
||||
en: b76bd7ea9880f529
|
||||
zh: e80c7b9fa331dd9b
|
||||
/deepseek-ai-dsh-sdk-app/model-experience:
|
||||
en: 215e7ba838619b7b
|
||||
zh: a311da3843709f90
|
||||
@@ -24,17 +24,17 @@
|
||||
en: 4f88e647c6df4f42
|
||||
zh: 2bd3e4b0bdfe9412
|
||||
/deepseek-ai-dsh-sdk-app/model-experience/sdk-model-context/what-the-model-sees:
|
||||
en: fc42eb5995eb2078
|
||||
zh: 4e47602e46ba5123
|
||||
en: 0cce2b88aae3a0e5
|
||||
zh: fcb31314096ef15e
|
||||
/deepseek-ai-dsh-sdk-app/model-experience/sdk-model-context/token-effect:
|
||||
en: 4a0cdca06d358f53
|
||||
zh: d27080dcb68c661c
|
||||
en: 0d4a38b2448396b3
|
||||
zh: e63408a146bf0100
|
||||
/deepseek-ai-dsh-sdk-app/model-experience/sdk-model-context/kv-cache-effect:
|
||||
en: 5b0e33295fb90e19
|
||||
zh: 4d48ca75bb677202
|
||||
en: 0503df0971e5f8d7
|
||||
zh: 18cae8c6e25cda04
|
||||
/deepseek-ai-dsh-sdk-app/known-limitations-and-deferred-work:
|
||||
en: 416e2793f9786480
|
||||
zh: 05cc4fd289d0185d
|
||||
en: 451497677de7e6b9
|
||||
zh: dcfdc671c38d54d4
|
||||
/deepseek-ai-dsh-sdk-app/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: e88aeb617ffb74ea
|
||||
@@ -23,6 +23,8 @@ The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/RE
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
The `sdk` profile includes native desktop tools and model context. Standard tool calling is the default; set `DSH_TOOLS_MODE=ptc` for programmatic tool calling. Both modes use the same plugins.
|
||||
|
||||
The startup provider binds stdin EOF to the launcher's bounded successful shutdown. SDK protocol `shutdown`, SIGINT, and SIGTERM retain their owning server or launcher paths; disposal drains the root profile tree and persistence. Stdout is reserved for newline-delimited JSON-RPC frames. The bundle disables model-generated session titles because the SDK exposes no title surface; deterministic fallback titles remain durable without an auxiliary model request. The inherited projection cache checkpoints SDK-created sessions for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. A deployment selects a different complete composition through profile bundles and patch files, not another app bin.
|
||||
|
||||
| Config | Default | Behavior |
|
||||
@@ -44,20 +46,24 @@ The Python runtime exposes explicitly downloaded authoring resources and Office
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The profile inherits the base system prompt, including the `You are an AI agent powered by DeepSeek Harness.` identity, and adds the same model-interpolated coding persona as the Web and headless profiles. Each Session receives its current directory in required user-role context from [`dsh-working-directory`](../../session/working-directory/README.md). Default file tool schemas include `read`, `write`, and `edit`; they omit `str_replace_editor`.
|
||||
The profile inherits the base system prompt, including the `You are an AI agent powered by DeepSeek Harness.` identity, and adds the same model-interpolated coding persona as the Web and headless profiles. Each Session receives its current directory in required user-role context from [`dsh-working-directory`](../../session/working-directory/README.md). Default file tool schemas include `read`, `write`, and `edit`; they omit `str_replace_editor`. The model also receives `present`, `ask_user_question`, schedule tools, shared file-reference guidance, and time context. Child agents cannot use schedule tools. The [`time-context`](../../context/time-context/README.md) row accepts a fallback time zone. Skills use the configured resources described above.
|
||||
|
||||
#### Token effect
|
||||
|
||||
The base prompt sections and selected tool schemas determine token usage; this profile adds one coding-persona sentence.
|
||||
The profile adds a coding-persona sentence, the mounted tool definitions and guidance, and logged runtime-context and time messages.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Stable for a fixed profile, provider, model, and tool roster. Profile changes take effect on the next process because the shipped SDK profile uses startup-only patches.
|
||||
The static prefix is stable for a fixed profile, provider, model, and tool roster. Runtime values append to logged user context. Profile changes take effect on the next process because the shipped SDK profile uses startup-only patches.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **No graphical client** — file cards, native file opening, and browser state require an embedding client; the SDK starts no Web server.
|
||||
- **User time zone is unknown** — the SDK protocol does not carry a user time zone, so time context reports `Browser time zone for this request: unavailable.` The process or container time zone may differ from the user's. The `time-context.timeZone` setting only controls fallback timestamp formatting; it does not establish the user's time zone. Automatic detection and SDK time-zone input are deferred.
|
||||
- **Questions require an answerer** — configure `onUserQuestion` in TypeScript or `on_user_question` in Python, or supply a native `user-questions/request` handler; otherwise `ask_user_question` returns `NO_PROVIDER`.
|
||||
- **Schedules share the runtime lifetime** — delivery needs a running process and its persistent Session storage.
|
||||
- **A profile can omit the SDK server** — a custom profile selected by the TypeScript client must retain this bundle or another `dsh-sdk-jsonrpc-server` row; client initialization fails when no peer answers.
|
||||
- **User plugins can violate stdout purity** — profile and per-launch patches are trusted application composition. The shipped bundle writes no non-protocol stdout, but it cannot contain an arbitrary inserted plugin.
|
||||
- **Configuration changes require restart** — the `sdk-app` bundle disables HMR in YAML so one stdio connection never observes a replacement server or Agent dependency.
|
||||
|
||||
@@ -23,6 +23,8 @@ kind: "package-bundle"
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
`sdk` profile 包含原生桌面工具和模型上下文。默认使用标准工具调用;设置 `DSH_TOOLS_MODE=ptc` 可使用程序化工具调用。两种模式使用相同插件。
|
||||
|
||||
启动提供方把 stdin EOF 接到启动器的有界成功关闭流程。SDK 协议 `shutdown`、SIGINT 与 SIGTERM 继续使用各自所属的 server 或启动器路径;dispose(资源释放)会排空根 profile 配置树与持久化。stdout 专用于按换行分隔的 JSON-RPC 帧。SDK 不提供 title 表层,因此本组合包禁用模型生成的会话标题;确定性的 fallback title 仍会持久化,但不发起辅助模型请求。继承的投影缓存会为 SDK 创建的会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 行。部署通过 profile 组合包与 patch 文件选择另一套完整组合,而不是使用另一个应用 bin。
|
||||
|
||||
| 配置 | 默认值 | 行为 |
|
||||
@@ -44,20 +46,24 @@ Python 运行时向本 profile 提供显式下载的创作资源和 Office sidec
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
profile 继承 base 系统提示词,包括 `You are an AI agent powered by DeepSeek Harness.` 身份说明,并添加与 Web 和 headless profile 相同、按当前模型插值的 coding persona。每个 Session 通过 [`dsh-working-directory`](../../session/working-directory/README.zh.md) 提供的必需 user-role 上下文接收当前目录。默认文件工具 schema 包含 `read`、`write` 和 `edit`,不包含 `str_replace_editor`。
|
||||
profile 继承 base 系统提示词,包括 `You are an AI agent powered by DeepSeek Harness.` 身份说明,并添加与 Web 和 headless profile 相同、按当前模型插值的 coding persona。每个 Session 通过 [`dsh-working-directory`](../../session/working-directory/README.zh.md) 提供的必需 user-role 上下文接收当前目录。默认文件工具 schema 包含 `read`、`write` 和 `edit`,不包含 `str_replace_editor`。模型还会收到 `present`、`ask_user_question`、定时任务工具、共用文件引用说明和时间上下文。子 agent 不能使用定时任务工具。[`time-context`](../../context/time-context/README.zh.md) 配置项接受回退时区。Skills 使用上述配置的资源。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
base 提示词段落与所选工具 schema 决定 token 用量;此 profile 额外添加一句 coding persona。
|
||||
此 profile 添加一句 coding persona、所挂载的工具定义与说明,以及记录在日志中的运行时上下文和时间消息。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
对固定 profile、提供方、模型与工具清单保持稳定。由于随附 SDK profile 使用仅启动时 patch,profile 变化会在下一个进程生效。
|
||||
对固定 profile、提供方、模型与工具清单,静态前缀保持稳定。运行时值追加到已记录的 user 上下文中。由于随附 SDK profile 使用仅启动时 patch,profile 变化会在下一个进程生效。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **没有图形客户端**:文件卡片、本地文件打开和浏览器状态需要嵌入客户端;SDK 不启动 Web server。
|
||||
- **用户时区未知**:SDK 协议不携带用户时区,因此时间上下文显示 `Browser time zone for this request: unavailable.`。进程或容器的时区可能与用户时区不同。`time-context.timeZone` 配置只控制回退时间戳的显示格式,不能据此确定用户时区。自动检测和 SDK 时区传参暂缓支持。
|
||||
- **提问需要回答方**:在 TypeScript 中配置 `onUserQuestion`、在 Python 中配置 `on_user_question`,或提供原生 `user-questions/request` handler;否则 `ask_user_question` 返回 `NO_PROVIDER`。
|
||||
- **定时任务依赖运行时生命周期**:投递需要运行中的进程及其持久化 Session 存储。
|
||||
- **profile 可能省略 SDK server**:TypeScript client 选择的自定义 profile 必须保留本组合包或另一个 `dsh-sdk-jsonrpc-server` 配置项;没有 peer 响应时,client 初始化会失败。
|
||||
- **用户插件可以破坏 stdout 纯净性**:profile 与逐次启动 patch 属于受信任应用组合。随附组合包不会向 stdout 写入非协议内容,但无法约束任意插入插件。
|
||||
- **配置变化需要重启**:`sdk-app` 组合包在 YAML 中禁用 HMR,因此一个 stdio 连接不会观察到 server 或 agent 依赖被替换。
|
||||
|
||||
@@ -39,3 +39,40 @@
|
||||
assetRoot: !!js "process.env.DSH_BUNDLED_OFFICE_SKILLS ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', '..', 'office-skills')"
|
||||
cli: !!js "process.env.DSH_BUNDLED_OFFICE_CLI === '' ? false : process.env.DSH_BUNDLED_OFFICE_CLI"
|
||||
node: !!js "process.env.DSH_BUNDLED_OFFICE_NODE ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node')"
|
||||
|
||||
# Share native desktop tools and model context without starting a Web server.
|
||||
- id: tools
|
||||
config:
|
||||
mode: !!js process.env.DSH_TOOLS_MODE
|
||||
|
||||
- insert:
|
||||
- id: connection
|
||||
name: '@deepseek-ai/dsh-client-connection'
|
||||
|
||||
- id: file-upload
|
||||
name: '@deepseek-ai/dsh-client-file-upload'
|
||||
|
||||
- id: workspace
|
||||
name: '@deepseek-ai/dsh-workspace'
|
||||
|
||||
- id: schedule
|
||||
name: '@deepseek-ai/dsh-schedule'
|
||||
inject: [sdkSessionActivation]
|
||||
|
||||
- id: time-context
|
||||
name: '@deepseek-ai/dsh-time-context'
|
||||
|
||||
- id: tool-schedule
|
||||
name: '@deepseek-ai/dsh-tool-schedule'
|
||||
|
||||
- id: tool-ask-user
|
||||
name: '@deepseek-ai/dsh-tool-ask-user'
|
||||
|
||||
- id: present
|
||||
name: '@deepseek-ai/dsh-tool-present'
|
||||
|
||||
- id: file-reference-local
|
||||
name: '@deepseek-ai/dsh-file-reference-local'
|
||||
|
||||
- id: deliverable-file-references
|
||||
name: '@deepseek-ai/dsh-deliverable-file-references'
|
||||
@@ -39,7 +39,17 @@
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"commander": "^15.0.0",
|
||||
"@deepseek-ai/dsh-skill-office": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:*"
|
||||
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-file-upload": "workspace:*",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:*",
|
||||
"@deepseek-ai/dsh-schedule": "workspace:*",
|
||||
"@deepseek-ai/dsh-time-context": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-schedule": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-present": "workspace:*",
|
||||
"@deepseek-ai/dsh-file-reference-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-deliverable-file-references": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~"
|
||||
|
||||
@@ -346,6 +346,9 @@
|
||||
- id: ui-cordis
|
||||
name: '@deepseek-ai/dsh-client-ui-cordis'
|
||||
|
||||
- id: deliverable-file-references
|
||||
name: '@deepseek-ai/dsh-deliverable-file-references'
|
||||
|
||||
# Turn tail: the changed-files card and delivery cards under each closing
|
||||
# assistant message. Remove this entry to turn the surface off; the tail
|
||||
# hole renders empty.
|
||||
|
||||
@@ -188,7 +188,8 @@
|
||||
"@deepseek-ai/dsh-client-product-analytics": "workspace:*",
|
||||
"@deepseek-ai/dsh-host-product-telemetry-otel": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-settings-session-log": "workspace:*",
|
||||
"@deepseek-ai/dsh-harness-dev-context": "workspace:*"
|
||||
"@deepseek-ai/dsh-harness-dev-context": "workspace:*",
|
||||
"@deepseek-ai/dsh-deliverable-file-references": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
en: b0d806604bc17006
|
||||
zh: 18dabe5dc2d0d419
|
||||
/deepseek-ai-dsh-client-ui-chat/grouped-rendering:
|
||||
en: 408664597c1480ed
|
||||
zh: 287a9b981337f288
|
||||
en: f78cbc73be5dc3ac
|
||||
zh: 893ba03d66b8ab65
|
||||
/deepseek-ai-dsh-client-ui-chat/scroll-ownership:
|
||||
en: 284737900cb75668
|
||||
zh: 12e0844082ff79ce
|
||||
|
||||
@@ -107,7 +107,7 @@ The Chat Builder publishes `bottomSource` per Node: true for visible Nodes belon
|
||||
|
||||
Live tool deltas share reasoning's frame-batched publication; durable calls and results publish immediately. Native and nested results retain event presentation metadata, so historical file and command labels use the operation’s location. Repeated deltas retain the Tool node when observed argument answers, the anchor, location, and visibility are unchanged. The Definition supplies the same lazy argument reader to tool rows and group detail, without tool-specific registration.
|
||||
|
||||
The process group uses a stable `div` layout box, a scroll body, and an uncapped content box that reports growth inside the body. Business styles must adapt spacing within and across groups, including hidden or empty members and the answer-spacing exception. The `conversation.chat.flow` slot adds a `display: contents` DOM anchor; row selectors match that anchor directly, without repeating its column ancestry. The running status shares the rows' parent, so its divider uses the same visible-sibling rules. The anchor contributes no spacing of its own. CSS variables do not belong in the Group Definition.
|
||||
The process group uses a stable `div` layout box, a scroll body, and an uncapped content box that reports growth inside the body. Business styles must adapt spacing within and across groups, including hidden or empty members and the answer-spacing exception. The `conversation.chat.flow` slot adds a `display: contents` DOM anchor; row selectors match that anchor directly, without repeating its column ancestry. The running status shares the rows' parent, so its divider uses the same visible-sibling rules. The anchor contributes no spacing of its own. The earlier-history control directly precedes the flow anchor; their sibling relationship supplies the leading gap. CSS variables do not belong in the Group Definition.
|
||||
|
||||
Running group titles, reasoning previews, tool and command rows, the active retry line, and the bottom running status use the shared [TextShimmer](../ui-primitives/README.md#component-catalog). Each row’s text shares one left-to-right highlight, including collapsed group titles; icons and hover chevrons retain their base appearance. Tool and process rows sit 6px apart. An expanded group title has 8px before its content, Assistant responses have 12px from adjacent process rows, and completed-Turn duration/status controls have 16px from their input and from a directly following visible row. A reply that follows the control after folded rows keeps the 12px reply gap, so folding never changes its spacing. Hidden and empty rows, including retained empty slot anchors, add no gap. The running “Deep diving” text retains its blue base while its theme-specific highlight sweeps across it.
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ Chat Builder 按节点发布 `bottomSource`:节点可见且属于已加载的
|
||||
|
||||
实时工具 delta 与推理共用按帧合并的发布节奏;持久调用和结果立即发布。原生与嵌套结果保留事件中的展示元数据,使历史文件与命令标签使用执行时的位置。当已读取参数的答案、锚点、位置及可见性均未变化时,重复 delta 保持 Tool 节点不变。Definition 向工具行和分组详情提供同一个懒计算参数读器,无需按工具注册。
|
||||
|
||||
过程组使用稳定的 `div` 布局盒子、滚动正文及不限高的内容盒子,后者报告正文内部的内容增长。业务样式必须适配组内及组边界间距,处理隐藏或空成员以及回答前的间距特例。`conversation.chat.flow` 插槽添加一个 `display: contents` DOM 锚点;行选择器直接匹配该锚点,不重复匹配其正文容器祖先。运行状态与各行共享父级,使分隔线使用同一套可见兄弟节点规则。锚点自身不产生间距。CSS 变量不属于 Group Definition。
|
||||
过程组使用稳定的 `div` 布局盒子、滚动正文及不限高的内容盒子,后者报告正文内部的内容增长。业务样式必须适配组内及组边界间距,处理隐藏或空成员以及回答前的间距特例。`conversation.chat.flow` 插槽添加一个 `display: contents` DOM 锚点;行选择器直接匹配该锚点,不重复匹配其正文容器祖先。运行状态与各行共享父级,使分隔线使用同一套可见兄弟节点规则。锚点自身不产生间距。更早历史控件紧邻流锚点之前,两者的兄弟关系决定首行间距。CSS 变量不属于 Group Definition。
|
||||
|
||||
运行中的分组标题、思考预览、工具与命令行、当前重试行以及底部运行状态使用共享的 [TextShimmer](../ui-primitives/README.zh.md#component-catalog)。每行的文字共用一道从左到右的高光,包括折叠的分组标题;图标和悬停箭头保留底色。工具和过程行之间相隔 6px;展开的分组标题与内容之间相隔 8px;Assistant 回复与相邻过程行之间相隔 12px;每轮完成/耗时控件与其输入、以及紧随其后的可见行之间保留 16px。控件与回复之间隔着已收起的行时,回复保持 12px 回复间距,因此折叠不会改变它的间距。隐藏行与空行(包括保留的空 slot 锚点)不产生间距。“深度求索”运行文字保留蓝色底色,并使用主题专用高光扫过文字。
|
||||
|
||||
|
||||
@@ -64,10 +64,11 @@
|
||||
|
||||
/* The flow slot's display:contents anchor preserves layout but not selector ancestry.
|
||||
`hidden="until-found"` retains a searchable zero-height box; hidden and empty
|
||||
Seats do not contribute spacing. */
|
||||
Seats do not contribute spacing. The older-history control precedes the anchor;
|
||||
an ancestor :has() here also invalidates the history on unrelated descendant edits. */
|
||||
:is(.column, :global([data-slot="conversation.chat.flow"])) > :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)))
|
||||
~ :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))),
|
||||
.column:has(> .older) > :global([data-slot="conversation.chat.flow"])
|
||||
.column > .older + :global([data-slot="conversation.chat.flow"])
|
||||
> :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))) {
|
||||
margin-top: var(--dsh-chat-flow-gap, 6px);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 4a2fcdcf13276fef
|
||||
zh: 8bde03bbbf15840c
|
||||
/deepseek-ai-dsh-client-ui-deliverables/use-this-package/explicit-deliveries:
|
||||
en: 0ee4bbe45bb5e473
|
||||
zh: 9c99d829b25f59ba
|
||||
en: 6ae1652a01354a6f
|
||||
zh: baa44d415d7a9f0e
|
||||
/deepseek-ai-dsh-client-ui-deliverables/use-this-package/the-changed-files-card:
|
||||
en: 4b279e2bddddc083
|
||||
zh: a3135393299c935b
|
||||
@@ -30,26 +30,26 @@
|
||||
en: ef13aac8ac36a5c3
|
||||
zh: 1e2ae4b3eaf98818
|
||||
/deepseek-ai-dsh-client-ui-deliverables/understand-the-implementation:
|
||||
en: 13e06395a2c447bf
|
||||
zh: c2c29bbfbfabe756
|
||||
en: 7ee5ee783902662c
|
||||
zh: fdf2c244fb22b8ab
|
||||
/deepseek-ai-dsh-client-ui-deliverables/further-exploration:
|
||||
en: 476b59e8671db982
|
||||
zh: b3d0495a03e8940a
|
||||
/deepseek-ai-dsh-client-ui-deliverables/model-experience:
|
||||
en: 215e7ba838619b7b
|
||||
zh: a311da3843709f90
|
||||
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance:
|
||||
en: eb244aa21a4e5210
|
||||
zh: e3450addfa19cea5
|
||||
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance/what-the-model-sees:
|
||||
en: 78339e353ae47a53
|
||||
zh: 582efa623a5dfd72
|
||||
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance/token-effect:
|
||||
en: 66bd094b37a29c22
|
||||
zh: e657d66ac6f95a22
|
||||
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance/kv-cache-effect:
|
||||
en: 52626a05acc8b320
|
||||
zh: 6a8e62b0d47d5297
|
||||
/deepseek-ai-dsh-client-ui-deliverables/model-experience/desktop-file-references:
|
||||
en: 18f9c56e10a44603
|
||||
zh: b774e6e5f2a5db6d
|
||||
/deepseek-ai-dsh-client-ui-deliverables/model-experience/desktop-file-references/what-the-model-sees:
|
||||
en: 635d8304709cfbc9
|
||||
zh: 8ee33c6046743fb7
|
||||
/deepseek-ai-dsh-client-ui-deliverables/model-experience/desktop-file-references/token-effect:
|
||||
en: 2f1bfc1df1d8d8f3
|
||||
zh: 4af1b4599f3d6a6a
|
||||
/deepseek-ai-dsh-client-ui-deliverables/model-experience/desktop-file-references/kv-cache-effect:
|
||||
en: 4dc7674aa58029a5
|
||||
zh: 8b605cd009e60775
|
||||
/deepseek-ai-dsh-client-ui-deliverables/known-limitations-and-deferred-work:
|
||||
en: dcebaf6fd7885452
|
||||
zh: 017ef058d4dcdc25
|
||||
|
||||
@@ -34,7 +34,7 @@ Changed-file and delivery entry cards and hover previews follow the [R16 preview
|
||||
|
||||
The `present` tool's preparing stage is one non-expandable document-icon row. It does not show filenames, create delivery cards, or enable file actions before the recorded call supplies arguments.
|
||||
|
||||
The Web `standard`, `ptc`, and `cordis` presets expose `present` for final files accessible through the Session filesystem, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../deliverables/tool-present/README.md) owns file-count limits and Session declarations. A single delivery fills the row; multiple deliveries form a two-column grid, collapsed after four files. Cards show the basename, description or file type, and a shared native opening control. Delivery and changed-file icon tiles use white at 50% opacity in the light theme and 5% in the dark theme. Icon tiles and single-file changed cards use the same border as delivery cards. Clicking the card previews the file in the right Sidebar. The control opens the default application and lists associated applications, with file reveal last. Matching inline-code references also open Sidebar previews. Duplicate declarations use the last description before the closing reply.
|
||||
The Web `standard`, `ptc`, and `cordis` presets expose `present` for final files accessible through the Session filesystem, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../deliverables/deliverable-file-references/README.md) owns file-count limits and Session declarations. A single delivery fills the row; multiple deliveries form a two-column grid, collapsed after four files. Cards show the basename, description or file type, and a shared native opening control. Delivery and changed-file icon tiles use white at 50% opacity in the light theme and 5% in the dark theme. Icon tiles and single-file changed cards use the same border as delivery cards. Clicking the card previews the file in the right Sidebar. The control opens the default application and lists associated applications, with file reveal last. Matching inline-code references also open Sidebar previews. Duplicate declarations use the last description before the closing reply.
|
||||
|
||||
The `present` tool row retains its document icon and recorded result across progress, success, failure, and interruption. Native actions share pending state and publish progress or retryable errors on the card. Successful open or reveal feedback stays visible for five seconds, fades over 200ms, then restores the file description; failures remain visible until another attempt. Desktop metadata is loaded when cards mount and invalidated on connection replacement. Without a Host desktop, cards retain Sidebar preview but omit native controls; failed metadata reads offer Retry. Delivery cards and change review expose `deliverables.file.actions` and `deliverables.review.file.actions`, supplied by [ui-open-in-app](../ui-open-in-app/README.md). Association GET requests and native POST actions both validate the recorded file coordinates and Session filesystem mapping before using the Host desktop.
|
||||
|
||||
@@ -64,7 +64,7 @@ The closing prose links produced or delivered paths: an inline-code token resolv
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
The Node half registers the static `ui:deliverable-file-references` system-prompt section described under [Model Experience](#model-experience). Explicit Markdown links use the shared [Markdown renderer](../ui-primitives/README.md); inline-code matching remains limited to produced or delivered files. The browser half registers a wrapper around the changed-files card and explicit deliveries into the chat view's `conversation.chat.turnTail` list alongside other feature artifacts. `deliverablesDefinition` folds the sequence of each Turn's latest validated `workspace/changes` announcement into `DeliverablesTurnData.changes`, whose summary the card reads from the Host and caches until the connection is replaced, its `deliverables/presented` events into deliveries, and the successful first-party mutation calls of `write`, `edit`, and mutating `str_replace_editor` commands into recorded file targets and exact argument aliases after validating their raw arguments; the produced paths feed only the prose mention resolver. Successful absolute `meta.path` values identify those targets. Root results without that field retain the legacy argument fallback; malformed relative targets contribute no guessed path. Reads, deletes, unsupported tools, malformed calls, malformed events, and failed results contribute nothing. Each row opens `dsh-resource://changes-review/session/<sessionId>/<seq>/<turn>` through `ctx.sidebarRight.openResource` with the file's index as the `changes-review` navigation parameter; the package registers the `changes-review` tab type at the `builtin` band for that pattern and its body under the keyed `sidebar.right.pane.tab` seat with an exclusive store of per-tab choices, and the body reads the summary and the comparisons through the authenticated routes into stores cleared on connection replacement. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes every surface and leaves the view's empty list at zero cost.
|
||||
The Node half serves authenticated native file opens; the shared guidance comes from the separately mounted file-reference plugin described under [Model Experience](#model-experience). Explicit Markdown links use the shared [Markdown renderer](../ui-primitives/README.md); inline-code matching remains limited to produced or delivered files. The browser half registers a wrapper around the changed-files card and explicit deliveries into the chat view's `conversation.chat.turnTail` list alongside other feature artifacts. `deliverablesDefinition` folds the sequence of each Turn's latest validated `workspace/changes` announcement into `DeliverablesTurnData.changes`, whose summary the card reads from the Host and caches until the connection is replaced, its `deliverables/presented` events into deliveries, and the successful first-party mutation calls of `write`, `edit`, and mutating `str_replace_editor` commands into recorded file targets and exact argument aliases after validating their raw arguments; the produced paths feed only the prose mention resolver. Successful absolute `meta.path` values identify those targets. Root results without that field retain the legacy argument fallback; malformed relative targets contribute no guessed path. Reads, deletes, unsupported tools, malformed calls, malformed events, and failed results contribute nothing. Each row opens `dsh-resource://changes-review/session/<sessionId>/<seq>/<turn>` through `ctx.sidebarRight.openResource` with the file's index as the `changes-review` navigation parameter; the package registers the `changes-review` tab type at the `builtin` band for that pattern and its body under the keyed `sidebar.right.pane.tab` seat with an exclusive store of per-tab choices, and the body reads the summary and the comparisons through the authenticated routes into stores cleared on connection replacement. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes every surface and leaves the view's empty list at zero cost.
|
||||
|
||||
Native opening uses an authenticated POST addressed by the viewed Session, event sequence, and original file index; the review tab's native open of a changed file uses the same coordinates. For a declaration the Host reads the viewed Session header with the event and passes its cwd, or the deployment workspace root when absent, to `workspaceFiles.stat`; for a changed file it passes the working directory the served summary carries. This uses the same composed filesystem as Sidebar previews and does not activate an Agent, including for child Sessions. Native actions require the canonical process path to map from a Host path back to that same process path. Providers without this mapping return 422, after which the review tab hides its native open; a same-named Host file is insufficient. The same configured desktop availability governs metadata and execution. Edits affect subsequent opens; deletion returns an error. No file-content copy or attachment is created. Plugin disposal cancels and awaits pending native-open requests.
|
||||
|
||||
@@ -89,19 +89,19 @@ Read these pages when the deliverables surface is not enough. They move from the
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
### Clickable file-reference guidance
|
||||
### Desktop file references
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The guidance prefers primary results in the final reply: inline images support explanations and comparisons, image links serve references and file lists, and image file cards serve separate deliverables. File destinations use angle brackets, including paths with spaces. It recommends separate `present` cards for complete file deliverables, especially Office files, usually selecting the most important one or two, with at most four files per call when more are needed. It discourages cards solely for code edits and extra commands to inspect diff visibility. This guidance applies even with Coding Tools disabled: it accepts fewer file surfaces to avoid duplicate presentation and unnecessary tool calls. It asks the model to link every existing-file mention outside commands, configuration expressions, and code blocks, including repeats and tables. Labels default to filenames or clear aliases, with only enough parent directories to distinguish files. Precise references display `filename:24` or `filename:24–30`; their destinations retain full relative or absolute paths with `#L24` or `#L24-L30` anchors. The display suffix contains neither `#` nor `L`.
|
||||
The Web composition mounts the shared [`dsh-deliverable-file-references`](../../deliverables/deliverable-file-references/README.md#model-experience) guidance. This UI package renders the resulting file references and cards.
|
||||
|
||||
#### Token effect
|
||||
|
||||
One fixed paragraph containing output-selection, rendering, and file-reference guidance whenever this package is loaded. The [present tool](../../deliverables/tool-present/README.md#model-experience) owns the delivery schema and result text.
|
||||
No direct tokens; the separate prompt plugin owns the guidance.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The section is static at first-party order 9000 for the lifetime of the package mount, so it remains in the reusable prompt prefix and does not change across Turns.
|
||||
No direct effect on model requests.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ kind: "package-reference"
|
||||
|
||||
`present` 工具的准备阶段是带文档图标的不可展开单行。记录的调用提供参数前,该行不显示文件名、不创建交付卡片,也不启用文件操作。
|
||||
|
||||
Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present`,用于声明会话文件系统可访问的最终文件,包括通过 Bash 创建的文件。创建后以 `files: [{ path, description? }]` 调用。[present 工具](../../deliverables/tool-present/README.zh.md)拥有文件数量限制和会话声明。单个交付占满一行,多个交付使用双列网格,超过四个时默认收起。卡片显示文件名、说明或文件类型,以及共享的原生打开控件。交付和改动文件卡片的图标底块在浅色主题下使用 50% 不透明度的白色,深色主题下为 5%。图标底块与单文件改动卡片的外框使用与交付卡片相同的描边。点击卡片会在右侧 Sidebar 预览文件。控件打开默认应用,并列出关联应用,文件定位始终放在最后。匹配的行内代码引用也打开 Sidebar 预览。重复声明使用收尾回复之前最近一次的说明。
|
||||
Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present`,用于声明会话文件系统可访问的最终文件,包括通过 Bash 创建的文件。创建后以 `files: [{ path, description? }]` 调用。[present 工具](../../deliverables/deliverable-file-references/README.zh.md)拥有文件数量限制和会话声明。单个交付占满一行,多个交付使用双列网格,超过四个时默认收起。卡片显示文件名、说明或文件类型,以及共享的原生打开控件。交付和改动文件卡片的图标底块在浅色主题下使用 50% 不透明度的白色,深色主题下为 5%。图标底块与单文件改动卡片的外框使用与交付卡片相同的描边。点击卡片会在右侧 Sidebar 预览文件。控件打开默认应用,并列出关联应用,文件定位始终放在最后。匹配的行内代码引用也打开 Sidebar 预览。重复声明使用收尾回复之前最近一次的说明。
|
||||
|
||||
`present` 工具行在执行、成功、失败和中断状态下保留文档图标及记录的结果。原生操作共享等待状态,并在卡片显示进度或可重试错误。打开或定位成功的反馈显示五秒后,在 200ms 内淡出并恢复文件说明;失败信息保留到下次尝试。卡片挂载时读取桌面信息,连接更换时清除缓存。Host 没有桌面时,卡片保留 Sidebar 预览并隐藏原生控件;元数据读取失败时提供重试。交付卡片和变更对比页通过 `deliverables.file.actions` 和 `deliverables.review.file.actions` 使用 [ui-open-in-app](../ui-open-in-app/README.zh.md) 提供的控件。关联应用 GET 查询与原生 POST 操作都会先校验记录中的文件坐标及会话文件系统映射,再调用 Host 桌面。
|
||||
|
||||
@@ -64,7 +64,7 @@ Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present`,用于声明
|
||||
<details>
|
||||
<summary>实现细节——点击展开</summary>
|
||||
|
||||
Node 半部注册[模型体验](#model-experience)所述的静态 `ui:deliverable-file-references` 系统提示词段。显式 Markdown 链接使用共享的 [Markdown 渲染器](../ui-primitives/README.zh.md);行内代码匹配仍仅限产出或交付文件。浏览器半部把组合改动文件卡片与显式交付的包装组件注册进 chat 视图的 `conversation.chat.turnTail` 列表,与其他功能产物并列。`deliverablesDefinition` 把每个轮次最新且通过校验的 `workspace/changes` 宣告的序号折叠进 `DeliverablesTurnData.changes`,卡片按它向 Host 读取摘要并缓存到连接被替换为止,把 `deliverables/presented` 事件折叠为交付,并根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数与结果记录的绝对 `meta.path` 把成功的第一方修改调用折叠为产出路径及精确参数别名;产出路径只供正文提及解析器使用。不带该字段的成功根调用保留历史参数回退;格式错误的相对目标不产生猜测路径。读取、删除、不受支持的工具、格式错误的调用、格式错误的事件和失败结果不贡献任何条目。每一行通过 `ctx.sidebarRight.openResource` 打开 `dsh-resource://changes-review/session/<sessionId>/<seq>/<turn>`,并以文件下标作为 `changes-review` 的导航参数;本包在 `builtin` 档为该模式注册 `changes-review` tab 类型,把其 body 连同一个按 tab 保存选择的独占 store 注册到按键的 `sidebar.right.pane.tab` 座位下,body 通过经过认证的路由把摘要和对比读进连接更换时清空的 store。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会移除全部表面,视图的空列表以零成本留下。
|
||||
Node 半部提供经过身份验证的原生文件打开;共用指导由[模型体验](#model-experience)所述的独立文件引用插件提供。显式 Markdown 链接使用共享的 [Markdown 渲染器](../ui-primitives/README.zh.md);行内代码匹配仍仅限产出或交付文件。浏览器半部把组合改动文件卡片与显式交付的包装组件注册进 chat 视图的 `conversation.chat.turnTail` 列表,与其他功能产物并列。`deliverablesDefinition` 把每个轮次最新且通过校验的 `workspace/changes` 宣告的序号折叠进 `DeliverablesTurnData.changes`,卡片按它向 Host 读取摘要并缓存到连接被替换为止,把 `deliverables/presented` 事件折叠为交付,并根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数与结果记录的绝对 `meta.path` 把成功的第一方修改调用折叠为产出路径及精确参数别名;产出路径只供正文提及解析器使用。不带该字段的成功根调用保留历史参数回退;格式错误的相对目标不产生猜测路径。读取、删除、不受支持的工具、格式错误的调用、格式错误的事件和失败结果不贡献任何条目。每一行通过 `ctx.sidebarRight.openResource` 打开 `dsh-resource://changes-review/session/<sessionId>/<seq>/<turn>`,并以文件下标作为 `changes-review` 的导航参数;本包在 `builtin` 档为该模式注册 `changes-review` tab 类型,把其 body 连同一个按 tab 保存选择的独占 store 注册到按键的 `sidebar.right.pane.tab` 座位下,body 通过经过认证的路由把摘要和对比读进连接更换时清空的 store。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会移除全部表面,视图的空列表以零成本留下。
|
||||
|
||||
原生打开使用经过认证的 POST,通过当前查看的会话、事件序号和原始文件索引定位声明;review tab 对改动文件的原生打开使用同一组坐标。对声明,Host 读取事件及当前查看的会话 header,将其中的 cwd 传给 `workspaceFiles.stat`,未记录 cwd 时使用部署的工作目录;对改动文件,传的是所提供摘要携带的工作目录。它与侧栏预览使用同一组合文件系统,无需启动 Agent,子会话也适用。原生操作要求规范化的进程路径能从 Host 路径映射回同一进程路径。提供方没有这种映射时返回 422,之后 review tab 隐藏原生打开;Host 上存在同名文件并不足够。同一份桌面可用性配置同时约束信息查询和实际执行。编辑会影响后续打开的内容;删除后返回错误。不创建文件内容副本或附件。插件释放时取消并等待进行中的原生打开请求。
|
||||
|
||||
@@ -89,19 +89,19 @@ Node 半部注册[模型体验](#model-experience)所述的静态 `ui:deliverabl
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
### 可点击文件引用指引
|
||||
### 桌面文件引用
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
提示词建议在最终回复中展示主要结果:内嵌图片用于讲解与对比,图片链接用于引用和文件列表,图片文件卡片用于独立交付。文件目标路径使用尖括号包裹,包括含空格的路径。对于需要完整文件的交付,尤其是 Office 文件,建议使用独立的 `present` 卡片,通常选最重要的一两项,需要时可以更多,但每次调用最多四项。提示词建议避免仅为列出代码改动生成卡片,也不为确认 diff 是否显示额外运行命令。代码工作工具关闭时仍采用这条建议,接受较少的文件展示入口,以减少重复展示和不必要的工具调用。文件引用要求链接命令、配置表达式和代码块以外的每次现有文件提及,包括重复提及和表格。标签默认使用文件名或清楚的别名,仅添加足以区分文件的父目录。精确引用显示为 `filename:24` 或 `filename:24–30`;目标保留完整相对路径或绝对路径,以及 `#L24` 或 `#L24-L30` 锚点。显示后缀不含 `#` 或 `L`。
|
||||
Web 组合挂载共用的 [`dsh-deliverable-file-references`](../../deliverables/deliverable-file-references/README.zh.md#model-experience) 指导。本 UI 包渲染相应的文件引用和卡片。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
加载本包时增加一段包含产出选择、渲染效果和文件引用指导的固定提示词。[present 工具](../../deliverables/tool-present/README.zh.md#model-experience)拥有交付 schema 和结果文本。
|
||||
不直接增加 token;指导由单独的 prompt 插件负责。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
该段落在本包挂载期间始终以 first-party 顺序 9000 保持静态,因此留在可复用的提示词前缀中,不会随轮次改变。
|
||||
不直接影响模型请求。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
|
||||
@@ -1,36 +1,20 @@
|
||||
/**
|
||||
* Deliverables plugin, node half. Registers Web file-reference guidance and
|
||||
* serves authenticated native opens of declared files. The browser
|
||||
* Deliverables plugin, node half. Serves authenticated native opens of declared files. The browser
|
||||
* half ships via exports["./client"], discovered through the package.json
|
||||
* dsh.client declaration.
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-workspace-changes/types'
|
||||
import { registerPresentOpen } from './present-open.ts'
|
||||
|
||||
/** Services required for file-reference guidance, change summaries, and authenticated native opens. */
|
||||
export const inject = ['systemPrompt', 'connection', 'sessionQuery', 'sessionController', 'workspaceFiles', 'fs', 'sandboxPolicy', 'workspaceChanges']
|
||||
|
||||
/** Static Web guidance for primary outputs and existing-file references. */
|
||||
const FILE_REFERENCE_PROMPT = 'Prefer showing the primary results within your final response alongside a brief explanation. '
|
||||
+ 'Use  when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. '
|
||||
+ 'Do not call present just to list edited source files, or run commands to check whether a diff view will appear. '
|
||||
+ 'Use present when a separate file card helps the user open the complete deliverable, including images, Office documents, spreadsheets, and slide decks. Each presented file adds a card below the reply, with preview and native-open actions. '
|
||||
+ 'Avoid repeating results already shown inline unless the separate card adds useful access. '
|
||||
+ 'Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. '
|
||||
+ 'Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.'
|
||||
/** Services required for change summaries and authenticated native opens. */
|
||||
export const inject = ['connection', 'sessionQuery', 'sessionController', 'workspaceFiles', 'fs', 'sandboxPolicy', 'workspaceChanges']
|
||||
|
||||
/**
|
||||
* Register Web file-reference guidance and native opens for declared files.
|
||||
* @param ctx - host context carrying the system-prompt registry.
|
||||
* Register native opens for declared files.
|
||||
* @param ctx - host context carrying file and connection services.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
registerPresentOpen(ctx)
|
||||
ctx.systemPrompt.section({
|
||||
name: 'ui:deliverable-file-references',
|
||||
order: ctx.systemPrompt.getSectionOrder('DELIVERABLE_FILE_REFERENCES'),
|
||||
text: FILE_REFERENCE_PROMPT,
|
||||
})
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { SessionQueryError } from '@deepseek-ai/dsh-session-query'
|
||||
import type { SessionEventReadRequest } from '@deepseek-ai/dsh-session-query'
|
||||
import type { WorkspaceChangedFile, WorkspaceChangesSummary, WorkspaceFileDiff } from '@deepseek-ai/dsh-workspace-changes/types'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { registerPresentOpen } from '../src/present-open.ts'
|
||||
import * as deliverables from '../src/index.ts'
|
||||
import {
|
||||
changedFileUrl, changesDiffUrl, changesSummaryUrl, CHANGES_DIFF_PATH, CHANGES_OPEN_PATH, CHANGED_FILES_PATH, isChangedFile, isChangesDiff,
|
||||
isChangesEvent, isChangesSummary,
|
||||
@@ -64,10 +64,7 @@ async function fixture() {
|
||||
const applications = vi.fn(async () => [{ id: 'player', name: 'Player', default: true, icon: null }])
|
||||
ctx.provide('sessionController', { workspacePathApplications: applications, openWorkspacePath: opener, workspaceDesktop: () => ({ name: 'desktop', available: true, fileManager: 'finder' }) } as never)
|
||||
const connection = new HostConnectionService(ctx, [], {} as BrowserAuth)
|
||||
await ctx.plugin({
|
||||
inject: ['connection', 'sessionQuery', 'sessionController', 'workspaceFiles', 'fs', 'sandboxPolicy', 'workspaceChanges'],
|
||||
apply: registerPresentOpen,
|
||||
})
|
||||
await ctx.plugin(deliverables)
|
||||
const handler = connection.createSharedFetchHandler('/api')
|
||||
const open = (query = '?sessionId=owner&seq=9&index=0') => handler.fetch(new Request(`http://localhost${CHANGES_OPEN_PATH}${query}`, { method: 'POST' }), connection.browserPeer)
|
||||
const read = (query = '?sessionId=owner&seq=9') => handler.fetch(new Request(`http://localhost${CHANGED_FILES_PATH}${query}`), connection.browserPeer)
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 1eb35b015a6c423f
|
||||
zh: a8a3dceee2c9e943
|
||||
/packages-deliverables/packages:
|
||||
en: a421c20359e2a77b
|
||||
zh: 70286affa23c2af4
|
||||
en: a437bcc95b45e6bb
|
||||
zh: 181a543289f1386a
|
||||
/packages-deliverables/related-documentation:
|
||||
en: f640329953d0787b
|
||||
zh: 3d4065e793f426ee
|
||||
|
||||
@@ -24,6 +24,7 @@ The deliverables family records what a turn hands to the user as durable Session
|
||||
|
||||
| Package | Role | ctx key |
|
||||
|---|---|---|
|
||||
| [`deliverable-file-references`](deliverable-file-references/README.md) | Shared file-link and result-delivery guidance for Web and SDK hosts | registers on `ctx.systemPrompt` |
|
||||
| [`tool-present`](tool-present/README.md) | Declares existing files as final deliverables through the `present` tool | registers on `ctx.tools` |
|
||||
| [`workspace-changes`](workspace-changes/README.md) | Summarizes each top-level turn's changed files from git working-tree snapshots and whole-file captures, and serves their comparisons | provides `ctx.workspaceChanges`; listens to `session/event`, appends `workspace/changes` |
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ deliverables 系列把一轮交给用户的内容记录为只有客户端读取
|
||||
|
||||
| 包 | 角色 | ctx key |
|
||||
|---|---|---|
|
||||
| [`deliverable-file-references`](deliverable-file-references/README.zh.md) | Web 与 SDK 宿主共用的文件链接与结果交付指导 | 注册到 `ctx.systemPrompt` |
|
||||
| [`tool-present`](tool-present/README.zh.md) | 通过 `present` 工具把已有文件声明为最终交付物 | 注册到 `ctx.tools` |
|
||||
| [`workspace-changes`](workspace-changes/README.zh.md) | 用 git 工作树快照和整文件捕获汇总每个顶层轮次改动的文件,并提供其对比 | 提供 `ctx.workspaceChanges`;监听 `session/event`,追加 `workspace/changes` |
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/deliverables/deliverable-file-references/README.md
|
||||
/:
|
||||
en: be4edd7a1427808e
|
||||
zh: 11cd071f23d83084
|
||||
/deepseek-ai-dsh-deliverable-file-references:
|
||||
en: 86c55674ad26ea6f
|
||||
zh: 6c1e7fbd9808e727
|
||||
/deepseek-ai-dsh-deliverable-file-references/summary:
|
||||
en: c1d53b592282a502
|
||||
zh: c9fe1ed8de02ea68
|
||||
/deepseek-ai-dsh-deliverable-file-references/table-of-contents:
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-deliverable-file-references/use-this-package:
|
||||
en: 582e00c45e6b82f2
|
||||
zh: 516580c07a0d8abd
|
||||
/deepseek-ai-dsh-deliverable-file-references/understand-the-implementation:
|
||||
en: 4fe7c3a943dbfee1
|
||||
zh: c9df999dcf922e37
|
||||
/deepseek-ai-dsh-deliverable-file-references/further-exploration:
|
||||
en: 87bee2f9ae099907
|
||||
zh: c6906e52aae6e670
|
||||
/deepseek-ai-dsh-deliverable-file-references/model-experience:
|
||||
en: 215e7ba838619b7b
|
||||
zh: a311da3843709f90
|
||||
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance:
|
||||
en: def6db96da0ca05a
|
||||
zh: a0956e37e0a362c9
|
||||
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance/what-the-model-sees:
|
||||
en: 1dfb9b2c748c87fe
|
||||
zh: 9b4308986ae22e3a
|
||||
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance/what-the-model-sees/shared-guidance:
|
||||
en: f6f9454c369cd5a2
|
||||
zh: 127b54d86cdf0cd5
|
||||
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance/token-effect:
|
||||
en: f3c44754e5881a8d
|
||||
zh: cf72c0d320ee4881
|
||||
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance/kv-cache-effect:
|
||||
en: a1a94134f784ac31
|
||||
zh: d763a15160ef4043
|
||||
/deepseek-ai-dsh-deliverable-file-references/known-limitations-and-deferred-work:
|
||||
en: 8cf1186304d4f10b
|
||||
zh: b38c0eb9558b3bdc
|
||||
/deepseek-ai-dsh-deliverable-file-references/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: e88aeb617ffb74ea
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
description: "Give Web and SDK agents shared instructions for linking files and delivering results."
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-deliverable-file-references
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Agents receive the same file-link and result-delivery instructions in Web and SDK sessions. Use this guidance when a host can expose links or delivered files. It adds one fixed system-prompt paragraph; hosts retain responsibility for file access and rendering.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Use this package](#use-this-package)
|
||||
- [Understand the implementation](#understand-the-implementation)
|
||||
- [Further Exploration](#further-exploration)
|
||||
- [Model Experience](#model-experience)
|
||||
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
||||
- [Dev Note](#dev-note)
|
||||
|
||||
-----
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
The Web and `sdk` bundles mount this plugin independently of the `present` tool. Custom compositions need only `systemPrompt`:
|
||||
|
||||
```yaml
|
||||
- id: deliverable-file-references
|
||||
name: '@deepseek-ai/dsh-deliverable-file-references'
|
||||
```
|
||||
|
||||
There are no configuration fields. Mount [`dsh-tool-present`](../tool-present/README.md) separately to execute file deliveries. Disable this row to omit its guidance.
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
## Understand the implementation
|
||||
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
The [plugin entry](src/index.ts) registers `ui:deliverable-file-references` at the shared `DELIVERABLE_FILE_REFERENCES` order. Cordis removes the section on unload. It reads no files and starts no GUI or transport.
|
||||
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
<a id="further-exploration"></a>
|
||||
## Further Exploration
|
||||
|
||||
- [System prompts](../../core/system-prompt/README.md) — section assembly and ordering.
|
||||
- [Present tool](../tool-present/README.md) — executable file-delivery declarations.
|
||||
- [Web deliverables](../../client/ui-deliverables/README.md) — file cards and file opening.
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
### File-reference guidance
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Each assembly includes the same static paragraph:
|
||||
|
||||
##### Shared guidance
|
||||
|
||||
```markdown
|
||||
Prefer showing the primary results within your final response alongside a brief explanation. Use  when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
|
||||
```
|
||||
|
||||
#### Token effect
|
||||
|
||||
One fixed paragraph while mounted, with no tool schema or extra model request.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The paragraph stays at first-party order 9000 across turns. Mounting, removing, or changing it changes the assembled prompt prefix; it adds no per-turn values.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- Guidance requires a host that supports file links or deliveries; this plugin does not implement rendering, file access, or the `present` tool.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
<details>
|
||||
<summary>Working context for maintainers — click to expand</summary>
|
||||
|
||||
None.
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
description: "为 Web 和 SDK agent 提供共用的文件链接与结果交付说明。"
|
||||
kind: "package-reference"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-deliverable-file-references
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
## 概述
|
||||
|
||||
Agent 在 Web 和 SDK 会话中接收相同的文件链接与结果交付说明。宿主能够提供链接或已交付文件时,可使用这段指导。它增加一个固定的系统提示词段落;文件访问与渲染仍由宿主负责。
|
||||
|
||||
## 目录
|
||||
|
||||
- [使用本包](#use-this-package)
|
||||
- [理解实现](#understand-the-implementation)
|
||||
- [进一步探索](#further-exploration)
|
||||
- [模型体验](#model-experience)
|
||||
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
||||
- [开发备注](#dev-note)
|
||||
|
||||
-----
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
Web 和 `sdk` bundle 独立于 `present` 工具挂载本插件。自定义组合仅需提供 `systemPrompt`:
|
||||
|
||||
```yaml
|
||||
- id: deliverable-file-references
|
||||
name: '@deepseek-ai/dsh-deliverable-file-references'
|
||||
```
|
||||
|
||||
本包没有配置字段。执行文件交付时,需单独挂载 [`dsh-tool-present`](../tool-present/README.zh.md)。禁用本行即可省略这段指导。
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
## 理解实现
|
||||
|
||||
<details>
|
||||
<summary>实现细节——点击展开</summary>
|
||||
|
||||
[插件入口](src/index.ts)按共享的 `DELIVERABLE_FILE_REFERENCES` 顺序注册 `ui:deliverable-file-references`。卸载时 Cordis 移除该段落。本插件不读取文件,也不启动 GUI 或传输服务。
|
||||
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
<a id="further-exploration"></a>
|
||||
## 进一步探索
|
||||
|
||||
- [系统提示词](../../core/system-prompt/README.zh.md)——段落组装与顺序。
|
||||
- [Present 工具](../tool-present/README.zh.md)——可执行的文件交付声明。
|
||||
- [Web 交付](../../client/ui-deliverables/README.zh.md)——文件卡片与文件打开。
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
### 文件引用指导
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
每次组装均包含同一个静态段落:
|
||||
|
||||
##### 共用指导
|
||||
|
||||
```markdown
|
||||
Prefer showing the primary results within your final response alongside a brief explanation. Use  when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
|
||||
```
|
||||
|
||||
#### Token 影响
|
||||
|
||||
挂载期间增加一个固定段落,不增加工具 schema 或额外模型请求。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
该段落跨轮次保持 first-party 顺序 9000。挂载、移除或修改它会改变组装后的提示词前缀;它不增加逐轮变化的值。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- 指导要求宿主支持文件链接或交付;本插件不实现渲染、文件访问或 `present` 工具。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
<details>
|
||||
<summary>维护者工作上下文——点击展开</summary>
|
||||
|
||||
无。
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-deliverable-file-references",
|
||||
"description": "Shared model guidance for file links and deliverables across DeepSeek Harness hosts",
|
||||
"version": "0.2.1-rc.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/deliverables/deliverable-file-references"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/** Shared file-delivery guidance for clients that render desktop-style file references. */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
|
||||
/** Stable Loader identity. */
|
||||
export const name = 'deliverable-file-references'
|
||||
|
||||
/** Prompt registry shared by the Web and SDK desktop compositions. */
|
||||
export const inject = ['systemPrompt']
|
||||
|
||||
/** Static Web guidance for primary outputs and existing-file references. */
|
||||
const FILE_REFERENCE_PROMPT = 'Prefer showing the primary results within your final response alongside a brief explanation. '
|
||||
+ 'Use  when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. '
|
||||
+ 'Do not call present just to list edited source files, or run commands to check whether a diff view will appear. '
|
||||
+ 'Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. '
|
||||
+ 'Avoid repeating results already shown inline unless the separate card adds useful access. '
|
||||
+ 'Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. '
|
||||
+ 'Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.'
|
||||
|
||||
/**
|
||||
* Register the desktop file-reference guidance without mounting a GUI.
|
||||
* @param ctx - context carrying the prompt registry.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.systemPrompt.section({
|
||||
name: 'ui:deliverable-file-references',
|
||||
order: ctx.systemPrompt.getSectionOrder('DELIVERABLE_FILE_REFERENCES'),
|
||||
text: FILE_REFERENCE_PROMPT,
|
||||
})
|
||||
}
|
||||
+7
-9
@@ -13,23 +13,16 @@ afterEach(async () => {
|
||||
ctx = undefined
|
||||
})
|
||||
|
||||
describe('ui-deliverables node plugin', () => {
|
||||
describe('desktop file-reference prompt', () => {
|
||||
it('registers final-response file-reference guidance only while mounted', async () => {
|
||||
ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
|
||||
ctx.provide('connection', { fetch: { register: () => () => {} } } as never)
|
||||
ctx.provide('sessionQuery', {} as never)
|
||||
ctx.provide('sessionController', {} as never)
|
||||
ctx.provide('workspaceFiles', {} as never)
|
||||
ctx.provide('fs', {} as never)
|
||||
ctx.provide('sandboxPolicy', {} as never)
|
||||
ctx.provide('workspaceChanges', {} as never)
|
||||
const mounted = ctx.plugin({ apply, inject })
|
||||
await mounted.await()
|
||||
|
||||
const section = (await ctx.systemPrompt.assemble()).sections
|
||||
.find(entry => entry.name === 'ui:deliverable-file-references')
|
||||
expect(section?.text).toMatchInlineSnapshot('"Prefer showing the primary results within your final response alongside a brief explanation. Use  when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present when a separate file card helps the user open the complete deliverable, including images, Office documents, spreadsheets, and slide decks. Each presented file adds a card below the reply, with preview and native-open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix."')
|
||||
expect(section?.text).toMatchInlineSnapshot('"Prefer showing the primary results within your final response alongside a brief explanation. Use  when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix."')
|
||||
|
||||
// Only live request pins track current guidance; seeded sessions retain their recorded prompt.
|
||||
for (const scenario of ['fresh-round-trip', 'ptc-round']) {
|
||||
@@ -37,6 +30,11 @@ describe('ui-deliverables node plugin', () => {
|
||||
expect(sidecar, scenario).toContain(section!.text)
|
||||
}
|
||||
|
||||
for (const scenario of ['desktop-context', 'desktop-ptc-context']) {
|
||||
const sidecar = await readFile(new URL(`../../../../snapshots/sdk/${scenario}/system-prompt.expected.md`, import.meta.url), 'utf8')
|
||||
expect(sidecar, scenario).toContain(section!.text)
|
||||
}
|
||||
|
||||
await mounted.dispose()
|
||||
expect((await ctx.systemPrompt.assemble()).sections
|
||||
.some(entry => entry.name === 'ui:deliverable-file-references')).toBe(false)
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../core/system-prompt"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-tool-present/use-this-package:
|
||||
en: 3bb0413b8aa3bd10
|
||||
zh: ab753bb651a62209
|
||||
en: d1a1e22686f9041d
|
||||
zh: 8053a913199195fa
|
||||
/deepseek-ai-dsh-tool-present/understand-the-implementation:
|
||||
en: 6e73ca41dddc096e
|
||||
zh: b59fdfa3f3cc47e1
|
||||
|
||||
@@ -25,6 +25,8 @@ Use `present` to declare final files accessible through the Session filesystem,
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
Shared output and file-link guidance comes from [`dsh-deliverable-file-references`](../deliverable-file-references/README.md), mounted separately by Web and SDK bundles.
|
||||
|
||||
`present` validates paths against the Session's current working directory and records absolute paths, so later directory changes do not redirect delivered files.
|
||||
|
||||
The `standard`, `ptc`, and `cordis` agent presets mount this plugin. Call `present` with `files: [{ path, description? }]` after creating the files. Files must be regular files accessible through the Session filesystem. Relative paths resolve against the Session working directory; absolute paths may name files outside it, including `/tmp` or Downloads. Missing files, directories, final symbolic links, and provider-denied paths fail the call. Files in a shell sandbox’s private `/tmp` must first be written somewhere the Session filesystem can access.
|
||||
|
||||
@@ -25,6 +25,8 @@ kind: "package-reference"
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
共用的输出与文件链接指导由 [`dsh-deliverable-file-references`](../deliverable-file-references/README.zh.md) 提供,Web 与 SDK bundle 单独挂载该插件。
|
||||
|
||||
`present` 按 Session 的当前工作目录校验路径并记录绝对路径,因此后续目录切换不会让已交付文件指向其他位置。
|
||||
|
||||
`standard`、`ptc` 与 `cordis` Agent preset 挂载本插件。创建文件后,以 `files: [{ path, description? }]` 调用 `present`。文件必须是 Session 文件系统可访问的普通文件。相对路径按 Session 工作目录解析;绝对路径可以指向工作区外的文件,包括 `/tmp` 或 Downloads。文件缺失、为目录、最终路径为符号链接或提供方拒绝访问时,调用失败。Shell 沙箱私有 `/tmp` 中的文件需要先写入 Session 文件系统可访问的位置。
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:*",
|
||||
"@deepseek-ai/dsh-scope": "workspace:*",
|
||||
"@deepseek-ai/dsh-working-directory": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 6d290aabc543f878
|
||||
zh: 7ad283b159ae2c29
|
||||
/deepseek-ai-dsh-experimental-inspector/runtime-layout:
|
||||
en: 13f495af2fec307d
|
||||
zh: ec4bebf24a87ec54
|
||||
en: 7db0c435e07d5005
|
||||
zh: 1573972326c33893
|
||||
/deepseek-ai-dsh-experimental-inspector/configuration:
|
||||
en: d094842893700446
|
||||
zh: 7a7f880ba795948f
|
||||
|
||||
@@ -48,7 +48,7 @@ The frontend uses browser runtime adapters and excludes Node-only transports at
|
||||
<a id="runtime-layout"></a>
|
||||
## Runtime layout
|
||||
|
||||
The Worker never accesses live Cordis objects: the shared Host/Client collector projects them into validated snapshots before transport. Cordis owns plugin composition, `ctx.inspector` registration, bootstrap injection, and disposal. A late-loading Client fetches its bootstrap through an authenticated Host route, which the Host registers before it starts the Worker: a request waits for Worker readiness and then receives the bootstrap, or `503` when startup fails; an aborted request stops waiting. When the platform cannot run the Worker and reports Node's `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM`, such as the browser preview's Worker host, the Host logs one warning and the plugin still starts with only that route, which answers `503`; it provides no `ctx.inspector`, DevTools routes, bootstrap injection, or `devtools://` URL. Any other startup error still rejects the plugin. Bootstrap transport and source-setup failures warn and retain connection-reset recovery for both injected and fetched startup; invalid bootstrap data or service-registration failures reject startup. Source replacement joins old service-consumer cleanup before claiming another source, and unloading an active plugin cancels its pending bootstrap refresh.
|
||||
The Worker never accesses live Cordis objects: the shared Host/Client collector projects them into validated snapshots before transport. Cordis owns plugin composition, `ctx.inspector` registration, bootstrap injection, and disposal. A late-loading Client fetches its bootstrap through an authenticated Host route, which the Host registers before it starts the Worker: a request waits for Worker readiness and then receives the bootstrap, or `503` when startup fails; an aborted request stops waiting even if garbage collection runs during startup. When the platform cannot run the Worker and reports Node's `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM`, such as the browser preview's Worker host, the Host logs one warning and the plugin still starts with only that route, which answers `503`; it provides no `ctx.inspector`, DevTools routes, bootstrap injection, or `devtools://` URL. Any other startup error still rejects the plugin. Bootstrap transport and source-setup failures warn and retain connection-reset recovery for both injected and fetched startup; invalid bootstrap data or service-registration failures reject startup. Source replacement joins old service-consumer cleanup before claiming another source, and unloading an active plugin cancels its pending bootstrap refresh.
|
||||
|
||||
The Host plugin starts the Worker and connects a dedicated `MessagePort`. The Client uses injected or fetched browser-source parameters to open an authenticated WebSocket directly to the Worker; it receives no DevTools URL. Chrome DevTools connects to the Worker's CDP WebSocket. A private `node:inspector.Session` per DevTools connection attaches from the Worker to the Host main thread, so Host debugging remains available while Host JavaScript is paused.
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Host 在相对于应用根目录的 `inspector/devtools/devtools_app.html` 路
|
||||
<a id="runtime-layout"></a>
|
||||
## 运行时布局
|
||||
|
||||
Worker 不访问实时 Cordis 对象;共享 Host/Client collector 会在传输前把它们投影成已验证快照。Cordis 负责插件组合、注册 `ctx.inspector`、注入 bootstrap 和 dispose(资源释放)。后加载的 Client 通过已鉴权的 Host 路由获取 bootstrap。Host 在启动 Worker 之前注册该路由,请求会等到 Worker 就绪后返回 bootstrap,启动失败时返回 `503`;请求中止时立即停止等待。当前平台无法运行 Worker 并报告 Node 的 `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` 时(例如浏览器预览的 Worker host),Host 记录一条警告,插件仍照常启动,只保留该路由并返回 `503`,不提供 `ctx.inspector`、DevTools 路由、bootstrap 注入或 `devtools://` URL;其他任何启动错误仍使插件启动失败。初始注入与主动获取两条路径中,bootstrap 传输或 source 建立失败均记录警告并保留连接重置后的重试;bootstrap 数据非法或服务注册失败则使启动失败。替换 source 会等待旧服务消费者清理完成,再申请新 source;卸载已激活插件会取消其未完成的 bootstrap 刷新。
|
||||
Worker 不访问实时 Cordis 对象;共享 Host/Client collector 会在传输前把它们投影成已验证快照。Cordis 负责插件组合、注册 `ctx.inspector`、注入 bootstrap 和 dispose(资源释放)。后加载的 Client 通过已鉴权的 Host 路由获取 bootstrap。Host 在启动 Worker 之前注册该路由,请求会等到 Worker 就绪后返回 bootstrap,启动失败时返回 `503`;即使启动期间发生垃圾回收,请求中止时也立即停止等待。当前平台无法运行 Worker 并报告 Node 的 `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` 时(例如浏览器预览的 Worker host),Host 记录一条警告,插件仍照常启动,只保留该路由并返回 `503`,不提供 `ctx.inspector`、DevTools 路由、bootstrap 注入或 `devtools://` URL;其他任何启动错误仍使插件启动失败。初始注入与主动获取两条路径中,bootstrap 传输或 source 建立失败均记录警告并保留连接重置后的重试;bootstrap 数据非法或服务注册失败则使启动失败。替换 source 会等待旧服务消费者清理完成,再申请新 source;卸载已激活插件会取消其未完成的 bootstrap 刷新。
|
||||
|
||||
Host 插件启动 Worker 并连接专用 `MessagePort`。Client 使用注入或主动获取的浏览器 source 参数,直接向 Worker 打开带鉴权的 WebSocket,不接收 DevTools URL。Chrome DevTools 连接 Worker 的 CDP WebSocket。每条 DevTools 连接在 Worker 中独占一个连接 Host 主线程的 `node:inspector.Session`,因此 Host JavaScript 暂停时仍可调试 Host。
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function apply(ctx: Context, config: HostPluginConfig): Promise<voi
|
||||
try {
|
||||
disposers.push(ctx.connection.fetch.register({
|
||||
path: INSPECTOR_BOOTSTRAP_PATH, methods: ['GET'], requestBody: 'buffered',
|
||||
fetch: request => bootstrapResponse(ready.promise, request.signal),
|
||||
fetch: request => bootstrapResponse(ready.promise, request),
|
||||
}))
|
||||
const handle = await startInspector(spec).catch((error: unknown) => {
|
||||
if (!isUnavailableOnPlatform(error)) throw error
|
||||
@@ -196,10 +196,11 @@ function isUnavailableOnPlatform(error: unknown): boolean {
|
||||
/**
|
||||
* Answer one bootstrap request once Host initialization settles or the browser abandons the request.
|
||||
* @param ready - Resolves with the Client bootstrap, or with undefined after initialization failed or found the platform unavailable.
|
||||
* @param signal - Request signal; an aborted request stops waiting and receives no endpoint.
|
||||
* @param request - Retained until settlement so Node keeps forwarding the caller's abort to its signal.
|
||||
* @returns The bootstrap JSON, or 503 when the Inspector is unavailable or the request was aborted.
|
||||
*/
|
||||
async function bootstrapResponse(ready: Promise<InspectorClientBootstrap | undefined>, signal: AbortSignal): Promise<Response> {
|
||||
async function bootstrapResponse(ready: Promise<InspectorClientBootstrap | undefined>, request: Request): Promise<Response> {
|
||||
const signal = request.signal
|
||||
const headers = { 'cache-control': 'no-store' }
|
||||
const abandoned = Promise.withResolvers<undefined>()
|
||||
const abandon = () => { abandoned.resolve(undefined) }
|
||||
@@ -209,6 +210,6 @@ async function bootstrapResponse(ready: Promise<InspectorClientBootstrap | undef
|
||||
const client = await Promise.race([ready, abandoned.promise])
|
||||
return client === undefined ? new Response(null, { status: 503, headers }) : Response.json(client, { headers })
|
||||
} finally {
|
||||
signal.removeEventListener('abort', abandon)
|
||||
request.signal.removeEventListener('abort', abandon)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import type { Message } from '@deepseek-ai/cordis'
|
||||
import type { IndexInjection, WebServer } from '@deepseek-ai/dsh-host-webserver'
|
||||
import { HostConnectionService } from '@deepseek-ai/dsh-client-connection'
|
||||
import type { BrowserAuth } from '@deepseek-ai/dsh-client-connection/src/browser-auth.ts'
|
||||
import { setFlagsFromString } from 'node:v8'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import open from 'open'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { apply, Config, inject, name } from '../src/index.ts'
|
||||
@@ -154,7 +156,7 @@ describe('experimental Inspector Host bootstrap route', () => {
|
||||
expect(messages.filter(message => message.type === 'error')).toEqual([])
|
||||
})
|
||||
|
||||
it('stops waiting when a bootstrap request is aborted before the Worker is ready', async () => {
|
||||
it('stops waiting after garbage collection when a bootstrap request is aborted before the Worker is ready', async () => {
|
||||
const gate = gateStartup('start')
|
||||
const { fiber, bootstrap } = mount()
|
||||
await vi.waitFor(() => { expect(startInspector).toHaveBeenCalledOnce() })
|
||||
@@ -162,6 +164,7 @@ describe('experimental Inspector Host bootstrap route', () => {
|
||||
const abort = new AbortController()
|
||||
const abandoned = bootstrap(abort.signal)
|
||||
expect(await settledWithin(abandoned, 50)).toBe(false)
|
||||
await collectGarbage()
|
||||
abort.abort()
|
||||
expect((await abandoned).status).toBe(503)
|
||||
expect((await bootstrap(AbortSignal.abort())).status).toBe(503)
|
||||
@@ -172,6 +175,16 @@ describe('experimental Inspector Host bootstrap route', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/** Collect unreachable Request controllers before the caller aborts. */
|
||||
async function collectGarbage(): Promise<void> {
|
||||
setFlagsFromString('--expose-gc')
|
||||
const gc = runInNewContext('gc') as () => void
|
||||
for (let pass = 0; pass < 5; pass += 1) {
|
||||
gc()
|
||||
await new Promise<void>((resolve) => { setImmediate(resolve) })
|
||||
}
|
||||
}
|
||||
|
||||
/** Report whether a promise settles before a real-time deadline. */
|
||||
async function settledWithin(promise: Promise<unknown>, ms: number): Promise<boolean> {
|
||||
const timeout = new Promise<false>((resolve) => { setTimeout(() => { resolve(false) }, ms) })
|
||||
|
||||
@@ -4569,6 +4569,14 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
description: 'Durable task set changed; clients refetch global task and Session-active catalogs.',
|
||||
parameters: [],
|
||||
},
|
||||
{
|
||||
name: 'schedule/resolve-agent',
|
||||
mode: 'waterfall',
|
||||
signature: '\'schedule/resolve-agent\'(sessionId: SessionId, next: () => Promise<Agent>): Promise<Agent>',
|
||||
summary: 'Restore a reminder\'s Session through its embedding\'s owning controller.',
|
||||
description: 'Restore a reminder\'s Session through its embedding\'s owning controller. Call next() to use the Web Session controller.',
|
||||
parameters: [{ name: 'sessionId', description: 'Original Session receiving the reminder.' }],
|
||||
},
|
||||
{
|
||||
name: 'session-telemetry/record',
|
||||
mode: 'waterfall',
|
||||
|
||||
+1
@@ -171,6 +171,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo
|
||||
|
||||
| Package | Config | Description |
|
||||
|---|---|---|
|
||||
| `@deepseek-ai/dsh-deliverable-file-references` | no | Shared model guidance for file links and deliverables across DeepSeek Harness hosts |
|
||||
| `@deepseek-ai/dsh-tool-present` | yes | Explicit workspace file delivery declarations for the DeepSeek Harness |
|
||||
| `@deepseek-ai/dsh-workspace-changes` | yes | Per-turn workspace file changes recorded from git working-tree snapshots and whole-file captures, with per-file comparisons, for the DeepSeek Harness |
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
en: 23386452a66b2fbd
|
||||
zh: 4a8d91b2f313fead
|
||||
/deepseek-ai-dsh-schedule/use-this-package:
|
||||
en: 88aba0cd93ff0903
|
||||
zh: a8e4c257c319de53
|
||||
en: ec60f8eec2f735bc
|
||||
zh: ed06b44badc93456
|
||||
/deepseek-ai-dsh-schedule/understand-the-implementation:
|
||||
en: 3d11c664125e32d8
|
||||
zh: 01a007a5ffb2ab63
|
||||
en: 02848e6fd6317b2a
|
||||
zh: 1807a423210f81ef
|
||||
/deepseek-ai-dsh-schedule/further-exploration:
|
||||
en: d4404aa2dd7b3a94
|
||||
zh: 2dcd3d98cf281c32
|
||||
|
||||
@@ -23,9 +23,9 @@ Schedule delivers one-shot, fixed-rate, daily, weekly, and cron wall-clock remin
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
The shipped Web composition mounts the service alongside storage-domain and the Session controller. Its `Config` states `deliveryHistoryDays` (default 30) and `deliveryHistoryRecords` (default 200). Storage backend routing belongs to storage-domain; Session model and preset restoration belong to the Session controller. Schedule cannot be mounted alone in a headless or SDK-only composition: delivery requires the Host Web Session controller and a Session persistence backend, because a delivery commits only after the Session acknowledges `session/flush`.
|
||||
The shipped Web composition mounts the service alongside storage-domain and the Session controller. Its `Config` states `deliveryHistoryDays` (default 30) and `deliveryHistoryRecords` (default 200). Storage backend routing belongs to storage-domain; Session model and preset restoration belong to the Session controller. Delivery asks `schedule/resolve-agent` to activate the original Session through its owning controller; without a handler it uses the Web Session controller. The SDK server supplies its own handler and does not mount the Web controller. Web restoration is registered inside `ctx.inject(['sessionController'])`; storage and management do not wait for the Web controller. When the controller becomes ready or remounts, overdue tasks are reconsidered. The SDK composition waits for its own activation handler before mounting Schedule. A Session persistence backend is required because a delivery commits only after the Session acknowledges `session/flush`.
|
||||
|
||||
The reminder tools `schedule_create`, `schedule_list`, `schedule_delete`, and `schedule_update` come from [`@deepseek-ai/dsh-tool-schedule`](../tool-schedule/README.md), which a preset mounts; this package contributes the Host service those tools call. Update replaces the name, instruction, or timing of one reminder in place and keeps its id and saved records; it is not offered for the relative `after` delay. Creation requires a non-empty prompt, a title, and exactly one of six selectors:
|
||||
The reminder tools `schedule_create`, `schedule_list`, `schedule_delete`, and `schedule_update` come from [`@deepseek-ai/dsh-tool-schedule`](../tool-schedule/README.md), which Web presets and the SDK bundle mount; this package contributes the Host service those tools call. Update replaces the name, instruction, or timing of one reminder in place and keeps its id and saved records; it is not offered for the relative `after` delay. Creation requires a non-empty prompt, a title, and exactly one of six selectors:
|
||||
|
||||
| Selector | Example | Timing |
|
||||
|---|---|---|
|
||||
@@ -62,13 +62,13 @@ Every update is a complete-record compare-and-set inside the same FIFO as creati
|
||||
|
||||
`ScheduleService` owns one version-1 `schedule` domain with globally unique task ids and one Host timer. Every task stores its Session binding, record, and `active` or `inactive` status together; only active tasks drive the timer. Stored records without a status normalize to `active`, without scanning or recreating historical Sessions. Management and dispatch writes share one FIFO. Updates compare the complete expected record and sample `Date.now()` inside that queue; one task put changes the rule, name, or instruction without replacing the binding or delivery history. Creation, deletion, and update recheck supplied cancellation after queueing, before persistence; once a write begins, cancellation does not roll it back. The timer rechecks the wall clock, including due members after Session restoration if the clock rolls backward, and segments delays beyond the platform timer limit. Due Every, Daily, Weekly, and Cron tasks for the same Session share one message; each task advances independently after the delivery decision. Successfully advanced tasks remain eligible for the next Host timer even if another batch member fails to persist.
|
||||
|
||||
Delivery resolves the original Session through `sessionController.resolveAgent`. A plugin-sourced `followup()` synchronously appends the message to the Session inbox; successful Session flush acknowledges durable delivery. One task-row put then updates `lastDelivery`, appends the actual receipt and sent prompt snapshot to `deliveryHistory.records`, and stores the one-shot's `inactive` status or the recurring task's next target. Receipts contain the occurrence's `scheduledAt`, acknowledgment time `deliveredAt`, and `messageId`; they acknowledge inbox delivery, not model execution. Failed flush or task put publishes no new saved record. Session persistence and the task put are separate durable writes; a crash or task-write failure after Session flush can leave a delivered message unrecorded and deliver the same reminder again.
|
||||
Delivery resolves the original Session through `schedule/resolve-agent`; the Web fallback calls `sessionController.resolveAgent` only while that controller is injected. A plugin-sourced `followup()` synchronously appends the message to the Session inbox; successful Session flush acknowledges durable delivery. One task-row put then updates `lastDelivery`, appends the actual receipt and sent prompt snapshot to `deliveryHistory.records`, and stores the one-shot's `inactive` status or the recurring task's next target. Receipts contain the occurrence's `scheduledAt`, acknowledgment time `deliveredAt`, and `messageId`; they acknowledge inbox delivery, not model execution. Failed flush or task put publishes no new saved record. Session persistence and the task put are separate durable writes; a crash or task-write failure after Session flush can leave a delivered message unrecorded and deliver the same reminder again.
|
||||
|
||||
The optional version-1 `deliveryHistory` stays in the task row so its oldest-first `records` and `earlierRecordsUnavailable` flag share the status and target commit. New tasks start with empty records and a false flag. Reading a task without history exposes only its existing `lastDelivery`, if any, with no prompt snapshot and a true flag; it neither rewrites the task nor reconstructs missing deliveries or prompts from Session logs or the current prompt. Future appends preserve that true flag and retain the existing receipt. Stored history rejects duplicate message ids or a latest receipt that differs from `lastDelivery`.
|
||||
|
||||
The optional `earlierRecordsPruned` flag records confirmed removal by an append, remains true after later deliveries and restarts, and is never inferred from `earlierRecordsUnavailable`. Missing flags in existing rows mean pruning is unconfirmed. History responses expose this distinction and the current Host retention limits; reads do not prune records.
|
||||
|
||||
`schedule/changed` notifies clients after committed task changes. Timing updates request timer recomputation only after the task put commits. Tool and browser consumers use the same service; list and delete read storage directly. Recomputations keep at most one pending timer, including changes during delivery. Shutdown cancels it and drains accepted work before closing the domain. Dispatch admission failures are logged without automatic retry and do not invalidate an already-persisted management result. Storage validation and cleanup registration failures still reject initialization. The Session controller owns Agents it restores.
|
||||
`schedule/changed` notifies clients after committed task changes. Timing updates request timer recomputation only after the task put commits. Tool and browser consumers use the same service; list and delete read storage directly. Recomputations keep at most one pending timer, including changes during delivery. Shutdown cancels it and drains accepted work before closing the domain. Dispatch admission failures are logged without automatic retry and do not invalidate an already-persisted management result. Storage validation and cleanup registration failures still reject initialization. The activation provider owns Agents it restores.
|
||||
|
||||
The Schedule domain declares the whole-unit layout because tasks are authoritative. When routed to the JSON backend, an unreadable file, malformed document, unsupported version, or invalid task rejects startup instead of publishing a partial task catalog. Failed recovery leaves `schedule.json` unchanged, while an initially absent file opens as an empty domain. Repairing the file permits reopening the same task identities. Registered startup awaits storage validation and runtime setup through `Service.init`; unloading during open releases the acquired domain without starting dispatch.
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ Schedule 将一次性、固定周期、按每日、按每周以及 cron 本地
|
||||
<a id="use-this-package"></a>
|
||||
## 使用此包
|
||||
|
||||
随发行版交付的 Web 组合把此服务与 storage-domain、Session controller 一起挂载。其 `Config` 声明 `deliveryHistoryDays`(默认 30)与 `deliveryHistoryRecords`(默认 200)。存储后端路由由 storage-domain 管理;会话模型与 preset 恢复由 Session controller 管理。Schedule 无法在 headless 或仅 SDK 的组合中单独挂载:投递需要 Host 的 Web Session controller 和 Session 持久化后端,因为只有在 Session 确认 `session/flush` 之后一次投递才会提交。
|
||||
随发行版交付的 Web 组合把此服务与 storage-domain、Session controller 一起挂载。其 `Config` 声明 `deliveryHistoryDays`(默认 30)与 `deliveryHistoryRecords`(默认 200)。存储后端路由由 storage-domain 管理;会话模型与 preset 恢复由 Session controller 管理。投递通过 `schedule/resolve-agent` 请求会话所属的控制器激活原 Session;没有处理方时使用 Web Session controller。SDK server 提供自己的处理方,不挂载 Web 控制器。Web 恢复在 `ctx.inject(['sessionController'])` 内注册;存储和管理不等待 Web 控制器。控制器就绪或重新挂载时,会重新检查到期任务。SDK 组合在其自身激活处理方就绪后挂载 Schedule。投递仍需要 Session 持久化后端,因为只有在 Session 确认 `session/flush` 之后一次投递才会提交。
|
||||
|
||||
提醒工具 `schedule_create`、`schedule_list`、`schedule_delete` 和 `schedule_update` 来自 [`@deepseek-ai/dsh-tool-schedule`](../tool-schedule/README.zh.md),由 preset 挂载;本包提供这些工具调用的宿主服务。更新原地替换一条提醒的名称、指令或时间,保留其 id 与已保存记录;相对的 `after` 延迟不支持更新。创建时需要非空提示文本、标题,且必须只提供以下六个选择器之一:
|
||||
提醒工具 `schedule_create`、`schedule_list`、`schedule_delete` 和 `schedule_update` 来自 [`@deepseek-ai/dsh-tool-schedule`](../tool-schedule/README.zh.md),由 Web preset 和 SDK bundle 挂载;本包提供这些工具调用的宿主服务。更新原地替换一条提醒的名称、指令或时间,保留其 id 与已保存记录;相对的 `after` 延迟不支持更新。创建时需要非空提示文本、标题,且必须只提供以下六个选择器之一:
|
||||
|
||||
| 选择器 | 示例 | 时间语义 |
|
||||
|---|---|---|
|
||||
@@ -62,13 +62,13 @@ cron 输入携带 `expression` 和 `time_zone`。表达式是标准五字段 Vix
|
||||
|
||||
`ScheduleService` 拥有一个版本 1 的 `schedule` domain、全局唯一的任务 id 和一个宿主定时器。每条任务同时存储会话绑定、记录及 `active` 或 `inactive` 状态;仅活动任务驱动定时器。已存储但缺少状态的记录规范化为 `active`,不会扫描或重建历史会话。管理写入与投递写入共用一个 FIFO。更新在队列内比较完整预期记录并采样 `Date.now()`;一次任务 put 修改规则、名称或指令,不替换绑定或发送历史。创建、删除和更新在排队结束、持久化开始前重新检查传入的取消信号;写入一旦开始,取消不会将其回滚。定时器重新读取实际时间,包括时钟回拨时在会话恢复后重新核对到期成员,并分段处理超过平台定时器上限的延迟。同一会话到期的 Every、Daily、Weekly 和 Cron 任务合并为一条消息;每条任务在投递判断后分别推进。即使批次中另一条任务持久化失败,已成功推进的任务仍参与宿主下一次定时计算。
|
||||
|
||||
投递通过 `sessionController.resolveAgent` 解析原会话。插件来源的 `followup()` 同步将消息追加到会话收件箱;会话 flush 成功后确认持久投递。随后一次任务行 put 更新 `lastDelivery`,将实际回执与已发送提示文本快照追加到 `deliveryHistory.records`,并保存单次任务的 `inactive` 状态或周期任务的下一目标时间。回执包含发生时点 `scheduledAt`、确认时间 `deliveredAt` 和 `messageId`;它确认收件箱投递,不表示模型执行。flush 或任务 put 失败均不发布新的已保存记录。会话持久化与任务 put 是两次独立的持久写入;会话 flush 后崩溃或任务写入失败可能留下未记入发送记录的已投递消息,并再次投递相同提醒。
|
||||
投递通过 `schedule/resolve-agent` 解析原会话;仅在 Web 控制器已注入时,Web 回退路径才调用 `sessionController.resolveAgent`。插件来源的 `followup()` 同步将消息追加到会话收件箱;会话 flush 成功后确认持久投递。随后一次任务行 put 更新 `lastDelivery`,将实际回执与已发送提示文本快照追加到 `deliveryHistory.records`,并保存单次任务的 `inactive` 状态或周期任务的下一目标时间。回执包含发生时点 `scheduledAt`、确认时间 `deliveredAt` 和 `messageId`;它确认收件箱投递,不表示模型执行。flush 或任务 put 失败均不发布新的已保存记录。会话持久化与任务 put 是两次独立的持久写入;会话 flush 后崩溃或任务写入失败可能留下未记入发送记录的已投递消息,并再次投递相同提醒。
|
||||
|
||||
版本 1 的可选 `deliveryHistory` 保留在任务行中,其从旧到新排列的 `records` 和 `earlierRecordsUnavailable` 标志与状态及目标时间共同提交。新任务的记录为空,标志为 false。读取没有历史的任务时,仅呈现其已有的 `lastDelivery`(若存在),不含提示文本快照,标志为 true;读取不重写任务,也不从会话日志或当前提示文本重建缺失的投递或提示文本。后续追加保留 true 标志及已有回执。存储历史拒绝重复消息 id,以及与 `lastDelivery` 不一致的最新回执。
|
||||
|
||||
可选的 `earlierRecordsPruned` 标志记录追加时确实删除过旧记录的事实,在后续投递和重启后保持为 true,不从 `earlierRecordsUnavailable` 推断。已有任务行缺少此标志时,裁剪情况视为未确认。历史接口返回该标志及当前宿主保留上限;读取不裁剪记录。
|
||||
|
||||
`schedule/changed` 在任务变更提交后通知客户端。时间更新仅在任务 put 提交后请求重新计算定时器。工具与浏览器使用同一服务;查询和删除直接读取存储。重新计算后至多保留一个待触发定时器,包括投递期间发生的变更。关闭时取消它,等待已接受的工作结束,再关闭 domain。投递调度准入失败会记录日志而不自动重试,不会使已持久化的管理结果失效。存储校验和清理注册失败仍会拒绝初始化。恢复的 Agent 由 Session controller 拥有。
|
||||
`schedule/changed` 在任务变更提交后通知客户端。时间更新仅在任务 put 提交后请求重新计算定时器。工具与浏览器使用同一服务;查询和删除直接读取存储。重新计算后至多保留一个待触发定时器,包括投递期间发生的变更。关闭时取消它,等待已接受的工作结束,再关闭 domain。投递调度准入失败会记录日志而不自动重试,不会使已持久化的管理结果失效。存储校验和清理注册失败仍会拒绝初始化。恢复的 Agent 由激活提供方拥有。
|
||||
|
||||
Schedule domain 声明整 unit 布局,因为任务是权威数据。路由到 JSON 后端时,文件不可读、文档损坏、版本不受支持或任务非法都会拒绝启动,而不是发布部分任务目录。恢复失败不会改写 `schedule.json`,初始不存在的文件则作为空 domain 打开;修复该文件后可按相同任务身份重新打开。注册启动通过 `Service.init` 等待存储校验与运行时初始化;打开期间卸载会释放已取得的 domain,而不开始投递。
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { Context, Service } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-api-session-controller'
|
||||
import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { Domain } from '@deepseek-ai/dsh-storage-domain'
|
||||
import { delegationDepthOf } from '@deepseek-ai/dsh-subagent'
|
||||
@@ -62,6 +64,15 @@ export {
|
||||
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Events {
|
||||
/**
|
||||
* Restore a reminder's Session through its embedding's owning controller.
|
||||
* Call next() to use the Web Session controller.
|
||||
* @param sessionId - Original Session receiving the reminder.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'schedule/resolve-agent'(sessionId: SessionId, next: () => Promise<Agent>): Promise<Agent>
|
||||
}
|
||||
interface Context {
|
||||
/** Durable Host-wide reminder management. */
|
||||
schedule: ScheduleService
|
||||
@@ -97,7 +108,7 @@ const DEFAULT_DELIVERY_HISTORY_RECORDS = 200
|
||||
* service is the plugin that registers that listener.
|
||||
*/
|
||||
export class ScheduleService extends TypertRemoteService {
|
||||
static inject = ['agents', 'sessions', 'storageDomain', 'sessionController', 'sessionPersistence']
|
||||
static inject = ['agents', 'sessions', 'storageDomain', 'sessionPersistence']
|
||||
|
||||
static Config: z<Config> = z.object({
|
||||
deliveryHistoryDays: z.number().step(1).min(1).max(3650).default(DEFAULT_DELIVERY_HISTORY_DAYS),
|
||||
@@ -110,6 +121,7 @@ export class ScheduleService extends TypertRemoteService {
|
||||
private readonly initialized: PromiseLike<unknown>
|
||||
private chain: Promise<unknown> = Promise.resolve()
|
||||
private runtime: ScheduleRuntime | undefined
|
||||
private resolveWebAgent: ((sessionId: SessionId) => Promise<Agent>) | undefined
|
||||
private stopping = false
|
||||
|
||||
/**
|
||||
@@ -118,6 +130,18 @@ export class ScheduleService extends TypertRemoteService {
|
||||
*/
|
||||
constructor(ctx: Context, config: Config) {
|
||||
super(ctx, 'schedule')
|
||||
ctx.inject(['sessionController'], (webCtx) => {
|
||||
webCtx.effect(() => {
|
||||
this.resolveWebAgent = async (sessionId) => {
|
||||
const resolved = await webCtx.sessionController.resolveAgent(sessionId)
|
||||
if ('error' in resolved) throw resolved.error
|
||||
return resolved.agent
|
||||
}
|
||||
// Resume overdue tasks when the Web activation provider becomes ready.
|
||||
this.runtime?.requestDrive()
|
||||
return () => { this.resolveWebAgent = undefined }
|
||||
})
|
||||
})
|
||||
this.retention = {
|
||||
days: config.deliveryHistoryDays ?? DEFAULT_DELIVERY_HISTORY_DAYS,
|
||||
records: config.deliveryHistoryRecords ?? DEFAULT_DELIVERY_HISTORY_RECORDS,
|
||||
@@ -159,7 +183,11 @@ export class ScheduleService extends TypertRemoteService {
|
||||
await tasks.put(task.record.id, task)
|
||||
this.emitChanged()
|
||||
},
|
||||
this.retention)
|
||||
this.retention,
|
||||
(sessionId) => {
|
||||
if (this.resolveWebAgent === undefined) throw new Error('schedule: no Session activation provider')
|
||||
return this.resolveWebAgent(sessionId)
|
||||
})
|
||||
this.runtime.requestDrive()
|
||||
return cleanup
|
||||
})
|
||||
@@ -398,8 +426,8 @@ export class ScheduleService extends TypertRemoteService {
|
||||
/**
|
||||
* Refuse a Session no delivery can reach because a delegated child owns it.
|
||||
*
|
||||
* Delivery resolves the bound Session through `ctx.sessionController.resolveAgent`,
|
||||
* which rejects the live Agent of a delegated child; a stored task for such a Session
|
||||
* Delivery uses `schedule/resolve-agent`; the Web fallback rejects the live Agent
|
||||
* of a delegated child; a stored task for such a Session
|
||||
* would stay permanently overdue and retry on every drive. Both operations that can
|
||||
* arm a delivery read that same fact here instead of restating it: a delegated child
|
||||
* is one whose {@link delegationDepthOf} is above zero, the accounting the delegation
|
||||
|
||||
@@ -7,7 +7,8 @@ declare module '@deepseek-ai/dsh-llm' {
|
||||
}
|
||||
}
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-api-session-controller'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { isRecurringScheduleRecord, renderReminderFraming, renderRecurringReminderBatchFraming, resolveRecurringOccurrence } from './domain.ts'
|
||||
import type { DeliveryRetentionBounds, RecurringScheduleRecord } from './types.ts'
|
||||
import type { ScheduleTask } from './storage.ts'
|
||||
@@ -28,6 +29,7 @@ export class ScheduleRuntime {
|
||||
* @param tasks - Current durable tasks.
|
||||
* @param transact - Serialize delivery against management writes.
|
||||
* @param commit - Persist task status, target, receipt, and history together after durable inbox delivery.
|
||||
* @param resolveAgent - Host fallback after embedding activation handlers delegate.
|
||||
*/
|
||||
constructor(
|
||||
private readonly ctx: Context,
|
||||
@@ -35,6 +37,7 @@ export class ScheduleRuntime {
|
||||
private readonly transact: (work: () => Promise<void>) => Promise<void>,
|
||||
private readonly commit: (task: ScheduleTask) => Promise<void>,
|
||||
private readonly retention: DeliveryRetentionBounds,
|
||||
private readonly resolveAgent: (sessionId: SessionId) => Promise<Agent>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -98,8 +101,8 @@ export class ScheduleRuntime {
|
||||
let admitted = group
|
||||
const committed = new Set<ScheduleTask['record']['id']>()
|
||||
try {
|
||||
const resolved = await this.ctx.sessionController.resolveAgent(task.sessionId)
|
||||
if ('error' in resolved) throw resolved.error
|
||||
const agent = await this.ctx.waterfall('schedule/resolve-agent', task.sessionId,
|
||||
() => this.resolveAgent(task.sessionId))
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- Disposal can run while Session restoration is awaited.
|
||||
if (this.stopping) return
|
||||
const now = Date.now()
|
||||
@@ -120,8 +123,8 @@ export class ScheduleRuntime {
|
||||
content: [{ type: 'text', text }], source: { kind: 'schedule' },
|
||||
})
|
||||
// followup synchronously appends the inbox splice before flush observes the Session.
|
||||
resolved.agent.followup(message)
|
||||
const flushed = await this.ctx.sessions.flush(resolved.agent.session)
|
||||
agent.followup(message)
|
||||
const flushed = await this.ctx.sessions.flush(agent.session)
|
||||
if (!flushed) throw new Error('Session persistence did not acknowledge the reminder')
|
||||
const deliveredAt = new Date(Date.now()).toISOString()
|
||||
if (!isRecurringScheduleRecord(task.record)) {
|
||||
|
||||
@@ -19,7 +19,7 @@ it('acknowledges the real synchronous inbox splice after flush without claiming
|
||||
const releaseMaintenance = Promise.withResolvers<undefined>()
|
||||
const maintenance = agent.runMaintenance(() => releaseMaintenance.promise)
|
||||
try {
|
||||
const resolve = vi.fn(async () => ({ agent }))
|
||||
const resolve = vi.fn(async (_sessionId: SessionId) => ({ agent }))
|
||||
ctx.provide('sessionController', { resolveAgent: resolve } as never)
|
||||
const record = createAfterScheduleRecord(ScheduleId('receipt'), 'Inbox only', 1, Date.now() - 2000, 'Inbox only')
|
||||
const tasks: ScheduleTask[] = [{ sessionId: agent.session.id, record, status: 'active' }]
|
||||
@@ -32,7 +32,8 @@ it('acknowledges the real synchronous inbox splice after flush without claiming
|
||||
return releaseFlush.promise
|
||||
})
|
||||
const commit = vi.fn(async (task: ScheduleTask) => { tasks[0] = task })
|
||||
const runtime = new ScheduleRuntime(ctx, () => tasks, work => work(), commit, { days: 30, records: 200 })
|
||||
const runtime = new ScheduleRuntime(ctx, () => tasks, work => work(), commit, { days: 30, records: 200 },
|
||||
async sessionId => (await resolve(sessionId)).agent)
|
||||
try {
|
||||
runtime.requestDrive()
|
||||
await flushStarted.promise
|
||||
|
||||
@@ -68,6 +68,7 @@ export async function harness(options: {
|
||||
pool?: MemoryMediaPool
|
||||
backend?: StorageBackend
|
||||
direct?: true
|
||||
sessionController?: false
|
||||
config?: Config
|
||||
} = {}) {
|
||||
const ctx = new Context()
|
||||
@@ -89,7 +90,7 @@ export async function harness(options: {
|
||||
return async () => { await facility.closeAll(); unmount() }
|
||||
})
|
||||
const resolve = vi.fn<Context['sessionController']['resolveAgent']>(async () => { throw new Error('missing Session') })
|
||||
ctx.provide('sessionController', { resolveAgent: resolve } as never)
|
||||
if (options.sessionController !== false) ctx.provide('sessionController', { resolveAgent: resolve } as never)
|
||||
const flush = vi.fn(async () => {})
|
||||
ctx.on('session/flush', flush)
|
||||
// This fixture supplies flush behavior above; Schedule does not invoke persistence methods directly.
|
||||
@@ -103,7 +104,14 @@ export async function harness(options: {
|
||||
} else {
|
||||
fiber = await ctx.plugin(ScheduleService, options.config)
|
||||
}
|
||||
return { ctx, service: ctx.schedule, pool, resolve, flush, fiber }
|
||||
const resolveAgent = async (sessionId: SessionId): Promise<Agent> => {
|
||||
const controller = ctx.get('sessionController')
|
||||
if (controller === undefined) throw new Error('schedule: no Session activation provider')
|
||||
const result = await controller.resolveAgent(sessionId)
|
||||
if ('error' in result) throw result.error
|
||||
return result.agent
|
||||
}
|
||||
return { ctx, service: ctx.schedule, pool, resolve, resolveAgent, flush, fiber }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,68 @@ function scheduleEvent(data: unknown, seq: number): SessionEvent {
|
||||
}
|
||||
|
||||
describe('shared Schedule management', () => {
|
||||
it('delivers overdue tasks when the Web controller mounts and remounts', async () => {
|
||||
const test = await harness({ sessionController: false })
|
||||
tests.push(test)
|
||||
const agent = agentFor(test.ctx)
|
||||
test.resolve.mockResolvedValue({ agent })
|
||||
const warn = vi.spyOn(test.ctx.logger, 'warn').mockImplementation(() => {})
|
||||
const controller = {
|
||||
apply(ctx: typeof test.ctx) {
|
||||
ctx.provide('sessionController', { resolveAgent: test.resolve } as never)
|
||||
},
|
||||
}
|
||||
try {
|
||||
await test.service.create(agent.session.id, { prompt: 'First', title: 'First', after_seconds: 1 })
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
expect(agent.followup).not.toHaveBeenCalled()
|
||||
const first = await test.ctx.plugin(controller)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(agent.followup).toHaveBeenCalledOnce()
|
||||
expect(await test.service.list({ sessionId: agent.session.id })).toEqual([])
|
||||
|
||||
await first.dispose()
|
||||
await test.service.create(agent.session.id, { prompt: 'Second', title: 'Second', after_seconds: 1 })
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
expect(agent.followup).toHaveBeenCalledOnce()
|
||||
await test.ctx.plugin(controller)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(agent.followup).toHaveBeenCalledTimes(2)
|
||||
expect(await test.service.list({ sessionId: agent.session.id })).toEqual([])
|
||||
} finally { warn.mockRestore() }
|
||||
})
|
||||
|
||||
it('does not resume delivery after Schedule unload when the Web controller arrives', async () => {
|
||||
const test = await harness({ sessionController: false })
|
||||
tests.push(test)
|
||||
const agent = agentFor(test.ctx)
|
||||
test.resolve.mockResolvedValue({ agent })
|
||||
await test.service.create(agent.session.id, { prompt: 'Stopped', title: 'Stopped', after_seconds: 1 })
|
||||
await test.fiber.dispose()
|
||||
await test.ctx.plugin({
|
||||
apply(ctx: typeof test.ctx) {
|
||||
ctx.provide('sessionController', { resolveAgent: test.resolve } as never)
|
||||
},
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
expect(test.resolve).not.toHaveBeenCalled()
|
||||
expect(agent.followup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps an active task when the injected Web controller refuses restoration', async () => {
|
||||
const test = await setup()
|
||||
const warn = vi.spyOn(test.ctx.logger, 'warn').mockImplementation(() => {})
|
||||
test.resolve.mockResolvedValueOnce({ error: new Error('Web restoration unavailable') } as never)
|
||||
try {
|
||||
const sessionId = SessionId('refused-web-restoration')
|
||||
const record = await test.service.create(sessionId, { prompt: 'Keep', title: 'Keep', after_seconds: 1 })
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
expect(test.resolve).toHaveBeenCalledOnce()
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Web restoration unavailable'))
|
||||
expect((await test.service.list({ sessionId })).map(task => task.id)).toEqual([record.id])
|
||||
} finally { warn.mockRestore() }
|
||||
})
|
||||
|
||||
it('initializes a new task with empty known delivery history', async () => {
|
||||
const { service, pool } = await setup()
|
||||
const sessionId = SessionId('new-history')
|
||||
|
||||
@@ -18,8 +18,8 @@ afterEach(async () => {
|
||||
await Promise.all(tests.splice(0).map(test => test.ctx.fiber.dispose()))
|
||||
vi.useRealTimers()
|
||||
})
|
||||
async function setup(tasks: ScheduleTask[]) {
|
||||
const test = await harness(); tests.push(test)
|
||||
async function setup(tasks: ScheduleTask[], options: Parameters<typeof harness>[0] = {}) {
|
||||
const test = await harness(options); tests.push(test)
|
||||
const agent = agentFor(test.ctx)
|
||||
const followup = vi.fn<Agent['followup']>()
|
||||
agent.followup = followup
|
||||
@@ -28,7 +28,7 @@ async function setup(tasks: ScheduleTask[]) {
|
||||
const index = tasks.findIndex(current => current.record.id === task.record.id)
|
||||
tasks[index] = task
|
||||
})
|
||||
const runtime = new ScheduleRuntime(test.ctx, () => tasks, work => work(), commit, { days: 30, records: 200 })
|
||||
const runtime = new ScheduleRuntime(test.ctx, () => tasks, work => work(), commit, { days: 30, records: 200 }, test.resolveAgent)
|
||||
runtimes.push(runtime)
|
||||
return { ...test, agent, followup, runtime, commit }
|
||||
}
|
||||
@@ -59,6 +59,33 @@ it('seeds legacy history only from its actual receipt and appends through wall-c
|
||||
})
|
||||
|
||||
describe('Host Schedule timer', () => {
|
||||
it('keeps reminders unacknowledged when no Session activation provider is mounted', async () => {
|
||||
const tasks = [task(createAfterScheduleRecord(ScheduleId('unavailable'), 'Remember', 2, Date.now(), 'Remember'))]
|
||||
const test = await setup(tasks, { sessionController: false })
|
||||
const warn = vi.spyOn(test.ctx.logger, 'warn').mockImplementation(() => {})
|
||||
try {
|
||||
test.runtime.requestDrive()
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('no Session activation provider'))
|
||||
expect(test.followup).not.toHaveBeenCalled()
|
||||
expect(test.commit).not.toHaveBeenCalled()
|
||||
expect(tasks[0]?.status).toBe('active')
|
||||
} finally { warn.mockRestore() }
|
||||
})
|
||||
|
||||
it('uses the embedding activation handler instead of the Web controller', async () => {
|
||||
const tasks = [task(createAfterScheduleRecord(ScheduleId('embedded'), 'Remember', 2, Date.now(), 'Remember'))]
|
||||
const test = await setup(tasks)
|
||||
const activate = vi.fn(async () => test.agent)
|
||||
test.ctx.on('schedule/resolve-agent', activate)
|
||||
test.runtime.requestDrive()
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
expect(activate).toHaveBeenCalledWith(SessionId('original'), expect.any(Function))
|
||||
expect(test.resolve).not.toHaveBeenCalled()
|
||||
expect(test.followup).toHaveBeenCalledOnce()
|
||||
expect(tasks[0]?.status).toBe('inactive')
|
||||
})
|
||||
|
||||
it('restores the original Session and retains an ended one-shot with its flushed message receipt', async () => {
|
||||
const tasks = [task(createAfterScheduleRecord(ScheduleId('once'), 'Remember', 2, Date.now(), 'Remember'))]
|
||||
const test = await setup(tasks)
|
||||
@@ -674,7 +701,7 @@ describe('Schedule dispatch failures and concurrent changes', () => {
|
||||
const transaction = vi.fn<(work: () => Promise<void>) => Promise<void>>()
|
||||
.mockReturnValueOnce(failed.promise)
|
||||
.mockImplementation(work => work())
|
||||
const runtime = new ScheduleRuntime(test.ctx, () => [], transaction, async () => {}, { days: 30, records: 200 })
|
||||
const runtime = new ScheduleRuntime(test.ctx, () => [], transaction, async () => {}, { days: 30, records: 200 }, test.resolveAgent)
|
||||
runtimes.push(runtime)
|
||||
const warning = vi.spyOn(test.ctx.logger, 'warn').mockImplementationOnce(() => { runtime.requestDrive() })
|
||||
runtime.requestDrive()
|
||||
@@ -695,7 +722,7 @@ describe('Schedule dispatch failures and concurrent changes', () => {
|
||||
entered.resolve(undefined)
|
||||
await released.promise
|
||||
})
|
||||
const runtime = new ScheduleRuntime(test.ctx, () => records, transaction, async () => {}, { days: 30, records: 200 })
|
||||
const runtime = new ScheduleRuntime(test.ctx, () => records, transaction, async () => {}, { days: 30, records: 200 }, test.resolveAgent)
|
||||
runtimes.push(runtime)
|
||||
test.ctx.effect(() => () => runtime.dispose())
|
||||
const stop = vi.spyOn(runtime, 'dispose')
|
||||
@@ -739,7 +766,8 @@ describe('Schedule dispatch failures and concurrent changes', () => {
|
||||
it('drains a rejected serialization promise during disposal', async () => {
|
||||
const test = await setup([])
|
||||
const transaction = Promise.withResolvers<undefined>()
|
||||
const runtime = new ScheduleRuntime(test.ctx, () => [], () => transaction.promise, async () => {}, { days: 30, records: 200 })
|
||||
const runtime = new ScheduleRuntime(test.ctx, () => [], () => transaction.promise, async () => {},
|
||||
{ days: 30, records: 200 }, test.resolveAgent)
|
||||
runtimes.push(runtime)
|
||||
runtime.requestDrive()
|
||||
const stopped = runtime.dispose()
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
en: 01c58dddd7e62de2
|
||||
zh: 6dbd46e244f3f135
|
||||
/deepseek-ai-dsh-sdk-client/use-this-package/lower-level-control-with-harnessclient:
|
||||
en: 8778901987da6e59
|
||||
zh: 3d09fe4b0b393a15
|
||||
en: 2eb13706964444cb
|
||||
zh: a2640ec27b83113f
|
||||
/deepseek-ai-dsh-sdk-client/use-this-package/native-user-questions:
|
||||
en: ea40f378444c8153
|
||||
zh: 96e29c99aa5c15f0
|
||||
/deepseek-ai-dsh-sdk-client/understand-the-implementation:
|
||||
en: 8773f50c51da85f0
|
||||
zh: 4649ebc69357deb0
|
||||
@@ -48,8 +51,8 @@
|
||||
en: 7abe0be104a791e7
|
||||
zh: 445311f78b1069ca
|
||||
/deepseek-ai-dsh-sdk-client/known-limitations-and-deferred-work:
|
||||
en: 3f51394ef0699943
|
||||
zh: 6d4c3180fced8db1
|
||||
en: 60100c38c111a0b0
|
||||
zh: 2085486bd36f5e48
|
||||
/deepseek-ai-dsh-sdk-client/known-limitations-and-deferred-work/dev-note:
|
||||
en: 6177a56f053936a4
|
||||
zh: 0f4b20228c2d15da
|
||||
@@ -59,6 +59,13 @@ Task-owning consumers can subscribe before `prompt()`, then call `request('sessi
|
||||
|
||||
The client exports typed errors for every failure mode: `JsonRpcResponseError` (a wire error response, code and data preserved), `RequestTimeoutError` (a configured bound elapsed), `SdkProtocolError` (a response outside the documented protocol), and `TransportClosedError` (the runtime is gone — the message carries the exit code and a bounded stderr tail). `close()` requests protocol `shutdown` (bounded by `shutdownTimeoutMs`, default 1000 ms), then walks a stdin-EOF → SIGTERM → SIGKILL ladder until the process has exited; it is idempotent, and a closed client refuses reuse. `HarnessClientOptions.env` replaces the child environment entirely when given (`undefined` inherits the parent's); callers own credential policy — `scrubbedParentEnv` from `dsh-subprocess` is the shared scrub base for isolation-minded launches.
|
||||
|
||||
<a id="native-user-questions"></a>
|
||||
### Native user questions
|
||||
|
||||
Pass `onUserQuestion` to `DeepSeekHarness` or `HarnessClient` to answer the built-in `ask_user_question` tool through the native SDK protocol. The callback receives `{ requestId, sessionId, questions }` and `{ signal }`; return `{ answers: [{ id, selected, custom? }] }`, using one item per question id. Your application owns the UI or simulated-user policy. The SDK automatically advertises the callback at initialization and can receive answers while other requests are pending. `HarnessClient.initialize()` starts the process when needed; an explicit `start()` is optional.
|
||||
|
||||
Observe the signal to release host work when the runtime cancels the question, disconnects, or the client closes. The SDK stops awaiting cancelled callbacks and ignores their later answers. A throwing callback reports a provider error; missing or malformed answers never count as approval. Without a callback, native in-process answerers remain available, and a missing answerer reports `NO_PROVIDER`.
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
@@ -127,7 +134,7 @@ These limits define when the client is a poor fit or needs special care. They ar
|
||||
- **No bundled-runtime resolution** — the client resolves the same-version `@deepseek-ai/dsh` package (or a caller-provided `dshBin`); packaged-executable discovery stays Python-side until a TypeScript distribution consumer exists.
|
||||
- **No mid-turn cancel** — the wire has no prompt-cancel method; abandoning a turn means closing the runtime (see the [protocol limitations](../protocol/README.md#known-limitations-and-deferred-work)).
|
||||
- **No per-prompt result** — low-level `prompt()` returns only an enqueue receipt; high-level `run()` owns receipt-to-idle collection, and abandoning it means closing the runtime.
|
||||
- **Client→server notifications and server→client requests are unimplemented** on both wire ends; the transport carries them for future approval flows.
|
||||
- **Callbacks must release their own resources** — the SDK delivers cancellation through `AbortSignal`; application work that ignores it may continue after the SDK stops awaiting its answer.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
@@ -59,6 +59,13 @@ console.log(result.finalResponse)
|
||||
|
||||
本客户端为每种失败模式导出类型化错误:`JsonRpcResponseError`(协议错误响应,保留 code 与 data)、`RequestTimeoutError`(配置的时限已到)、`SdkProtocolError`(响应超出文档化协议)、`TransportClosedError`(运行时已消失——消息携带退出码与有界 stderr 尾部)。`close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走 stdin-EOF → SIGTERM → SIGKILL 阶梯直到进程退出;幂等,已关闭的客户端拒绝复用。`HarnessClientOptions.env` 给定时整体替换子进程环境(`undefined` 原样继承父进程环境);凭据策略归调用方——`dsh-subprocess` 的 `scrubbedParentEnv` 是面向隔离启动的共享擦除基底。
|
||||
|
||||
<a id="native-user-questions"></a>
|
||||
### 原生用户问答
|
||||
|
||||
向 `DeepSeekHarness` 或 `HarnessClient` 传入 `onUserQuestion`,即可通过原生 SDK 协议回答内建的 `ask_user_question` 工具。回调接收 `{ requestId, sessionId, questions }` 与 `{ signal }`;返回 `{ answers: [{ id, selected, custom? }] }`,每个问题 id 对应一项。应用自行提供 UI 或模拟用户策略。SDK 在初始化时自动声明此回调,也能在其他请求待决时接收答案。`HarnessClient.initialize()` 会在需要时启动进程,无需显式调用 `start()`。
|
||||
|
||||
运行时取消问题、断开连接或客户端关闭时,应响应 signal 释放宿主工作。SDK 停止等待已取消的回调,并忽略晚到答案。回调抛错会报告提供方错误;缺失或格式错误的答案均不构成批准。未提供回调时,原生进程内回答方仍可用,缺少回答方则报告 `NO_PROVIDER`。
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
@@ -127,7 +134,7 @@ console.log(result.finalResponse)
|
||||
- **无捆绑运行时解析**——客户端解析同版本 `@deepseek-ai/dsh` 包(或调用方提供的 `dshBin`);打包可执行文件的发现留在 Python 侧,直到出现 TypeScript 发行版消费方。
|
||||
- **无轮次中取消**——协议层没有提示词取消方法;放弃轮次意味着关闭运行时(见[协议限制](../protocol/README.zh.md#known-limitations-and-deferred-work))。
|
||||
- **没有逐提示词结果**——低层 `prompt()` 只返回入队回执;高层 `run()` 负责从回执到 idle 的收集,放弃该过程意味着关闭运行时。
|
||||
- **客户端→服务端通知与服务端→客户端请求**在协议两端都未实现;传输层为未来审批流程保留了承载能力。
|
||||
- **回调必须释放自己的资源**——SDK 通过 `AbortSignal` 发出取消;忽略它的应用工作可能在 SDK 停止等待答案后继续运行。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type InitializeResult,
|
||||
type SessionPromptParams,
|
||||
type SdkPromptContentBlock,
|
||||
type SdkUserQuestionRequest,
|
||||
} from '@deepseek-ai/dsh-sdk-protocol'
|
||||
import { disposeRuntimeProcess } from './dispose.ts'
|
||||
import { resolveDshLaunch, type RuntimeProcessOptions } from './launch.ts'
|
||||
@@ -196,6 +197,8 @@ export class HarnessClient {
|
||||
private spawnError: Error | undefined
|
||||
private streamsSettled: Promise<void> = Promise.resolve()
|
||||
private closeTask: Promise<void> | undefined
|
||||
private readonly userQuestions = new Map<string, AbortController>()
|
||||
private userQuestionHandler: HarnessClientOptions['onUserQuestion']
|
||||
|
||||
/** @param options - dsh profile, patch, home, process, environment, and timeout options. */
|
||||
constructor(options?: HarnessClientOptions)
|
||||
@@ -211,6 +214,7 @@ export class HarnessClient {
|
||||
start(): void {
|
||||
if (this.closeTask !== undefined) throw new TransportClosedError('DeepSeek Harness runtime client is closed')
|
||||
if (this.child !== undefined) return
|
||||
this.userQuestionHandler = this.options.onUserQuestion
|
||||
const child = spawn(this.runtime.command, this.runtime.args, {
|
||||
cwd: this.runtime.cwd,
|
||||
env: this.runtime.environment(),
|
||||
@@ -263,18 +267,54 @@ export class HarnessClient {
|
||||
this.transport?.close()
|
||||
})
|
||||
const transport = new JsonRpcLineTransport(child.stdout, child.stdin)
|
||||
transport.onNotification((method, params) => { this.dispatchNotification({ method, params }) })
|
||||
transport.onRequest(async (method, params) => {
|
||||
if (method !== 'user-question/ask') throw new SdkProtocolError(`unknown runtime request: ${method}`)
|
||||
const handler = this.userQuestionHandler
|
||||
if (handler === undefined) throw new SdkProtocolError('user-question/ask requires an onUserQuestion callback')
|
||||
if (!isUserQuestionRequest(params)) {
|
||||
throw new SdkProtocolError('user-question/ask requires requestId, sessionId, and questions')
|
||||
}
|
||||
const requestId = params.requestId
|
||||
if (this.userQuestions.has(requestId)) throw new SdkProtocolError('duplicate user-question requestId')
|
||||
const controller = new AbortController()
|
||||
this.userQuestions.set(requestId, controller)
|
||||
const cancelled = Promise.withResolvers<never>()
|
||||
const onAbort = (): void => { cancelled.reject(controller.signal.reason) }
|
||||
controller.signal.addEventListener('abort', onAbort, { once: true })
|
||||
try {
|
||||
return await Promise.race([
|
||||
Promise.resolve().then(() => {
|
||||
controller.signal.throwIfAborted()
|
||||
return handler(params, { signal: controller.signal })
|
||||
}),
|
||||
cancelled.promise,
|
||||
])
|
||||
} finally {
|
||||
controller.signal.removeEventListener('abort', onAbort)
|
||||
this.userQuestions.delete(requestId)
|
||||
}
|
||||
})
|
||||
transport.onNotification((method, params) => {
|
||||
if (method === 'user-question.cancelled' && typeof params.requestId === 'string') {
|
||||
this.userQuestions.get(params.requestId)?.abort(new Error('SDK runtime cancelled the user question'))
|
||||
}
|
||||
this.dispatchNotification({ method, params })
|
||||
})
|
||||
transport.start()
|
||||
this.transport = transport
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the process-wide handshake.
|
||||
* Start the runtime if needed, then perform the process-wide handshake.
|
||||
* @param params - workspace cwd plus the provider/model route.
|
||||
* @returns the runtime's wire identity.
|
||||
*/
|
||||
async initialize(params: InitializeParams): Promise<InitializeResult> {
|
||||
const result = await this.request('initialize', { ...params }, this.runtime.initializeTimeoutMs)
|
||||
this.start()
|
||||
const result = await this.request('initialize', {
|
||||
...params,
|
||||
...this.userQuestionHandler === undefined ? {} : { capabilities: { ...params.capabilities, userQuestions: true } },
|
||||
}, this.runtime.initializeTimeoutMs)
|
||||
if (!isRecord(result) || !isRecord(result.serverInfo)
|
||||
|| typeof result.serverInfo.name !== 'string' || typeof result.serverInfo.version !== 'string') {
|
||||
throw new SdkProtocolError(`initialize returned no server identity: ${JSON.stringify(result)}`)
|
||||
@@ -428,6 +468,7 @@ export class HarnessClient {
|
||||
}
|
||||
|
||||
private async performClose(): Promise<void> {
|
||||
this.abortUserQuestions(new TransportClosedError('SDK client is closing'))
|
||||
const child = this.child
|
||||
if (child === undefined) return
|
||||
try {
|
||||
@@ -475,9 +516,14 @@ export class HarnessClient {
|
||||
}
|
||||
|
||||
private failSubscriptions(error: Error): void {
|
||||
this.abortUserQuestions(error)
|
||||
for (const subscription of this.subscriptions.values()) subscription.fail(error)
|
||||
}
|
||||
|
||||
private abortUserQuestions(error: Error): void {
|
||||
for (const controller of this.userQuestions.values()) controller.abort(error)
|
||||
}
|
||||
|
||||
private appendStderr(lines: string[]): void {
|
||||
const kept = lines.filter(line => line.length > 0)
|
||||
this.stderrTail.push(...kept)
|
||||
@@ -525,3 +571,11 @@ function errorMessage(error: unknown): string {
|
||||
/* v8 ignore next -- the transport and dispose ladder reject only with Errors */
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
/** Check the question envelope before handing runtime input to application code. */
|
||||
function isUserQuestionRequest(value: unknown): value is SdkUserQuestionRequest {
|
||||
return isRecord(value) && typeof value.requestId === 'string' && value.requestId.length > 0
|
||||
&& typeof value.sessionId === 'string' && Array.isArray(value.questions)
|
||||
&& value.questions.length > 0 && value.questions.every((question: unknown) => isRecord(question)
|
||||
&& typeof question.id === 'string' && typeof question.question === 'string')
|
||||
}
|
||||
@@ -22,6 +22,8 @@ export { JsonRpcResponseError } from '@deepseek-ai/dsh-sdk-protocol'
|
||||
export type {
|
||||
ContentBlock,
|
||||
SdkPromptContentBlock,
|
||||
SdkUserQuestionAnswer,
|
||||
SdkUserQuestionRequest,
|
||||
DeepSeekHarnessOptions,
|
||||
HarnessClientOptions,
|
||||
HarnessNotification,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { ContentBlock, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
import type { SdkPromptContentBlock } from '@deepseek-ai/dsh-sdk-protocol'
|
||||
import type { SdkPromptContentBlock, SdkUserQuestionAnswer, SdkUserQuestionRequest } from '@deepseek-ai/dsh-sdk-protocol'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** One server-to-client notification as received off the wire. */
|
||||
@@ -22,6 +22,8 @@ export type NotificationFilter = (notification: HarnessNotification) => boolean
|
||||
|
||||
/** Launch and timeout options for {@link HarnessClient}. */
|
||||
export interface HarnessClientOptions {
|
||||
/** Answer runtime questions without prescribing a UI; observe the signal to release host work on cancellation. */
|
||||
onUserQuestion?: (request: SdkUserQuestionRequest, context: { signal: AbortSignal }) => Promise<SdkUserQuestionAnswer>
|
||||
/** Absolute or caller-relative dsh CLI module; omitted resolves this package's same-version dependency. */
|
||||
dshBin?: string
|
||||
/** Named profile serving the SDK protocol (default `sdk`). */
|
||||
@@ -81,3 +83,4 @@ export interface RunResult {
|
||||
/** Re-exported content-block alias so SDK callers need no extra import. */
|
||||
export type { ContentBlock }
|
||||
export type { SdkPromptContentBlock }
|
||||
export type { SdkUserQuestionAnswer, SdkUserQuestionRequest }
|
||||
@@ -0,0 +1,43 @@
|
||||
/** Protocol-only peer: requests host input while an unrelated client request is pending. */
|
||||
import { createInterface } from 'node:readline'
|
||||
|
||||
const pending = new Map<string, string | number>()
|
||||
let serial = 0
|
||||
const input = createInterface({ input: process.stdin })
|
||||
function write(value: object): void { process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...value })}\n`) }
|
||||
input.on('line', (line) => {
|
||||
const frame = JSON.parse(line) as {
|
||||
id: string | number
|
||||
method?: string
|
||||
params?: Record<string, unknown>
|
||||
result?: unknown
|
||||
error?: unknown
|
||||
}
|
||||
if (frame.method === undefined) {
|
||||
const original = pending.get(String(frame.id))
|
||||
if (original !== undefined) {
|
||||
pending.delete(String(frame.id))
|
||||
write({ id: original, result: { answer: frame.result ?? null, error: frame.error ?? null } })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (frame.method === 'initialize') {
|
||||
write({ id: frame.id, result: { serverInfo: { name: 'question-test', version: JSON.stringify(frame.params?.capabilities ?? null) } } })
|
||||
} else if (frame.method === 'test/question') {
|
||||
const id = `host-${String(++serial)}`
|
||||
pending.set(id, frame.id)
|
||||
write({ id, method: frame.params?.method ?? 'user-question/ask', params: {
|
||||
requestId: typeof frame.params?.requestId === 'string' ? frame.params.requestId : id, sessionId: 'main',
|
||||
questions: frame.params?.questions ?? [{ id: 'format', question: 'Which format?' }],
|
||||
} })
|
||||
} else if (frame.method === 'test/cancel') {
|
||||
write({ method: 'user-question.cancelled', params: frame.params })
|
||||
write({ id: frame.id, result: {} })
|
||||
} else if (frame.method === 'test/disconnect') {
|
||||
process.exit(0)
|
||||
} else if (frame.method === 'shutdown') {
|
||||
write({ id: frame.id, result: {} })
|
||||
input.close()
|
||||
process.stdin.destroy()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { expect, it, onTestFinished } from 'vitest'
|
||||
import type { HarnessClientOptions } from '../src/types.ts'
|
||||
import { createProcessHarnessClient } from '../src/client.ts'
|
||||
|
||||
async function client(onUserQuestion?: HarnessClientOptions['onUserQuestion'], explicitStart = true) {
|
||||
const result = createProcessHarnessClient({
|
||||
command: process.execPath,
|
||||
args: [fileURLToPath(new URL('./fixtures/user-questions.ts', import.meta.url))],
|
||||
environment: () => ({ ...process.env }),
|
||||
description: 'question protocol fixture', initializeTimeoutMs: 5000, requestTimeoutMs: 5000,
|
||||
})
|
||||
onTestFinished(() => result.close())
|
||||
if (onUserQuestion !== undefined) result.options.onUserQuestion = onUserQuestion
|
||||
if (explicitStart) result.start()
|
||||
const initialized = await result.initialize({ cwd: process.cwd(), provider: 'mock', model: 'mock' })
|
||||
const capabilities: unknown = JSON.parse(initialized.serverInfo.version)
|
||||
return { client: result, capabilities }
|
||||
}
|
||||
|
||||
it.each([false, true])('advertises a callback and answers a runtime request with explicit start: %s', async (explicitStart) => {
|
||||
const answer = { answers: [{ id: 'format', selected: ['CSV'] }] }
|
||||
const seen: string[] = []
|
||||
const f = await client(async (request, { signal }) => {
|
||||
seen.push(request.sessionId)
|
||||
expect(signal.aborted).toBe(false)
|
||||
return answer
|
||||
}, explicitStart)
|
||||
expect(f.capabilities).toEqual({ userQuestions: true })
|
||||
await expect(f.client.request('test/question', {})).resolves.toEqual({ answer, error: null })
|
||||
expect(seen).toEqual(['main'])
|
||||
})
|
||||
|
||||
it('does not advertise a missing callback and reports an unexpected question instead of hanging', async () => {
|
||||
const f = await client()
|
||||
expect(f.capabilities).toBeNull()
|
||||
await expect(f.client.request('test/question', {})).resolves.toMatchObject({ error: { message: expect.stringContaining('onUserQuestion') as string } })
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ params: { method: 'unexpected/request' }, message: 'unknown runtime request' },
|
||||
{ params: { questions: [{ question: 'Missing identity' }] }, message: 'requires requestId, sessionId, and questions' },
|
||||
])('rejects unsupported or malformed runtime requests: $message', async ({ params, message }) => {
|
||||
const f = await client(async () => { throw new Error('invalid requests must not reach the host') })
|
||||
await expect(f.client.request('test/question', params)).resolves.toMatchObject({
|
||||
error: { message: expect.stringContaining(message) as string },
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a duplicate question identity without cancelling the original host callback', async () => {
|
||||
const entered = Promise.withResolvers<AbortSignal>()
|
||||
const answer = { answers: [{ id: 'format', selected: ['CSV'] }] }
|
||||
const result = Promise.withResolvers<typeof answer>()
|
||||
const f = await client(async (_request, { signal }) => {
|
||||
entered.resolve(signal)
|
||||
return result.promise
|
||||
})
|
||||
const original = f.client.request('test/question', { requestId: 'shared' })
|
||||
const signal = await entered.promise
|
||||
await expect(f.client.request('test/question', { requestId: 'shared' })).resolves.toMatchObject({
|
||||
error: { message: expect.stringContaining('duplicate') as string },
|
||||
})
|
||||
expect(signal.aborted).toBe(false)
|
||||
result.resolve(answer)
|
||||
await expect(original).resolves.toEqual({ answer, error: null })
|
||||
})
|
||||
|
||||
it('notifies a callback of cancellation and ignores its later answer', async () => {
|
||||
const entered = Promise.withResolvers<AbortSignal>()
|
||||
const lateAnswer = Promise.withResolvers<{ answers: { id: string; selected: string[] }[] }>()
|
||||
const f = await client(async (_request, { signal }) => {
|
||||
entered.resolve(signal)
|
||||
return lateAnswer.promise
|
||||
})
|
||||
const pending = f.client.request('test/question', { requestId: 'cancel-me' })
|
||||
const signal = await entered.promise
|
||||
await f.client.request('test/cancel', { requestId: 'cancel-me', sessionId: 'main' })
|
||||
expect(signal.aborted).toBe(true)
|
||||
await expect(pending).resolves.toMatchObject({ answer: null, error: { message: expect.stringContaining('cancelled') as string } })
|
||||
lateAnswer.resolve({ answers: [{ id: 'format', selected: ['late'] }] })
|
||||
await f.client.close()
|
||||
})
|
||||
|
||||
it.each(['close', 'disconnect'] as const)('aborts host work when the client or runtime %ss', async (kind) => {
|
||||
const entered = Promise.withResolvers<AbortSignal>()
|
||||
const stopped = Promise.withResolvers<undefined>()
|
||||
const f = await client(async (_request, { signal }) => {
|
||||
entered.resolve(signal)
|
||||
return new Promise((_resolve, reject) => {
|
||||
signal.addEventListener('abort', () => { stopped.resolve(undefined); reject(new Error('question cancelled')) }, { once: true })
|
||||
})
|
||||
})
|
||||
const pending = f.client.request('test/question', {}).catch(() => undefined)
|
||||
const signal = await entered.promise
|
||||
if (kind === 'close') await f.client.close()
|
||||
else await f.client.request('test/disconnect', {}).catch(() => undefined)
|
||||
await stopped.promise
|
||||
expect(signal.aborted).toBe(true)
|
||||
await pending
|
||||
})
|
||||
@@ -21,8 +21,11 @@
|
||||
en: be53c71505c0d1b4
|
||||
zh: 6210dee577715539
|
||||
/deepseek-ai-dsh-sdk-protocol/use-this-package/the-sdk-methods:
|
||||
en: 8057053746fc2755
|
||||
zh: 36d5989488c1a351
|
||||
en: 89ec24feb09e173c
|
||||
zh: 4072cfc63b5e25e4
|
||||
/deepseek-ai-dsh-sdk-protocol/use-this-package/native-user-questions:
|
||||
en: 9dcd999ae8c66b71
|
||||
zh: 174c5fe0cbb0a9e4
|
||||
/deepseek-ai-dsh-sdk-protocol/use-this-package/payload-semantics:
|
||||
en: 3cf5d4ffbaf37f69
|
||||
zh: 23c4450e31c91d67
|
||||
@@ -48,8 +51,8 @@
|
||||
en: ca75c51c89c2c9b0
|
||||
zh: 696d26adaf4eda6d
|
||||
/deepseek-ai-dsh-sdk-protocol/known-limitations-and-deferred-work:
|
||||
en: c83a6b621a8f8793
|
||||
zh: 32accb03ee0b8ac8
|
||||
en: 7146ace77de7e19c
|
||||
zh: 5eb747177357bb54
|
||||
/deepseek-ai-dsh-sdk-protocol/known-limitations-and-deferred-work/dev-note:
|
||||
en: 951fa06249790ac1
|
||||
zh: da66e7f53df3ab00
|
||||
@@ -33,7 +33,7 @@ Wire one JSON-RPC 2.0 message per `\n`-terminated line over byte streams you own
|
||||
|
||||
### The SDK methods
|
||||
|
||||
Both wire ends share one method set: seven client-to-server requests and four server-to-client notifications.
|
||||
Both wire ends share one method set: seven client-to-server requests, one server-to-client request, and five server-to-client notifications.
|
||||
|
||||
| Direction | Method | Payload types |
|
||||
|---|---|---|
|
||||
@@ -44,17 +44,26 @@ Both wire ends share one method set: seven client-to-server requests and four se
|
||||
| client→server | `session/working-directory/get` | `SessionWorkingDirectoryParams` → `SessionWorkingDirectoryResult` |
|
||||
| client→server | `session/working-directory/set` | `SessionWorkingDirectorySetParams` → `SessionWorkingDirectoryResult` |
|
||||
| client→server | `shutdown` | no params → `{}` |
|
||||
| server→client | `user-question/ask` | `SdkUserQuestionRequest` → `SdkUserQuestionAnswer` |
|
||||
| server→client | `user-question.cancelled` | `SdkUserQuestionCancelled` |
|
||||
| server→client | `session.event` | `SessionEventNotification` (every session in the runtime, unfiltered) |
|
||||
| server→client | `session.status` | `SessionStatusNotification` (whole-agent `running`/`idle` transition) |
|
||||
| server→client | `subagent.started` | `SubagentStartedNotification` |
|
||||
| server→client | `subagent.finished` | `SubagentFinishedNotification` (in-process runs only) |
|
||||
|
||||
`HarnessSdkRequestMap` and `HarnessSdkNotificationMap` index these shapes by method name; the package root exports them together with the transport.
|
||||
`HarnessSdkRequestMap`, `HarnessSdkHostRequestMap`, and `HarnessSdkNotificationMap` index these shapes by method name; the package root exports them together with the transport.
|
||||
|
||||
`session/wait` accepts `{ sessionId }` for an existing SDK-owned session and returns `{}` after the root stays idle and progressing managed descendant work finishes. Idle descendants with parked input remain resident without blocking this wait. Unknown ids and live Agent failures without a newer committed terminal are rejected. Notifications committed during the wait precede its response on the same transport; raw `session.status` transitions retain their whole-agent meaning.
|
||||
|
||||
`session/resume` accepts a non-empty persisted `{ sessionId }` and returns `{}` after restoring that exact session. It requires initialization and the original persistence store, sends no new input, and rejects missing, unreadable, incompatible, or occupied sessions without creating a replacement. Historical events remain in the stored log; notifications report newly committed events.
|
||||
|
||||
<a id="native-user-questions"></a>
|
||||
### Native user questions
|
||||
|
||||
An answering host sets `initialize.capabilities.userQuestions: true`. The runtime sends `user-question/ask` with `{ requestId, sessionId, questions }`; questions retain their original ids, options, detail, and presentation intent. The host returns `{ answers: [{ id, selected, custom? }] }`, naming every question id exactly once. Invalid batches fail with `BAD_ANSWER`; host or transport failures fail with `PROVIDER_ERROR`. The SDK does not synthesize answers.
|
||||
|
||||
`user-question.cancelled` identifies the request that stopped waiting. The host cancels its own work and discards the question; the runtime abandons its pending RPC and ignores late responses. Without the advertised capability, existing in-process answerers remain responsible and an absent answerer reports `NO_PROVIDER`. This flow requires no GUI.
|
||||
|
||||
### Payload semantics
|
||||
|
||||
`SessionPromptResult.messageId` identifies the queued user message; it does not identify a later assistant message, turn ending, or prompt result. `SdkPromptContentBlock` accepts ordinary durable content plus `SdkEncodedImageBlock { type: "image", data, mimeType }`; the server converts encoded images to durable references before enqueue. `InitializeParams.reasoningEffort` is an optional non-empty adapter-owned identifier for the selected provider/model route; omission preserves that model's default. `InitializeParams.maxTokens` is an optional positive safe integer that caps each conversation-model output for SDK-created agents and their in-process descendants; omission lets the selected adapter's exact-model default apply. The server resolves the exact route during initialization and rejects `session/prompt` until that handshake succeeds, so a missing adapter, unavailable model, or unsupported effort cannot fall back to constructor defaults. `SubagentFinishedNotification.lastAssistantMessage` carries the child's last non-empty assistant message, or its accumulated assistant text when no such message exists; the field is absent when the child produced neither. `serverInfo.name` stays the wire-stable `deepseek-harness-sdk-runtime`. Notification payloads depend on `SessionEvent` (`dsh-session`), `ContentBlock` (`dsh-llm`), and `SubagentStopReason` (`dsh-subagent`), so the session vocabulary is part of the wire contract.
|
||||
@@ -119,7 +128,7 @@ These limits define what the protocol does not cover or promise. They are curren
|
||||
|
||||
- **No protocol-version negotiation** — the handshake carries only `serverInfo.version` (`0.0.1`, unvalidated by clients); pre-release stance, no compatibility promise.
|
||||
- **No cancel or session-close methods** — a client abandons a turn by closing the runtime process; see the [JSON-RPC serving plugin](../server/README.md).
|
||||
- **Server→client requests are a dead capability** — the transport supports them, but the server never sends one; the Python SDK's responder surface exists for future approval flows.
|
||||
- **Question waits are process-local** — question request ids are not durable handles for replying after runtime restart. Session history retains completed tool results through its ordinary persistence path.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
@@ -33,7 +33,7 @@ kind: "package-library"
|
||||
|
||||
### SDK 方法
|
||||
|
||||
两个协议端共享同一套方法:七个客户端到服务端请求与四个服务端到客户端通知。
|
||||
两个协议端共享同一套方法:七个客户端到服务端请求、一个服务端到客户端请求与五个服务端到客户端通知。
|
||||
|
||||
| 方向 | 方法 | 载荷类型 |
|
||||
|---|---|---|
|
||||
@@ -44,17 +44,26 @@ kind: "package-library"
|
||||
| 客户端→服务端 | `session/working-directory/get` | `SessionWorkingDirectoryParams` → `SessionWorkingDirectoryResult` |
|
||||
| 客户端→服务端 | `session/working-directory/set` | `SessionWorkingDirectorySetParams` → `SessionWorkingDirectoryResult` |
|
||||
| client→server | `shutdown` | 无参数 → `{}` |
|
||||
| server→client | `user-question/ask` | `SdkUserQuestionRequest` → `SdkUserQuestionAnswer` |
|
||||
| server→client | `user-question.cancelled` | `SdkUserQuestionCancelled` |
|
||||
| server→client | `session.event` | `SessionEventNotification`(运行时内每个会话,不过滤) |
|
||||
| server→client | `session.status` | `SessionStatusNotification`(整个 agent(智能体)的 `running`/`idle` 转换) |
|
||||
| server→client | `subagent.started` | `SubagentStartedNotification` |
|
||||
| server→client | `subagent.finished` | `SubagentFinishedNotification`(仅进程内运行) |
|
||||
|
||||
`HarnessSdkRequestMap` 与 `HarnessSdkNotificationMap` 按方法名索引这些结构;包根与传输一起导出它们。
|
||||
`HarnessSdkRequestMap`、`HarnessSdkHostRequestMap` 与 `HarnessSdkNotificationMap` 按方法名索引这些结构;包根与传输一起导出它们。
|
||||
|
||||
`session/wait` 接受已有 SDK 会话的 `{ sessionId }`,在根 Agent 持续空闲且活跃的受管理后代工作结束后返回 `{}`。带有停放输入的空闲后代保持驻留,不阻塞这次等待。未知 id,以及没有后续终止事件记录的 Agent 运行时错误,都会使请求失败。等待期间提交的通知在同一传输上先于响应到达;原始 `session.status` 转换仍表示整个 Agent 的状态。
|
||||
|
||||
`session/resume` 接受非空的持久化 `{ sessionId }`,恢复该会话后返回 `{}`。它要求先完成初始化并使用原持久化存储,不发送新输入;会话缺失、不可读、不兼容或被占用时,请求失败且不创建替代会话。历史事件保留在存储日志中,通知只报告新提交的事件。
|
||||
|
||||
<a id="native-user-questions"></a>
|
||||
### 原生用户问答
|
||||
|
||||
提供回答能力的宿主设置 `initialize.capabilities.userQuestions: true`。运行时发送带有 `{ requestId, sessionId, questions }` 的 `user-question/ask`;问题保留原始 id、选项、detail 与展示意图。宿主返回 `{ answers: [{ id, selected, custom? }] }`,每个问题 id 必须恰好出现一次。无效答案批次返回 `BAD_ANSWER`,宿主或传输失败返回 `PROVIDER_ERROR`。SDK 不生成替代答案。
|
||||
|
||||
`user-question.cancelled` 标识已停止等待的请求。宿主取消自己的工作并丢弃问题;运行时放弃对应的待决 RPC,忽略晚到响应。未声明该能力时,仍由现有进程内回答方处理;缺少回答方则报告 `NO_PROVIDER`。此流程不要求 GUI。
|
||||
|
||||
### 载荷语义
|
||||
|
||||
`SessionPromptResult.messageId` 标识已排队的用户消息;它不标识后续的助手消息、轮次结束或提示词结果。`SdkPromptContentBlock` 接受普通持久内容以及 `SdkEncodedImageBlock { type: "image", data, mimeType }`;服务器在入队前把编码图像转换为持久引用。`InitializeParams.reasoningEffort` 是所选提供方/模型路由可选的非空适配器自有标识符;省略时保留该模型的默认值。`InitializeParams.maxTokens` 是可选的正安全整数,用于限制 SDK 创建的 agent 及其进程内后代的每次对话模型输出;省略时应用所选适配器的确切模型默认值。服务器会在初始化期间解析确切路由,并在握手成功前拒绝 `session/prompt`,因此缺少适配器、模型不可用或推理强度不受支持时,不会回退到构造期默认值。`SubagentFinishedNotification.lastAssistantMessage` 携带子 agent 最后一条非空 assistant 消息;若不存在这类消息,则携带其累积的 assistant 文本;子 agent 两种输出均未产生时,该字段缺省。`serverInfo.name` 的协议值固定为 `deepseek-harness-sdk-runtime`。通知载荷依赖 `SessionEvent`(`dsh-session`)、`ContentBlock`(`dsh-llm`)与 `SubagentStopReason`(`dsh-subagent`),因此会话词汇是协议格式约定的一部分。
|
||||
@@ -119,7 +128,7 @@ kind: "package-library"
|
||||
|
||||
- **无协议版本协商**——握手只携带 `serverInfo.version`(`0.0.1`,客户端不校验);处于预发布阶段,无兼容承诺。
|
||||
- **无取消与会话关闭方法**——客户端放弃轮次的方式是关闭运行时进程;见 [JSON-RPC 服务插件](../server/README.zh.md)。
|
||||
- **server→client 请求是未使用的能力**——传输层支持,但服务器从不发送;Python SDK 的应答接口为未来审批流程预留。
|
||||
- **问题等待仅属于当前进程**——问题请求 id 不能在运行时重启后继续用于回答。已完成的工具结果通过普通持久化路径保留在 Session 历史中。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
@@ -29,12 +29,14 @@
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/cordis": "workspace:~"
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-user-questions": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/cordis": "workspace:~"
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-user-questions": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,15 @@ export type { JsonRpcTransportPeer } from './transport.ts'
|
||||
export type {
|
||||
HarnessSdkNotificationMap,
|
||||
HarnessSdkRequestMap,
|
||||
HarnessSdkHostRequestMap,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
SdkEncodedImageBlock,
|
||||
SdkPromptContentBlock,
|
||||
SdkRunStatus,
|
||||
SdkUserQuestionRequest,
|
||||
SdkUserQuestionAnswer,
|
||||
SdkUserQuestionCancelled,
|
||||
SessionEventNotification,
|
||||
SessionStatusNotification,
|
||||
SessionPromptParams,
|
||||
|
||||
@@ -36,10 +36,11 @@ export interface JsonRpcTransportPeer {
|
||||
* Send a request and await its response.
|
||||
* @param method - the JSON-RPC method name.
|
||||
* @param params - the request parameters object.
|
||||
* @param signal - optional cancellation; removes the pending request and ignores late responses.
|
||||
* @returns the result; rejects with {@link JsonRpcResponseError} on an error
|
||||
* response, and with a plain `Error` on a write failure or closure.
|
||||
*/
|
||||
request(method: string, params: object): Promise<unknown>
|
||||
request(method: string, params: object, signal?: AbortSignal): Promise<unknown>
|
||||
/**
|
||||
* Send a notification; omitted params produce no `params` member.
|
||||
* @param method - the JSON-RPC method name.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import type { ContentBlock, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { SubagentStopReason } from '@deepseek-ai/dsh-subagent'
|
||||
import type { AskUserQuestionAnswer, AskUserQuestionItem } from '@deepseek-ai/dsh-user-questions/types'
|
||||
|
||||
/** Parameters for the process-wide SDK handshake. */
|
||||
export interface InitializeParams {
|
||||
@@ -24,6 +25,34 @@ export interface InitializeParams {
|
||||
reasoningEffort?: ReasoningEffortId
|
||||
/** Optional positive output-token cap inherited by SDK-created agents and their in-process descendants. */
|
||||
maxTokens?: number
|
||||
/** Host-provided capabilities; omission retains native in-process answerers. */
|
||||
capabilities?: { userQuestions?: boolean }
|
||||
}
|
||||
|
||||
/** Runtime-to-host request for one SDK-owned Session's structured user input. */
|
||||
export interface SdkUserQuestionRequest {
|
||||
/** Correlates cancellation with this request independently of the JSON-RPC id. */
|
||||
requestId: string
|
||||
/** Session whose tool or approval is waiting. */
|
||||
sessionId: string
|
||||
/** Original questions, including any plan-review detail and presentation intent. */
|
||||
questions: AskUserQuestionItem[]
|
||||
}
|
||||
|
||||
/** Host answers, one item per original question id. */
|
||||
export type SdkUserQuestionAnswer = AskUserQuestionAnswer
|
||||
|
||||
/** Runtime cancellation of a pending host question. Late answers are ignored. */
|
||||
export interface SdkUserQuestionCancelled {
|
||||
/** Identity from the original question request. */
|
||||
requestId: string
|
||||
/** Session that stopped waiting. */
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
/** Runtime-to-host requests, handled independently of outbound Session requests. */
|
||||
export interface HarnessSdkHostRequestMap {
|
||||
'user-question/ask': { params: SdkUserQuestionRequest; result: SdkUserQuestionAnswer }
|
||||
}
|
||||
|
||||
/** Wire-stable server identity returned by initialization. */
|
||||
@@ -135,6 +164,7 @@ export interface SubagentFinishedNotification {
|
||||
|
||||
/** Server-to-client notifications by JSON-RPC method name. */
|
||||
export interface HarnessSdkNotificationMap {
|
||||
'user-question.cancelled': SdkUserQuestionCancelled
|
||||
'session.event': SessionEventNotification
|
||||
'session.status': SessionStatusNotification
|
||||
'subagent.started': SubagentStartedNotification
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../subagent/subagent"
|
||||
},
|
||||
{
|
||||
"path": "../../interaction/user-questions"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package:
|
||||
en: e136ff3a0b42cc22
|
||||
zh: f6c4df5bbafa335f
|
||||
en: c39393999db960c0
|
||||
zh: 1d6a8460684ae97a
|
||||
/deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package/wiring:
|
||||
en: 47a33e9397e36e15
|
||||
zh: e6d173820f9ea6e5
|
||||
@@ -27,8 +27,11 @@
|
||||
en: 784b68b5a498f2d5
|
||||
zh: 2c3c5a1490845179
|
||||
/deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package/what-sdk-clients-can-do:
|
||||
en: b4351b6b407fe65f
|
||||
zh: 52d711d554d2ad33
|
||||
en: 3e657fa4c9586454
|
||||
zh: 26d572f91e124cc5
|
||||
/deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package/native-user-questions:
|
||||
en: 5100c311c8476f93
|
||||
zh: 60d4bc3165731246
|
||||
/deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package/shutdown-and-exit:
|
||||
en: 08c3858f53090f39
|
||||
zh: dfae960e11515b06
|
||||
@@ -39,8 +42,8 @@
|
||||
en: e83e01184c1a45ee
|
||||
zh: d54ed126d31ee6f1
|
||||
/deepseek-ai-dsh-sdk-jsonrpc-server/understand-the-implementation/source-map:
|
||||
en: 9ef78eca4d95d97c
|
||||
zh: 45ff05f4d6c1cd0b
|
||||
en: 783bd2b6628c0680
|
||||
zh: ca5b6b730893ddec
|
||||
/deepseek-ai-dsh-sdk-jsonrpc-server/understand-the-implementation/request-flow:
|
||||
en: 2658804b2f90b135
|
||||
zh: 085aa00c8f0bbbcd
|
||||
|
||||
@@ -27,6 +27,8 @@ English | [中文](README.zh.md)
|
||||
|
||||
`session/resume` accepts a persisted `{ sessionId }` after initialization and returns `{}` once its Agent is ready. It loads history through `agents.resume()`, repairs an interrupted final turn, and preserves the stored origin and effective directory while applying the initialized model route. It sends no new input. Missing, unreadable, incompatible, or occupied sessions reject without creating a replacement. Prompts for an identity currently being restored wait for that restoration; shutdown drains pending restoration and releases its Agent. Historical events are retained in the log rather than streamed again as new notifications.
|
||||
|
||||
The plugin publishes the `sdkSessionActivation` readiness marker once its reminder activation handler is installed; an SDK composition injects this marker into Schedule instead of the Web Session controller. Scheduled delivery uses this same SDK Session owner. Due startup reminders wait for successful SDK initialization, then resume with its provider, model, reasoning effort, and output limit. The client can resume a reminder-restored Session once; concurrent reminder and client restoration share one handle. Shutdown releases reminders waiting for initialization and drains accepted restorations.
|
||||
|
||||
`session/working-directory/get` accepts `{ sessionId }`; `session/working-directory/set` accepts `{ sessionId, path }`. Both return `{ cwd }` and create an unknown Session without starting a model turn. Reads recover missing directories through the working-directory service; changes preserve origin metadata and permissions, and reach the model through logged user context.
|
||||
|
||||
Mount this plugin when a runtime must serve SDK clients: add it to a `cordis.yml` that composes the agent service, boot the runtime, and clients connect over stdio. The common path is explicit — the plugin needs the `agents` service; every other capability comes from the surrounding tree.
|
||||
@@ -53,6 +55,11 @@ Stdout carries only JSON-RPC frames, so clients can parse every byte; diagnostic
|
||||
|
||||
`session/wait` observes an existing SDK-owned session through managed descendant completion and any resulting root turns. It rejects unknown or retired roots, never creates a session, and responds after the root stays idle across the descendant check. Idle children with parked inbox messages remain resident but do not delay this response; later waking input can resume them. Session notifications emitted before completion precede the response on the stdio transport. A live Agent failure remains associated with that exact session until a later committed `turn/end` accounts for the outcome; otherwise the wait rejects, including when the failure preceded the wait request. Errors already represented by a terminal remain in Session events, and later successful activity clears an earlier live failure.
|
||||
|
||||
<a id="native-user-questions"></a>
|
||||
### Native user questions
|
||||
|
||||
An initialized client advertising `capabilities.userQuestions` handles questions for this server's exact owned root Agents. The server forwards the shared `user-questions/request` event as a `user-question/ask` RPC and validates the answer before a tool or approval consumes it. Foreign Agents and clients without that capability retain their existing native answerers. Tool cancellation and server shutdown abort pending requests and notify the host; connection failure returns an error. The [protocol](../protocol/README.md#native-user-questions) defines the wire shapes and failure codes.
|
||||
|
||||
### Shutdown and exit
|
||||
|
||||
The plugin answers `shutdown`, flushes the response, disposes the root context so SDK-owned agents, subscriptions, and persistence reach quiescence, then exits 0. EOF and signal exits belong to the app bin, which also disposes the root context. Unloading only this plugin stops serving without exiting the process.
|
||||
@@ -76,6 +83,7 @@ The plugin is a thin presentation adapter: [`HarnessSdkJsonRpcServer`](src/serve
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| [`src/index.ts`](src/index.ts) | Plugin entry: `Config` schema, stdio wiring, request dispatch, shared shutdown/exit task |
|
||||
| [`src/user-questions.ts`](src/user-questions.ts) | Native question adapter: host forwarding, answer validation, cancellation |
|
||||
| [`src/server.ts`](src/server.ts) | `HarnessSdkJsonRpcServer`: protocol methods, per-session agent creation, lifecycle subscriptions, teardown |
|
||||
|
||||
### Request flow
|
||||
|
||||
@@ -27,6 +27,8 @@ kind: "package-reference"
|
||||
|
||||
`session/resume` 在初始化后接受持久化的 `{ sessionId }`,待其 Agent 就绪后返回 `{}`。它通过 `agents.resume()` 加载历史、修复最后一个被中断的轮次,并保留存储的起始目录和当前有效目录,同时应用初始化时选定的模型路由。它不发送新输入。会话缺失、不可读、不兼容或被占用时,请求失败且不创建替代会话。对正在恢复的标识发送提示词时,会等待恢复完成;shutdown 会等待进行中的恢复并释放其 Agent。历史事件保留在日志中,不会作为新通知重新发送。
|
||||
|
||||
插件在注册提醒激活处理方后发布 `sdkSessionActivation` 就绪标记;SDK 组合让 Schedule 依赖此标记,而非 Web Session controller。定时提醒也通过同一个 SDK Session 所有者投递。启动时已到期的提醒等待 SDK 成功初始化,再使用初始化指定的提供方、模型、推理强度和输出上限恢复会话。客户端可以首次接管提醒恢复的 Session;并发的提醒与客户端恢复共用一个 handle。关闭时释放等待初始化的提醒,并等待已经接纳的恢复完成。
|
||||
|
||||
`session/working-directory/get` 接收 `{ sessionId }`;`session/working-directory/set` 接收 `{ sessionId, path }`。两者均返回 `{ cwd }`,并可创建未知 Session,而不启动模型轮次。读取通过工作目录服务恢复缺失目录;修改保留起始目录元数据与权限,并通过已记录的用户上下文告知模型。
|
||||
|
||||
当运行时必须服务 SDK 客户端时挂载本插件:把它加入组合了 agent 服务的 `cordis.yml`,启动运行时,客户端即可通过 stdio 连接。常用路径是显式的——插件需要 `agents` 服务;其余每个能力都来自外围插件树。
|
||||
@@ -53,6 +55,11 @@ Stdout 只承载 JSON-RPC 帧,客户端可以逐字节解析;诊断信息应
|
||||
|
||||
`session/wait` 观察已有 SDK 会话,直到受管理后代及其触发的根 Agent 后续轮次完成。它拒绝未知或已释放的根 Agent,不会创建会话,并在后代检查期间根 Agent 持续空闲后响应。收件箱消息处于停放状态的空闲子级继续驻留,但不延迟此响应;后续唤醒输入可使其继续运行。完成前发出的会话通知在 stdio 传输上先于响应到达。Agent 的运行时错误会保留在对应的确切会话中,直到后续提交的 `turn/end` 记录结果;否则等待请求会失败,即使错误发生在等待请求之前。终止事件已经记录的错误仍通过会话事件表达,后续成功的活动会清除较早的运行时错误。
|
||||
|
||||
<a id="native-user-questions"></a>
|
||||
### 原生用户问答
|
||||
|
||||
已初始化且声明 `capabilities.userQuestions` 的客户端负责此服务器所拥有的确切根 Agent 的问题。服务器把共享的 `user-questions/request` 事件转为 `user-question/ask` RPC,并在工具或审批消费答案前校验。其他 Agent 和未声明此能力的客户端仍使用原生回答方。工具取消和服务器关闭会中止待决请求并通知宿主;连接失败则返回错误。[协议](../protocol/README.zh.md#native-user-questions)定义载荷结构和错误码。
|
||||
|
||||
### 关闭与退出
|
||||
|
||||
插件应答 `shutdown`,刷新响应并 dispose 根上下文,使 SDK 持有的 agent、订阅与持久化达到完全停稳,然后以 0 退出。EOF 与信号退出归 app bin 负责,后者也会 dispose 根上下文。仅卸载此插件会停止服务,但不会退出进程。
|
||||
@@ -76,6 +83,7 @@ Stdout 只承载 JSON-RPC 帧,客户端可以逐字节解析;诊断信息应
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| [`src/index.ts`](src/index.ts) | 插件入口:`Config` schema、stdio 接线、请求分发、共享关闭与退出任务 |
|
||||
| [`src/user-questions.ts`](src/user-questions.ts) | 原生问答适配器:宿主转发、答案校验、取消 |
|
||||
| [`src/server.ts`](src/server.ts) | `HarnessSdkJsonRpcServer`:协议方法、逐会话 agent 创建、生命周期订阅、清理 |
|
||||
|
||||
### 请求流程
|
||||
|
||||
@@ -36,11 +36,13 @@
|
||||
"@deepseek-ai/dsh-attachment": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
|
||||
"@deepseek-ai/dsh-schedule": "workspace:*",
|
||||
"@deepseek-ai/dsh-scope": "workspace:*",
|
||||
"@deepseek-ai/dsh-sdk-protocol": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-working-directory": "workspace:*"
|
||||
"@deepseek-ai/dsh-working-directory": "workspace:*",
|
||||
"@deepseek-ai/dsh-user-questions": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
@@ -51,12 +53,14 @@
|
||||
"@deepseek-ai/dsh-attachment": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
|
||||
"@deepseek-ai/dsh-schedule": "workspace:*",
|
||||
"@deepseek-ai/dsh-scope": "workspace:*",
|
||||
"@deepseek-ai/dsh-sdk-protocol": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-working-directory": "workspace:*"
|
||||
"@deepseek-ai/dsh-working-directory": "workspace:*",
|
||||
"@deepseek-ai/dsh-user-questions": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,9 @@ export function apply(ctx: Context, config: JsonRpcConfig): void {
|
||||
const server = new HarnessSdkJsonRpcServer(ctx, transport, {
|
||||
maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,
|
||||
})
|
||||
// Compositions can order Schedule after its activation handler is installed.
|
||||
// Like the app startup marker, this carries readiness, not another control API.
|
||||
ctx.provide('sdkSessionActivation', { ready: true })
|
||||
|
||||
// Share one exit task so racing shutdown requests cannot dispose the root or
|
||||
// exit the process more than once.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { resolve } from 'node:path'
|
||||
import { SdkUserQuestionBridge } from './user-questions.ts'
|
||||
import { brandString } from '@deepseek-ai/dsh-brand'
|
||||
import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
|
||||
import { admitEncodedImages, type EncodedImageAttachment, type ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
@@ -14,6 +15,7 @@ import { createUserMessage, ReasoningEffortId, type ContentBlock, type LlmRuntim
|
||||
import { carrierKeyOf, type Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import type { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-working-directory'
|
||||
import type {} from '@deepseek-ai/dsh-schedule'
|
||||
import type SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
@@ -93,6 +95,10 @@ export class HarnessSdkJsonRpcServer {
|
||||
private shutdownTask: Promise<Record<string, never>> | undefined
|
||||
private shuttingDown = false
|
||||
private initialized = false
|
||||
private readonly userQuestions: SdkUserQuestionBridge
|
||||
private readonly initialization = Promise.withResolvers<void>()
|
||||
/** Restored by a reminder and available for the client's first resume. */
|
||||
private readonly scheduledSessions = new Map<string, Promise<SessionRecord>>()
|
||||
|
||||
constructor(
|
||||
private readonly ctx: Context,
|
||||
@@ -100,6 +106,28 @@ export class HarnessSdkJsonRpcServer {
|
||||
private readonly options: HarnessSdkJsonRpcServerOptions = {},
|
||||
) {
|
||||
const serverOptions = this.options
|
||||
this.userQuestions = new SdkUserQuestionBridge(ctx, transport, agent =>
|
||||
this.sessions.get(agent.session.id)?.handle.agent === agent)
|
||||
this.disposers.push(ctx.on('schedule/resolve-agent', async (sessionId) => {
|
||||
// Startup delivery must wait for the SDK model route and output limit.
|
||||
await this.initialization.promise
|
||||
if (this.shuttingDown) throw new Error('SDK server is shutting down')
|
||||
const existing = this.sessions.get(sessionId)
|
||||
const pending = this.sessionCreations.get(sessionId)
|
||||
if (existing !== undefined) {
|
||||
this.assertLiveAgent(existing, sessionId)
|
||||
return existing.handle.agent
|
||||
}
|
||||
if (pending !== undefined) return (await pending).handle.agent
|
||||
const restoration = this.openSession(sessionId, true)
|
||||
this.scheduledSessions.set(sessionId, restoration)
|
||||
try {
|
||||
return (await restoration).handle.agent
|
||||
} catch (error) {
|
||||
this.scheduledSessions.delete(sessionId)
|
||||
throw error
|
||||
}
|
||||
}))
|
||||
this.disposers.push(ctx.on('session/event', (session, event) => {
|
||||
const record = this.sessions.get(session.id)
|
||||
if (event.type === 'turn/end' && record?.handle.agent.session === session
|
||||
@@ -184,7 +212,9 @@ export class HarnessSdkJsonRpcServer {
|
||||
this.model = model
|
||||
this.reasoningEffort = reasoningEffort
|
||||
this.maxTokens = params.maxTokens
|
||||
this.userQuestions.enabled = params.capabilities?.userQuestions === true
|
||||
this.initialized = true
|
||||
this.initialization.resolve()
|
||||
return { serverInfo: { name: 'deepseek-harness-sdk-runtime', version: '0.0.1' } }
|
||||
}
|
||||
|
||||
@@ -223,6 +253,13 @@ export class HarnessSdkJsonRpcServer {
|
||||
if (!this.initialized) throw new Error('SDK server is not initialized')
|
||||
if (this.shuttingDown) throw new Error('SDK server is shutting down')
|
||||
const sessionId = params.sessionId
|
||||
const restoration = this.scheduledSessions.get(sessionId)
|
||||
if (restoration !== undefined) {
|
||||
this.scheduledSessions.delete(sessionId)
|
||||
const record = await restoration
|
||||
this.assertLiveAgent(record, sessionId)
|
||||
return {}
|
||||
}
|
||||
if (this.sessions.has(sessionId) || this.sessionCreations.has(sessionId)) {
|
||||
throw new Error(`SDK session is already open or opening: ${sessionId}`)
|
||||
}
|
||||
@@ -302,9 +339,12 @@ export class HarnessSdkJsonRpcServer {
|
||||
|
||||
private async performShutdown(): Promise<Record<string, never>> {
|
||||
this.shuttingDown = true
|
||||
this.userQuestions.close()
|
||||
this.initialization.resolve()
|
||||
const pendingCreations = [...this.sessionCreations.values()]
|
||||
await Promise.allSettled(pendingCreations)
|
||||
this.sessionCreations.clear()
|
||||
this.scheduledSessions.clear()
|
||||
const records = [...this.sessions.values()]
|
||||
this.sessions.clear()
|
||||
const failures: unknown[] = []
|
||||
@@ -378,6 +418,7 @@ export class HarnessSdkJsonRpcServer {
|
||||
|
||||
private async getOrCreateSession(sessionId: string): Promise<SessionRecord> {
|
||||
if (this.shuttingDown) throw new Error('SDK server is shutting down')
|
||||
this.scheduledSessions.delete(sessionId)
|
||||
const existing = this.sessions.get(sessionId)
|
||||
if (existing) return existing
|
||||
const pending = this.sessionCreations.get(sessionId)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/** Native SDK adapter for the shared user-questions seam. */
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { UserQuestionError, type AskUserQuestionRequest } from '@deepseek-ai/dsh-user-questions'
|
||||
import type { JsonRpcTransportPeer, SdkUserQuestionAnswer, SdkUserQuestionRequest } from '@deepseek-ai/dsh-sdk-protocol'
|
||||
|
||||
/** Owns question forwarding and cancellation for one SDK transport. */
|
||||
export class SdkUserQuestionBridge {
|
||||
/** Enabled only after the host advertises an answerer at initialization. */
|
||||
enabled = false
|
||||
private readonly questionShutdown = new AbortController()
|
||||
private readonly unregister: () => void
|
||||
|
||||
constructor(
|
||||
private readonly ctx: Context,
|
||||
private readonly transport: JsonRpcTransportPeer,
|
||||
ownsAgent: (agent: Agent) => boolean,
|
||||
) {
|
||||
this.unregister = ctx.on('user-questions/request', async (request, next) => {
|
||||
const agent = request.agent
|
||||
if (!this.enabled || agent === undefined || !ownsAgent(agent)) return next()
|
||||
return this.ask(request, String(agent.session.id))
|
||||
})
|
||||
}
|
||||
|
||||
/** Stop admission and cancel outstanding questions before SDK Agents drain. */
|
||||
close(): void {
|
||||
this.enabled = false
|
||||
this.unregister()
|
||||
this.questionShutdown.abort(new UserQuestionError('SDK server is shutting down', 'ASK_ABORTED'))
|
||||
}
|
||||
|
||||
private async ask(request: AskUserQuestionRequest, sessionId: string): Promise<SdkUserQuestionAnswer> {
|
||||
const params: SdkUserQuestionRequest = { requestId: randomUUID(), sessionId, questions: request.questions }
|
||||
const signal = request.signal === undefined ? this.questionShutdown.signal
|
||||
: AbortSignal.any([request.signal, this.questionShutdown.signal])
|
||||
signal.throwIfAborted()
|
||||
const cancelled = (): void => {
|
||||
try {
|
||||
this.transport.notify('user-question.cancelled', { requestId: params.requestId, sessionId })
|
||||
} catch (error) {
|
||||
this.ctx.logger.warn(`SDK question cancellation could not reach the host: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
signal.addEventListener('abort', cancelled, { once: true })
|
||||
try {
|
||||
const result = await this.transport.request('user-question/ask', params, signal)
|
||||
return parseHostAnswer(result, request.questions.map(question => question.id))
|
||||
} catch (error) {
|
||||
if (error instanceof UserQuestionError) throw error
|
||||
throw new UserQuestionError(signal.aborted ? 'SDK user question was cancelled' : 'SDK host could not answer the user question',
|
||||
signal.aborted ? 'ASK_ABORTED' : 'PROVIDER_ERROR', { cause: error })
|
||||
} finally {
|
||||
signal.removeEventListener('abort', cancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Validate the untrusted host response before it enters a tool result or approval decision. */
|
||||
function parseHostAnswer(value: unknown, questionIds: readonly string[]): SdkUserQuestionAnswer {
|
||||
const badAnswer = (): never => { throw new UserQuestionError('SDK host must answer each question id exactly once with selected labels and optional custom text', 'BAD_ANSWER') }
|
||||
if (value === null || typeof value !== 'object' || !('answers' in value) || !Array.isArray(value.answers)) return badAnswer()
|
||||
const answers = value.answers.map((answer: unknown) => {
|
||||
if (answer === null || typeof answer !== 'object' || !('id' in answer) || typeof answer.id !== 'string'
|
||||
|| !('selected' in answer) || !Array.isArray(answer.selected) || !answer.selected.every((label: unknown) => typeof label === 'string')
|
||||
|| ('custom' in answer && typeof answer.custom !== 'string')) return badAnswer()
|
||||
return { id: answer.id, selected: answer.selected,
|
||||
...'custom' in answer ? { custom: answer.custom as string } : {} }
|
||||
})
|
||||
const ids = new Set(answers.map(answer => answer.id))
|
||||
if (ids.size !== answers.length || answers.length !== questionIds.length || !questionIds.every(id => ids.has(id))) return badAnswer()
|
||||
return { answers }
|
||||
}
|
||||
@@ -49,6 +49,73 @@ async function initialized(root: string) {
|
||||
}
|
||||
|
||||
describe('SDK persisted session resume', () => {
|
||||
it('restores due reminders through the SDK owner after initialization and shares client resume', async () => {
|
||||
const root = await storageRoot()
|
||||
const first = await initialized(root)
|
||||
await first.server.prompt({ sessionId: 'saved', contentBlocks: [{ type: 'text', text: 'original' }] })
|
||||
await first.server.wait({ sessionId: 'saved' })
|
||||
await first.server.shutdown()
|
||||
await first.ctx.fiber.dispose()
|
||||
|
||||
const { ctx, server, adapter } = await runtime(root)
|
||||
const fallback = vi.fn(async () => { throw new Error('must not activate through the Web controller') })
|
||||
const restoring = ctx.waterfall('schedule/resolve-agent', SessionId('saved'), fallback)
|
||||
expect(ctx.agents.list()).toHaveLength(0)
|
||||
const actualOpen = ctx.sessionPersistence.open.bind(ctx.sessionPersistence)
|
||||
const opened = Promise.withResolvers<undefined>()
|
||||
const gate = Promise.withResolvers<undefined>()
|
||||
const open = vi.spyOn(ctx.sessionPersistence, 'open').mockImplementation(async (...args) => {
|
||||
const handle = await actualOpen(...args)
|
||||
opened.resolve(undefined)
|
||||
await gate.promise
|
||||
return handle
|
||||
})
|
||||
try {
|
||||
await server.initialize({ cwd: tmpdir(), provider: 'mock', model: 'reminder-model', maxTokens: 1234 })
|
||||
await opened.promise
|
||||
const resuming = server.resume({ sessionId: 'saved' })
|
||||
const anotherReminder = ctx.waterfall('schedule/resolve-agent', SessionId('saved'), fallback)
|
||||
gate.resolve(undefined)
|
||||
const [agent, sameAgent] = await Promise.all([restoring, anotherReminder, resuming])
|
||||
expect(sameAgent).toBe(agent)
|
||||
expect(open).toHaveBeenCalledTimes(1)
|
||||
expect(fallback).not.toHaveBeenCalled()
|
||||
expect(agent.session.header.cwd).toBe(root)
|
||||
await server.prompt({ sessionId: 'saved', contentBlocks: [{ type: 'text', text: 'scheduled reminder' }] })
|
||||
await server.wait({ sessionId: 'saved' })
|
||||
expect(adapter.requests[0]).toMatchObject({ model: 'reminder-model', maxTokens: 1234 })
|
||||
await expect(server.resume({ sessionId: 'saved' })).rejects.toThrow('already open')
|
||||
await server.shutdown()
|
||||
expect(ctx.agents.list()).toHaveLength(0)
|
||||
} finally {
|
||||
gate.resolve(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
it('reuses an SDK-owned Session for reminders and releases failed reminder restoration for retry', async () => {
|
||||
const { ctx, server } = await initialized(await storageRoot())
|
||||
const fallback = vi.fn(async () => { throw new Error('unexpected fallback') })
|
||||
const restore = () => ctx.waterfall('schedule/resolve-agent', SessionId('saved'), fallback)
|
||||
await expect(restore()).rejects.toThrow()
|
||||
await server.prompt({ sessionId: 'saved', contentBlocks: [{ type: 'text', text: 'create after failed reminder' }] })
|
||||
await server.wait({ sessionId: 'saved' })
|
||||
const agent = ctx.agents.get(SessionId('saved'))
|
||||
await expect(restore()).resolves.toBe(agent)
|
||||
await expect(server.resume({ sessionId: 'saved' })).rejects.toThrow('already open')
|
||||
expect(fallback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('releases a reminder waiting for initialization when the SDK shuts down', async () => {
|
||||
const { ctx, server } = await runtime(await storageRoot())
|
||||
const restoring = ctx.waterfall('schedule/resolve-agent', SessionId('saved'), async () => {
|
||||
throw new Error('unexpected fallback')
|
||||
})
|
||||
const rejected = expect(restoring).rejects.toThrow('shutting down')
|
||||
await server.shutdown()
|
||||
await rejected
|
||||
expect(ctx.agents.list()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('restores a non-main identity, preserves history and origin, and appends a new turn', async () => {
|
||||
const root = await storageRoot()
|
||||
const first = await initialized(root)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import UserQuestions from '@deepseek-ai/dsh-user-questions'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { JsonRpcTransportPeer } from '@deepseek-ai/dsh-sdk-protocol'
|
||||
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { HarnessSdkJsonRpcServer } from '../src/server.ts'
|
||||
|
||||
const questions = [{ id: 'format', question: 'Which format?', options: [{ label: 'CSV' }, { label: 'JSON' }] }]
|
||||
const answer = { answers: [{ id: 'format', selected: ['CSV'], custom: 'include a header' }] }
|
||||
|
||||
async function fixture(request: JsonRpcTransportPeer['request'], hostQuestions = true) {
|
||||
const root = await mkdtemp(join(tmpdir(), 'sdk-user-questions-'))
|
||||
const ctx = new Context()
|
||||
const state: { server?: HarnessSdkJsonRpcServer } = {}
|
||||
onTestFinished(async () => {
|
||||
try { await state.server?.shutdown() } finally {
|
||||
try { await ctx.fiber.dispose() } finally { await rm(root, { recursive: true, force: true }) }
|
||||
}
|
||||
})
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(UserQuestions)
|
||||
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('ready')]))
|
||||
const transport = { request: vi.fn(request), notify: vi.fn() }
|
||||
const server = state.server = new HarnessSdkJsonRpcServer(ctx, transport)
|
||||
await server.initialize({ cwd: root, provider: 'mock', model: 'mock', capabilities: { userQuestions: hostQuestions } })
|
||||
await server.prompt({ sessionId: 'main', contentBlocks: [{ type: 'text', text: 'start' }] })
|
||||
await server.wait({ sessionId: 'main' })
|
||||
const agent = ctx.agents.get(SessionId('main'))!
|
||||
return { ctx, server, transport, agent }
|
||||
}
|
||||
|
||||
it('forwards the original questions and maps the host answer to the SDK-owned root', async () => {
|
||||
const f = await fixture(async () => answer)
|
||||
await expect(f.ctx.userQuestions.ask({ agent: f.agent, questions })).resolves.toEqual(answer)
|
||||
expect(f.transport.request).toHaveBeenCalledWith('user-question/ask', {
|
||||
requestId: expect.any(String) as string, sessionId: 'main', questions,
|
||||
}, expect.any(AbortSignal))
|
||||
expect(f.transport.notify).not.toHaveBeenCalledWith('user-question.cancelled', expect.anything())
|
||||
})
|
||||
|
||||
it('keeps native answerers when the SDK host did not advertise question support', async () => {
|
||||
const f = await fixture(async () => { throw new Error('must not ask the host') }, false)
|
||||
await expect(f.ctx.userQuestions.ask({ agent: f.agent, questions })).rejects.toMatchObject({ code: 'NO_PROVIDER' })
|
||||
f.ctx.on('user-questions/request', async () => answer)
|
||||
await expect(f.ctx.userQuestions.ask({ agent: f.agent, questions })).resolves.toEqual(answer)
|
||||
expect(f.transport.request).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not forward questions belonging to another runtime root', async () => {
|
||||
const f = await fixture(async () => answer)
|
||||
const foreign = await f.ctx.agents.create({ sessionId: SessionId('foreign'), agentOptions: { provider: 'mock', model: 'mock' } })
|
||||
try {
|
||||
await expect(f.ctx.userQuestions.ask({ agent: foreign.agent, questions })).rejects.toMatchObject({ code: 'NO_PROVIDER' })
|
||||
expect(f.transport.request).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
await foreign.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
null,
|
||||
42,
|
||||
{},
|
||||
{ answers: false },
|
||||
{ answers: [null] },
|
||||
{ answers: ['wrong'] },
|
||||
{ answers: [{ selected: [] }] },
|
||||
{ answers: [{ id: 42, selected: [] }] },
|
||||
{ answers: [{ id: 'format' }] },
|
||||
{ answers: [{ id: 'format', selected: false }] },
|
||||
{ answers: [] },
|
||||
{ answers: [{ id: 'wrong', selected: [] }] },
|
||||
{ answers: [{ id: 'format', selected: [] }, { id: 'format', selected: [] }] },
|
||||
{ answers: [{ id: 'format', selected: [1] }] },
|
||||
{ answers: [{ id: 'format', selected: [], custom: false }] },
|
||||
])('rejects an invalid host answer without treating it as approval: %j', async (value) => {
|
||||
const f = await fixture(async () => value)
|
||||
await expect(f.ctx.userQuestions.ask({ agent: f.agent, questions })).rejects.toMatchObject({ code: 'BAD_ANSWER' })
|
||||
})
|
||||
|
||||
it.each(['tool', 'shutdown'] as const)('cancels an outstanding host request on %s cancellation', async (kind) => {
|
||||
const entered = Promise.withResolvers<AbortSignal>()
|
||||
const f = await fixture(async (_method, _params, signal) => {
|
||||
if (signal === undefined) throw new Error('question must have a cancellation signal')
|
||||
entered.resolve(signal)
|
||||
return new Promise((_resolve, reject) => {
|
||||
signal.addEventListener('abort', () => { reject(new Error('question cancelled')) }, { once: true })
|
||||
})
|
||||
})
|
||||
const controller = new AbortController()
|
||||
const pending = f.ctx.userQuestions.ask({ agent: f.agent, questions, signal: controller.signal })
|
||||
const rejected = expect(pending).rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
const signal = await entered.promise
|
||||
if (kind === 'tool') controller.abort(new Error('user cancelled the tool'))
|
||||
else await f.server.shutdown()
|
||||
await rejected
|
||||
expect(signal.aborted).toBe(true)
|
||||
expect(f.transport.notify).toHaveBeenCalledWith('user-question.cancelled', {
|
||||
requestId: expect.any(String) as string, sessionId: 'main',
|
||||
})
|
||||
})
|
||||
|
||||
it('reports a host or transport failure and does not synthesize an answer', async () => {
|
||||
const f = await fixture(async () => { throw new Error('host disconnected') })
|
||||
await expect(f.ctx.userQuestions.ask({ agent: f.agent, questions })).rejects.toMatchObject({ code: 'PROVIDER_ERROR' })
|
||||
})
|
||||
|
||||
it('contains cancellation-notification write failure while aborting the pending question', async () => {
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const f = await fixture(async (_method, _params, signal) => {
|
||||
entered.resolve(undefined)
|
||||
return new Promise((_resolve, reject) => {
|
||||
signal?.addEventListener('abort', () => { reject(new Error('cancelled')) }, { once: true })
|
||||
})
|
||||
})
|
||||
const warn = vi.spyOn(f.ctx.logger, 'warn').mockImplementation(() => {})
|
||||
onTestFinished(() => { warn.mockRestore() })
|
||||
const notify = f.transport.notify.getMockImplementation()
|
||||
f.transport.notify.mockImplementation((method) => {
|
||||
if (method === 'user-question.cancelled') throw new Error('broken output')
|
||||
})
|
||||
const controller = new AbortController()
|
||||
const rejected = expect(f.ctx.userQuestions.ask({ agent: f.agent, questions, signal: controller.signal }))
|
||||
.rejects.toMatchObject({ code: 'ASK_ABORTED' })
|
||||
await entered.promise
|
||||
controller.abort(new Error('cancel'))
|
||||
await rejected
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('cancellation could not reach the host'))
|
||||
warn.mockRestore()
|
||||
if (notify !== undefined) f.transport.notify.mockImplementation(notify)
|
||||
})
|
||||
@@ -37,6 +37,12 @@
|
||||
},
|
||||
{
|
||||
"path": "../../session/working-directory"
|
||||
},
|
||||
{
|
||||
"path": "../../schedule/schedule"
|
||||
},
|
||||
{
|
||||
"path": "../../interaction/user-questions"
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+51
-3
@@ -2615,18 +2615,48 @@ importers:
|
||||
|
||||
packages/bundle/sdk-app:
|
||||
dependencies:
|
||||
'@deepseek-ai/dsh-client-connection':
|
||||
specifier: workspace:*
|
||||
version: link:../../client/connection
|
||||
'@deepseek-ai/dsh-client-file-upload':
|
||||
specifier: workspace:*
|
||||
version: link:../../client/file-upload
|
||||
'@deepseek-ai/dsh-cmdline':
|
||||
specifier: workspace:*
|
||||
version: link:../../boot/cmdline
|
||||
'@deepseek-ai/dsh-deliverable-file-references':
|
||||
specifier: workspace:*
|
||||
version: link:../../deliverables/deliverable-file-references
|
||||
'@deepseek-ai/dsh-file-reference-local':
|
||||
specifier: workspace:*
|
||||
version: link:../../context/file-reference-local
|
||||
'@deepseek-ai/dsh-schedule':
|
||||
specifier: workspace:*
|
||||
version: link:../../schedule/schedule
|
||||
'@deepseek-ai/dsh-sdk-jsonrpc-server':
|
||||
specifier: workspace:*
|
||||
version: link:../../sdk/server
|
||||
'@deepseek-ai/dsh-skill-office':
|
||||
specifier: workspace:*
|
||||
version: link:../../skill/skill-office
|
||||
'@deepseek-ai/dsh-time-context':
|
||||
specifier: workspace:*
|
||||
version: link:../../context/time-context
|
||||
'@deepseek-ai/dsh-tool-ask-user':
|
||||
specifier: workspace:*
|
||||
version: link:../../interaction/tool-ask-user
|
||||
'@deepseek-ai/dsh-tool-present':
|
||||
specifier: workspace:*
|
||||
version: link:../../deliverables/tool-present
|
||||
'@deepseek-ai/dsh-tool-schedule':
|
||||
specifier: workspace:*
|
||||
version: link:../../schedule/tool-schedule
|
||||
'@deepseek-ai/dsh-tool-workspace-dependencies':
|
||||
specifier: workspace:*
|
||||
version: link:../../skill/tool-workspace-dependencies
|
||||
'@deepseek-ai/dsh-workspace':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/workspace
|
||||
'@deepseek-ai/schemastery':
|
||||
specifier: link:../../../vendor/schemastery
|
||||
version: link:../../../vendor/schemastery
|
||||
@@ -2998,6 +3028,9 @@ importers:
|
||||
'@deepseek-ai/dsh-cordis-host-runner':
|
||||
specifier: workspace:*
|
||||
version: link:../../extensions/cordis-host-runner
|
||||
'@deepseek-ai/dsh-deliverable-file-references':
|
||||
specifier: workspace:*
|
||||
version: link:../../deliverables/deliverable-file-references
|
||||
'@deepseek-ai/dsh-file-reference':
|
||||
specifier: workspace:*
|
||||
version: link:../../context/file-reference
|
||||
@@ -7505,6 +7538,15 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../host/webserver
|
||||
|
||||
packages/deliverables/deliverable-file-references:
|
||||
devDependencies:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: workspace:~
|
||||
version: link:../../../vendor/cordis
|
||||
'@deepseek-ai/dsh-system-prompt':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/system-prompt
|
||||
|
||||
packages/deliverables/tool-present:
|
||||
dependencies:
|
||||
'@deepseek-ai/schemastery':
|
||||
@@ -7541,9 +7583,6 @@ importers:
|
||||
'@deepseek-ai/dsh-session-projection':
|
||||
specifier: workspace:*
|
||||
version: link:../../session/session-projection
|
||||
'@deepseek-ai/dsh-system-prompt':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/system-prompt
|
||||
'@deepseek-ai/dsh-tools':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/tools
|
||||
@@ -12307,6 +12346,9 @@ importers:
|
||||
'@deepseek-ai/dsh-subagent':
|
||||
specifier: workspace:*
|
||||
version: link:../../subagent/subagent
|
||||
'@deepseek-ai/dsh-user-questions':
|
||||
specifier: workspace:*
|
||||
version: link:../../interaction/user-questions
|
||||
|
||||
packages/sdk/server:
|
||||
dependencies:
|
||||
@@ -12341,6 +12383,9 @@ importers:
|
||||
'@deepseek-ai/dsh-llm-deepseek-api-key':
|
||||
specifier: workspace:*
|
||||
version: link:../../llm/llm-deepseek-api-key
|
||||
'@deepseek-ai/dsh-schedule':
|
||||
specifier: workspace:*
|
||||
version: link:../../schedule/schedule
|
||||
'@deepseek-ai/dsh-scope':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/scope
|
||||
@@ -12359,6 +12404,9 @@ importers:
|
||||
'@deepseek-ai/dsh-subagent':
|
||||
specifier: workspace:*
|
||||
version: link:../../subagent/subagent
|
||||
'@deepseek-ai/dsh-user-questions':
|
||||
specifier: workspace:*
|
||||
version: link:../../interaction/user-questions
|
||||
'@deepseek-ai/dsh-working-directory':
|
||||
specifier: workspace:*
|
||||
version: link:../../session/working-directory
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
/deepseek-harness-python-sdk/customize-plugins:
|
||||
en: 69a6a6745c6e89c1
|
||||
zh: 85e82a4cc31e16e5
|
||||
/deepseek-harness-python-sdk/native-user-questions:
|
||||
en: d9c60c5f43755782
|
||||
zh: 78c82635209ed22d
|
||||
/deepseek-harness-python-sdk/results-and-notifications:
|
||||
en: d4f7366c2d260639
|
||||
zh: 7de99aed2b1bdd0a
|
||||
@@ -87,6 +87,12 @@ with DeepSeekHarness(
|
||||
|
||||
The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides a platform-selected persistent shell, `read_image`, `working_directory`, local execution, JSONL sessions, and automatic compaction. Text file tools, settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles. SDK prompt text does not execute `/compact`.
|
||||
|
||||
## Native user questions
|
||||
|
||||
Pass `on_user_question` to `DeepSeekHarnessConfig`, `DeepSeekHarness`, or `HarnessConfig` to answer the built-in `ask_user_question` tool through the native SDK protocol. The callback receives a `UserQuestionRequest` (`request_id`, `session_id`, and the original `questions`) and a `threading.Event` signalling cancellation. Return `UserQuestionAnswer` or a JSON object shaped as `{"answers": [{"id": "question-id", "selected": ["option label"], "custom": "optional free text"}]}`. Each original question id must appear exactly once. The application supplies its UI or simulated-user policy; the SDK does not choose answers.
|
||||
|
||||
Callbacks run on separate threads so the reader can process cancellation and other requests. They may run concurrently and may make further client requests. Release callback-owned work when the cancellation Event is set. Closing the client signals outstanding callbacks and gives their workers a shared 0.5-second join window; callbacks that ignore cancellation may continue, but their late answers cannot reach the runtime or a later process. A callback exception becomes a protocol error, and an absent callback retains existing native answerers or reports `NO_PROVIDER`.
|
||||
|
||||
## Results and notifications
|
||||
|
||||
`Session.run()` owns an activity interval from its prompt's durable inbox receipt through the next whole-agent idle and returns `RunResult(session_id, final_response, finish_reason, events, notifications)`. `final_response` is the last committed root-session assistant text in the interval. `finish_reason` is the `kind` of the last root-session `turn/end`, such as `completed`, `max-tokens`, or `error`, and is `None` when no turn ended. A `turn/end` without a string `data.reason.kind` violates the protocol and raises `SdkProtocolError`.
|
||||
|
||||
@@ -87,6 +87,12 @@ with DeepSeekHarness(
|
||||
|
||||
随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 ID。它提供按平台选择的持久 shell、`read_image`、`working_directory`、本地执行、JSONL 会话和自动压缩。文本文件工具、设置、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。SDK 提示词文本不会执行 `/compact`。
|
||||
|
||||
## 原生用户问答
|
||||
|
||||
向 `DeepSeekHarnessConfig`、`DeepSeekHarness` 或 `HarnessConfig` 传入 `on_user_question`,即可通过原生 SDK 协议回答内建的 `ask_user_question` 工具。回调接收 `UserQuestionRequest`(包含 `request_id`、`session_id` 和原始 `questions`)以及表示取消的 `threading.Event`。返回 `UserQuestionAnswer`,或形如 `{"answers": [{"id": "question-id", "selected": ["option label"], "custom": "optional free text"}]}` 的 JSON 对象。每个原问题 id 必须恰好出现一次。应用自行提供 UI 或模拟用户策略;SDK 不选择答案。
|
||||
|
||||
回调在独立线程执行,读取线程仍可处理取消和其他请求。回调可以并发执行,也可以再次调用客户端请求。取消 Event 置位时应释放回调拥有的工作。客户端关闭会通知所有待决回调,并给其工作线程总计 0.5 秒的 join 窗口;忽略取消的回调可能继续执行,但晚到答案不会发送到运行时或后续新进程。回调异常转为协议错误;未提供回调时保留原生回答方,或报告 `NO_PROVIDER`。
|
||||
|
||||
## 结果与通知
|
||||
|
||||
`Session.run()` 的活动区间从提示词被持久 inbox 接收时开始,到整个 agent 下一次进入空闲状态时结束,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications)`。`final_response` 是该区间内根会话最后提交的助手文本。`finish_reason` 是最后一个根会话 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反协议,并会抛出 `SdkProtocolError`。
|
||||
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user