fix(trajectory): retain raw tool results only for live Sessions

This commit is contained in:
_Kerman committed 2026-10-24 14:01:59 +08:00
1 parent 22c2493322
commit e1e3f3a2c2
41 files changed
+653 -54

No files matched your search

+50 -1
View File
@@ -12,7 +12,9 @@ import { join } from 'node:path'
import type { Browser, Page, Response } from 'playwright'
import { chromium } from 'playwright'
import { strFromU8, unzipSync } from 'fflate'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed } from 'vitest'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed, onTestFinished } from 'vitest'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import { SESSION_FORMAT_VERSION, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import {
@@ -306,9 +308,12 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
await page.getByRole('tab', { name: 'Result' }).click()
await expect.poll(() => page.getByText('NAVIGATION_OK', { exact: false }).count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(1)
expect(await page.locator('[data-timeline-span="message"][data-assistant-timing="true"]').count()).toBeGreaterThan(0)
await details.getByRole('button', { name: 'Raw result', exact: true }).click()
await page.getByText('Raw results are only available for live tool calls', { exact: true }).waitFor()
const snapshot = (await captureStableAria(page, '[class*="viewArea"]', scaffold.workspaceCwd))
.split(SEED_ID).join('{{seededId}}')
await compareOrRefreshGolden(TRAJECTORY_EXPECTED, snapshot, MODE)
await details.getByRole('button', { name: 'Raw result', exact: true }).click()
await details.getByRole('button', { name: 'Close details' }).click()
}, 60_000)
@@ -630,4 +635,48 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
'terminal-card.expected.md', 'timing.expected.md',
])
})
it.skipIf(MODE === 'record')('inspects a live tool value without adding it to the Session journal', async () => {
await ensureSeedOpen(page)
await expect.poll(() => scaffold.ctx.agents.get(SessionId(SEED_ID)) !== undefined).toBe(true)
const agent = scaffold.ctx.agents.get(SessionId(SEED_ID))!
const unregister = scaffold.ctx.tools.register(defineTool({
name: 'live_result_probe', description: 'Live result inspection fixture.', parameters: {},
output: { schema: { type: 'json' }, render: () => [{ type: 'text', text: 'LIVE_RESULT_RENDERED' }] },
execute: async () => ({ answer: 42 }),
}))
onTestFinished(unregister)
const callId = ToolCallId('live-result-probe')
let resultEvent: SessionEvent | undefined
const off = scaffold.ctx.on('session/event', (_session, event) => {
if (event.type === 'tool/result' && event.data.message.toolCallId === callId) resultEvent = event
}, { global: true })
onTestFinished(() => { off() })
const batch = await scaffold.ctx.tools.execute({
agent, signal: new AbortController().signal, log: { turn: 2, step: 1 },
calls: [{ type: 'tool-call', id: callId, name: 'live_result_probe', arguments: '{}' }],
})
expect(batch.results[0]?.result.isError).toBe(false)
expect(resultEvent).toBeDefined()
expect(resultEvent?.data).not.toHaveProperty('value')
const abort = new AbortController()
const observed = scaffold.ctx.sessionToolResults.watch(
{ sessionId: agent.id, seq: resultEvent!.seq }, abort.signal,
)[Symbol.asyncIterator]()
try {
expect((await observed.next()).value).toEqual({ available: true, value: { answer: 42 } })
} finally { abort.abort(); await observed.return?.() }
await page.getByRole('tab', { name: 'Trajectory' }).click()
await page.locator('tr[data-kind="tool"]').filter({ hasText: 'live_result_probe' }).click()
const details = page.getByRole('complementary', { name: 'Event details' })
await details.getByRole('tab', { name: 'Result', exact: true }).click()
await details.getByText('LIVE_RESULT_RENDERED', { exact: true }).waitFor()
await details.getByRole('button', { name: 'Raw result', exact: true }).click()
await expect.poll(() => details.getByRole('tabpanel').textContent()).toContain('answer:')
expect(await details.getByRole('tabpanel').textContent()).toContain('42')
expect(await details.getByText('Raw results are only available for live tool calls').count()).toBe(0)
await details.getByRole('button', { name: 'Raw result', exact: true }).click()
await details.getByText('LIVE_RESULT_RENDERED', { exact: true }).waitFor()
})
})
+2 -2
View File
@@ -6,5 +6,5 @@
en: 41d772192075e133
zh: 5ef3da69b4de1d8a
/capability-seams-and-core-services:
en: 506d61dfa8c71186
zh: 46f05c4ef2f5c322
en: 6b3dff34b2714738
zh: 48494c81a4e113f4
+3
View File
@@ -96,6 +96,7 @@ flowchart LR
svc_speechController["ctx.speechController<br/>Experimental transcription Remote"]
svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
svc_sessionFileReferences["ctx.sessionFileReferences<br/>Session-addressed file-reference Remote adapter"]
svc_sessionToolResults["ctx.sessionToolResults<br/>Live Session tool-result Remote"]
svc_sessionSkillCatalog["ctx.sessionSkillCatalog<br/>Session-addressed skill Remote adapter"]
pkg_api_job_controller["api-job-controller"]
svc_jobController["ctx.jobController<br/>Host job Remote controller"]
@@ -316,6 +317,7 @@ flowchart LR
pkg_api_session_controller --> svc_sessionController
pkg_api_session_controller --> svc_sessionFileReferences
pkg_api_session_controller --> svc_sessionSkillCatalog
pkg_api_session_controller --> svc_sessionToolResults
pkg_api_settings_controller --> svc_credentialsController
pkg_api_settings_controller --> svc_settingsController
pkg_api_terminal_controller --> svc_terminalController
@@ -659,6 +661,7 @@ flowchart LR
| `ctx.speechController` | `core` | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | - | - | Validates bounded browser audio before provider dispatch. |
| `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | Owns Session commands, cold reads, durable-event following, live control state, model catalogs, workspace opening, and Agent activation policy. |
| `ctx.sessionFileReferences` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | Delegates file-reference discovery through the Session Controller's established Agent lookup policy. |
| `ctx.sessionToolResults` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | Retains final tool values only for their live Session instance and invalidates inspectors when that instance unloads. |
| `ctx.sessionSkillCatalog` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | Lists the Session composition's user-invocable skills without activating a cold Agent. |
| `ctx.jobController` | `core` | [`api-job-controller`](../packages/api/job-controller) | - | - | - | Streams one background job's observation record over the generated Remote namespace; the roster stays on the session control stream. |
| `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | Projects the credential-reference seam onto the generated Remote namespace: batch fan-out, view projection, and refusal mapping live here, not on the seam Definition. |
+3
View File
@@ -98,6 +98,7 @@ flowchart LR
svc_speechController["ctx.speechController<br/>Experimental transcription Remote"]
svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
svc_sessionFileReferences["ctx.sessionFileReferences<br/>Session-addressed file-reference Remote adapter"]
svc_sessionToolResults["ctx.sessionToolResults<br/>Live Session tool-result Remote"]
svc_sessionSkillCatalog["ctx.sessionSkillCatalog<br/>Session-addressed skill Remote adapter"]
pkg_api_job_controller["api-job-controller"]
svc_jobController["ctx.jobController<br/>Host job Remote controller"]
@@ -318,6 +319,7 @@ flowchart LR
pkg_api_session_controller --> svc_sessionController
pkg_api_session_controller --> svc_sessionFileReferences
pkg_api_session_controller --> svc_sessionSkillCatalog
pkg_api_session_controller --> svc_sessionToolResults
pkg_api_settings_controller --> svc_credentialsController
pkg_api_settings_controller --> svc_settingsController
pkg_api_terminal_controller --> svc_terminalController
@@ -661,6 +663,7 @@ flowchart LR
| `ctx.speechController` | `core` | [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | - | - | - | 在 Provider 调用前校验受限的浏览器音频。 |
| `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | 负责 Session 命令、冷读取、持久事件跟随、实时控制状态、模型目录、workspace 打开与 Agent 激活策略。 |
| `ctx.sessionFileReferences` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | 通过 Session Controller 的既有 Agent lookup 策略委托文件引用发现。 |
| `ctx.sessionToolResults` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | 仅在具体 Session 实例存活期间保留最终工具值,并在实例卸载时使检查器失效。 |
| `ctx.sessionSkillCatalog` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | 在不激活冷 Agent 的前提下列出 Session 组合中允许用户调用的 skill。 |
| `ctx.jobController` | `core` | [`api-job-controller`](../packages/api/job-controller) | - | - | - | 经生成的 Remote namespace 流式发送一个后台任务的观测 record;名册仍在会话控制流上。 |
| `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | 把凭据引用 seam 投影到生成的 Remote namespace:批量扇出、视图投影与拒绝映射都在这里,而不在 seam Definition 上。 |
+1 -1
View File
@@ -243,7 +243,7 @@ export interface Config {
## `@deepseek-ai/dsh-api-session-controller`
- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
- `source`: [`packages/api/session-controller/src/index.ts:109`](../packages/api/session-controller/src/index.ts)
- `source`: [`packages/api/session-controller/src/index.ts:111`](../packages/api/session-controller/src/index.ts)
```ts config-catalog
/** Session Controller deployment policy. */
+1 -1
View File
@@ -245,7 +245,7 @@ export interface Config {
## `@deepseek-ai/dsh-api-session-controller`
- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
- `source`: [`packages/api/session-controller/src/index.ts:109`](../packages/api/session-controller/src/index.ts)
- `source`: [`packages/api/session-controller/src/index.ts:111`](../packages/api/session-controller/src/index.ts)
```ts config-catalog
/** Session Controller deployment policy. */
+1 -1
View File
@@ -80,7 +80,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.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/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `session-controller`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../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:44`](../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) |
+1 -1
View File
@@ -82,7 +82,7 @@
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.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/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `session-controller`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../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:44`](../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) |
+20 -2
View File
@@ -130,8 +130,6 @@ interface SessionEventMap {
*/
error?: { name: string; code: string; reason?: string }
meta?: JsonValue
/** Final successful canonical value for inspection; absent on failures and older records. */
value?: JsonValue
}
/**
* Full header for the next request, appended inside its step before dispatch.
@@ -1146,6 +1144,26 @@ Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persist
Source: [`packages/core/session/src/index.ts`](../../packages/core/session/src/index.ts)
<a id="ctxsessiontoolresults--sessiontoolresults"></a>
### `ctx.sessionToolResults` — `SessionToolResults`
Read-only Remote inspection of tool values produced in this Session lifetime.
```ts cordis-catalog
/**
* Observe one committed result without loading a Session or reading its history.
* Values disappear when the exact Session instance leaves the registry. A cold
* read stays unavailable; opening a new stream observes the current instance.
* @param request - Authorized Session identity and durable result event sequence.
* @param signal - Remote consumer lifetime.
* @returns Current availability and changes until cancellation or service disposal.
*/
@Remote({ mode: 'stream' }) async *watch(request: SessionToolResultRequest, signal: AbortSignal): AsyncIterable<SessionToolResultValue>
```
Source: [`packages/api/session-controller/src/tool-results.ts`](../../packages/api/session-controller/src/tool-results.ts)
<a id="api-session-events"></a>
### `api-session/*` events
+20 -2
View File
@@ -130,8 +130,6 @@ interface SessionEventMap {
*/
error?: { name: string; code: string; reason?: string }
meta?: JsonValue
/** Final successful canonical value for inspection; absent on failures and older records. */
value?: JsonValue
}
/**
* Full header for the next request, appended inside its step before dispatch.
@@ -1150,6 +1148,26 @@ Types: [CreateSessionOptions](persistence.zh.md) · [PrepareSessionOptions](pers
Source: [`packages/core/session/src/index.ts`](../../packages/core/session/src/index.ts)
<a id="ctxsessiontoolresults--sessiontoolresults"></a>
### `ctx.sessionToolResults` — `SessionToolResults`
Read-only Remote inspection of tool values produced in this Session lifetime.
```ts cordis-catalog
/**
* Observe one committed result without loading a Session or reading its history.
* Values disappear when the exact Session instance leaves the registry. A cold
* read stays unavailable; opening a new stream observes the current instance.
* @param request - Authorized Session identity and durable result event sequence.
* @param signal - Remote consumer lifetime.
* @returns Current availability and changes until cancellation or service disposal.
*/
@Remote({ mode: 'stream' }) async *watch(request: SessionToolResultRequest, signal: AbortSignal): AsyncIterable<SessionToolResultValue>
```
Source: [`packages/api/session-controller/src/tool-results.ts`](../../packages/api/session-controller/src/tool-results.ts)
<a id="api-session-events"></a>
### `api-session/*` events
@@ -9,14 +9,14 @@
en: c7a4c58ea6d3437b
zh: 45c2b472a074bb1b
/session-controller/summary:
en: 4914824c347fdab5
zh: 684a7b7fa970c39f
en: 1d55e8672055ab8b
zh: 48afdd612dfa19e8
/session-controller/table-of-contents:
en: 1fdf707322fb6996
zh: 0204f7e9d4aa3cce
/session-controller/use-this-package:
en: 0c8945a47f87b5f7
zh: b099d66521dca175
en: a89cf41dd0f6192f
zh: 7b824980c2960e12
/session-controller/client-references:
en: 41bdda1ac32df558
zh: aa5d0e325924066b
+3 -1
View File
@@ -10,7 +10,7 @@ Desktop analytics follows the [product collection policy](../../client/product-a
## Summary
`@deepseek-ai/dsh-api-session-controller` owns the Host `ctx.sessionController` service and the generated Client `session`, `skills`, and `fileReferences` Remote namespaces. It serves Session lifecycle and history, the Host-generation model catalog, human background-job kill, workspace-path opening, user-invocable skill discovery, and Agent-scoped file references. Use it through API Gateway when a Client needs operations addressed by a Session.
`@deepseek-ai/dsh-api-session-controller` owns the Host `ctx.sessionController` service and the generated Client `session`, `skills`, `fileReferences`, and `toolResults` Remote namespaces. It serves Session lifecycle and history, the Host-generation model catalog, human background-job kill, workspace-path opening, user-invocable skill discovery, and Agent-scoped file references. Use it through API Gateway when a Client needs operations addressed by a Session.
## Table of Contents
@@ -31,6 +31,8 @@ The controller reads a Session's recorded preset through the optional `agentPres
History pages and follow opening snapshots carry one `{ type: 'event', event: SessionWireEvent }` record per durable Session event. The Client retains each accepted record as one durable `SessionEventLikeEntry`; Assistant token boundaries remain inside the compact stream on `assistant/message` or `assistant/attempt`. Tool arguments, result content, failures, and `tool/result.data.meta` pass through unchanged; the controller does not resolve a Tool definition, run a presenter, or attach UI data.
`toolResults.watch` inspects a successful tool’s final post-policy value by Session and result-event sequence. Values are held only in memory for the exact live Session instance: unloading the Session clears pending and committed values and invalidates open inspectors. Cold reads, restored sessions, forks, and exports do not recover or inherit values. A new inspector or reconnect can read retained values while that instance survives. This capability never writes Session events or projection checkpoints; retained memory grows with successful result sizes during that Session lifetime. The browser subscribes only to the selected result, and ordinary failures leave rendered content usable.
The Client journal validates current Session event envelopes before publishing follow snapshots, live entries, or history pages. It reuses the browser-safe Session validators for required surface markers, exact replacement endpoints, earlier unique source seqs, embedded Assistant provider metadata, request-header omissions, and tool-error consistency. Invalid records fail without field stripping or normalization; range membership and source existence remain durable-log checks on the Host.
Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, attachment, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. A receipt names a staged file or a staged image, and an image receipt subjects the prompt to the same image-input check as an encoded image. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent's effective working directory and scoped registry when present. Cold listing uses the projected current directory, falling back to the recorded origin and then the configured directory default. It retains the preset standing scope without activating an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` normally reads an exact baseline, including current-format cold logs, and returns `kind: sequenced`. Only a read requiring migration returns `kind: migration-required` hints without `asOfSeq`; missing keys remain absent. It returns null for a missing Session. Fork callers may set `allowMigration: false` to receive `session/migration-required` instead of starting source migration; omission retains automatic migration. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Sequenced reads and live projection frames use the same sequence ordering; cached reads fill hints without replacing sequenced values.
+3 -1
View File
@@ -10,7 +10,7 @@ kind: "package-reference"
## 概述
`@deepseek-ai/dsh-api-session-controller` 拥有 Host 的 `ctx.sessionController` 服务,以及生成的 Client `session`、`skills` 和 `fileReferences` Remote namespace。它提供 Session 生命周期与历史、Host generation 模型目录、人工后台任务终止、工作区路径打开、用户可调用 skill(技能)发现和 Agent(智能体)范围的文件引用。当 Client 需要按 Session 寻址的操作时,请通过 API Gateway 使用它。
`@deepseek-ai/dsh-api-session-controller` 拥有 Host 的 `ctx.sessionController` 服务,以及生成的 Client `session`、`skills`、`fileReferences` 和 `toolResults` Remote namespace。它提供 Session 生命周期与历史、Host generation 模型目录、人工后台任务终止、工作区路径打开、用户可调用 skill(技能)发现和 Agent(智能体)范围的文件引用。当 Client 需要按 Session 寻址的操作时,请通过 API Gateway 使用它。
## 目录
@@ -31,6 +31,8 @@ kind: "package-reference"
历史页与 follow opening 快照为每个持久 Session 事件携带一条 `{ type: 'event', event: SessionWireEvent }` record。Client 把每条已接受 record 保留为一个持久 `SessionEventLikeEntry`;Assistant token 边界保留在 `assistant/message` 或 `assistant/attempt` 的紧凑流内。工具参数、结果内容、失败信息和 `tool/result.data.meta` 原样通过;控制器不解析工具定义、不运行展示转换器,也不附加 UI 数据。
`toolResults.watch` 按 Session 与结果事件序号查看成功调用经过策略处理后的最终值。值仅保存在具体活跃 Session 实例的内存中:卸载 Session 会清除待关联及已提交的值,并使打开的检查器失效。冷读、恢复的会话、fork 及导出不会恢复或继承值;只要原实例仍存活,新检查器或重连就能读取已保留的值。此能力不写 Session 事件或投影 checkpoint;保留内存在 Session 存活期间随成功结果大小增长。浏览器只订阅选中的结果,普通读取失败不影响渲染内容的查看。
Client journal 在发布 follow 快照、live entry 或历史页之前验证当前 Session 事件 envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 提供方元数据、request header 可选字段的省略规则以及工具错误一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。
每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。凭证指向一个暂存文件或一张暂存图片;图片凭证与编码图片一样要经过图片输入检查。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent 的有效工作目录与限定范围内的注册表。冷态列表使用投影中的当前目录,依次回退到记录的起始目录与配置的默认目录。它保留 preset 常驻 scope,不会激活 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 正常读取准确基线,包括当前格式的冷日志,返回 `kind: sequenced`。只有需要迁移时才返回不带 `asOfSeq` 的 `kind: migration-required` 提示,缺少的键保持缺席。Session 不存在时返回 null。Fork 调用者可设置 `allowMigration: false`,在源历史需要迁移时收到 `session/migration-required`;省略则保持自动迁移。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。有序读取与实时 projection 帧使用相同的序号排序规则;缓存读取只补充提示,不替换有序值。
+3 -1
View File
@@ -166,6 +166,8 @@
"@deepseek-ai/dsh-util-workspace-path": "workspace:*",
"@deepseek-ai/dsh-workspace": "workspace:*",
"@types/mime-types": "^3.0.1",
"@deepseek-ai/dsh-working-directory": "workspace:*"
"@deepseek-ai/dsh-working-directory": "workspace:*",
"@deepseek-ai/dsh-tools": "workspace:*",
"@deepseek-ai/dsh-tool-executor-ptc": "workspace:*"
}
}
@@ -26,6 +26,7 @@ import { SessionHistoryController } from './history.ts'
import { SessionFileReferences } from './file-references.ts'
import { ApiSessionList } from './list.ts'
import { buildModelCatalog, hasProviderApiKey } from './catalog.ts'
import { SessionToolResults } from './tool-results.ts'
import { SessionSkillCatalog } from './skill-catalog.ts'
import { SessionMediaReferences } from './media-references.ts'
import { ArchivedSessionGate } from './archived-session-gate.ts'
@@ -69,6 +70,7 @@ export type * from './types.ts'
export { ApiSessionNotFound } from './agent.ts'
export { SessionFileReferences } from './file-references.ts'
export { SessionSkillCatalog } from './skill-catalog.ts'
export { SessionToolResults } from './tool-results.ts'
declare module '@deepseek-ai/cordis' {
interface Context {
@@ -204,6 +206,7 @@ export class SessionController extends TypertRemoteService {
ctx.plugin(SessionFileReferences)
ctx.plugin(SessionMediaReferences)
ctx.plugin(SessionSkillCatalog)
ctx.plugin(SessionToolResults)
// An archived Session, or a subagent descendant of one, runs no model step
// until it is restored; what it still runs is stopped by the owners that
// answer the Workspace registry's archive-admission events.
@@ -0,0 +1,121 @@
/** Ephemeral tool values owned by live Session instances, outside the durable journal. */
import { Context } from '@deepseek-ai/cordis'
import type { ToolCallId } from '@deepseek-ai/dsh-llm'
import type { Session } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-tool-executor-ptc'
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import type { SessionToolResultRequest, SessionToolResultValue } from './types.ts'
declare module '@deepseek-ai/cordis' {
interface Context {
/** Non-restorable tool values retained only while their Session is live. */
sessionToolResults: SessionToolResults
}
}
interface LiveResults {
readonly pending: Map<ToolCallId, JsonValue>
readonly values: Map<number, JsonValue>
readonly listeners: Map<number, Set<() => void>>
}
function emptyResults(): LiveResults {
return { pending: new Map(), values: new Map(), listeners: new Map() }
}
/** Read-only Remote inspection of tool values produced in this Session lifetime. */
export class SessionToolResults extends TypertRemoteService {
static inject = ['sessions', 'typert']
private results = new WeakMap<Session, LiveResults>()
private readonly followers = new Set<() => void>()
/** @param ctx - Owner of the live Session registry and tool result notifications. */
constructor(ctx: Context) {
super(ctx, 'sessionToolResults', { namespace: 'toolResults' })
ctx.on('tools/result', (exec, result) => {
const session = exec.agent?.session
if (result.isError || session === undefined || ctx.sessions.get(session.id) !== session) return
this.forSession(session).pending.set(exec.callId, result.value)
}, { global: true })
ctx.on('session/event', (session, event) => {
const callId = event.type === 'tool/result' ? event.data.message.toolCallId
: event.type === 'tool/ptc-dispatch' ? event.data.subCallId : undefined
if (callId === undefined) return
const state = this.results.get(session)
const value = state?.pending.get(callId)
if (state === undefined || value === undefined) return
state.pending.delete(callId)
state.values.set(event.seq, value)
for (const listener of state.listeners.get(event.seq) ?? []) listener()
}, { global: true })
ctx.on('session/disposed', (session) => {
const state = this.results.get(session)
if (state === undefined) return
this.results.delete(session)
state.pending.clear()
state.values.clear()
for (const listeners of state.listeners.values()) for (const listener of listeners) listener()
}, { global: true })
ctx.effect(() => () => {
this.results = new WeakMap()
for (const close of this.followers) close()
this.followers.clear()
}, 'tool-results: live values and followers')
}
private forSession(session: Session): LiveResults {
let state = this.results.get(session)
if (state === undefined) {
state = emptyResults()
this.results.set(session, state)
}
return state
}
private forRequest(request: SessionToolResultRequest): LiveResults {
const session = this.ctx.sessions.get(request.sessionId)
return session === undefined ? emptyResults() : this.forSession(session)
}
/**
* Observe one committed result without loading a Session or reading its history.
* Values disappear when the exact Session instance leaves the registry. A cold
* read stays unavailable; opening a new stream observes the current instance.
* @param request - Authorized Session identity and durable result event sequence.
* @param signal - Remote consumer lifetime.
* @returns Current availability and changes until cancellation or service disposal.
*/
@Remote({ mode: 'stream' })
async *watch(request: SessionToolResultRequest, signal: AbortSignal): AsyncIterable<SessionToolResultValue> {
const state = this.forRequest(request)
let dirty = true
const lifetime = { closed: false }
let wake: (() => void) | undefined
const changed = (): void => { dirty = true; wake?.() }
const close = (): void => { lifetime.closed = true; state.values.clear(); state.pending.clear(); wake?.() }
let listeners = state.listeners.get(request.seq)
if (listeners === undefined) { listeners = new Set(); state.listeners.set(request.seq, listeners) }
listeners.add(changed)
this.followers.add(close)
signal.addEventListener('abort', changed, { once: true })
try {
while (!signal.aborted && !lifetime.closed) {
if (dirty) {
dirty = false
const value = state.values.get(request.seq)
yield value === undefined ? { available: false } : { available: true, value }
} else {
await new Promise<void>((resolve) => { wake = resolve })
wake = undefined
}
}
} finally {
listeners.delete(changed)
if (listeners.size === 0) state.listeners.delete(request.seq)
this.followers.delete(close)
signal.removeEventListener('abort', changed)
}
}
}
@@ -613,3 +613,14 @@ export type SessionProjectionValue = JsonValue
/** Application metadata returned by the serving desktop for one file. */
export type SessionWorkspacePathApplication = NativeFileApplication
/** Live-only tool result inspection; never part of Session history or projections. */
export interface SessionToolResultRequest {
readonly sessionId: SessionId
readonly seq: number
}
/** Availability distinguishes missing results from valid JSON null. */
export type SessionToolResultValue =
| { readonly available: false }
| { readonly available: true; readonly value: JsonValue }
@@ -0,0 +1,138 @@
/** Live tool inspection shares the real Session's unload lifetime, never its replay data. */
import { Context } from '@deepseek-ai/cordis'
import { expect, it, onTestFinished } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import { SessionToolResults } from '../src/tool-results.ts'
async function setup(value: JsonValue = { answer: 42 }) {
const ctx = new Context()
onTestFinished(() => ctx.fiber.dispose())
await mountAgentLoopTestDependencies(ctx)
const results = new SessionToolResults(ctx)
ctx.effect(() => ctx.tools.register(defineTool({
name: 'value', description: 'Return a structured value.', parameters: {},
output: { schema: { type: 'json' }, render: () => [{ type: 'text', text: 'rendered' }] },
execute: async () => value,
})))
await mountAgentLoopTestHarness(ctx)
let agent!: Agent
const owner = ctx.plugin({ inject: ['agentLoop'], async apply(inner: Context) {
agent = await inner.agentLoop.create(SessionId('live-values'))
} })
await owner
const events: SessionEvent[] = []
ctx.on('session/event', (session, event) => { if (session === agent.session) events.push(event) }, { global: true })
async function execute() {
await ctx.tools.execute({
agent, signal: new AbortController().signal, log: { turn: 1, step: 1 },
calls: [{ type: 'tool-call', id: ToolCallId('value-call'), name: 'value', arguments: '{}' }],
})
const event = events.findLast(event => event.type === 'tool/result')
if (event === undefined) throw new Error('tool result missing')
return event
}
function watch(seq: number) {
const abort = new AbortController()
const iterator = results.watch({ sessionId: agent.session.id, seq }, abort.signal)[Symbol.asyncIterator]()
onTestFinished(async () => { abort.abort(); await iterator.return?.() })
return iterator
}
return { ctx, results, agent, owner, execute, watch }
}
it.each([null, false, 0, '', { answer: 42 }])('retains final live value %j without adding it to the journal', async (value) => {
const { execute, watch } = await setup(value)
const event = await execute()
expect(event.data).not.toHaveProperty('value')
expect((await watch(event.seq).next()).value).toEqual({ available: true, value })
})
it('releases values on Session unload and never restores them for a same-id instance', async () => {
const { ctx, agent, owner, execute, watch } = await setup()
const event = await execute()
const first = watch(event.seq)
expect((await first.next()).value).toEqual({ available: true, value: { answer: 42 } })
const removed = first.next()
await owner.dispose()
expect(ctx.sessions.get(agent.id)).toBeUndefined()
expect((await removed).value).toEqual({ available: false })
ctx.sessions.create(agent.id)
expect((await watch(event.seq).next()).value).toEqual({ available: false })
})
it('observes the final post-policy value and keeps historical and failed results unavailable', async () => {
const { ctx, execute, watch } = await setup()
ctx.on('tools/post-execute', async () => ({ kind: 'accept', value: { replacement: true } }))
const event = await execute()
expect((await watch(event.seq).next()).value).toEqual({ available: true, value: { replacement: true } })
expect((await watch(0).next()).value).toEqual({ available: false })
ctx.on('tools/pre-execute', async () => ({ kind: 'deny', reason: 'denied' }))
const failed = await execute()
expect((await watch(failed.seq).next()).value).toEqual({ available: false })
})
it('pairs PTC dispatch values with their committed event sequence', async () => {
const { ctx, agent, watch } = await setup()
const callId = ToolCallId('nested:ptc:1')
await ctx.tools.execute({
agent, signal: new AbortController().signal,
calls: [{ type: 'tool-call', id: callId, name: 'value', arguments: '{}' }],
})
const event = agent.session.append('tool/ptc-dispatch', {
rootCallId: ToolCallId('nested'), parentCallId: ToolCallId('nested'), subCallId: callId,
name: 'value', arguments: {}, content: [{ type: 'text', text: 'rendered' }], isError: false,
})
expect(event.data).not.toHaveProperty('value')
expect((await watch(event.seq).next()).value).toEqual({ available: true, value: { answer: 42 } })
})
it('does not let closing one inspector discard values used by another', async () => {
const { execute, watch } = await setup()
const event = await execute()
const first = watch(event.seq)
await first.next()
await first.return?.()
expect((await watch(event.seq).next()).value).toEqual({ available: true, value: { answer: 42 } })
})
it('publishes a committed value to concurrent observers and releases only the closed observer', async () => {
const { ctx, agent, watch } = await setup()
const callId = ToolCallId('observed:ptc:1')
await ctx.tools.execute({
agent, signal: new AbortController().signal,
calls: [{ type: 'tool-call', id: callId, name: 'value', arguments: '{}' }],
})
const seq = agent.session.seq
const first = watch(seq)
const second = watch(seq)
expect((await first.next()).value).toEqual({ available: false })
expect((await second.next()).value).toEqual({ available: false })
await first.return?.()
const changed = second.next()
agent.session.append('tool/ptc-dispatch', {
rootCallId: ToolCallId('observed'), parentCallId: ToolCallId('observed'), subCallId: callId,
name: 'value', arguments: {}, content: [{ type: 'text', text: 'rendered' }], isError: false,
})
expect((await changed).value).toEqual({ available: true, value: { answer: 42 } })
})
it('keeps cold reads unavailable after unloading a Session with no retained results', async () => {
const { owner, watch } = await setup()
await owner.dispose()
expect((await watch(0).next()).value).toEqual({ available: false })
})
it('ends active observers when the service owner is disposed', async () => {
const { ctx, execute, watch } = await setup()
const event = await execute()
const observer = watch(event.seq)
await observer.next()
const finished = observer.next()
await ctx.fiber.dispose()
expect((await finished).done).toBe(true)
})
@@ -19,7 +19,8 @@
"src/list.ts",
"src/media-references.ts",
"src/archived-session-gate.ts",
"src/skill-catalog.ts"
"src/skill-catalog.ts",
"src/tool-results.ts"
],
"references": [
{
@@ -114,6 +115,12 @@
},
{
"path": "../../session/working-directory"
},
{
"path": "../../core/tools"
},
{
"path": "../../core/tool-executor-ptc"
}
]
}
@@ -24,8 +24,8 @@
en: 0f4ee2189324ac06
zh: 12e76fed22c09bc1
/deepseek-ai-dsh-client-ui-trajectory/understand-the-implementation:
en: 9b8478a403486fca
zh: 8679d31b30594774
en: 5c3542fa3ab53ab7
zh: 05265f449ca6fb05
/deepseek-ai-dsh-client-ui-trajectory/understand-the-implementation/virtual-rows:
en: c85e342bc7eee4e6
zh: 9feab66ef1755b94
+1 -1
View File
@@ -57,7 +57,7 @@ The view is a pure projection: Trajectory-owned Definitions assemble business re
User, steering, and context inputs and request prompt changes share their request's Step group in log order, before the request separator and assistant output.
Native and nested PTC Tool results retain their raw structured error details and presentation metadata. The Result tab switches between rendered content and the recorded canonical value with a compact toggle. Rendered content is the default; the browser-wide preference survives record, tab, and Session changes and reloads. Missing values in older or failed records show an explicit notice without resetting the preference. JSON primitives, including `null`, remain inspectable and copyable. Summary previews continue to show rendered content. Failed records show the error code in the ledger and the error name and code in the inspector.
Native and nested PTC Tool results retain their raw structured error details and presentation metadata. The Result tab switches between rendered content and a live canonical value with a compact toggle. Rendered content is the default; the browser-wide preference survives record, tab, and Session changes and reloads. Historical, failed, and unloaded results remain switchable but show “Raw results are only available for live tool calls” without resetting the preference. The inspector subscribes only to the selected raw result and releases it on selection changes or disposal; Session unload and connection loss clear the displayed value. JSON primitives, including `null`, remain inspectable and copyable. Summary previews continue to show rendered content. Failed records show the error code in the ledger and the error name and code in the inspector.
Tool records begin at durable tool/call events and retain complete argument text with a lazy `args` view, reused by paired results. Chat's transient preparing stage does not create Trajectory tool rows or alter historical tool timing.
+1 -1
View File
@@ -57,7 +57,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间
用户消息、中途引导、上下文输入及请求提示词变更按日志顺序放在所属请求的 Step 分组内,位于请求分隔行和助手输出之前。
原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。结果标签页通过紧凑按钮切换渲染内容与已记录的规范值。默认显示渲染内容;浏览器级偏好在切换记录、标签页、Session 及刷新页面后保留。旧记录或失败记录缺少原始值时显示明确提示,不重置偏好。包括 `null` 在内的 JSON 基本值均可查看和复制。概述预览仍显示渲染内容。失败记录在记录表中显示错误代码,在检查器中显示错误名称和代码。
原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。结果标签页通过紧凑按钮切换渲染内容与实时规范值。默认显示渲染内容;浏览器级偏好在切换记录、标签页、Session 及刷新页面后保留。历史、失败及已卸载的结果仍可切换,但显示“原始结果仅在实时工具调用中可用”,不重置偏好。检查器只订阅当前选中的原始结果,在切换选择或销毁时释放;Session 卸载及连接断开会清除已展示的值。包括 `null` 在内的 JSON 基本值均可查看和复制。概述预览仍显示渲染内容。失败记录在记录表中显示错误代码,在检查器中显示错误名称和代码。
工具记录从持久 tool/call 事件开始,保留完整参数文本和由配对结果复用的懒计算 `args` 视图。Chat 的瞬态 preparing 阶段不创建 Trajectory 工具行,也不改变历史工具计时。
+2 -1
View File
@@ -68,7 +68,8 @@
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-attachment": "workspace:*",
"@tanstack/react-virtual": "3.14.10",
"diff": "^9.0.0"
"diff": "^9.0.0",
"@deepseek-ai/dsh-api-gateway": "workspace:*"
},
"files": [
"locale/*.json",
@@ -50,7 +50,7 @@ export function TrajectoryCell({
promptDetail: _promptDetail,
previousPromptDetail: _previousPromptDetail,
outputDetail: _outputDetail,
resultValue: _resultValue,
resultSeq: _resultSeq,
thinkingDetail: _thinkingDetail,
sourceBlocks: _sourceBlocks,
outputBlocks: _outputBlocks,
@@ -2,6 +2,8 @@
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react'
import type { CSSProperties, ReactNode } from 'react'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import type { WatchToolResult } from './watch-tool-result.ts'
import { useVirtualizer } from '@tanstack/react-virtual'
import {
CodeBlock,
@@ -388,6 +390,8 @@ function AssistantTimingPanel({
/** Props for the trajectory ledger. */
export interface TrajectoryTableProps {
/** Subscribe only while the raw result inspector is selected. */
watchToolResult?: WatchToolResult | undefined
/** Browser-wide result mode, independent of record and inspector tab selection. */
rawResult?: boolean
/** Persist an explicit result mode selection. */
@@ -2088,6 +2092,7 @@ function OverviewSection({
*/
export function TrajectoryTable({
rawResult = false,
watchToolResult,
onResultModeChange,
t,
stringWrapping,
@@ -2151,6 +2156,18 @@ export function TrajectoryTable({
const selected = selectedTemplate === undefined
? undefined
: currentRecord(selectedTemplate)
const resultSeq = activeTab === 'output' && rawResult ? selected?.cell.resultSeq : undefined
const [liveResult, setLiveResult] = useState<{ seq: number; owner: WatchToolResult; value: JsonValue | undefined }>()
useEffect(() => {
setLiveResult(undefined)
if (resultSeq === undefined || watchToolResult === undefined) return
let active = true
const dispose = watchToolResult(resultSeq, (value) => {
if (active) setLiveResult({ seq: resultSeq, owner: watchToolResult, value })
})
return () => { active = false; dispose() }
}, [resultSeq, watchToolResult])
const resultValue = liveResult?.seq === resultSeq && liveResult?.owner === watchToolResult ? liveResult?.value : undefined
const selectedProgram = selected === undefined ? undefined : codeProgram(selected.cell)
const thinkingExpanded = thinkingDisclosure?.recordId === selectedRecordId
? thinkingDisclosure.expanded
@@ -3539,8 +3556,8 @@ export function TrajectoryTable({
<IconCodeOutlineRegular size={12} />
</button>
</Tooltip>
{rawResult && selected.cell.resultValue !== undefined && (
<InspectorCopyButton text={JSON.stringify(selected.cell.resultValue, null, 2)}
{rawResult && resultValue !== undefined && (
<InspectorCopyButton text={JSON.stringify(resultValue, null, 2)}
label={t('copy.json')} t={t} />
)}
</span>
@@ -3548,12 +3565,12 @@ export function TrajectoryTable({
)}
<div id="trajectory-result-content">
{rawResult && (selected.cell.kind === 'tool' || selected.cell.kind === 'subtool')
? selected.cell.resultValue === undefined
? resultValue === undefined
? <p className={css.noPayload}>{t('record.rawResultUnavailable')}</p>
: selected.cell.resultValue !== null && typeof selected.cell.resultValue === 'object'
? <JsonTree data={selected.cell.resultValue} label={t('record.resultJson')}
: resultValue !== null && typeof resultValue === 'object'
? <JsonTree data={resultValue} label={t('record.resultJson')}
labels={jsonTreeLabels(t)} stringWrapping={stringWrapping} />
: <pre className={css.jsonPayload}>{JSON.stringify(selected.cell.resultValue)}</pre>
: <pre className={css.jsonPayload}>{JSON.stringify(resultValue)}</pre>
: selectedProgram === undefined
? <RecordPayload record={selected} direction="output" renderSlot={renderSlot} loadImage={loadImage} stringWrapping={stringWrapping} t={t} />
: <ProgramOutput record={selected} stringWrapping={stringWrapping} t={t} />}
@@ -13,6 +13,7 @@ import {
type TrajectoryRequestNumber,
type TrajectoryUsage,
} from './TrajectoryTable.tsx'
import type { WatchToolResult } from './watch-tool-result.ts'
import { TrajectoryToolbar } from './TrajectoryToolbar.tsx'
import { TrajectoryTimeline } from './TrajectoryTimeline.tsx'
import {
@@ -85,6 +86,7 @@ export interface TrajectoryViewInjected {
loadImage: MessageImageLoader
setActualDuration: (actualDuration: boolean) => void
setResultMode: (raw: boolean) => void
watchToolResult?: WatchToolResult
}
interface UsageLike {
@@ -132,7 +134,7 @@ function addUsage(
}
export function TrajectoryView({
useSession, useTrajectory, useDuration, useResultMode, setResultMode, loadOlder, loadImage, setActualDuration,
useSession, useTrajectory, useDuration, useResultMode, setResultMode, watchToolResult, loadOlder, loadImage, setActualDuration,
viewRequest, completeViewRequest, renderSlot, t, jsonStringWrapping,
}: ConvViewProps
& Pick<PropsRenderSlots<'conversation.trajectory.images'>, 'renderSlot'>
@@ -550,6 +552,7 @@ export function TrajectoryView({
<TrajectoryTable
t={t}
rawResult={rawResult}
watchToolResult={watchToolResult}
onResultModeChange={setResultMode}
stringWrapping={jsonStringWrapping === undefined ? undefined : {
...jsonStringWrapping,
@@ -14,6 +14,7 @@ import type {} from '@deepseek-ai/dsh-client-locale/client'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
import type {} from '@deepseek-ai/dsh-client-ui-session/client'
import { createToolResultWatcher } from './watch-tool-result.ts'
import { createTrajectoryResultModeStore } from './result-mode-store.ts'
import { createTrajectoryDurationStore } from './duration-store.ts'
import { createTrajectoryStringWrappingStore } from './string-wrapping-store.ts'
@@ -39,7 +40,7 @@ export type {
} from './trajectory-contract.ts'
/** Required services: the conversation slot, registries, ordinary Session paging, and the locale service. */
export const inject = ['slots', 'sessions', 'uiSession', 'uiConversation', 'locale']
export const inject = ['slots', 'sessions', 'uiSession', 'uiConversation', 'locale', 'remote', 'remote.toolResults']
/**
* Client plugin body: register the trajectory view tab. The registration
@@ -93,6 +94,9 @@ export function apply(ctx: Context): void {
const trajectory = ctx.uiConversation.binding(sessionId).target('trajectory')
return {
hooks: { duration, resultMode },
watchToolResult: createToolResultWatcher(ctx.remote, sessionId, (error) => {
ctx.logger.warn('live tool result unavailable: %s', error)
}),
setResultMode: (value) => { resultMode.set(value) },
jsonStringWrapping: {
getDefault: () => stringWrapping.getSnapshot(),
@@ -465,7 +465,7 @@ export function deriveTrajectoryLayout(
: resultAsText(resultPreview)),
...(node.call !== null ? { inputDetail: node.call.argsRaw } : {}),
outputDetail: detailResult(node, t),
...node.value === undefined ? {} : { resultValue: node.value },
resultSeq: node.seq,
outputBlocks: node.content.map(block => sourceBlock(block)),
...resultPreview,
callId: node.callId,
@@ -786,7 +786,7 @@ function expandAssistant(
...(result !== undefined
? {
outputDetail: detailResult(result, t),
...result.value === undefined ? {} : { resultValue: result.value },
resultSeq: result.seq,
outputBlocks: result.content.map(block => sourceBlock(block)),
...resultPreview,
isError: result.isError,
@@ -1034,7 +1034,7 @@ function expandSubCalls(
...(settled
? {
outputDetail: detailResult(sub, t),
...sub.value === undefined ? {} : { resultValue: sub.value },
resultSeq: sub.seq,
outputBlocks: sub.content.map(block => sourceBlock(block)),
...resultPreview,
isError: sub.isError,
@@ -110,7 +110,7 @@ export const zh = {
'record.resultJson': '结果 JSON',
'record.rawResult': '原始结果',
'record.renderedResult': '渲染结果',
'record.rawResultUnavailable': '此记录未保存原始结果',
'record.rawResultUnavailable': '原始结果仅在实时工具调用中可用',
'record.json': 'JSON',
'record.parametersJson': '参数 JSON',
'record.namedParametersJson': '{name} 参数 JSON',
@@ -323,7 +323,7 @@ export const en: Record<TrajectoryKey, string> = {
'record.resultJson': 'Result JSON',
'record.rawResult': 'Raw result',
'record.renderedResult': 'Rendered result',
'record.rawResultUnavailable': 'No raw result was recorded',
'record.rawResultUnavailable': 'Raw results are only available for live tool calls',
'record.json': 'JSON',
'record.parametersJson': 'parameters JSON',
'record.namedParametersJson': '{name} parameters JSON',
@@ -522,7 +522,7 @@ export const ja = {
'record.resultJson': '結果 JSON',
'record.rawResult': '元の結果',
'record.renderedResult': '表示用の結果',
'record.rawResultUnavailable': '元の結果は記録されていません',
'record.rawResultUnavailable': '元の結果はリアルタイムのツール呼び出しでのみ利用できます',
'record.json': 'JSON',
'record.parametersJson': 'パラメータ JSON',
'record.namedParametersJson': '{name} パラメータ JSON',
@@ -721,7 +721,7 @@ export const es = {
'record.resultJson': 'Resultado JSON',
'record.rawResult': 'Resultado original',
'record.renderedResult': 'Resultado renderizado',
'record.rawResultUnavailable': 'No se registró ningún resultado original',
'record.rawResultUnavailable': 'Los resultados originales solo están disponibles para llamadas de herramientas en vivo',
'record.json': 'JSON',
'record.parametersJson': 'parámetros JSON',
'record.namedParametersJson': 'Parámetros de {name} JSON',
@@ -920,7 +920,7 @@ export const fr = {
'record.resultJson': 'Résultat JSON',
'record.rawResult': 'Résultat brut',
'record.renderedResult': 'Résultat rendu',
'record.rawResultUnavailable': 'Aucun résultat brut enregistré',
'record.rawResultUnavailable': 'Les résultats bruts ne sont disponibles que pour les appels d’outils en direct',
'record.json': 'JSON',
'record.parametersJson': 'paramètres JSON',
'record.namedParametersJson': 'Paramètres {name} JSON',
@@ -1119,7 +1119,7 @@ export const de = {
'record.resultJson': 'Ergebnis JSON',
'record.rawResult': 'Rohergebnis',
'record.renderedResult': 'Gerendertes Ergebnis',
'record.rawResultUnavailable': 'Kein Rohergebnis aufgezeichnet',
'record.rawResultUnavailable': 'Rohergebnisse sind nur für Live-Toolaufrufe verfügbar',
'record.json': 'JSON',
'record.parametersJson': 'Parameter JSON',
'record.namedParametersJson': '{name} Parameter JSON',
@@ -1318,7 +1318,7 @@ export const ru = {
'record.resultJson': 'Результат JSON',
'record.rawResult': 'Исходный результат',
'record.renderedResult': 'Отображаемый результат',
'record.rawResultUnavailable': 'Исходный результат не записан',
'record.rawResultUnavailable': 'Исходные результаты доступны только для вызовов инструментов в реальном времени',
'record.json': 'JSON',
'record.parametersJson': 'параметры JSON',
'record.namedParametersJson': '{name} параметры JSON',
@@ -1517,7 +1517,7 @@ export const pt = {
'record.resultJson': 'Resultado JSON',
'record.rawResult': 'Resultado original',
'record.renderedResult': 'Resultado renderizado',
'record.rawResultUnavailable': 'Nenhum resultado original foi registrado',
'record.rawResultUnavailable': 'Os resultados originais só estão disponíveis para chamadas de ferramentas ao vivo',
'record.json': 'JSON',
'record.parametersJson': 'parâmetros JSON',
'record.namedParametersJson': '{name} parâmetros JSON',
@@ -1,7 +1,6 @@
/** Shared trajectory record data and formatting contracts. */
import type { HTMLAttributes } from 'react'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import type { FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { ConversationPromptSnapshot } from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { TrajectoryTranslate } from './locales.ts'
@@ -67,8 +66,8 @@ export interface TrajectoryCellProps extends HTMLAttributes<HTMLDivElement> {
previousPromptDetail?: ConversationPromptSnapshot
/** Full assistant/tool result content for the details panel. */
outputDetail?: string
/** Final canonical tool value; absent on failures and records created before value recording. */
resultValue?: JsonValue
/** Committed result event used to inspect a live-only value. */
resultSeq?: number
/** Full assistant reasoning content for the details panel. */
thinkingDetail?: string
/** Original message blocks in source order for the details panel. */
@@ -0,0 +1,49 @@
/** Selected-result subscriptions retain no history-sized Client cache. */
import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
import type { SessionToolResultValue } from '@deepseek-ai/dsh-api-session-controller/types'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
/** Observe one result until the inspector releases it; undefined denotes unavailable. */
export type WatchToolResult = (seq: number, publish: (value: JsonValue | undefined) => void) => () => void
/**
* Bind an ephemeral inspector stream to one Session.
* @param remote - Generated Remote transport and reconnecting stream owner.
* @param sessionId - Session inspected by this view.
* @param report - Diagnostic sink for unavailable transport and teardown failures.
* @returns Subscription factory; closing a subscription releases its selected value.
*/
export function createToolResultWatcher(
remote: Pick<ClientRemote, '$stream' | 'toolResults'>,
sessionId: SessionId,
report: (error: unknown) => void,
): WatchToolResult {
return (seq, publish) => {
const lifetime = { stopped: false }
const clear = (): void => { if (!lifetime.stopped) publish(undefined) }
const stream = remote.$stream<SessionToolResultValue>({
name: 'live tool result',
open: signal => remote.toolResults.watch({ sessionId, seq }, signal),
ended: () => new Error('live tool result stream ended'),
carrierFailed: clear,
})
void (async () => {
try {
for await (const frame of stream) {
if (lifetime.stopped) return
frame.accept()
publish(frame.value.available ? frame.value.value : undefined)
}
} catch (error: unknown) {
if (!lifetime.stopped) { clear(); report(error) }
} finally {
clear()
}
})()
return () => {
lifetime.stopped = true
void stream.dispose().catch(report)
}
}
}
@@ -67,7 +67,7 @@ describe('tsdown client artifact', () => {
expect(handoff.id).toBe(PLUGIN_ID)
expect(exports.apply).toBeTypeOf('function')
expect(exports.inject).toEqual([
'slots', 'sessions', 'uiSession', 'uiConversation', 'locale',
'slots', 'sessions', 'uiSession', 'uiConversation', 'locale', 'remote', 'remote.toolResults',
])
})
@@ -92,6 +92,7 @@ describe('tsdown client artifact', () => {
const { events, views } = uiConversation
ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never)
ctx.provide('remote', { $on: () => () => {} } as never)
ctx.provide('remote.toolResults', {})
ctx.provide('configForms', { developerTools: { enabled: createSnapshotStore(true) }, get: () => stubConfigForm().scope } as never)
const locale = await import('@deepseek-ai/dsh-client-locale/client')
await ctx.plugin({ inject: [...locale.inject], apply: locale.apply })
@@ -1135,13 +1135,17 @@ describe('TrajectoryTable', () => {
it.each([null, false, 0, '', { answer: 42 }])('switches to the recorded value %j and retains the mode across records and tabs', (value) => {
const turns: readonly TrajectoryTurnModel[] = [{ turn: 1, groups: [{ title: 'Step 1', cells: [
{ index: 1, kind: 'tool', text: 'recorded', outputDetail: 'rendered answer', resultValue: value, timeSeconds: 0.1 },
{ index: 1, kind: 'tool', text: 'recorded', outputDetail: 'rendered answer', resultSeq: 42, timeSeconds: 0.1 },
{ index: 2, kind: 'subtool', text: 'historical', outputDetail: 'old answer', timeSeconds: 0.1 },
] }] }]
const changes = vi.fn()
const watchToolResult: NonNullable<import('../src/client/TrajectoryTable.tsx').TrajectoryTableProps['watchToolResult']> = (_seq, publish) => {
publish(value)
return () => {}
}
function Harness() {
const [raw, setRaw] = useState(false)
return <TrajectoryTable turns={turns} {...FOLD_PROPS} rawResult={raw}
return <TrajectoryTable turns={turns} {...FOLD_PROPS} rawResult={raw} watchToolResult={watchToolResult}
onResultModeChange={(next) => { changes(next); setRaw(next) }} />
}
render(<Harness />)
@@ -1153,18 +1157,44 @@ describe('TrajectoryTable', () => {
typeof value === 'object' && value !== null ? 'answer:' : JSON.stringify(value),
)
fireEvent.click(screen.getByRole('row', { name: /historical/ }))
expect(screen.getByText('No raw result was recorded')).toBeTruthy()
expect(screen.getByText('Raw results are only available for live tool calls')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Raw result' }).getAttribute('aria-pressed')).toBe('true')
fireEvent.click(screen.getByRole('tab', { name: 'Summary' }))
fireEvent.click(screen.getByRole('tab', { name: 'Result' }))
fireEvent.click(screen.getByRole('row', { name: /recorded/ }))
expect(screen.queryByText('No raw result was recorded')).toBeNull()
expect(screen.queryByText('Raw results are only available for live tool calls')).toBeNull()
expect(changes.mock.calls).toEqual([[true]])
fireEvent.click(screen.getByRole('button', { name: 'Raw result' }))
expect(screen.getByText('rendered answer')).toBeTruthy()
expect(changes.mock.calls).toEqual([[true], [false]])
})
it('clears an unloaded result and ignores callbacks after inspector disposal', () => {
const callbacks: ((value: { live: boolean } | undefined) => void)[] = []
const dispose = vi.fn()
const watchToolResult: NonNullable<import('../src/client/TrajectoryTable.tsx').TrajectoryTableProps['watchToolResult']> = (_seq, publish) => {
callbacks.push(publish)
publish({ live: true })
return dispose
}
const turns: readonly TrajectoryTurnModel[] = [{ turn: 1, groups: [{ title: 'Step 1', cells: [
{ index: 1, kind: 'tool', text: 'live result', outputDetail: 'rendered', resultSeq: 42, timeSeconds: 0.1 },
{ index: 2, kind: 'tool', text: 'historical result', outputDetail: 'historical', timeSeconds: 0.1 },
] }] }]
const view = render(<TrajectoryTable turns={turns} {...FOLD_PROPS} rawResult watchToolResult={watchToolResult} />)
fireEvent.click(screen.getByRole('row', { name: /live result/ }))
fireEvent.click(screen.getByRole('tab', { name: 'Result' }))
expect(document.getElementById('trajectory-result-content')?.textContent).toContain('live:')
act(() => { callbacks[0]?.(undefined) })
expect(screen.getByText('Raw results are only available for live tool calls')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Raw result' }).getAttribute('aria-pressed')).toBe('true')
fireEvent.click(screen.getByRole('row', { name: /historical result/ }))
expect(dispose).toHaveBeenCalledOnce()
act(() => { callbacks[0]?.({ live: true }) })
expect(screen.getByText('Raw results are only available for live tool calls')).toBeTruthy()
view.unmount()
})
it('renders a single-text JSON tool result as a JSON tree', () => {
const turns: readonly TrajectoryTurnModel[] = [{
turn: 1,
@@ -264,6 +264,7 @@ type ConversationTargetSources = {
/** Real-stack bench: root Context + real SlotRegistry ring + the plugin fiber. */
async function bench(snapshot = historySnapshot(NODES)) {
const runtime = await SlotTestRuntime.create()
runtime.remote.provideNamespaces({ toolResults: {} })
runtimes.push(runtime)
const ctx = runtime.ctx
const slots = runtime.slots
@@ -0,0 +1,71 @@
/** Selected-result transport releases values on carrier loss and closes on inspector disposal. */
import { expect, it, onTestFinished, vi } from 'vitest'
import { RemoteStream, RemoteStreamCarrierError } from '@deepseek-ai/dsh-api-gateway/client'
import { SessionId } from '@deepseek-ai/dsh-session/types'
import type { SessionToolResultValue } from '@deepseek-ai/dsh-api-session-controller/types'
import { createToolResultWatcher } from '../src/client/watch-tool-result.ts'
function deferred() {
let resolve!: () => void
const promise = new Promise<void>((done) => { resolve = done })
return { promise, resolve }
}
const connection = {
generation: {
getSnapshot: () => ({ id: 1, host: { home: '/fixture' } }),
subscribe: () => () => {},
},
}
function remote(source: (signal: AbortSignal) => AsyncIterable<SessionToolResultValue>): Parameters<typeof createToolResultWatcher>[0] {
return {
$stream: options => new RemoteStream(connection, options),
toolResults: {
watch: (_request, signal) => ({
[Symbol.asyncIterator]: () => source(signal ?? new AbortController().signal)[Symbol.asyncIterator](),
send: () => undefined, end: () => undefined, dispose: () => undefined,
}),
},
}
}
it('cancels the selected subscription to quiescence without publishing late values', async () => {
const opened = deferred()
const finished = deferred()
const publish = vi.fn(opened.resolve)
const report = vi.fn()
const watcher = createToolResultWatcher(remote(async function* (signal) {
try {
const aborted = new Promise<void>((resolve) => { signal.addEventListener('abort', () => { resolve() }, { once: true }) })
yield { available: true, value: { answer: 42 } }
await aborted
yield { available: true, value: 'late' }
} finally {
finished.resolve()
}
}), SessionId('selected'), report)
const close = watcher(42, publish)
onTestFinished(async () => { close(); await finished.promise })
await opened.promise
close()
await finished.promise
expect(publish.mock.calls).toEqual([[{ answer: 42 }]])
expect(report).not.toHaveBeenCalled()
})
it('clears the displayed object on carrier loss while the reconnect owner waits', async () => {
const cleared = deferred()
const values: unknown[] = []
const watcher = createToolResultWatcher(remote(async function* () {
yield { available: true, value: { answer: 42 } }
throw new RemoteStreamCarrierError('connection lost')
}), SessionId('selected'), () => {})
const close = watcher(42, (value) => {
values.push(value)
if (value === undefined) cleared.resolve()
})
onTestFinished(close)
await cleared.promise
expect(values).toEqual([{ answer: 42 }, undefined])
})
@@ -52,6 +52,9 @@
},
{
"path": "../../core/tool-executor-ptc"
},
{
"path": "../../api/gateway/tsconfig.client.json"
}
]
}
@@ -2833,6 +2833,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
],
},
{
key: 'sessionToolResults',
summary: 'Read-only Remote inspection of tool values produced in this Session lifetime.',
description: 'Read-only Remote inspection of tool values produced in this Session lifetime.',
methods: [
{
signature: '@Remote({ mode: \'stream\' }) async *watch(request: SessionToolResultRequest, signal: AbortSignal): AsyncIterable<SessionToolResultValue>',
description: 'Observe one committed result without loading a Session or reading its history. Values disappear when the exact Session instance leaves the registry. A cold read stays unavailable; opening a new stream observes the current instance.',
parameters: [{ name: 'request', description: 'Authorized Session identity and durable result event sequence.' }, { name: 'signal', description: 'Remote consumer lifetime.' }],
returns: 'Current availability and changes until cancellation or service disposal.',
},
],
},
{
key: 'settings',
summary: 'Project Config schemas into forms and own optional instance-level UI policy.',
@@ -7458,7 +7471,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SessionEventMap',
declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': UserMessage;\n \'developer/message\': {\n turn: number;\n step: number;\n message: DeveloperMessage;\n headerSeq?: SessionSeq;\n };\n \'system/message\': {\n turn: number;\n step: number;\n message: SystemMessage;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n message: AssistantMessage;\n stream: AssistantStreamRecord[];\n usage?: TokenUsage;\n interrupted?: true;\n };\n \'assistant/attempt\': {\n turn: number;\n step: number;\n stream: AssistantStreamRecord[];\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: ToolCallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n message: ToolResultMessage;\n error?: {\n name: string;\n code: string;\n reason?: string;\n };\n meta?: JsonValue;\n value?: JsonValue;\n };\n \'request/header\': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n startsSeries?: true;\n };\n \'request/context\': RequestContext;\n /* …truncated — full shape in source */',
declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': UserMessage;\n \'developer/message\': {\n turn: number;\n step: number;\n message: DeveloperMessage;\n headerSeq?: SessionSeq;\n };\n \'system/message\': {\n turn: number;\n step: number;\n message: SystemMessage;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n message: AssistantMessage;\n stream: AssistantStreamRecord[];\n usage?: TokenUsage;\n interrupted?: true;\n };\n \'assistant/attempt\': {\n turn: number;\n step: number;\n stream: AssistantStreamRecord[];\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: ToolCallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n message: ToolResultMessage;\n error?: {\n name: string;\n code: string;\n reason?: string;\n };\n meta?: JsonValue;\n };\n \'request/header\': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n startsSeries?: true;\n };\n \'request/context\': RequestContext;\n \'session/end-seed\': {\n /* …truncated — full shape in source */',
},
{
name: 'SessionEventMetadataFilter',
@@ -7916,6 +7929,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
name: 'SessionTitleUserMessage',
declaration: 'export interface SessionTitleUserMessage {\n readonly seq: SessionSeq;\n readonly text: string;\n}',
},
{
name: 'SessionToolResultRequest',
declaration: 'export interface SessionToolResultRequest {\n readonly sessionId: SessionId;\n readonly seq: number;\n}',
},
{
name: 'SessionToolResultValue',
declaration: 'export type SessionToolResultValue = {\n readonly available: false;\n} | {\n readonly available: true;\n readonly value: JsonValue;\n};',
},
{
name: 'SessionUpdateQueueRequest',
declaration: 'export interface SessionUpdateQueueRequest {\n readonly sessionId: SessionId;\n readonly itemId: MessageId;\n readonly action: QueueAction;\n}',
+9
View File
@@ -1692,6 +1692,12 @@ importers:
'@deepseek-ai/dsh-subagent':
specifier: workspace:*
version: link:../../subagent/subagent
'@deepseek-ai/dsh-tool-executor-ptc':
specifier: workspace:*
version: link:../../core/tool-executor-ptc
'@deepseek-ai/dsh-tools':
specifier: workspace:*
version: link:../../core/tools
'@deepseek-ai/dsh-typert-protocol':
specifier: workspace:*
version: link:../../typert/protocol
@@ -6641,6 +6647,9 @@ importers:
'@deepseek-ai/dsh-agent':
specifier: workspace:*
version: link:../../core/agent
'@deepseek-ai/dsh-api-gateway':
specifier: workspace:*
version: link:../../api/gateway
'@deepseek-ai/dsh-api-session-controller':
specifier: workspace:*
version: link:../../api/session-controller
+3
View File
@@ -120,6 +120,7 @@ export const SERVICE_PAGE: Record<string, string> = {
workingDirectory: 'working-directory.md',
worktrees: 'worktrees.md',
sessionController: 'session.md',
sessionToolResults: 'session.md',
sessionSkillCatalog: 'skills.md',
sessions: 'session.md',
settings: 'settings.md',
@@ -434,6 +435,8 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
SessionEvent: 'session.md',
SessionFollowFrame: 'session.md',
SessionFollowRequest: 'session.md',
SessionToolResultRequest: 'session.md',
SessionToolResultValue: 'session.md',
SessionForkRequest: 'session.md',
SessionForkValue: 'session.md',
SessionId: 'core.md',
+7
View File
@@ -329,6 +329,13 @@ const SERVICE_ROLES: ServiceRole[] = [
mode: 'core',
note: 'Delegates file-reference discovery through the Session Controller\'s established Agent lookup policy.',
},
{
key: 'sessionToolResults',
pkg: 'api-session-controller',
title: 'Live Session tool-result Remote',
mode: 'core',
note: 'Retains final tool values only for their live Session instance and invalidates inspectors when that instance unloads.',
},
{
key: 'sessionSkillCatalog',
pkg: 'api-session-controller',
@@ -49,4 +49,7 @@
- tab "Result" [selected]
- tab "Schema"
- tab "Timing"
- tabpanel "Result": NAVIGATION_OK
- tabpanel "Result":
- text: Raw result
- button "Raw result" [pressed]
- paragraph: Raw results are only available for live tool calls