Merge commit '5427cbcc19cfd1ce9f3af1ae22207852cc5740fa' into codex/workflow-runs-chat-node-f6

This commit is contained in:
pku-xht
2026-08-11 18:09:43 +08:00
3060 changed files with 45805 additions and 34331 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/agent-default-model/README.md
README.md: 98bc7d082e62a764868f8acd323c4617e9839e61
README.zh.md: 807b612bd25e49aa318c13c8c8dc7595a6459080
README.md: e86be7c37a1f994ca52f018144ef6a2409bd1eea
README.zh.md: 00250c28ef8c03d4b33fe1c1bfca138a022f6638

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The deployment default used when an entry point creates an Agent that has no session-local model selection. `AgentDefaultModelService` provides `ctx.agentDefaultModel`; direct entry points such as `dsh run` and Host-backed entry points such as ApiProxy read the same service instead of owning parallel provider/model defaults.
The deployment default used when an entry point creates an Agent that has no session-local model selection. `AgentDefaultModelService` provides `ctx.agentDefaultModel`; direct entry points such as `dsh --profile headless` and Host-backed entry points such as ApiProxy read the same service instead of owning parallel provider/model defaults.
The plugin config requires `{ provider, model }`. That composition entry is the base of the `agent-default-model` Settings section; a mounted settings provider layers the user's choice over it and changes are visible on the next `currentSelection()` read. `reasoningEffort` belongs to the Settings section but deliberately not to plugin config: a complete saved selection can clear an effort when the next selected model has none, while a composition value would be inherited again.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
该部署默认值供入口在创建尚无会话级模型选择的 Agent 时使用。`AgentDefaultModelService` 提供 `ctx.agentDefaultModel``dsh run` 这类直接入口与 ApiProxy 这类由 Host 支撑的入口读取同一服务,而不是分别持有平行的提供方/模型默认值。
该部署默认值供入口在创建尚无会话级模型选择的 Agent 时使用。`AgentDefaultModelService` 提供 `ctx.agentDefaultModel``dsh --profile headless` 这类直接入口与 ApiProxy 这类由 Host 支撑的入口读取同一服务,而不是分别持有平行的提供方/模型默认值。
插件配置必须提供 `{ provider, model }`。该组合配置项构成 Settings 中 `agent-default-model` 分节的基础层;挂载的设置提供方在其上叠加用户选择,更改会在下一次调用 `currentSelection()` 时可见。`reasoningEffort` 属于该 Settings 分节但特意不属于插件配置完整保存的选择必须能在下一个选定模型没有推理reasoning强度时清除旧值而组合配置值会再次被继承。

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-agent-default-model",
"description": "Default model selection shared by Agent entry points",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/core/agent-default-model"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,20 +32,20 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-settings": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -4,13 +4,13 @@
* @module @deepseek-ai/dsh-agent-default-model
*/
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { ModelSelection } from '@deepseek-ai/dsh-agent'
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** Default model selection for Agents created without an explicit model. */
agentDefaultModel: AgentDefaultModelService

View File

@@ -8,7 +8,7 @@
* @module @deepseek-ai/dsh-agent-default-model/invariant
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-agent-default-model'

View File

@@ -1,7 +1,7 @@
/** Default Agent model settings layered over a real settings provider. */
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentDefaultModelService, { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '../src/index.ts'
import { Settings } from '@deepseek-ai/dsh-settings'
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md
README.md: 6092363fae2853d6c5d92aaf8cd01e41e18e0b52
README.md: 889fd74671eddc202b814cdf2749069ec3cea02c
README.zh.md: b65b5334d735a1e0b51fa517ce41c0c953f87cf7

View File

@@ -12,7 +12,7 @@ This is the only package in the harness that contains concrete loop logic. Every
Creation and resume are one rollback-covered transaction: construct a private session, concrete agent, and scoped context; await optional setup; enter both registries; announce `session/created` then `agent/created`; emit `agent/session-start`; and only then start the driver. Setup receives the full scoped `Context` as trusted same-process composition code and must not drive the unpublished agent. Ordinary typed identity and option inputs are borrowed under their readonly contract, while seed events and session metadata are validated and snapshotted because they cross the durable session boundary. An optional `AbortSignal` cancels only load/setup/publication and is detached before the returned handle becomes visible.
The caller fiber and the AgentLoop provider are co-owners. `AgentFactory.createAgent(ownerCtx, options)` and `resume(ownerCtx, options)` receive caller ownership explicitly, while the factory keeps its own dependency context for `sessions`/`llm`/`tools`/`systemPrompt`; this lets a caller inject only `agents` without shrinking the new agent's service surface. Caller unload, handle disposal, or provider unload converge on one memoized quiescence boundary. Provider shutdown waits both resource teardown and the public create/resume wrapper that observed deactivation, so no continuation can publish after dependencies disappear.
The caller fiber and the AgentLoop provider are co-owners. `AgentFactory.createAgent(ownerCtx, options)` and `resume(ownerCtx, options)` receive caller ownership explicitly, while the factory keeps its own dependency context for `sessions`/`llm`/`tools`/`systemPrompt`; this lets a caller inject only `agents` without shrinking the new agent's service set. Caller unload, handle disposal, or provider unload converge on one memoized quiescence boundary. Provider shutdown waits both resource teardown and the public create/resume wrapper that observed deactivation, so no continuation can publish after dependencies disappear.
Each agent and its session share one caller-chosen `SessionId`, assumed globally unique; accidental UUID collisions are outside the supported model. Two concurrent operations with the same id may both prepare, but the final `enter()` calls arbitrate publication and every loser rolls its private resources back. Each detach is bound to the exact entered object, so a stale disposer cannot remove a later same-id replacement. A detach requested during a synchronous creation notification waits for that dispatch to unwind, preserving created/disposed pairing. Teardown runs stop and drain → unwind scope → detach agent → detach session; the id becomes reusable after private scope cleanup. Ordinary non-vetoing `agent/*` notifications go through `agentEvents(ctx, agent)`, and per-step assembly goes through `assembleContextFor(agent)`.

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-agent-loop",
"description": "The concrete agent loop plugin for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/core/agent-loop"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -24,18 +31,18 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-scope": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -47,6 +54,6 @@
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -31,7 +31,7 @@ import type { EpochHeader, RequestContext, Session, SessionId, TurnEndReason, Us
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
import { joinContextSections, renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { RuntimeContextProjection } from './runtime-context.ts'
import { executeToolCalls } from './tool-calls.ts'

View File

@@ -5,9 +5,9 @@
* @module @deepseek-ai/dsh-agent-loop
*/
import { Context, FiberState, Service } from 'cordis'
import { Context, FiberState, Service } from '@deepseek-ai/cordis'
import { randomUUID } from 'node:crypto'
import z from 'schemastery'
import z from '@deepseek-ai/schemastery'
import { emitAgentEvent } from '@deepseek-ai/dsh-agent'
import type {
Agent,
@@ -156,7 +156,7 @@ interface PreparedAgent {
dispose(): Promise<void>
}
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
agentLoop: AgentLoop
/**

View File

@@ -3,7 +3,7 @@
* @module @deepseek-ai/dsh-agent-loop/invariant
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { isAgentLoopRequest, type GenerateOptions } from '@deepseek-ai/dsh-llm'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import { foldRequestHeader } from '@deepseek-ai/dsh-session'

View File

@@ -7,7 +7,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContextSnapshotSection } from '@deepseek-ai/dsh-llm'
import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
import { isReplacementSurfaceEvent } from '@deepseek-ai/dsh-session'
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
const SOURCE = '@deepseek-ai/dsh-system-prompt'
const CLEARED = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'

View File

@@ -11,7 +11,7 @@
* @module dsh-agent-loop/tool-calls
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { assertNever, createToolResultMessage, type ToolCallBlock } from '@deepseek-ai/dsh-llm'
import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
import { TOOL_ABORTED_BEFORE_DISPATCH, TOOL_REGISTRY_SCHEDULER, type ToolExecutionInput, type ToolExecutionMode, type ToolExecutionResult, type ToolRunContext } from '@deepseek-ai/dsh-tools'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context, type Fiber } from 'cordis'
import { Context, type Fiber } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmService, { createUserMessage, CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'

View File

@@ -1,6 +1,6 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmService from '@deepseek-ai/dsh-llm'

View File

@@ -8,7 +8,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'

View File

@@ -1,6 +1,6 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -113,27 +113,19 @@ describe('config-driven session id', () => {
const config = { agents: [{ id: 'main', sessionId: SessionId('config-exact-reload'), provider: 'mock', model: 'mock' }] }
const firstLoop = await ctx.plugin(AgentLoop, config)
let first: Agent | undefined
for (let i = 0; i < 50 && first === undefined; i++) {
await new Promise(resolve => setTimeout(resolve, 5))
first = ctx.agents.get(SessionId('config-exact-reload'))
}
expect(first).toBeDefined()
first!.followup(createUserMessage({ content: [{ type: 'text', text: 'remember me' }], source: { kind: 'user' } }))
await waitForIdle(ctx, first!)
await expect.poll(() => ctx.agents.get(SessionId('config-exact-reload')), { timeout: 5_000 }).toBeDefined()
const first = ctx.agents.get(SessionId('config-exact-reload'))!
first.followup(createUserMessage({ content: [{ type: 'text', text: 'remember me' }], source: { kind: 'user' } }))
await waitForIdle(ctx, first)
await firstLoop.dispose()
const secondLoop = await ctx.plugin(AgentLoop, config)
let second: Agent | undefined
for (let i = 0; i < 50 && second === undefined; i++) {
await new Promise(resolve => setTimeout(resolve, 5))
second = ctx.agents.get(SessionId('config-exact-reload'))
}
expect(second).toBeDefined()
expect(JSON.stringify(second!.session.deriveMessages())).toContain('remember me')
second!.followup(createUserMessage({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } }))
await waitForIdle(ctx, second!)
await ctx.sessions.flush(second!.session)
await expect.poll(() => ctx.agents.get(SessionId('config-exact-reload')), { timeout: 5_000 }).toBeDefined()
const second = ctx.agents.get(SessionId('config-exact-reload'))!
expect(JSON.stringify(second.session.deriveMessages())).toContain('remember me')
second.followup(createUserMessage({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } }))
await waitForIdle(ctx, second)
await ctx.sessions.flush(second.session)
const loaded = await ctx.sessionPersistence.load(SessionId('config-exact-reload'))
expect(loaded.events.filter(event => event.type === 'turn/start')).toHaveLength(2)
@@ -423,18 +415,14 @@ describe('config-driven session id', () => {
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('second')]))
// The deferred resume runs on a microtask after the backend is available.
let resumed: Agent | undefined
for (let i = 0; i < 50 && !resumed; i++) {
await new Promise(r => setTimeout(r, 5))
resumed = ctx2.agents.get(SessionId('sticky-1'))
}
expect(resumed).toBeDefined()
// The deferred resume runs after the backend is available.
await expect.poll(() => ctx2.agents.get(SessionId('sticky-1')), { timeout: 5_000 }).toBeDefined()
const resumed = ctx2.agents.get(SessionId('sticky-1'))!
// The live session id IS the resumed id (NOT a fresh ${id}-session-<uuid>),
// and the prior turn's user message is in the derived history.
expect(resumed!.id).toBe(SessionId('sticky-1'))
expect(resumed!.session.id).toBe('sticky-1')
const derived = resumed!.session.deriveMessages()
expect(resumed.id).toBe(SessionId('sticky-1'))
expect(resumed.session.id).toBe('sticky-1')
const derived = resumed.session.deriveMessages()
expect(JSON.stringify(derived)).toContain('remember me')
await ctx2.fiber.dispose()
})

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, LlmError, MessageSource, ProviderRequestId, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionEvent, SessionId, TurnEndReason, type UserMessage } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, LlmError, StreamChunk, errorChain } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import SessionStore, {
SessionId,
@@ -23,7 +23,7 @@ import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
* `agent/session-start`, `agent/turn-stopping`, and the
* `tools/pre-execute` / `tools/post-execute`
* split with `additionalContexts` buffering. These verify the canonical event
* surface a hook bridge (or a native plugin) programs against, WITHOUT any
* API a hook bridge (or a native plugin) programs against, WITHOUT any
* external protocol — a native plugin uses the typed decisions directly.
*/

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, LlmError, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'

View File

@@ -10,7 +10,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import { createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'

View File

@@ -1,6 +1,6 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmService, { createUserMessage, LlmError } from '@deepseek-ai/dsh-llm'

View File

@@ -6,7 +6,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmModelReasoningInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session'

View File

@@ -1,6 +1,6 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import { RuntimeContextProjection } from '../src/runtime-context.ts'

View File

@@ -1,6 +1,6 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { describe, expect, it } from 'vitest'
import { Context, symbols, type EffectMeta, type Fiber } from 'cordis'
import { Context, symbols, type EffectMeta, type Fiber } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
@@ -605,7 +605,7 @@ describe('agent scope lifecycle', () => {
agentCtx.systemPrompt.section({
name: 'dependency-origin-section',
order: 1,
text: 'factory dependency surface',
text: 'factory dependency API',
})
},
})

View File

@@ -4,7 +4,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createUserMessage, CallId, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'

View File

@@ -8,7 +8,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session'
import SystemPrompt, { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-agent-tool-mode",
"description": "Agent-plane presentation selector: composes one agent's tools as Code Mode, native, or both",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/core/agent-tool-mode"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,12 +32,12 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -40,6 +47,6 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -5,8 +5,10 @@
* The tool registry itself stays on the host plane — the agent loop's
* scheduler, the API proxy's presenters, and every tool plugin are all its
* consumers, so it cannot move into a preset. What a preset CAN own is the
* presentation: `ctx.tools.presentAs()` declares it for the mounting agent
* alone, so a Code Mode agent runs beside native ones in one process.
* presentation: `ctx.tools.presentAs()` declares it for the mounting SCOPE,
* which is the preset's standing mount, so the declaration covers every agent
* joined to that preset and a Code Mode preset runs beside native ones in one
* process. One row per composition, not one per session.
*
* A code mode needs a TypeScript code runtime, which is a host-plane service
* ([`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md)).
@@ -16,8 +18,8 @@
* @module @deepseek-ai/dsh-agent-tool-mode
*/
import type { Context } from 'cordis'
import z from 'schemastery'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { ToolPresentationMode } from '@deepseek-ai/dsh-tools'
// Type-only: brings the `ctx.tools` Context merge into this program.
import type {} from '@deepseek-ai/dsh-tools'
@@ -50,8 +52,8 @@ export const Config: z<Config> = z.object({
})
/**
* Declare this agent's tool presentation.
* @param ctx - the mounting agent's scope context.
* Declare the tool presentation for every agent this composition covers.
* @param ctx - the mounting composition's scope context (a preset's standing scope).
* @param config - the selected presentation.
*/
export function apply(ctx: Context, config: Config): void {

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-agent-tool-mode'

View File

@@ -7,7 +7,7 @@
*/
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createScope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/agent/README.md
README.md: 0fb65b94a3b311aa9f0df09d39dd937cba4cc7b4
README.zh.md: 44f67483343a98c280317793ece544bd0b984596
README.md: f6b698e93b254c97786155e7d2c7e81f07c0d981
README.zh.md: 8b108ca93b11ce5e4af57f9d9c823a4b10eac5ab

View File

@@ -58,6 +58,8 @@ Inbox live notifications are deliberately per-message and minimal: `agent/inbox/
Turn and step boundaries and the model token stream are durable `session/event` facts rather than mirrored `agent/*` notifications. Consumers read `turn/*`, `step/*`, and `assistant/chunk` from the session feed; tool policy and outcome observation belong to the complete pipeline documented by [`dsh-tools`](../tools/README.md).
`foldConsumedWork(events)` reads that feed back for the one question the turn sequence cannot answer alone: what became of the work a log consumed. It returns the latest `turn/end` that accounts for consumed work — a turn that entered a model step, or one that claimed inbox input and then failed, was stopped, or was rejected before reaching one — plus whether accepted work was later cancelled out of the inbox unrun. Both facts come from the log, so a cancellation reads the same whichever owner issued it. A no-step turn that took nothing, or emptied its claim and completed, describes no work and is skipped; a `blocked` end over claimed input is an account, because rejection discarded that input.
### Agent interface (`types.ts`)
The handle every plugin programs against:
@@ -114,6 +116,6 @@ Prefix-stable while an agent's scoped registrations are unchanged. Setup or relo
- **Ambient identity may outlive liveness** — consumers still check `agent.status`, cancellation, and the owning capability contract before lifecycle-sensitive work.
- **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam.
- **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead.
- **`cancel()` clears the inbox by default** — it aborts the in-flight turn plus queued and steering work; `cancel(cause, { keepInbox: true })` aborts only the turn and preserves pending items. There is still no step-only abort that keeps the in-flight turn running ([stop-surface Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md)).
- **`cancel()` clears the inbox by default** — it aborts the in-flight turn plus queued and steering work; `cancel(cause, { keepInbox: true })` aborts only the turn and preserves pending items. There is still no step-only abort that keeps the in-flight turn running ([stop API Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-api.md)).
- **Each additional `UserMessage` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source, so the message cannot name several producers.
- **`SessionStartSource` reserves `'clear'`/`'compact'` with no emitter yet** — only `'startup'`/`'resume'` occur until the driving subsystems land (`TODO(compaction)`).

View File

@@ -58,6 +58,8 @@ inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserte
轮次和步骤边界以及模型 token 流是持久 `session/event` 事实,而不是镜像的 `agent/*` 通知。消费方从会话事件流读取 `turn/*``step/*``assistant/chunk`;工具策略与结果观测属于 [`dsh-tools`](../tools/README.md) 记录的完整流水线。
`foldConsumedWork(events)` 把这条事件流读回来,回答仅凭轮次序列无法回答的那个问题:一份日志消费掉的工作最终怎样了。它返回能够为已消费工作作出交代的最新 `turn/end`——即进入过模型 step 的轮次,或者认领了 inbox 输入、但在进入 step 之前失败、被停下或被拒绝的轮次——并额外给出「已接受的工作此后是否被从 inbox 中取消且从未运行」。两项事实都来自日志,因此无论由哪个所有者发起取消,读出来都一样。没有取走任何输入、或认领批次被改写清空后正常结束的无 step 轮次不描述工作,会被跳过;认领过输入、以 `blocked` 结束的轮次则是一份交代,因为拒绝把这些输入一并丢弃了。
### Agent 接口(`types.ts`
每个插件面向的 handle
@@ -114,6 +116,6 @@ inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserte
- **环境身份可能比存活状态更久**:消费方在生命周期敏感工作前,仍要检查 `agent.status`、取消状态和所属能力约定。
- **委派以外的 agent 间通道**:共享状态、流式子输出和后台/轮询语义仍在当前同步 `ctx.subagents` seam 之外。
- **`agent/session-start` 不能为启动设置门禁**:它仍是同步且不可 veto 的通知;必须在发布前完成的异步组合属于工厂的 `setup(agentCtx)` 事务。
- **`cancel()` 默认清空 inbox**:它会中止正在处理的轮次以及排队和 steering 工作;`cancel(cause, { keepInbox: true })` 只中止轮次并保留待处理项。仍不存在只中止步骤、同时让正在处理的轮次继续运行的操作([关于停止操作接口的 Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md))。
- **`cancel()` 默认清空 inbox**:它会中止正在处理的轮次以及排队和 steering 工作;`cancel(cause, { keepInbox: true })` 只中止轮次并保留待处理项。仍不存在只中止步骤、同时让正在处理的轮次继续运行的操作([关于停止操作接口的 Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-api.md))。
- **每条附加 `UserMessage` 恰好携带一个 `MessageSource`**:多个插件合并到一次工具调用上的贡献会归入同一来源,因此该消息无法列出多个生产者。
- **`SessionStartSource` 预留 `'clear'`/`'compact'`,但还没有发出方**:在驱动子系统落地前,只会出现 `'startup'`/`'resume'``TODO(compaction)`)。

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-agent",
"description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/core/agent"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -30,13 +37,13 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-scope": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-type-meta": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
@@ -46,6 +53,6 @@
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -0,0 +1,108 @@
/**
* How one agent log accounts for the work it consumed.
*
* The turn and step vocabulary alone cannot answer this. A turn that stops
* before its first step leaves a `turn/end` shaped exactly like the balanced
* no-op turns a rejection or an empty claim produces, so reading turns in
* isolation either credits cut-short work as finished or convicts every no-op.
* The missing fact is the inbox's own record: {@link Inbox} logs each mutation
* with `removedCount` and marks a cancellation `outcome: 'canceled'`, which
* separates a turn claiming its input from work being dropped unrun.
*
* @module @deepseek-ai/dsh-agent/consumed-work
*/
import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session'
/** How one agent log accounts for the work it consumed. */
export interface ConsumedWork {
/**
* The latest closed turn that accounts for consumed work: one that entered a
* model step, or one that claimed inbox input and then failed, was stopped,
* or was rejected. Absent when no turn closed over any work.
*/
readonly end?: SessionEvent<'turn/end'>
/**
* Whether accepted work was cancelled out of the inbox, unrun, after that
* turn. This is the only account of input a cancellation took before any turn
* could open over it — no `turn/end` describes it.
*/
readonly droppedUnrun: boolean
}
/**
* Whether a turn that consumed input but never reached a step ends in a way
* that accounts for that input. Only a `completed` end does not: it had
* nothing left to run once its claim was rewritten away. A `blocked` end is
* that input's ending too — the pre-step rejection that produced it discarded
* the claimed messages, so the work it took will never run.
* @param reason - the turn's recorded ending.
* @returns whether the ending accounts for the input the turn took.
*/
function accountsForClaim(reason: TurnEndReason): boolean {
switch (reason.kind) {
case 'completed':
return false
case 'blocked':
case 'aborted':
case 'interrupted':
case 'error':
return true
/* v8 ignore next 4 -- unreachable: the one unnamed built-in, `max-tokens`, requires a step,
* so its turn short-circuits as stepped before this call, and `TurnEndReasonMap` is
* merge-extensible, so a backend-added variant cannot be listed; an unnameable ending over
* consumed input must not read as success. */
default:
return true
}
}
/**
* Fold one agent log, or an owned suffix of one, into its account of consumed
* work. Single pass, and every input is the log itself: no caller has to sample
* live state before cancelling, so a cancellation issued by anyone — the owner's
* teardown, an ancestor's interrupt, an unloading plugin — reads the same.
* @param events - the log, or an owned suffix, to fold.
* @returns the accounting turn when one closed, and whether work was dropped unrun after it.
*/
export function foldConsumedWork(events: readonly SessionEvent[]): ConsumedWork {
const stepped = new Set<number>()
const claimed = new Set<number>()
let open: number | undefined
let end: SessionEvent<'turn/end'> | undefined
let droppedUnrun = false
for (const event of events) {
switch (event.type) {
case 'turn/start':
open = event.data.turn
break
case 'step/start':
stepped.add(event.data.turn)
break
case 'agent/inbox/spliced': {
const { removedCount, outcome, inserted } = event.data
if (removedCount === undefined) break
// A replacement keeps the work pending under a new identity, so only a
// cancellation that leaves nothing behind drops it.
if (outcome === 'canceled') droppedUnrun ||= inserted.length === 0
// Claims are the loop's own step-boundary reads, always inside a turn.
else if (open !== undefined) claimed.add(open)
break
}
case 'turn/end': {
const { turn, reason } = event.data
open = undefined
if (stepped.delete(turn) || (claimed.delete(turn) && accountsForClaim(reason))) {
end = event
// Anything dropped before this turn closed is what its own ending
// reports; only a later drop is still unaccounted for.
droppedUnrun = false
}
break
}
default:
break
}
}
return { ...end === undefined ? {} : { end }, droppedUnrun }
}

View File

@@ -6,7 +6,7 @@
* @module @deepseek-ai/dsh-agent/dispatch
*/
import type { Context, Events } from 'cordis'
import type { Context, Events } from '@deepseek-ai/cordis'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import type { AssembleContext } from '@deepseek-ai/dsh-system-prompt'

View File

@@ -5,8 +5,8 @@
* @module @deepseek-ai/dsh-agent
*/
import { Context, FiberState, getTraceable, Service, symbols } from 'cordis'
import type { Fiber } from 'cordis'
import { Context, FiberState, getTraceable, Service, symbols } from '@deepseek-ai/cordis'
import type { Fiber } from '@deepseek-ai/cordis'
import { AsyncLocalStorage } from 'node:async_hooks'
import { isPromise } from 'node:util/types'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
@@ -18,6 +18,7 @@ import type { Agent, AgentOptions } from './runtime-types.ts'
export * from './runtime-types.ts'
export * from './types.ts'
export * from './inbox.ts'
export * from './consumed-work.ts'
export * from './model-selection.ts'
export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from './dispatch.ts'
export type { AgentEventDispatch, AgentSubjectEvent } from './dispatch.ts'
@@ -32,7 +33,7 @@ declare module '@deepseek-ai/dsh-type-meta' {
}
}
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
agents: AgentRegistry
/**
@@ -159,7 +160,7 @@ export interface ResumeAgentOptions {
* {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers,
* only the holder can tear this agent down. The registered factory provider is
* also a structural owner because the scoped agent depends on that provider's
* service surface; provider unload stops and drains every live handle it made.
* service API; provider unload stops and drains every live handle it made.
* `dispose()` stops the loop, awaits its exit, unregisters the agent, removes
* its session from the store, and finally unwinds its scoped world.
*

View File

@@ -1,6 +1,6 @@
/** Package-owned agent lifecycle invariants. @module @deepseek-ai/dsh-agent/invariant */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'

View File

@@ -3,7 +3,7 @@
* @module @deepseek-ai/dsh-agent/model-selection
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { LlmCallConfig, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
/** Complete provider, model, and optional reasoning effort selected for one live Agent. */

View File

@@ -5,7 +5,7 @@
* @module @deepseek-ai/dsh-agent
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import type { LlmCallConfig, LlmFailure, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
import type { AgentCancelCause, Session, SessionId, UserMessage } from '@deepseek-ai/dsh-session'
@@ -143,7 +143,7 @@ export interface Agent {
inject(message: UserMessage): void
}
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Events {
// ---- lifecycle (emit) ----
/**

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { runInNewContext } from 'node:vm'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'

View File

@@ -1,5 +1,5 @@
import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context, Service, symbols } from 'cordis'
import { Context, Service, symbols } from '@deepseek-ai/cordis'
import { createUserMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
import { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session'
import AgentRegistry, {

View File

@@ -0,0 +1,160 @@
import { describe, expect, it } from 'vitest'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { TurnEndReason } from '@deepseek-ai/dsh-session'
import { foldConsumedWork } from '@deepseek-ai/dsh-agent'
/** One pending message, as the inbox records it. */
function message(text: string) {
return createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } })
}
/** Log an accepted message the way `Inbox.append()` does. */
function accept(session: Session, text: string): void {
session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [message(text)] })
}
/** Log the step-boundary read of one pending message, as `Inbox.claim()` does. */
function claim(session: Session): void {
session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, removedCount: 1, inserted: [] })
}
/** Log a cancellation of one pending message, as `Inbox.clear()` does. */
function cancelPending(session: Session): void {
session.append('agent/inbox/spliced', {
target: 'next-turn', start: 0, removedCount: 1, inserted: [], outcome: 'canceled',
})
}
/** Run one whole turn that reached a model step. */
function steppedTurn(session: Session, turn: number, reason: TurnEndReason): void {
session.append('turn/start', { turn })
claim(session)
session.append('step/start', { turn, step: 1 })
session.append('step/end', { turn, step: 1 })
session.append('turn/end', { turn, reason })
}
describe('foldConsumedWork', () => {
it('reports nothing for a log that consumed no work', () => {
const session = Session.create(SessionId('empty'))
accept(session, 'queued')
expect(foldConsumedWork(session.events)).toEqual({ droppedUnrun: false })
})
it('reports the latest turn that entered a model step', () => {
const session = Session.create(SessionId('stepped'))
steppedTurn(session, 1, { kind: 'completed' })
steppedTurn(session, 2, { kind: 'max-tokens' })
expect(foldConsumedWork(session.events).end?.data)
.toEqual({ turn: 2, reason: { kind: 'max-tokens' } })
})
it('reports a turn that claimed its input and then failed before any step', () => {
const session = Session.create(SessionId('failed-claim'))
steppedTurn(session, 1, { kind: 'completed' })
// The step boundary runs the durability checkpoint and prompt assembly, so a
// turn can take its input and then fail without entering a step.
session.append('turn/start', { turn: 2 })
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'error', error: { message: 'ENOSPC', code: 'UNKNOWN' } } })
expect(foldConsumedWork(session.events).end?.data.turn).toBe(2)
})
it('reports a turn that claimed its input and was then stopped before any step', () => {
const session = Session.create(SessionId('stopped-claim'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('turn/start', { turn: 2 })
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'aborted', reason: { kind: 'user' } } })
expect(foldConsumedWork(session.events).end?.data.turn).toBe(2)
})
it('ignores a turn stopped, failed, or rejected without taking any input', () => {
const session = Session.create(SessionId('no-claim'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('turn/start', { turn: 2 })
session.append('turn/end', { turn: 2, reason: { kind: 'aborted', reason: { kind: 'parent' } } })
session.append('turn/start', { turn: 3 })
session.append('turn/end', { turn: 3, reason: { kind: 'error', error: { message: 'x', code: 'UNKNOWN' } } })
session.append('turn/start', { turn: 4 })
session.append('turn/end', { turn: 4, reason: { kind: 'blocked' } })
// None of these turns describes work: they opened, found nothing of their own, and closed.
expect(foldConsumedWork(session.events).end?.data.turn).toBe(1)
})
it('reports a turn whose claimed input a pre-step rejection discarded', () => {
const session = Session.create(SessionId('rejected-claim'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('turn/start', { turn: 2 })
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'blocked' } })
// Rejection does not retain the claimed messages, so the `blocked` end is
// the only account of input that will never run.
expect(foldConsumedWork(session.events).end?.data.turn).toBe(2)
})
it('ignores a claim its own turn emptied', () => {
const session = Session.create(SessionId('emptied-claim'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('turn/start', { turn: 2 })
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
// An emptied claim ran nothing and dropped nothing: a listener rewrote the
// batch away, which is not this log's account of the work.
expect(foldConsumedWork(session.events).end?.data.turn).toBe(1)
})
it('credits a claim with no open turn to no turn at all', () => {
const session = Session.create(SessionId('mid-turn-suffix'))
steppedTurn(session, 1, { kind: 'completed' })
// An owned suffix can begin inside a turn whose start it does not contain,
// so a claim may appear with no turn to attribute it to.
claim(session)
session.append('turn/end', { turn: 2, reason: { kind: 'aborted', reason: { kind: 'user' } } })
expect(foldConsumedWork(session.events).end?.data.turn).toBe(1)
})
it('reports work cancelled out of the inbox after the last accounting turn', () => {
const session = Session.create(SessionId('dropped'))
steppedTurn(session, 1, { kind: 'completed' })
accept(session, 'never runs')
cancelPending(session)
// No turn opened over it, so only the cancellation says the work was cut short.
expect(foldConsumedWork(session.events)).toEqual({
end: session.events.find(event => event.type === 'turn/end'),
droppedUnrun: true,
})
})
it('keeps a replacement pending rather than counting it as dropped', () => {
const session = Session.create(SessionId('replaced'))
steppedTurn(session, 1, { kind: 'completed' })
session.append('agent/inbox/spliced', {
target: 'next-turn', start: 0, removedCount: 1, inserted: [message('rewritten')], outcome: 'canceled',
})
expect(foldConsumedWork(session.events).droppedUnrun).toBe(false)
})
it('lets a later accounting turn absorb an earlier drop', () => {
const session = Session.create(SessionId('absorbed'))
steppedTurn(session, 1, { kind: 'completed' })
cancelPending(session)
steppedTurn(session, 2, { kind: 'completed' })
expect(foldConsumedWork(session.events)).toEqual({
end: session.events.findLast(event => event.type === 'turn/end'),
droppedUnrun: false,
})
})
})

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import { scopeTarget } from '@deepseek-ai/dsh-scope'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import {
agentEvents,

View File

@@ -1,5 +1,5 @@
/**
* Negative-path tests for the export-surface JSDoc gate (`scripts/verify-export-jsdoc.ts`).
* Negative-path tests for the exported-API JSDoc gate (`scripts/verify-export-jsdoc.ts`).
*/
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
@@ -46,7 +46,7 @@ export function hiddenFn(value: string): string { return value }
expect(violations.every(violation => violation.includes('publicFn'))).toBe(true)
})
it('accepts a fully documented surface', () => {
it('accepts a fully documented API', () => {
expect(collectExportJsdocViolations(make(`
/**
* Add one to a count.
@@ -106,7 +106,7 @@ export const halve = (n: number): number => n / 2
'/**\n * Docs.\n * @param ghost - not real.\n */\nexport function f({ a }: { a: number }): void {}\n',
))
expect(violations).toEqual([
expect.stringMatching(/parameter '\{ a \}' is a binding pattern; the export surface needs simple identifier parameters/),
expect.stringMatching(/parameter '\{ a \}' is a binding pattern; the exported API needs simple identifier parameters/),
expect.stringMatching(/@param ghost does not match any parameter \(stale tag\?\)/),
])
})
@@ -154,7 +154,7 @@ describe('verify-export-jsdoc type-level exports', () => {
it('skips `declare module` augmentation bodies (the cordis gate owns them)', () => {
expect(collectExportJsdocViolations(make(
"declare module 'cordis' {\n interface Events {\n 'fix/x'(): void\n }\n}\nexport {}\n",
"declare module '@deepseek-ai/cordis' {\n interface Events {\n 'fix/x'(): void\n }\n}\nexport {}\n",
))).toEqual([])
})
})
@@ -166,9 +166,9 @@ describe('verify-export-jsdoc export forms', () => {
))).toEqual([expect.stringMatching(/exported function 'f' .* has no JSDoc\./)])
})
it('does not treat a never-exported sibling declarator as surface', () => {
it('does not treat a never-exported sibling declarator as API', () => {
// `export { publicValue }` resolves to the whole variable statement; only
// the named declarator is surface — the gate must not demand JSDoc for
// the named declarator is exported — the gate must not demand JSDoc for
// the private sibling sharing the statement.
expect(collectExportJsdocViolations(make(
'/** The public knob. */\nconst publicValue = 1, privateHelper = 2\nexport { publicValue }\nvoid privateHelper\n',
@@ -177,7 +177,7 @@ describe('verify-export-jsdoc export forms', () => {
it('unions declarators across multiple export lists over one statement', () => {
// Two lists each name one declarator of the same undocumented statement:
// both are surface (deduplicating on first resolution would drop `b`),
// both are exported (deduplicating on first resolution would drop `b`),
// while the never-exported `c` stays out.
const violations = collectExportJsdocViolations(make(
'const a = 1, b = 2, c = 3\nexport { a }\nexport { b }\nvoid c\n',
@@ -344,7 +344,7 @@ describe('verify-export-jsdoc fail-closed forms', () => {
))).toEqual([])
})
it('treats an inline function-type annotation as the surface signature', () => {
it('treats an inline function-type annotation as the API signature', () => {
expect(collectExportJsdocViolations(make(
'/** Maps a number. */\nexport declare const f: (x: number) => number\n',
))).toEqual([
@@ -402,7 +402,7 @@ describe('verify-export-jsdoc fail-closed forms', () => {
])
})
it('treats a single-call-signature type literal as the surface signature', () => {
it('treats a single-call-signature type literal as the API signature', () => {
expect(collectExportJsdocViolations(make(
'/** Maps. */\nexport declare const f: { (x: number): number }\n',
))).toEqual([

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/scope/README.md
README.md: a8fbe97ae3b59f223bb52e44860439803fda420c
README.md: 1ed09acd7ccb864b16fb5d2ac4390264085d1c9e
README.zh.md: af238232987c74e89cdc4e009d3d0c40f71b02d8

View File

@@ -28,10 +28,10 @@ The registration context determines both visibility and ownership, preventing a
Scope-aware services define a concrete `ScopeLayer` that aggregates their heterogeneous tables and domain helpers. `ScopedLayers.effect()` accepts one synchronous action returning one synchronous undo, installs that undo before optional notification, and reclaims an exact-scope layer only when the complete aggregate is empty. `notify` defaults to `true`; the supplied callback owns whether observer failures throw or are contained. `EntryValues` remains internal, the storage classes are imported from the package root rather than a `/store` subpath, and the shared storage does not define registry-specific filtering or iteration policy. See the [shared scoped-layer storage Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md).
Handing out a scoped context hands out the minting plugin's service-resolution surface (resolution walks the minting fiber's dependency chain, not the holder's) — mint it from the plugin whose dependencies the scoped registrations need to resolve.
Handing out a scoped context hands out the minting plugin's service-resolution API (resolution walks the minting fiber's dependency chain, not the holder's) — mint it from the plugin whose dependencies the scoped registrations need to resolve.
## Known Limitations and Deferred Work
- **Only scope-aware surfaces isolate state** — registries must file by `scopeOf()` and events must dispatch through `scopeTarget()`; an arbitrary Cordis service remains context-global merely because it is called through a scoped context.
- **Only scope-aware APIs isolate state** — registries must file by `scopeOf()` and events must dispatch through `scopeTarget()`; an arbitrary Cordis service remains context-global merely because it is called through a scoped context.
- **A context carries one nearest scope key** — the hierarchy lives in the key-level parent relation, not in context tags; nested scope CONTEXTS still shadow to a single tag, and multi-membership policy sets remain unsupported.
- **Service reachability comes from the scope minter** — handing out `Scope.ctx` also hands out the minting plugin's injected service surface, so a broader minter cannot later be narrowed by the holder.
- **Service reachability comes from the scope minter** — handing out `Scope.ctx` also hands out the minting plugin's injected services, so a broader minter cannot later be narrowed by the holder.

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-scope",
"description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/core/scope"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,11 +32,11 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -5,8 +5,8 @@
* @module @deepseek-ai/dsh-scope
*/
import type { Context, Fiber } from 'cordis'
import { Context as CordisContext } from 'cordis'
import type { Context, Fiber } from '@deepseek-ai/cordis'
import { Context as CordisContext } from '@deepseek-ai/cordis'
export { AnonymousEntries, NamedEntries, ScopedLayers } from './store.ts'
export type { ScopeLayer } from './store.ts'
@@ -128,8 +128,8 @@ export interface CreateScopeOptions {
/**
* Mint a scope under `ctx`. The scoped context inherits the minting plugin's
* dependency surface and owns every registration made through it.
* @param ctx - active context whose dependency surface the scope inherits.
* dependency API and owns every registration made through it.
* @param ctx - active context whose dependency API the scope inherits.
* @param key - opaque identity used for listener routing.
* @param options - optional scope-chain placement.
* @returns the scoped context and exact/shared disposal boundaries.

View File

@@ -1,6 +1,6 @@
/** Package-owned scoped-dispatch invariants. @module @deepseek-ai/dsh-scope/invariant */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import { carrierKeyOf, isScopeCarrier } from '@deepseek-ai/dsh-scope'
import { scopedSubjectResolverFor } from './scoped-events.generated.ts'

View File

@@ -4,7 +4,7 @@
* @module @deepseek-ai/dsh-scope
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { scopeChainOf, scopeOf } from './index.ts'
import type { ScopeKey } from './index.ts'

View File

@@ -1,7 +1,7 @@
import { freezeMessage, MessageId } from '@deepseek-ai/dsh-llm'
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import type { Events } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { Events } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import * as ScopeInvariant from '@deepseek-ai/dsh-scope/invariant'

View File

@@ -1,9 +1,9 @@
import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { bindScopeParent, carrierKeyOf, createScope, isScopeCarrier, scopeChainOf, scopeOf, scopeParentOf, scopeTarget } from '@deepseek-ai/dsh-scope'
import type { Scope, Scoped } from '@deepseek-ai/dsh-scope'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Events {
/**
* Test-only event for scope-filtered dispatch.

View File

@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import {
AnonymousEntries,
createScope,

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/session/README.md
README.md: db477d94037d3463870fc8e66ea35d5e607fb6fe
README.zh.md: 1ce1e823a7e0fdbcf7b6898764a89c52b74adf6a
README.md: 57569e9c0dbfa7cb696e3a561a9ff108c2ac981f
README.zh.md: 16629dc70c79ca838ba7088aeafcc5b38b124f87

View File

@@ -76,10 +76,11 @@ Also defines `TurnEndReasonMap`, the merge-extensible `kind`-tagged sum type for
An interrupted live turn ends with `{ kind: 'aborted', reason: AgentCancelCause }`, preserving the typed cancellation cause in the durable transcript. Persistence imports the coarse aborted outcome from the supported older format as `{ kind: 'aborted', reason: { kind: 'legacy' } }`, because that record did not retain its caller. A turn failure carries `{ kind: 'error', error }`; crash recovery alone synthesizes `{ kind: 'interrupted' }`.
Every `SessionEvent` carries two optional top-level fields (structural metadata):
Every `SessionEvent` carries three optional top-level fields (structural metadata):
- `sourceEventSeqs?: number[]` — seq numbers of earlier events cited as sources (e.g., the `assistant/chunk` seqs behind an `assistant/message`, or the shadowed entries behind a compaction replacement entry). On `assistant/message`, a present `[]` records a known empty provider stream, while omission means a legacy or foreign event did not record the source stream; other surface events require a non-empty list when this field is present.
- `surfaceOp?: SurfaceOp` — how this event entered the surface. Absent for non-surface events (boundaries, chunks, usage, errors).
- `ignorable?: true` — marks an event a reader may safely skip when it does not recognize the type; absent means required, so an unknown-type event refuses session reconstruction ([mechanism](../../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md)).
### Metadata types (`types.ts`)
@@ -139,5 +140,5 @@ Logging causes no invalidation, and exact reconstruction preserves request-prefi
- **Session branching/tree** (pi-style entry tree) — deferred unless needed beyond boundary-based `fork()`.
- **`fork()` cuts only at stable boundaries of live sessions** — the selected prefix must end outside an open turn and the source must be in the store; forking a persisted-but-unloaded session is excluded from the [fork API](../../../.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md).
- **`SESSION_FORMAT_VERSION` stays pinned at `0`** — pre-release, no broad compatibility implied: `Session` accepts only current seed shapes and a backend rejects any other version. Narrow storage import upgrades belong to the persistence boundary ([policy](../../../AGENTS.md), [pre-identity message recovery](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md)).
- **`SESSION_FORMAT_VERSION` stays pinned at `0`** — pre-release, no broad compatibility implied: `Session` accepts only current seed shapes, and a backend refuses any other version naming the direction (newer: "written by a newer harness — upgrade"; older: no upgrade path ships yet). Unknown event types refuse the same way unless marked `ignorable` in the envelope; the versioning mechanism is the [session-log-version-mechanism note](../../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md). Narrow storage import upgrades belong to the persistence boundary ([policy](../../../AGENTS.md), [pre-identity message recovery](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md)).
- **`TurnEndReasonMap` omits the ACP-named `refusal` / `max_turn_requests` variants** — producer-gated: they land when an adapter or the loop first emits them.

View File

@@ -76,10 +76,11 @@
被中断的实时轮次以 `{ kind: 'aborted', reason: AgentCancelCause }` 结束,在持久 transcript文本记录中保留类型化取消原因。持久化会将受支持旧格式中的粗粒度中止结果导入为 `{ kind: 'aborted', reason: { kind: 'legacy' } }`,因为该记录没有保留调用方。轮次失败携带 `{ kind: 'error', error }`;只有崩溃恢复会合成 `{ kind: 'interrupted' }`
每个 `SessionEvent` 都有个可选顶层字段(结构元数据):
每个 `SessionEvent` 都有个可选顶层字段(结构元数据):
- `sourceEventSeqs?: number[]`:被引用为来源的较早事件 seq例如 `assistant/message` 引用的 `assistant/chunk` seq或压缩替换条目引用的已遮蔽条目。对于 `assistant/message`,存在的 `[]` 表示已知提供方流为空;省略则表示旧版或外部事件没有记录源流。其他 surface 事件若有此字段,则要求非空列表。
- `surfaceOp?: SurfaceOp`:事件进入 surface 的方式。非 surface 事件(边界、分片、用量、错误)不含该字段。
- `ignorable?: true`:标记读取器在不认识事件类型时可以安全跳过该事件;缺失表示必需,不认识的事件类型会使会话重建被拒绝([机制](../../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md))。
### 元数据类型(`types.ts`
@@ -139,5 +140,5 @@
- **会话分支/树**pi 风格条目树):除非需要超越基于边界的 `fork()` 能力,否则暂缓。
- **`fork()` 仅在实时会话的稳定边界处切分**:所选前缀结束时不得有开放轮次,且源会话必须位于存储中;[fork API](../../../.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md) 不支持对已持久化但未加载的会话进行 fork。
- **`SESSION_FORMAT_VERSION` 固定为 `0`**:预发布阶段不承诺广泛兼容性;`Session` 只接受当前 seed 形状,后端拒绝其他任何版本。范围受限的存储导入升级应由持久化边界负责([政策](../../../AGENTS.md)、[消息标识机制引入前的消息恢复](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md))。
- **`SESSION_FORMAT_VERSION` 固定为 `0`**:预发布阶段不承诺广泛兼容性;`Session` 只接受当前 seed 形状,后端拒绝其他任何版本并说明方向(更新的版本提示"由更新的 harness 写入,请升级";更旧的版本说明尚无升级路径)。不认识的事件类型同样被拒绝,除非信封带 `ignorable` 标记;版本机制见 [session-log 版本机制 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md)。范围受限的存储导入升级应由持久化边界负责([政策](../../../AGENTS.md)、[消息标识机制引入前的消息恢复](../../../.agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md))。
- **`TurnEndReasonMap` 不含 ACPAgent Client Protocol命名的 `refusal``max_turn_requests` 变体**:受生产方约束;只有当适配器或循环首次产生这些变体时才加入。

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-session",
"description": "Event-sourced session store for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/core/session"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -34,12 +41,12 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-brand": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-scope": "^0.0.1",
"@deepseek-ai/dsh-type-meta": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
@@ -48,6 +55,6 @@
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -6,7 +6,7 @@
* @module @deepseek-ai/dsh-session
*/
import { Context, Service } from 'cordis'
import { Context, Service } from '@deepseek-ai/cordis'
import { isAbsolute } from 'node:path'
import { deepFreeze } from '@deepseek-ai/dsh-llm'
import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope'
@@ -32,29 +32,9 @@ export type { ChunkRow, StorageRecord } from './chunk-rows.ts'
export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult } from './surface.ts'
export { deriveEventMessage, foldSurface, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEvent, isSurfaceEligibleType } from './surface.ts'
export { canonicalHeader, foldRequestHeader, headerEquals } from './request-header.ts'
export { KNOWN_SESSION_EVENT_TYPES } from './known-event-types.ts'
/**
* Find the latest closed turn that entered at least one model step, ignoring
* balanced no-step turns produced by rejection, empty input, or cancellation.
* @param events - session events, or an owned suffix, to inspect.
* @returns the latest matching turn end, or `undefined`.
*/
export function findLastMessageTurnEnd(
events: readonly SessionEvent[],
): SessionEvent<'turn/end'> | undefined {
const steppedTurns = new Set<number>()
let latest: SessionEvent<'turn/end'> | undefined
for (const event of events) {
if (event.type === 'step/start') {
steppedTurns.add(event.data.turn)
continue
}
if (event.type === 'turn/end' && steppedTurns.delete(event.data.turn)) latest = event
}
return latest
}
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
sessions: SessionStore
}
@@ -243,6 +223,7 @@ function assertSessionEventEnvelope(value: Record<string, unknown>, index: numbe
case 'data':
case 'surfaceOp':
case 'sourceEventSeqs':
case 'ignorable':
break
default:
throw new Error(`seed event at index ${index} has an invalid event envelope`)
@@ -254,7 +235,8 @@ function assertSessionEventEnvelope(value: Record<string, unknown>, index: numbe
if (typeof type !== 'string'
|| typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < 0
|| typeof time !== 'number' || !Number.isSafeInteger(time)
|| event['data'] === undefined) {
|| event['data'] === undefined
|| (event['ignorable'] !== undefined && event['ignorable'] !== true)) {
throw new Error(`seed event at index ${index} has an invalid event envelope`)
}
switch (type) {

View File

@@ -5,7 +5,7 @@
* @module @deepseek-ai/dsh-session/invariant
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { assertNever } from '@deepseek-ai/dsh-llm'
import type { CallId } from '@deepseek-ai/dsh-llm'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'

View File

@@ -0,0 +1,63 @@
/**
* GENERATED by `scripts/gen-persistence-catalog.ts` — do not edit by hand; run
* `pnpm run gen-persistence-catalog` to regenerate (verified fresh by
* `pnpm run verify-persistence-catalog`, part of `doc-sync`).
* @module @deepseek-ai/dsh-session/known-event-types
*/
/**
* Every `SessionEventMap` member declared in this repository — the event
* vocabulary this build understands. The persistence read path refuses to
* interpret a log containing a type outside this set unless the event
* carries the envelope's `ignorable` marker (see `SessionEvent.ignorable`
* in `./types.ts`): such a log was likely written by a newer harness, and
* silently skipping a required event would reconstruct a wrong session.
* Downstream (out-of-repo) plugin events are outside this list by
* construction; a registration surface for them is deferred until such a
* consumer exists.
*/
export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([
'agent-preset/selected',
'agent/inbox/spliced',
'approval/asked',
'approval/decided',
'approval/policy',
'assistant/chunk',
'assistant/message',
'command/done',
'command/run',
'compact/end',
'compact/prune',
'compact/start',
'compact/summary',
'feedback/record',
'goal/change',
'hook/invoked',
'hook/result',
'llm/retry',
'llm/retry-started',
'permission/preset',
'plan/mode',
'request/context',
'request/header',
'sandbox/mode',
'session/end-seed',
'session/title',
'session/title-llm-request',
'step/end',
'step/start',
'subagent/descriptor',
'todo/write',
'tool-workflow/agent-end',
'tool-workflow/agent-start',
'tool-workflow/run-end',
'tool-workflow/run-start',
'tool/call',
'tool/code-dispatch',
'tool/code-dispatch-start',
'tool/result',
'turn/end',
'turn/start',
'user/message',
'web/deepseek-search-llm-request',
])

View File

@@ -30,8 +30,23 @@ export function SessionId(id: string): SessionId {
* and enforced by every persistence backend on load. The single source of truth for the
* version — write sites and the load-time check all read it.
* While the harness is unreleased it is pinned at `0`: no compatibility is
* implied, incompatible logs are rejected, and no migration is provided. A
* monotonic version policy starts with the first tagged release.
* implied, incompatible logs are rejected, and no migration is provided.
*
* The version is a single monotonic integer with no major/minor split. Whether
* a bump is needed is decided by what the WRITER emits, never by what a newer
* reader can accept: bump exactly when an older runtime could no longer handle
* a new log with full semantic correctness ("parses without error" is not
* correctness — silently skipping content that shapes reconstruction is a
* wrong read). Only structural changes reach that bar: the header shape, the
* {@link SessionEvent} envelope, core event semantics, or the surface
* mechanism (the {@link SurfaceEventType} set and {@link SurfaceOp} variants).
* Adding an ordinary event type does not bump — the per-event
* {@link SessionEvent.ignorable} guard covers vocabulary growth instead. When
* in doubt, bump: a near-identity upgrade step is almost free, a missed bump
* makes older runtimes read new logs wrong silently. The full mechanism
* (upgrade-step chain, in-memory view conversion, migrate-on-continue) is
* recorded in the session-log-version-mechanism Agent Note
* (`.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md`).
*/
export const SESSION_FORMAT_VERSION = 0
@@ -389,6 +404,17 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
/** Unix epoch milliseconds. */
time: number
data: SessionEventMap[K]
/**
* Marks an event a reader may safely skip when it does not recognize
* `type`. Absent means required: a reader meeting an unrecognized type
* without this marker MUST refuse to reconstruct the session instead of
* silently dropping the event, because an unrecognized required event may
* change how the rest of the log is interpreted. A writer sets `true` only
* on purely informational records whose loss cannot affect reconstruction;
* defaulting to required means a forgotten marker over-refuses (an
* inconvenience) rather than silently resuming a gutted session.
*/
ignorable?: true
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of earlier events that this event cites as sources

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createUserMessage, CallId , createMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionForkError, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createScope, scopeTarget } from '@deepseek-ai/dsh-scope'
import { createUserMessage, CallId, createMessage, createToolResultMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TOOL_NOT_STARTED } from '@deepseek-ai/dsh-session'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createScope, scopeOf } from '@deepseek-ai/dsh-scope'
import type { Scope, ScopeKey } from '@deepseek-ai/dsh-scope'
import SessionStore from '@deepseek-ai/dsh-session'

View File

@@ -1,5 +1,5 @@
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createUserMessage, CallId, createMessage, createToolResultMessage, MessageId, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import SessionStore, {
adoptSessionEvent,
@@ -7,28 +7,11 @@ import SessionStore, {
Session,
SessionEvent,
SessionId,
findLastMessageTurnEnd,
snapshotSessionEvent,
} from '@deepseek-ai/dsh-session'
import type { CreateSessionOptions, SessionEventType, SessionHeader, SessionSurface, TodoItem } from '@deepseek-ai/dsh-session'
describe('Session', () => {
it('finds the latest closed turn that entered a model step', () => {
const session = Session.create(SessionId('last-message-turn'))
session.append('turn/start', { turn: 1 })
session.append('turn/end', { turn: 1, reason: { kind: 'blocked' } })
expect(findLastMessageTurnEnd(session.events)).toBeUndefined()
session.append('turn/start', { turn: 2 })
session.append('step/start', { turn: 2, step: 1 })
session.append('step/end', { turn: 2, step: 1 })
session.append('turn/end', { turn: 2, reason: { kind: 'max-tokens' } })
expect(findLastMessageTurnEnd(session.events)?.data)
.toEqual({ turn: 2, reason: { kind: 'max-tokens' } })
})
it('exposes one stable readonly surface view', () => {
const session = Session.create(SessionId('surface-view'))
const surface = session.surface
@@ -1090,12 +1073,20 @@ describe('Session', () => {
{ ...base, time: '1' },
{ ...base, time: 0.5 },
{ type: base.type, seq: base.seq, time: base.time },
{ ...base, ignorable: false },
{ ...base, ignorable: 'yes' },
]
for (const [index, event] of cases.entries()) {
expect(() => Session.create(SessionId(`bad-envelope-${index}`), [event as SessionEvent]))
.toThrow(/invalid event envelope/)
}
// `ignorable: true` is the one accepted marker value (unknown-type skip contract).
const marked = Session.create(SessionId('ignorable-envelope'), [
{ ...base, ignorable: true } as SessionEvent,
])
expect(marked.events[0]?.ignorable).toBe(true)
})
})

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/system-prompt/README.md
README.md: 13b05bfcd19212ade42f22ece455871d022e6260
README.zh.md: 0f9e7a2358134018975db1bc3c6b7206a274b3ec
README.md: cedda783d549633f5be9765a9a074e968d99500d
README.zh.md: 41729cdd1cfe6ebbd86f38c15bab5c50bd6ff7d2

View File

@@ -16,19 +16,19 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem
### Public API
- `ctx.systemPrompt.section(section: PromptSection): () => void` Contribute a section. The layer is the calling context's scope: `agent.ctx` contributes to that agent alone, shadowing a same-named global section there. Duplicate names within one layer and non-finite orders throw. Disposed with the calling fiber.
- `ctx.systemPrompt.section(section: PromptSection): () => void` Contribute a section. The layer is the calling context's scope: `agent.ctx` contributes to that agent alone, shadowing a same-named global section there. A `complete: true` section becomes the exact complete prompt after the assembly waterfall; more than one effective complete section rejects assembly. Duplicate names within one layer and non-finite orders throw. Disposed with the calling fiber.
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void` Contribute tool schemas, evaluated at each assembly with that assembly's context. `ToolProviderResult` = `{ schemas, knownNames? }`: `schemas` is the post-restriction visible set; `knownNames` is the pre-restriction universe used by `toolOrder`. A provider must not return a schema named `TOOL_ORDER_REST`. Scoped providers are consulted only for their scope's assemblies. Disposed with the calling fiber.
- `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => void` Contribute a prompt variable, referenced from section text as `{{name}}`. Scoped variables shadow a same-named global for that agent. Duplicate-in-layer or unreferenceable names throw; `undefined` means "no value for this assembly". Disposed with the calling fiber.
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>` Assemble the prompt for one caller: the global layer merged with `context.scope`'s layer, with tool schemas detached before the transform waterfall. Runs through the scope-filtered `system-prompt/assemble` waterfall and returns its authoritative result. An optional `context.signal` explicitly controls this assembly request; providers and listeners may cooperate with it but must not retain it for another turn. Rejects when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name.
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>` Assemble the prompt for one caller: the global layer merged with `context.scope`'s layer, with tool schemas detached before the transform waterfall. Runs through the scope-filtered `system-prompt/assemble` waterfall, then restores an effective complete section as the sole prompt section. An optional `context.signal` explicitly controls this assembly request; providers and listeners may cooperate with it but must not retain it for another turn. Rejects for multiple complete sections, when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name.
### Live events
`system-prompt/assemble` is authoritative; listeners that replace entries must preserve any active Code Mode or structured-output protocol. Use [`ToolRegistry.restrict()`](../tools/README.md) when filtering must stay aligned across presentation, lookup, and execution. Registry-change notifications are unfiltered. The generated region of [system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) owns signatures and dispatch contracts.
`system-prompt/assemble` is authoritative for ordinary sections; a complete section is the final prompt constraint applied after the waterfall. Listeners that replace entries must preserve any active Code Mode or structured-output protocol. Use [`ToolRegistry.restrict()`](../tools/README.md) when filtering must stay aligned across presentation, lookup, and execution. Registry-change notifications are unfiltered. The generated region of [system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) owns signatures and dispatch contracts.
### Key types
- `AssembleContext` — what one `assemble()` call is FOR. Merge-extensible; declares `scope?: ScopeKey` (the layer selector) and `signal?: AbortSignal` (the explicit request control capability) here, while `dsh-agent` declares `agent?: Agent` (the typed DX field — never set without `scope`; use `assembleContextFor(agent, signal)`). Providers must tolerate absent fields because a bare `assemble()` carries an empty, scope-less, signal-less context. `signal` is a request value, not part of the ambient Agent execution frame.
- `PromptSection``{ name, order, text }`. Sections are concatenated in ascending `order`. Order bands: `-100` is the harness identity, `0` the deployment persona, tool guidance uses `100199`.
- `PromptSection``{ name, order, text, complete? }`. Sections are concatenated in ascending `order`. Order bands: `-100` is the harness identity, `0` the deployment persona, tool guidance uses `100199`. One effective `complete` section suppresses all other sections after cooperative assembly.
- `PromptAssembly``{ sections: AssembledSection[], tools: ToolSchema[], variables: Record<string, string | undefined> }`. Section texts arrive resolved but not yet interpolated; `variables` holds every registered variable resolved against the context. Tool schemas are part of the assembly by design: "what the model is told it can do" is one coherent thing, even though adapters transmit schemas as a separate wire field.
- `renderPrompt(assembly)` — interpolates `{{variable}}` references in each section, drops empty sections, joins with blank lines. STRICT: an unknown reference (`Object.hasOwn` lookup — prototype names like `{{constructor}}` are unknown), a registered-but-valueless reference, a malformed complete `{{…}}` group, or a `{{` that opens no complete group while a `}}` still follows (`{{{model}}}`) throws — fail loud beats shipping a malformed prompt. A lone `{{` with no `}}` anywhere after it passes through verbatim; substituted values are never re-scanned.
@@ -39,7 +39,7 @@ Merge-extensible: plugins can declare extra fields on `PromptAssembly` and `Asse
- Section providers: tool packages own their cross-call guidance (`tool:bash`, `tool:read`, …); this plugin owns `harness:identity` and `deployment:persona`.
- Variable providers: the agent loop registers `model` and `cwd`; any plugin can register the facts it owns (a future `date`, git state, …).
- Tool schema providers: `ToolRegistry` registers itself as a tool provider automatically.
- The [`system-prompt/assemble` waterfall](#live-events): cooperatively mutate or replace the assembly per caller.
- The [`system-prompt/assemble` waterfall](#live-events): cooperatively mutate or replace the assembly per caller before any complete-section constraint is enforced.
Design rationale: [the prompt-variables Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md).
@@ -49,7 +49,7 @@ Design rationale: [the prompt-variables Agent Note](../../../.agents/notes/imple
#### What the model sees
By default every assembly starts with the harness identity below, then the configured persona and ordered plugin sections after strict variable interpolation. `includeHarnessIdentity: false` omits only that fixed opener for a deployment that owns the complete compatibility persona. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The final `system-prompt/assemble` waterfall result is authoritative, so an expert listener's changes determine the delivered prompt and tool schemas.
By default every assembly starts with the harness identity below, then the configured persona and ordered plugin sections after strict variable interpolation. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete; that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain.
##### Harness identity

View File

@@ -16,21 +16,21 @@
### 公开 API
- `ctx.systemPrompt.section(section: PromptSection): () => void`:贡献一个段。层由调用上下文的作用域决定:`agent.ctx` 只为该 agent 贡献,并在该处遮蔽同名全局段。同一层中的重复名称和非有限顺序会抛出。随调用 fiber 一并 dispose资源释放
- `ctx.systemPrompt.section(section: PromptSection): () => void`:贡献一个段。层由调用上下文的作用域决定:`agent.ctx` 只为该 agent 贡献,并在该处遮蔽同名全局段。一个 `complete: true` 段会在组装 waterfall 之后成为精确的完整提示词;有效 complete 段超过一个时,组装会被拒绝。同一层中的重复名称和非有限顺序会抛出。随调用 fiber 一并 dispose资源释放
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void`:贡献工具 schema每次组装时使用该次组装的上下文求值。`ToolProviderResult` = `{ schemas, knownNames? }``schemas` 是限制后的可见集合;`knownNames` 是限制前由 `toolOrder` 使用的全集。提供方不得返回名为 `TOOL_ORDER_REST` 的 schema。带作用域提供方只在其作用域的组装中查询。随调用 fiber 一并 dispose。
- `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => void`:贡献提示词变量,在段文本中以 `{{name}}` 引用。带作用域变量会为该 agent 遮蔽同名全局变量。同层重复或无法引用的名称会抛出;`undefined` 表示「本次组装没有值」。随调用 fiber 一并 dispose。
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>`:为一个调用方组装提示词:将全局层与 `context.scope` 的层合并,并在变换 waterfall 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall并返回其权威结果。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>`:为一个调用方组装提示词:将全局层与 `context.scope` 的层合并,并在变换 waterfall 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall之后将一个有效的 complete 段恢复为唯一的提示词段落。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。存在多个 complete 段、已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。
<a id="live-events"></a>
### 实时事件
`system-prompt/assemble` 是权威来源;替换条目的监听器必须保留任何活动 Code Mode 或结构化输出协议。筛选需要在呈现、查找与执行之间保持一致时,应使用 [`ToolRegistry.restrict()`](../tools/README.md)。注册表变更通知不经过筛选。[system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) 的生成区块拥有签名与分发约定。
`system-prompt/assemble` 对普通段落具有权威性complete 段是在 waterfall 之后应用的最终提示词约束。替换条目的监听器必须保留任何活动 Code Mode 或结构化输出协议。筛选需要在呈现、查找与执行之间保持一致时,应使用 [`ToolRegistry.restrict()`](../tools/README.md)。注册表变更通知不经过筛选。[system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) 的生成区块拥有签名与分发约定。
### 关键类型
- `AssembleContext`:说明一次 `assemble()` 调用的用途。它可通过合并扩展;此处声明 `scope?: ScopeKey`(层选择器)与 `signal?: AbortSignal`(显式请求控制能力),而 `dsh-agent` 声明 `agent?: Agent`(类型化 DX 字段;绝不能在没有 `scope` 时设置,应使用 `assembleContextFor(agent, signal)`)。提供方必须容忍字段缺席,因为裸 `assemble()` 携带的是无作用域、无信号的空上下文。`signal` 是请求值,不是环境 Agent 执行 frame 的一部分。
- `PromptSection``{ name, order, text }`。各段按 `order` 升序拼接。顺序区间:`-100` 是 harness 身份,`0` 是部署 persona工具引导使用 `100199`
- `PromptSection``{ name, order, text, complete? }`。各段按 `order` 升序拼接。顺序区间:`-100` 是 harness 身份,`0` 是部署 persona工具引导使用 `100199`协作式组装完成后,一个有效的 `complete` 段会抑制其他所有段落。
- `PromptAssembly``{ sections: AssembledSection[], tools: ToolSchema[], variables: Record<string, string | undefined> }`。段文本到达时已解析,但尚未插值;`variables` 包含对上下文解析后的每个已注册变量。工具 schema 按设计属于组装结果:「模型获知自己能做什么」是一个连贯整体,尽管适配器把 schema 作为独立 wire 字段传输。
- `renderPrompt(assembly)`:插值每个段中的 `{{variable}}` 引用,删除空段,并用空行连接。严格规则:未知引用(使用 `Object.hasOwn` 查找,因此 `{{constructor}}` 等原型名称未知)、已注册但无值的引用、格式错误的完整 `{{…}}` 组,或一个起始 `{{` 没有打开完整组、但后面仍有 `}}``{{{model}}}`),都会抛出;明确失败胜过交付格式错误的提示词。孤立的 `{{` 如果后面任何位置都没有 `}}`,会按字面量通过;替换值绝不再次扫描。
@@ -41,7 +41,7 @@
- 段提供方:工具包拥有跨调用引导(`tool:bash``tool:read` 等);此插件拥有 `harness:identity``deployment:persona`
- 变量提供方agent loop智能体循环注册 `model``cwd`;任何插件都可以注册自己拥有的事实(未来的 `date`、git 状态等)。
- 工具 schema 提供方:`ToolRegistry` 自动将自身注册为工具提供方。
- [`system-prompt/assemble` waterfall](#live-events):按调用方协作式修改或替换组装结果。
- [`system-prompt/assemble` waterfall](#live-events):按调用方协作式修改或替换组装结果,之后再实施 complete 段约束
设计原理:[提示词变量 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)。
@@ -51,7 +51,7 @@
#### 模型看到的内容
默认情况下,每次组装都从下方 harness 身份开始,然后在严格变量插值后追加已配置 persona 与有序插件段。`includeHarnessIdentity: false`为拥有完整兼容 persona 的部署省略这个固定开场白。空段会消失;带作用域的段和变量可以为一个 agent 遮蔽全局项。最终 `system-prompt/assemble` waterfall 结果是权威来源,因此专家监听器的变更决定交付的提示词与工具 schema。
默认情况下,每次组装都从下方 harness 身份开始,然后在严格变量插值后追加已配置 persona 与有序插件段。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段和变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema,除非一个有效段声明自身为 complete此时该确切段落会成为完整的系统提示词而 waterfall 得到的上下文、工具和变量保持不变
##### Harness 身份

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-system-prompt",
"description": "System prompt assembly registry for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/core/system-prompt"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -25,18 +32,18 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-scope": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -4,13 +4,13 @@
* @module @deepseek-ai/dsh-system-prompt
*/
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { AnonymousEntries, NamedEntries, ScopedLayers, scopeTarget } from '@deepseek-ai/dsh-scope'
import type { ScopeKey, ScopeLayer, Scoped } from '@deepseek-ai/dsh-scope'
import type { ContextSnapshotSection, ToolSchema } from '@deepseek-ai/dsh-llm'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
systemPrompt: SystemPrompt
}
@@ -21,7 +21,9 @@ declare module 'cordis' {
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
* receive only that scope's assemblies. The returned value is authoritative.
* A supplied signal controls only this explicit assembly request and must not
* be retained to control later turns.
* be retained to control later turns. A registered complete section is
* restored after this waterfall, so listeners cannot add to or replace
* that scope's system prompt.
* @param assembly - the mutable assembly built from registered providers.
* @param context - the caller's per-assembly context.
* @mode waterfall
@@ -63,6 +65,13 @@ export interface PromptSection {
* interpolated later, by {@link renderPrompt}.
*/
readonly text: string | ((context: AssembleContext) => string)
/**
* Treat this contribution as the complete system prompt. Assembly still
* runs the cooperative waterfall so tools, contexts, and variables can be
* resolved, then restores this exact section as the sole prompt section.
* More than one effective complete section makes assembly fail.
*/
readonly complete?: boolean
}
/** Dynamic model context materialized as a durable user-role snapshot. */
@@ -428,9 +437,11 @@ export class SystemPrompt extends Service {
/**
* Assemble global and scoped providers, detach tool parameters, apply
* canonical ordering, then run the assembly waterfall. Scoped sections and
* variables shadow globals; the returned waterfall value is authoritative.
* variables shadow globals. The returned waterfall value is authoritative
* except that an effective complete section is restored afterwards as the
* sole prompt section.
* @param context - the optional scope and plugin-defined assembly fields.
* @returns the authoritative post-waterfall assembly.
* @returns the post-waterfall assembly with any complete prompt enforced.
*/
// Keep configuration failures on the declared asynchronous error path.
async assemble(context: AssembleContext = {}): Promise<PromptAssembly> {
@@ -467,13 +478,23 @@ export class SystemPrompt extends Service {
collected.push(...schemas)
for (const name of acceptedKnownNames) knownNames.add(name)
}
const assembly: PromptAssembly = {
sections: [...sectionByName.values()]
.sort((a, b) => a.order - b.order)
.map(section => ({
const sectionDefinitions = [...sectionByName.values()].sort((a, b) => a.order - b.order)
const completeSections = sectionDefinitions.filter(section => section.complete === true)
if (completeSections.length > 1) {
throw new Error(`multiple complete prompt sections are active: ${completeSections.map(section => JSON.stringify(section.name)).join(', ')}`)
}
let completeSection: AssembledSection | undefined
const sections = sectionDefinitions
.map((section) => {
const assembled = {
name: section.name,
text: typeof section.text === 'function' ? section.text(context) : section.text,
})),
}
if (section.complete === true) completeSection = { ...assembled }
return assembled
})
const assembly: PromptAssembly = {
sections,
contexts: [...contextByName.values()]
.sort((a, b) => a.order - b.order)
.map(entry => ({
@@ -483,10 +504,12 @@ export class SystemPrompt extends Service {
tools: orderTools(collected, this.toolOrder, knownNames),
variables,
}
return this.ctx.waterfall(
const transformed = await this.ctx.waterfall(
scopeTarget(this, scope), 'system-prompt/assemble', assembly, context,
() => Promise.resolve(assembly),
)
if (completeSection === undefined) return transformed
return { ...transformed, sections: [completeSection] }
}
}

View File

@@ -1,6 +1,6 @@
/** Package-owned prompt-assembly invariants. @module @deepseek-ai/dsh-system-prompt/invariant */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type { PromptAssembly } from './index.ts'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
import * as SystemPromptInvariant from '@deepseek-ai/dsh-system-prompt/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createScope, scopeOf } from '@deepseek-ai/dsh-scope'
import type { Scope, ScopeKey } from '@deepseek-ai/dsh-scope'
import SystemPrompt, { TOOL_ORDER_REST, renderContextSnapshot, renderPrompt } from '@deepseek-ai/dsh-system-prompt'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import SystemPrompt, { AssembleContext, PromptAssembly, renderContextSnapshot, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
/**
@@ -264,6 +264,34 @@ describe('SystemPrompt', () => {
expect(assembly.sections).toHaveLength(0)
})
it('restores one complete section after the assembly waterfall', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
ctx.systemPrompt.section({ name: 'complete', order: 10, text: 'Exact prompt.', complete: true })
ctx.systemPrompt.section({ name: 'extra', order: 20, text: 'extra' })
ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
const complete = assembly.sections.find(section => section.name === 'complete')
if (complete === undefined) throw new Error('complete section missing before waterfall')
complete.text = 'mutated'
assembly.sections.push({ name: 'late', text: 'late' })
return next()
}, { prepend: true })
expect((await ctx.systemPrompt.assemble()).sections).toEqual([
{ name: 'complete', text: 'Exact prompt.' },
])
})
it('rejects multiple effective complete sections', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
ctx.systemPrompt.section({ name: 'first', order: 10, text: 'first', complete: true })
ctx.systemPrompt.section({ name: 'second', order: 20, text: 'second', complete: true })
await expect(ctx.systemPrompt.assemble())
.rejects.toThrow('multiple complete prompt sections are active: "first", "second"')
})
it('assembles snapshots so one-step mutations do not leak into future assemblies', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import SystemPrompt, { PromptAssembly, TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import type { ToolSchema } from '@deepseek-ai/dsh-llm'

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/tools/README.md
README.md: 21851ca887147364c76612bae2e6a00ebdccec39
README.zh.md: aec3b434e52f473001505bbea5212d5e247eb46f
README.md: 75d18712a02ecc72c2ea3a7203d2a7377cef87c7
README.zh.md: 8d4ae42596483f77aa82b23a0b41168465e2b165

View File

@@ -66,7 +66,7 @@ First-party plugin authors can use the `defineTool()` helper (exported from this
```ts
import { readFile } from 'node:fs/promises'
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
declare const ctx: Context
@@ -111,7 +111,7 @@ Tools optionally own pure `presentCall()` and `presentResult()` render intents,
- Call views are `{ card: 'generic', title, kind?, rawInput?, content?, locations? }`, `{ card: 'terminal', title, description?, cwd? }`, or `{ card: 'diff', title, diffs, locations? }`.
- Result views are `{ card: 'generic', title?, content? }`, `{ card: 'terminal', title?, output?, exitCode?, signal? }`, `{ card: 'diff', title?, diffs }`, `{ card: 'search', shape, title?, truncated, total, … }` (a completed discovery search — grouped-by-file matches for `shape: 'matches'` (grep) or a flat path list for `shape: 'paths'` (glob), with `truncated`/`total` so a UI never presents a capped result as complete; the view carries no result text and a search has no `card: 'search'` call-time analogue), `{ card: 'read', title?, path, offset, lines, totalLines, lang?, content? }` (a completed file read → a line-numbered, optionally syntax-highlighted code view; `offset` is the 1-based first line the window requested, kept even when `lines` is empty; `lines` is `{ number, text }[]` keeping each file line number, and `content` is the envelope-stripped text a UI without read support falls back to), or `{ card: 'web', kind: 'search' | 'fetch', title?, … }` (a completed web retrieval; the `kind` arms carry the structured search sources or the fetch summary, and a UI without the `web` capability falls back to the raw result content).
Returning `undefined` selects generic fallback. Presenters depend only on their arguments and the durable result because UIs call them during live streaming and log replay. `output.presentationMeta(args, value)` derives JSON metadata for direct surface calls; that metadata persists with `tool/result` and returns to `presentResult`, while the canonical value itself remains execution-local and is never replayed. Nested Code dispatches do not compute metadata. `defineTool` soft-validates older logged arguments and falls back instead of crashing replay. `dsh-tool-bash` and `dsh-tool-fs` are the reference implementations; the [canonical-output Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) owns the value/presentation split and the [render-intent Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) owns card vocabulary.
Returning `undefined` selects generic fallback. Presenters depend only on their arguments and the durable result because UIs call them during live streaming and log replay. `output.presentationMeta(args, value)` derives JSON metadata for direct top-level calls; that metadata persists with `tool/result` and returns to `presentResult`, while the canonical value itself remains execution-local and is never replayed. Nested Code dispatches do not compute metadata. `defineTool` soft-validates older logged arguments and falls back instead of crashing replay. `dsh-tool-bash` and `dsh-tool-fs` are the reference implementations; the [canonical-output Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) owns the value/presentation split and the [render-intent Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) owns card vocabulary.
### Code Mode

View File

@@ -66,7 +66,7 @@ tools:
```ts
import { readFile } from 'node:fs/promises'
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
declare const ctx: Context

View File

@@ -1,8 +1,15 @@
{
"name": "@deepseek-ai/dsh-tools",
"description": "Tool registry and execution pipeline for the DeepSeek Harness",
"version": "0.0.1",
"private": true,
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/core/tools"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
@@ -34,18 +41,18 @@
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-code-runtime": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-scope": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-user-approval": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-code-runtime": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
@@ -56,6 +63,6 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -260,7 +260,7 @@ type RunCodeOutput = { logs: string[]; result?: JsonValue }
/**
* Registry-private capabilities the bridge receives at construction — the
* `requireRuntime` idiom: operations only the owning registry can mint stay
* off its public service surface and flow here as closures instead.
* off its public service API and flow here as closures instead.
*/
export interface RunCodeBridgeOptions {
/** Resolves `ctx.codeRuntime` or throws the loud misconfiguration error (shared with the registry's assembly-time checks). */
@@ -646,7 +646,7 @@ export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridge
kind: 'execute',
rawInput: args.code,
}),
// Deliberately no presentResult: the generic surface fallback keeps this
// Deliberately no presentResult: the generic card fallback keeps this
// title and reads durable result content without duplicating a large raw
// result into the host view payload.
})

View File

@@ -4,8 +4,8 @@
* @module @deepseek-ai/dsh-tools
*/
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { AnonymousEntries, NamedEntries, ScopedLayers, scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope'
import type { ScopeKey, ScopeLayer, Scoped } from '@deepseek-ai/dsh-scope'
import type { CallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm'
@@ -94,7 +94,7 @@ export { defineContentToolFixture, type ContentToolFixtureOptions } from './test
// The render-intent vocabulary a tool declares via `presentCall`/`presentResult`
// lives in its own UI-facing module; re-export it so `@deepseek-ai/dsh-tools`
// stays the single public surface for tool producers and UI adapters.
// stays the single public API for tool producers and UI adapters.
export type {
ToolCallKind,
FileLocation,
@@ -120,7 +120,7 @@ export type {
WebSource,
} from './presentation.ts'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
tools: ToolRegistry
}
@@ -200,7 +200,7 @@ export interface ToolOutputDefinition {
readonly schema: JsonSchemaNode
/** Pure projection from validated arguments and value to Native/model content. */
render(args: unknown, value: JsonValue): ContentBlock[]
/** Pure replayable presentation projection, computed only for surface calls. */
/** Pure replayable presentation projection, computed only for top-level calls. */
presentationMeta?(args: unknown, value: JsonValue): JsonValue
}
@@ -786,7 +786,7 @@ export class ToolRegistry extends Service {
scope => new ToolLayer(scope),
() => { this.ctx.emit('tools/change') },
)
/** Presentation for agents that declare none; {@link presentAs} shadows it per agent. */
/** Presentation for scopes that declare none; {@link presentAs} shadows it per scope. */
private readonly defaultMode: ToolPresentationMode
private readonly maxParallelSubCalls: number
/**
@@ -811,7 +811,7 @@ export class ToolRegistry extends Service {
/**
* The generated-SDK prompt section, registered globally by a code-mode
* deployment and per agent by {@link presentAs}.
* deployment and per scope by {@link presentAs}.
*
* The body regenerates from the CALLING scope, and renders empty for an
* agent presenting natively — an agent that opted out under a code-mode
@@ -880,12 +880,14 @@ export class ToolRegistry extends Service {
}
/**
* Present this agent's tools in `mode` instead of the deployment default.
* Present the calling scope's tools in `mode` instead of the deployment
* default. Nearest scope on the chain wins, so a preset's standing
* declaration covers every agent joined under it.
*
* Scoped only, and one declaration per agent: this is how an agent preset
* composes a Code Mode agent beside native ones in the same process, and a
* Scoped only, and one declaration per scope: this is how an agent preset
* composes Code Mode agents beside native ones in the same process, and a
* process-global override would be the `mode` config field instead.
* @param mode - the presentation this agent's model sees.
* @param mode - the presentation the covered agents' models see.
* @returns the exact disposer that restores the deployment default.
*/
presentAs(mode: ToolPresentationMode): () => void {
@@ -898,14 +900,14 @@ export class ToolRegistry extends Service {
ctx,
(layer) => {
if (layer.mode !== undefined) {
throw new Error(`tools.presentAs("${mode}") conflicts with "${layer.mode}" already declared for this agent; one composition selects one presentation`)
throw new Error(`tools.presentAs("${mode}") conflicts with "${layer.mode}" already declared for this scope; one composition selects one presentation`)
}
layer.mode = mode
return () => { layer.mode = undefined }
},
{ label: 'tools.presentAs()' },
)
// The SDK section is per agent for the same reason the mode is. Under a
// The SDK section is per scope for the same reason the mode is. Under a
// deployment that already defaults to a code mode this shadows the
// global registration with an identical body, which costs nothing and
// keeps one rule instead of a case analysis.
@@ -1007,7 +1009,7 @@ export class ToolRegistry extends Service {
* Restrict global tools for the calling agent scope. Empty filters, unknown
* names, scope-local names, and reserved transport names fail. Restrictions
* intersect; scoped registrations remain visible.
* @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).
* @param filter - global-tool mask: `allow` (keep only) and/or `deny` (remove).
* @returns the exact disposer that lifts this restriction.
*/
restrict(filter: ToolRestriction): () => void {

View File

@@ -1,6 +1,6 @@
/** Package-owned tool-pipeline invariants. @module @deepseek-ai/dsh-tools/invariant */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type { ToolExecution, ToolExecutionResult } from './index.ts'

View File

@@ -60,7 +60,7 @@ export interface GenericCallView {
/** Category for icon/treatment; defaults to `other` when omitted. */
kind?: ToolCallKind
/**
* The salient input to surface in a detail/expanded view (e.g. a background
* The salient input to show in a detail/expanded view (e.g. a background
* task id). Omit to show nothing; a string renders as-is, an object as pretty
* JSON. NOT the full raw args object unless that is genuinely what a reader wants.
*/

View File

@@ -493,7 +493,7 @@ export interface DefineToolOptions<S extends ParameterSchemaSpec, O extends Valu
readonly schema: O
/** Pure Native/model rendering of one validated canonical value. */
render(args: InferArgs<S>, value: InferValue<NoInfer<O>>): ContentBlock[]
/** Pure replayable presentation metadata for direct surface calls. */
/** Pure replayable presentation metadata for direct top-level calls. */
presentationMeta?(args: InferArgs<S>, value: InferValue<NoInfer<O>>): JsonValue
}
/** Optional positive cooperative timeout budget in milliseconds. */

View File

@@ -2,7 +2,7 @@
* Code Mode codegen: the pure projection from registered tool schemas to the TypeScript SDK
* text the model programs against (the `tools:sdk` prompt section). Sibling of
* `json-schema.ts` — `schemas()` (native function calling) and this module (the generated
* `declare const tools` surface) are two projections of the same store.
* `declare const tools` API) are two projections of the same store.
* @module @deepseek-ai/dsh-tools/src/ts-types
*/

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import { createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
@@ -1229,7 +1229,7 @@ describe('the run_code dispatch bridge', () => {
const tool = ctx.tools.get(RUN_CODE_NAME)!
expect(result.content).toEqual([{ type: 'text', text }])
// Surfaces keep the pending program title and render this durable content
// Presenters keep the pending program title and render this durable content
// through their generic fallback. Omitting a result view also prevents the
// host frame from carrying the same raw content a second time.
expect('presentResult' in tool).toBe(false)

View File

@@ -1,7 +1,7 @@
/** Covers fail-closed per-call classification and model-schema isolation. */
import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, {

View File

@@ -1,5 +1,5 @@
import { describe, expectTypeOf, it } from 'vitest'
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type {

View File

@@ -23,7 +23,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
it('boots every shipped tool package and harvests its model-facing schemas', async () => {
const catalog = await collectToolCatalog()
const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'report', 'run_code', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
// Every tool carries a JSON-Schema `parameters` object (what the model sees).
for (const entry of catalog) {
for (const schema of entry.schemas) {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import { CallId } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'

Some files were not shown because too many files have changed in this diff Show More