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:
Tianyi Cui
2026-07-22 23:50:41 +08:00
159 changed files with 3918 additions and 362 deletions
+2
View File
@@ -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.
+4
View File
@@ -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:^",
+7
View File
@@ -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'))
+6
View File
@@ -23,6 +23,12 @@
{
"path": "../../ui/acp"
},
{
"path": "../../session-query/session-query"
},
{
"path": "../../context/session-reference"
},
{
"path": "../../ui/commands"
},