Merge branch 'codex/tool-json-schema-dsl' into codex/canonical-tool-output
# Conflicts: # docs/config-catalog.md # docs/persistence-catalog.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-loop/tests/contract-regressions.spec.ts # packages/ui/acp/src/index.ts # packages/ui/tui/src/index.ts
This commit is contained in:
@@ -15,6 +15,7 @@ stdout is the ACP JSON-RPC channel, so the cluster is defined as much by what it
|
||||
| `@deepseek-ai/dsh-command-goal` | the discoverable direct `/goal` producer; the app enables the spine's persisted-goal stack with it |
|
||||
| `@deepseek-ai/dsh-user-interaction` | the human question/answer seam used by clients that can complete ACP elicitation requests |
|
||||
| `@deepseek-ai/dsh-session-persistence-jsonl` | durable JSONL session log (the bridge advertises `loadSession`) |
|
||||
| `@deepseek-ai/dsh-session-query` + `@deepseek-ai/dsh-session-reference` | exact current-surface reads and bounded `dsh-session:` snapshots |
|
||||
| `@deepseek-ai/dsh-session-checkpoint-policy` | semantic durability barriers before model requests and top-level tool effects, plus completed-step checkpoints |
|
||||
| `@deepseek-ai/dsh-acp` | the bridge that owns stdout for JSON-RPC and provides ACP-backed user answers when a leaf explicitly exposes a user-question tool |
|
||||
| ~~`@deepseek-ai/dsh-tool-ask-user`~~ | **omitted by default** — ACP elicitation support is still client-dependent, so leaves must opt in deliberately |
|
||||
@@ -45,6 +46,7 @@ The app owns this cluster through one ordered Cordis effect. Teardown drains the
|
||||
| `persistenceRoot` | `./.sessions` | the JSONL backend's root directory |
|
||||
| `packChunks` | `false` | write delta-chunk runs as packed storage rows (the JSONL backend's `packChunks`) |
|
||||
| `persistenceCompression` | `'zstd'` | JSONL artifact encoding (`'zstd'` or raw `'none'`) |
|
||||
| `sessionReferences` | service defaults | cross-session candidate and snapshot limits routed to `dsh-session-reference` |
|
||||
|
||||
The leaf supplies the swappable backends: an LLM adapter (`llm-deepseek` for the real model, `llm-replay` for keyless snapshot replay), a bash executor, and optionally a `ctx.fs` provider. Workspace context becomes a no-op without `ctx.fs`; the shipped [`examples/acp-agent/cordis.yml`](../../../examples/acp-agent/cordis.yml) selects `dsh-sandbox-policy`, `dsh-fs-sandbox`, `dsh-fs-policy`, and `dsh-tool-fs` so baseline instructions and model-facing `read`/`write`/`edit` share one provider, sandbox mode, workspace root, and observed-version policy.
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-query": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-reference": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/dsh-user-interaction": "^0.0.1",
|
||||
"@deepseek-ai/dsh-workspace-context": "^0.0.1",
|
||||
@@ -63,6 +65,8 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
|
||||
@@ -25,6 +25,8 @@ import SessionPersistenceJsonl, {
|
||||
} from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import * as sessionCheckpointPolicy from '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import SessionQueryService from '@deepseek-ai/dsh-session-query'
|
||||
import SessionReferenceService, { type Config as SessionReferenceConfig } from '@deepseek-ai/dsh-session-reference'
|
||||
|
||||
export const name = 'acp-demo'
|
||||
const DEFAULT_PERSISTENCE_ROOT = './.sessions'
|
||||
@@ -61,6 +63,8 @@ export interface Config {
|
||||
packChunks?: boolean
|
||||
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
|
||||
persistenceCompression?: JsonlCompression
|
||||
/** Cross-session reference discovery and snapshot byte budgets. */
|
||||
sessionReferences?: SessionReferenceConfig
|
||||
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
|
||||
workspaceContext: agentCore.Config['workspaceContext']
|
||||
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
|
||||
@@ -93,6 +97,7 @@ export const Config: z<Config> = z.object({
|
||||
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
|
||||
packChunks: z.boolean().default(false),
|
||||
persistenceCompression: JsonlCompressionSchema,
|
||||
sessionReferences: SessionReferenceService.Config,
|
||||
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
|
||||
skills: agentCore.SkillConfigSchema,
|
||||
toolBash: agentCore.ToolBashConfigSchema,
|
||||
@@ -128,6 +133,8 @@ export function apply(ctx: Context, config: Config): void {
|
||||
}).dispose
|
||||
/* jscpd:ignore-end */
|
||||
yield ctx.plugin(sessionCheckpointPolicy).dispose
|
||||
yield ctx.plugin(SessionQueryService).dispose
|
||||
yield ctx.plugin(SessionReferenceService, config.sessionReferences ?? {}).dispose
|
||||
yield ctx.plugin(acp, { provider: config.provider, model: config.model }).dispose
|
||||
}, 'acp-demo.composition')
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { Message } from '@deepseek-ai/dsh-llm'
|
||||
import * as acpAgent from '../src/index.ts'
|
||||
@@ -83,18 +84,26 @@ describe('dsh-acp-demo composition', () => {
|
||||
persona: 'hi',
|
||||
persistenceRoot: '/tmp/dsh-acp-demo-test',
|
||||
persistenceCompression: 'none',
|
||||
sessionReferences: { candidateLimit: 1 },
|
||||
skills: await isolatedSkillsConfig(),
|
||||
workspaceContext: false,
|
||||
})
|
||||
expect(ctx.get('agents')).toBeDefined()
|
||||
expect(ctx.get('sessions')).toBeDefined()
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
expect(ctx.get('sessionQuery')).toBeDefined()
|
||||
expect(ctx.get('sessionReferences')).toBeDefined()
|
||||
expect((ctx.get('sessionPersistence') as unknown as { config: { compression?: string } }).config.compression).toBe('none')
|
||||
expect(ctx.get('agentLoop')).toBeDefined()
|
||||
expect(ctx.get('userInteraction')).toBeDefined()
|
||||
expect(ctx.get('tools')?.get('ask_user_question')).toBeUndefined()
|
||||
expect(ctx.get('goals')).toBeDefined()
|
||||
expect(ctx.get('tools')?.get('get_goal')).toBeDefined()
|
||||
const target = ctx.sessions.create(SessionId('candidate-target'))
|
||||
ctx.sessions.create(SessionId('candidate-one'))
|
||||
ctx.sessions.create(SessionId('candidate-two'))
|
||||
await expect(ctx.sessionReferences.listCandidates({ id: target.id, session: target } as Agent))
|
||||
.resolves.toHaveLength(1)
|
||||
// No pre-created agents — ACP session/new creates them on demand.
|
||||
expect(ctx.get('agents')!.list()).toHaveLength(0)
|
||||
await ctx.fiber.dispose()
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Readable, Writable } from 'node:stream'
|
||||
import { promisify } from 'node:util'
|
||||
import { zstdDecompress } from 'node:zlib'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { ACP_SESSION_REFERENCE_META_KEY } from '@deepseek-ai/dsh-acp'
|
||||
|
||||
/**
|
||||
* Published-entry smoke: run `lib/bin.js` under plain Node in a symlinked external consumer and
|
||||
@@ -36,7 +37,7 @@ const dshPackages = [
|
||||
'bash/bash-local', 'bash/tool-bash', 'context/workspace-context', 'support/invariants', 'ui/app-boot',
|
||||
'session-persistence/session-persistence',
|
||||
'session-persistence/session-checkpoint-policy', 'session-persistence/session-persistence-jsonl',
|
||||
'ui/acp', 'examples/acp-demo', 'util/paths',
|
||||
'session-query/session-query', 'context/session-reference', 'ui/acp', 'examples/acp-demo', 'util/paths',
|
||||
]
|
||||
const vendorPackages = [
|
||||
'cordis', 'loader', 'include', 'timer', 'hmr', 'logger-console',
|
||||
@@ -166,10 +167,30 @@ describe.skipIf(!existsSync(acpBin))('dsh-acp-demo BUILT bin (node lib/bin.js, n
|
||||
// regression would exit before answering); loadSession proves the real app
|
||||
// mounted, not a collapsed export shape.
|
||||
expect(init.agentCapabilities?.loadSession).toBe(true)
|
||||
const { sessionId } = await client.newSession({ cwd: consumer, mcpServers: [] })
|
||||
expect(init.agentCapabilities?.sessionCapabilities?.list).toEqual({})
|
||||
const sessionCwd = consumer
|
||||
const { sessionId } = await client.newSession({ cwd: sessionCwd, mcpServers: [] })
|
||||
const result = await client.prompt({ sessionId, prompt: [{ type: 'text', text: 'reply' }] })
|
||||
expect(result.stopReason).toBe('end_turn')
|
||||
const sessionsRoot = join(consumer, '.sessions')
|
||||
await expect.poll(async () => {
|
||||
return (await client.listSessions({ cwd: sessionCwd })).sessions.find(candidate => candidate.sessionId === sessionId)
|
||||
}).toMatchObject({
|
||||
sessionId,
|
||||
cwd: sessionCwd,
|
||||
title: 'reply',
|
||||
})
|
||||
const listed = await client.listSessions({ cwd: sessionCwd })
|
||||
const reference = listed.sessions.find(candidate => candidate.sessionId === sessionId)
|
||||
?._meta?.[ACP_SESSION_REFERENCE_META_KEY]
|
||||
expect(reference).toBeTypeOf('object')
|
||||
expect(reference).not.toBeNull()
|
||||
expect(reference).toHaveProperty('uri')
|
||||
if (typeof reference !== 'object' || reference === null || !('uri' in reference)) {
|
||||
throw new Error('expected session reference metadata')
|
||||
}
|
||||
expect(reference.uri).toBeTypeOf('string')
|
||||
expect(reference.uri).toMatch(/^dsh-session:[A-Za-z0-9_-]+$/u)
|
||||
const sessionsRoot = join(sessionCwd, '.sessions')
|
||||
let log: string | undefined
|
||||
await expect.poll(async () => {
|
||||
log = (await readdir(sessionsRoot, { recursive: true })).find(file => file.endsWith('.jsonl.zstd'))
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
{
|
||||
"path": "../../ui/acp"
|
||||
},
|
||||
{
|
||||
"path": "../../session-query/session-query"
|
||||
},
|
||||
{
|
||||
"path": "../../context/session-reference"
|
||||
},
|
||||
{
|
||||
"path": "../../ui/commands"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,8 @@ Use [`@deepseek-ai/dsh-cli-demo`](../cli-demo/README.md) for pipes, scripts, and
|
||||
| `@deepseek-ai/dsh-commands` | Human-only discovery and dispatch consumed by the TUI and command plugins |
|
||||
| `@deepseek-ai/dsh-command-goal` | Direct `/goal` status and mutation over the spine's persisted-goal stack |
|
||||
| `@deepseek-ai/dsh-session-persistence-jsonl` | Durable session log under `persistenceRoot` |
|
||||
| `@deepseek-ai/dsh-session-checkpoint-policy` | Semantic durability barriers before model requests and top-level tool effects, plus completed-step checkpoints |
|
||||
| `@deepseek-ai/dsh-session-query` + `@deepseek-ai/dsh-session-reference` | Exact current-surface reads and bounded `@session` snapshots consumed by the TUI |
|
||||
| `@deepseek-ai/dsh-user-interaction` | Provider-neutral human question service |
|
||||
| `@deepseek-ai/dsh-tui` | Full-screen transcript, editor, tool cards, plan, and question overlays |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | Model-facing `ask_user_question` tool |
|
||||
@@ -37,6 +39,7 @@ Swappable LLM, bash, filesystem, and other capability providers remain in the le
|
||||
| `workspaceContext` | required | Workspace-instruction config, or `false` |
|
||||
| `persistenceRoot` | `./.sessions` | JSONL persistence root |
|
||||
| `persistenceCompression` | `'zstd'` | JSONL artifact encoding (`'zstd'` or raw `'none'`) |
|
||||
| `sessionReferences` | service defaults | Cross-session candidate and snapshot limits routed to `dsh-session-reference` |
|
||||
| `welcome` | `ready.` | TUI subtitle |
|
||||
| `ui` | owner defaults | TUI presentation settings such as reasoning, color, and card height |
|
||||
| `resumeSessionId` | — | Exact persisted session to resume |
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-query": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-reference": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tui": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "^0.0.1",
|
||||
@@ -69,6 +71,8 @@
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tui": "workspace:^",
|
||||
|
||||
@@ -23,12 +23,17 @@ import SessionPersistenceJsonl, {
|
||||
} from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import * as sessionCheckpointPolicy from '@deepseek-ai/dsh-session-checkpoint-policy'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import SessionQueryService from '@deepseek-ai/dsh-session-query'
|
||||
import SessionReferenceService, { type Config as SessionReferenceConfig } from '@deepseek-ai/dsh-session-reference'
|
||||
import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user'
|
||||
import * as uiTui from '@deepseek-ai/dsh-tui'
|
||||
|
||||
export const name = 'tui-demo'
|
||||
const DEFAULT_PERSISTENCE_ROOT = './.sessions'
|
||||
|
||||
// Each front door keeps a complete Loader contract so its deployment config is
|
||||
// readable without a cross-package facade.
|
||||
/* jscpd:ignore-start */
|
||||
/** App config routed to the spine, TUI, configured agent, and JSONL backend. */
|
||||
export interface Config {
|
||||
/** Provider route for the `main` agent. */
|
||||
@@ -51,6 +56,8 @@ export interface Config {
|
||||
persistenceRoot?: string
|
||||
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
|
||||
persistenceCompression?: JsonlCompression
|
||||
/** Cross-session reference discovery and snapshot byte budgets. */
|
||||
sessionReferences?: SessionReferenceConfig
|
||||
/** TUI transcript's optional first line; absent renders nothing on start. */
|
||||
welcome?: string
|
||||
/**
|
||||
@@ -76,9 +83,6 @@ export interface Config {
|
||||
workspaceContext: agentCore.Config['workspaceContext']
|
||||
}
|
||||
|
||||
// Each front door keeps a complete Loader schema so its deployment contract is
|
||||
// readable without a cross-package config facade.
|
||||
/* jscpd:ignore-start */
|
||||
export const Config: z<Config> = z.object({
|
||||
provider: z.string().required(),
|
||||
model: z.string().required(),
|
||||
@@ -91,6 +95,7 @@ export const Config: z<Config> = z.object({
|
||||
sessionTitle: agentCore.SessionTitleConfigSchema,
|
||||
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
|
||||
persistenceCompression: JsonlCompressionSchema,
|
||||
sessionReferences: SessionReferenceService.Config,
|
||||
welcome: z.string(),
|
||||
resumeCommand: z.string(),
|
||||
ui: uiTui.TuiConfigSchema,
|
||||
@@ -121,6 +126,8 @@ export function composeTuiApp(ctx: Context, config: Config): void {
|
||||
...(config.persistenceCompression === undefined ? {} : { compression: config.persistenceCompression }),
|
||||
})
|
||||
ctx.plugin(sessionCheckpointPolicy)
|
||||
ctx.plugin(SessionQueryService)
|
||||
ctx.plugin(SessionReferenceService, config.sessionReferences ?? {})
|
||||
ctx.plugin(UserInteractionService)
|
||||
ctx.plugin(uiTui, {
|
||||
...config.ui,
|
||||
|
||||
@@ -32,6 +32,11 @@ describe('dsh-tui-demo app', () => {
|
||||
dshHome: '/tmp/dsh-home',
|
||||
persistenceRoot: '/tmp/tui-sessions',
|
||||
persistenceCompression: 'none',
|
||||
sessionReferences: {
|
||||
maxReferences: 2,
|
||||
candidateLimit: 7,
|
||||
maxReferenceBytes: 1234,
|
||||
},
|
||||
welcome: 'TUI ready',
|
||||
resumeCommand: 'dsh --resume {session}',
|
||||
ui: { color: false, maxToolOutputLines: 3 },
|
||||
@@ -46,6 +51,8 @@ describe('dsh-tui-demo app', () => {
|
||||
'command-goal',
|
||||
'SessionPersistenceJsonl',
|
||||
'session-checkpoint-policy',
|
||||
'SessionQueryService',
|
||||
'SessionReferenceService',
|
||||
'UserInteractionService',
|
||||
'ui-tui',
|
||||
'agent-spine-demo',
|
||||
@@ -53,7 +60,12 @@ describe('dsh-tui-demo app', () => {
|
||||
])
|
||||
expect(calls[0]?.config).toBeUndefined()
|
||||
expect(calls[2]?.config).toEqual({ root: '/tmp/tui-sessions', compression: 'none' })
|
||||
const tuiConfig = calls[5]?.config as { sessionId: string }
|
||||
expect(calls[5]?.config).toEqual({
|
||||
maxReferences: 2,
|
||||
candidateLimit: 7,
|
||||
maxReferenceBytes: 1234,
|
||||
})
|
||||
const tuiConfig = calls[7]?.config as { sessionId: string }
|
||||
expect(tuiConfig).toMatchObject({
|
||||
welcome: 'TUI ready',
|
||||
resumeCommand: 'dsh --resume {session}',
|
||||
@@ -61,7 +73,7 @@ describe('dsh-tui-demo app', () => {
|
||||
maxToolOutputLines: 3,
|
||||
})
|
||||
expect(tuiConfig.sessionId).toMatch(/^main-session-[0-9a-f-]{36}$/)
|
||||
const spineConfig = calls[6]?.config as {
|
||||
const spineConfig = calls[8]?.config as {
|
||||
readonly agents: Array<Record<string, unknown>>
|
||||
readonly goals: Record<string, never>
|
||||
readonly maxParallelToolCalls: number
|
||||
@@ -95,9 +107,10 @@ describe('dsh-tui-demo app', () => {
|
||||
})
|
||||
|
||||
expect(calls[2]?.config).toEqual({ root: './.sessions' })
|
||||
expect(calls[5]?.config).toEqual({})
|
||||
// No configured welcome forwards none: the TUI banner sweeps in without a subtitle.
|
||||
expect(calls[5]?.config).toEqual({ sessionId: 'persisted-session' })
|
||||
expect((calls[6]?.config as { agents: Array<Record<string, unknown>> }).agents[0]).toMatchObject({
|
||||
expect(calls[7]?.config).toEqual({ sessionId: 'persisted-session' })
|
||||
expect((calls[8]?.config as { agents: Array<Record<string, unknown>> }).agents[0]).toMatchObject({
|
||||
id: 'main',
|
||||
resumeSessionId: 'persisted-session',
|
||||
})
|
||||
@@ -113,12 +126,12 @@ describe('dsh-tui-demo app', () => {
|
||||
workspaceContext: false,
|
||||
})
|
||||
|
||||
const tuiConfig = calls[4]?.config as { sessionId: string }
|
||||
const tuiConfig = calls[6]?.config as { sessionId: string }
|
||||
expect(tuiConfig.sessionId).toMatch(/^main-session-[0-9a-f-]{36}$/)
|
||||
expect((calls[5]?.config as { agents: Array<Record<string, unknown>> }).agents[0])
|
||||
expect((calls[7]?.config as { agents: Array<Record<string, unknown>> }).agents[0])
|
||||
.toMatchObject({ sessionId: tuiConfig.sessionId })
|
||||
expect(calls.map(call => call.name)).not.toContain('command-goal')
|
||||
expect(calls[5]?.config).toMatchObject({ goals: false })
|
||||
expect(calls[7]?.config).toMatchObject({ goals: false })
|
||||
})
|
||||
|
||||
it('has the namespace-plugin export shape so the Loader keeps its schema', () => {
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
{
|
||||
"path": "../../session-query/session-query"
|
||||
},
|
||||
{
|
||||
"path": "../../context/session-reference"
|
||||
},
|
||||
{
|
||||
"path": "../../ui/commands"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user