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:
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user