Merge remote-tracking branch 'origin/master' into feat/send-unify

# Conflicts:
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	packages/context/time-context/tests/time-context.spec.ts
#	packages/cordis/tool-cordis/src/api-catalog.ts
This commit is contained in:
Turtle
2026-07-23 20:54:48 +08:00
430 changed files with 20734 additions and 5783 deletions

View File

@@ -71,7 +71,7 @@ The user block is append-only conversation growth, while entering plan mode also
#### What the model sees
The [`exit_plan_mode` schema](../../../docs/tool-catalog.md#deepseek-aidsh-plan-mode) remains available in both states; execution outside plan mode fails, while an approved in-mode review returns the exit result and rejection returns feedback.
The [`exit_plan_mode` schema](../../../docs/tool-catalog.md#deepseek-aidsh-plan-mode) remains available in both states; execution outside plan mode fails, while an approved in-mode review returns the canonical `{ approved: true }` value and renders the existing confirmation text. Rejection remains a failed call carrying review feedback.
#### Token effect

View File

@@ -227,6 +227,16 @@ export class PlanModeService extends Service {
parameters: {
plan: { type: 'string', required: true, description: 'The complete plan, as markdown, starting with a # heading that names it.' },
},
output: {
schema: {
type: 'object',
additionalProperties: false,
properties: {
approved: { type: 'boolean', const: true, required: true },
},
},
render: () => [{ type: 'text', text: 'Plan approved — plan mode exited; carry out the plan starting with your next step.' }],
},
execute: async (args, exec) => {
const agent = exec.agent
if (agent === undefined) throw new Error(`${EXIT_PLAN_MODE} requires a calling agent (no session to switch)`)
@@ -270,7 +280,7 @@ export class PlanModeService extends Service {
// Keep plan guidance for the rest of this assistant tool batch. The
// silent intent flushes after the step, before the next assembly.
this.pendingIntents.set(agent.session, { active: false, narrate: false })
return [{ type: 'text', text: 'Plan approved — plan mode exited; carry out the plan starting with your next step.' }]
return { approved: true }
},
presentCall: args => ({
card: 'generic',

View File

@@ -3,7 +3,7 @@ import { Context } from 'cordis'
import LlmService, { type StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import PlanModeService, { foldPlanMode } from '@deepseek-ai/dsh-plan-mode'
@@ -30,7 +30,7 @@ async function harness(adapter: MockAdapter): Promise<Context> {
await ctx.plugin(PlanModeService, PLAN_CONFIG)
ctx.llm.registerAdapter(['mock'], adapter)
for (const name of ['read', 'write']) {
ctx.tools.register(defineTool({
ctx.tools.register(defineContentToolFixture({
name,
description: `test tool ${name}`,
parameters: {},

View File

@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools'
import ToolRegistry, { RUN_CODE_NAME, defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import { agentEvents, type Agent, type RequestErrorDecision } from '@deepseek-ai/dsh-agent'
import { createScope } from '@deepseek-ai/dsh-scope'
@@ -101,7 +101,7 @@ function noticeTexts(session: Session): string[] {
function registerNamedTools(ctx: Context, names: string[]): void {
for (const name of names) {
ctx.tools.register(defineTool({
ctx.tools.register(defineContentToolFixture({
name,
description: `test tool ${name}`,
parameters: {},
@@ -110,6 +110,16 @@ function registerNamedTools(ctx: Context, names: string[]): void {
}
}
/** Assert the mapped Code Mode SDK includes the stable plan exit binding and test tools. */
function expectPlanCodeSdkBindings(sdk: string): void {
expect(sdk).toContain('interface ToolArgsMap {')
expect(sdk).toContain('read: Record<string, JsonValue>;')
expect(sdk).toContain('write: Record<string, JsonValue>;')
expect(sdk).toContain('interface ToolOutputMap {')
expect(sdk).toContain('exit_plan_mode: {\n approved: true;\n };')
expect(sdk).toContain('[K in ToolName]: (args: ToolArgsMap[K]) => Promise<ToolOutputMap[K]>;')
}
let callCounter = 0
function execute(ctx: Context, name: string, agent?: Agent) {
return ctx.tools.execute({
@@ -445,9 +455,7 @@ describe('the soft layer', () => {
// The SDK documents the full binding set plus the exit; plan mode never
// prunes capabilities and restrains through guidance alone.
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(sdk).toContain('read(args:')
expect(sdk).toContain('write(args:')
expect(sdk).toContain('exit_plan_mode(args:')
expectPlanCodeSdkBindings(sdk)
})
it('keeps native wire schemas and the SDK in step under mode both', async () => {
@@ -468,9 +476,7 @@ describe('the soft layer', () => {
// is present on the wire AND in the SDK alongside the untouched toolset.
expect(assembly.tools.map(tool => tool.name).sort()).toEqual(['exit_plan_mode', 'read', 'run_code', 'write'])
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(sdk).toContain('read(args:')
expect(sdk).toContain('write(args:')
expect(sdk).toContain('exit_plan_mode(args:')
expectPlanCodeSdkBindings(sdk)
})
it('keeps the Code Mode SDK byte-identical across mode switches', async () => {
@@ -487,9 +493,7 @@ describe('the soft layer', () => {
registerNamedTools(withPlanMode, ['read', 'write'])
const agent = await agentWithSession(withPlanMode)
const defaultSdk = (await assembleFor(withPlanMode, agent)).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(defaultSdk).toContain('read(args:')
expect(defaultSdk).toContain('write(args:')
expect(defaultSdk).toContain('exit_plan_mode(args:')
expectPlanCodeSdkBindings(defaultSdk)
agent.session.append('plan/mode', { active: true })
const planSdk = (await assembleFor(withPlanMode, agent)).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(planSdk).toBe(defaultSdk)
@@ -502,7 +506,7 @@ describe('the soft layer', () => {
await bare.plugin(FakeRuntime)
registerNamedTools(bare, ['read', 'write'])
const bareSdk = (await bare.systemPrompt.assemble({ agent })).sections.find(section => section.name === 'tools:sdk')?.text ?? ''
expect(bareSdk).not.toContain('exit_plan_mode(args:')
expect(bareSdk).not.toContain('exit_plan_mode:')
expect(defaultSdk).not.toBe(bareSdk)
})
})
@@ -662,6 +666,8 @@ describe('exit_plan_mode', () => {
const { ctx, agent, asked } = await setupWithReview({ selected: ['Approve'] })
const result = await callExit(ctx, agent)
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected approved plan result')
expect(result.value).toEqual({ approved: true })
expect(result.content).toEqual([{ type: 'text', text: 'Plan approved — plan mode exited; carry out the plan starting with your next step.' }])
// Boundary-applied, not a direct append: the fold stays plan until the
// step's end, so the plan policy covers any remaining call of the SAME batch.