Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	docs/architecture.i18n.yaml
#	docs/architecture.md
#	docs/architecture.zh.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/core-data-structures/core.md
#	docs/core-data-structures/core.zh.md
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/core-data-structures/session.i18n.yaml
#	docs/event-producer-consumer.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	packages/core/agent-loop/README.i18n.yaml
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/README.zh.md
#	packages/core/agent-loop/src/loop.ts
#	packages/core/agent/README.i18n.yaml
#	packages/core/agent/tests/llm-target.spec.ts
#	packages/core/session/tests/request-header.spec.ts
This commit is contained in:
_Kerman
2026-07-27 16:44:06 +08:00
220 changed files with 4592 additions and 969 deletions

View File

@@ -162,7 +162,7 @@ interface ToolArgsMap {
content: string;
/** pending (not started) | in_progress (now) | completed (done). */
status: "pending" | "in_progress" | "completed";
} & Record<string, JsonValue>)[];
})[];
} & Record<string, JsonValue>;
/** Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason. */
update_goal: {

View File

@@ -366,7 +366,7 @@
"description": "The COMPLETE task list, replacing any previous list.",
"items": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"content": {
"type": "string",

View File

@@ -145,7 +145,7 @@ interface ToolArgsMap {
content: string;
/** pending (not started) | in_progress (now) | completed (done). */
status: "pending" | "in_progress" | "completed";
} & Record<string, JsonValue>)[];
})[];
} & Record<string, JsonValue>;
/** Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason. */
update_goal: {

View File

@@ -309,7 +309,7 @@
"description": "The COMPLETE task list, replacing any previous list.",
"items": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"content": {
"type": "string",

View File

@@ -145,7 +145,7 @@ interface ToolArgsMap {
content: string;
/** pending (not started) | in_progress (now) | completed (done). */
status: "pending" | "in_progress" | "completed";
} & Record<string, JsonValue>)[];
})[];
} & Record<string, JsonValue>;
/** Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason. */
update_goal: {

View File

@@ -145,7 +145,7 @@ interface ToolArgsMap {
content: string;
/** pending (not started) | in_progress (now) | completed (done). */
status: "pending" | "in_progress" | "completed";
} & Record<string, JsonValue>)[];
})[];
} & Record<string, JsonValue>;
/** Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason. */
update_goal: {

File diff suppressed because one or more lines are too long

View File

@@ -288,7 +288,7 @@
"description": "The COMPLETE task list, replacing any previous list.",
"items": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"content": {
"type": "string",

View File

@@ -325,7 +325,7 @@
"description": "The COMPLETE task list, replacing any previous list.",
"items": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"content": {
"type": "string",

View File

@@ -417,7 +417,7 @@
"description": "The COMPLETE task list, replacing any previous list.",
"items": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"content": {
"type": "string",

View File

@@ -492,7 +492,7 @@
"description": "The COMPLETE task list, replacing any previous list.",
"items": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"content": {
"type": "string",

View File

@@ -288,7 +288,7 @@
"description": "The COMPLETE task list, replacing any previous list.",
"items": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"content": {
"type": "string",

View File

@@ -288,7 +288,7 @@
"description": "The COMPLETE task list, replacing any previous list.",
"items": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"content": {
"type": "string",

View File

@@ -288,7 +288,7 @@
"description": "The COMPLETE task list, replacing any previous list.",
"items": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"properties": {
"content": {
"type": "string",

View File

@@ -1,8 +1,33 @@
import type { Context } from 'cordis'
import { CallId, LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
import {
CallId,
LlmAdapter,
ReasoningEffortId,
type GenerateOptions,
type LlmResolvedModelInfo,
type StreamChunk,
} from '@deepseek-ai/dsh-llm'
const HIGH = ReasoningEffortId('high')
const OFF = ReasoningEffortId('off')
/** Keyless headless-agent adapter: one real bash call followed by a final answer. */
class CliMockAdapter extends LlmAdapter {
override async resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
return {
provider,
id: model,
name: model,
reasoning: {
efforts: [
{ id: OFF, name: 'Off' },
{ id: HIGH, name: 'High' },
],
defaultEffort: HIGH,
},
}
}
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const toolResult = options.messages.at(-1)?.content.find(block => block.type === 'tool-result')
if (toolResult === undefined) {
@@ -34,4 +59,8 @@ export const inject = ['llm']
/** Register the keyless `cli-mock` adapter. */
export function apply(ctx: Context): void {
ctx.llm.registerAdapter(['cli-mock'], new CliMockAdapter())
ctx.on('agent/request', async (_agent, _turn, step, _signal, next) => {
const config = await next()
return step === 2 ? { ...config, reasoningEffort: OFF } : config
})
}

View File

@@ -28,6 +28,7 @@ const ralphScenarioDir = join(snapshotsDir, 'ralph-loop')
const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', import.meta.url))
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const reasoningConfigPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
interface JsonObject {
@@ -124,6 +125,46 @@ async function persistedLogs(cwd: string): Promise<PersistedLog[]> {
}
describe('headless stream-json snapshots', () => {
it('logs the model default and a dynamic next-step reasoning effort', async () => {
const result = await runLoaderSmoke({
label: 'reasoning effort headless stream-json snapshot',
tempDirPrefix: 'headless-snapshot-reasoning-effort-',
binScript,
configPath: reasoningConfigPath,
binArgs: ['--config', reasoningConfigPath, '--output-format', 'stream-json', 'prove dynamic reasoning effort'],
tsconfigPath,
})
expect(result.stderr).toBe('')
const headers = parseJsonl(result.stdout)
.map(record => record.event)
.filter((event): event is JsonObject => (
event !== null
&& typeof event === 'object'
&& !Array.isArray(event)
&& 'type' in event
&& event.type === 'request/header'
))
.map((event) => {
const data = event.data as JsonObject
return (data.header as JsonObject).config
})
expect(headers).toMatchInlineSnapshot(`
[
{
"model": "cli-mock",
"provider": "cli-mock",
"reasoningEffort": "high",
},
{
"model": "cli-mock",
"provider": "cli-mock",
"reasoningEffort": "off",
},
]
`)
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('replays the advanced toolchain through the one-shot app', async () => {
const prompt = await scenarioPrompt(advancedScenarioDir, 'advanced-toolchain')
const fixtureFiles = [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
README.md: fdf3972f98f8ff9690b71469ae415dc18d339f10
README.zh.md: 71f7ae949d034757a20adfae2cbe566011edc584
# pnpm run verify-translation-pairing --write examples/tui-agent/README.md
README.md: ea8695d37ea247a38644392a4572c1ea9855fd44
README.zh.md: b3f6dc18536b159379eac7433367ccf2cd8fcc53

View File

@@ -19,7 +19,7 @@ Type a coding task. The agent works through the `read`/`write`/`edit` filesystem
The `todo_write` task tracker is opt-in and not in the shipped config: add `@deepseek-ai/dsh-tool-todo` to `cordis.yml` (or a personal-config overlay under `~/.dsh`) to expose it. Once loaded, the model records a whole-list plan to the session log and the TUI renders it.
The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and — when `todo_write` is loaded — the latest plan. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/plan` selects plan mode for the next step; `/plan <message>` also submits the message into that step, while `/plan off` selects the default mode without model input. `/status` expands the current session's identity, activity counts, exact token/cache buckets, context use, and timestamps without interrupting a running turn. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model <model>` and `/model <provider>/<model>` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options.
The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and — when `todo_write` is loaded — the latest plan. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/plan` selects plan mode for the next step; `/plan <message>` also submits the message into that step, while `/plan off` selects the default mode without model input. `/status` expands the current session's identity, activity counts, exact token/cache buckets, context use, and timestamps without interrupting a running turn. `/model` opens a keyboard selector for the current provider catalog; use Up/Down to focus a model, Shift+Tab to cycle its advertised reasoning efforts, and Enter to select, or use `/model <model>` and `/model <provider>/<model>` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options.
### Resuming a prior session
@@ -53,7 +53,7 @@ This example is a thin leaf `cordis.yml`: it picks the swappable backends, loads
| Entry | Demonstrates |
|---|---|
| `hmr` (`@cordisjs/plugin-hmr`) | the dev/demo edit-reload loop — a **leaf** entry (not baked into the app) because it depends on the Loader's internal module access |
| `llm-deepseek` | real `LlmAdapter` via config (`!!js process.env.…` secrets); swap one line to `@deepseek-ai/dsh-llm-pi-ai` for the library-backed twin |
| `llm-deepseek` | the default native adapter |
| `bash` (`dsh-bash-local`) | the executor implementation — the swappable half of the bash seam. The model-facing `bash` schema (`tool-bash`) and generic `task_*` controls (`tool-tasks`) come from `dsh-agent-spine-demo`, so only the executor is a leaf choice |
| `tui-agent` (`@deepseek-ai/dsh-tui-demo`) | the app bundle: the agent-spine demo + JSONL persistence + the pi-tui channel + a pre-created `main` agent |
| `subagent`, `subagent-spawn`, `subagent-fork` | the subagent provider registry plus the two in-process backends: a fresh child and a child seeded with the parent's completed-turn prefix |

View File

@@ -19,7 +19,7 @@ pnpm run demo:tui
`todo_write` 任务跟踪器是选用的,不在已交付配置中:请将 `@deepseek-ai/dsh-tool-todo` 添加到 `cordis.yml`(或在 `~/.dsh` 下使用个人配置覆盖以公开该工具。加载后模型会把整表计划记录到会话日志TUI 则渲染它。
TUI 渲染 Markdown 历史、推理、工具所有的终端diff通用卡片、token 总量,以及加载 `todo_write` 时的最新计划。较长的工具正文保留首尾预览Ctrl+O 展开或折叠所有卡片。Enter 用于提交,或在 agent 运行时进行 steering中途引导Ctrl+R 切换推理Escape 取消,`/help` 列出命令。`/plan` 为下一步骤选择 plan mode`/plan <message>` 还会将消息提交到该步骤,`/plan off` 则在没有模型输入的情况下选择默认 mode。`/status` 会展开当前会话的标识、活动计数、精确 token缓存 bucket、上下文用量和时间戳而不中断正在运行的轮次。`/model` 打开当前提供方目录的键盘选择器;使用 Up/Down 和 Enter使用 `/model <model>``/model <provider>/<model>` 直接选择。`ask_user_question` 会打开一个位于左下方的宽键盘面板,包含批次进度和编号选项。
TUI 渲染 Markdown 历史、推理、工具所有的终端diff通用卡片、token 总量,以及加载 `todo_write` 时的最新计划。较长的工具正文保留首尾预览Ctrl+O 展开或折叠所有卡片。Enter 用于提交,或在 agent 运行时进行 steering中途引导Ctrl+R 切换推理Escape 取消,`/help` 列出命令。`/plan` 为下一步骤选择 plan mode`/plan <message>` 还会将消息提交到该步骤,`/plan off` 则在没有模型输入的情况下选择默认 mode。`/status` 会展开当前会话的标识、活动计数、精确 token缓存 bucket、上下文用量和时间戳而不中断正在运行的轮次。`/model` 打开当前提供方目录的键盘选择器;使用 Up/Down 聚焦模型,使用 Shift+Tab 循环切换为该模型公布的推理强度,再用 Enter 选择;也可以使用 `/model <model>``/model <provider>/<model>` 直接选择。`ask_user_question` 会打开一个位于左下方的宽键盘面板,包含批次进度和编号选项。
### 恢复早先的会话
@@ -53,7 +53,7 @@ pnpm run demo:code-mode acp # the acp-agent example's same-shaped overlay
| 配置项 | 演示内容 |
|---|---|
| `hmr` (`@cordisjs/plugin-hmr`) | 开发/演示的编辑-重载循环:它是 **叶节点** 配置项(不内置到应用),因为它依赖 Loader 的内部模块访问 |
| `llm-deepseek` | 通过配置提供真实 `LlmAdapter``!!js process.env.…` 密钥);将一行替换为 `@deepseek-ai/dsh-llm-pi-ai` 即可使用库后端对照实现 |
| `llm-deepseek` | 默认原生适配器 |
| `bash` (`dsh-bash-local`) | 执行器实现bash seam 的可替换一半。面向模型的 `bash` schema`tool-bash`)和通用 `task_*` 控制(`tool-tasks`)由 `dsh-agent-spine-demo` 提供,因此叶节点只选择执行器 |
| `tui-agent` (`@deepseek-ai/dsh-tui-demo`) | 应用组合包agent-spine 演示 + JSONL 持久化 + pi-tui 通道 + 预创建的 `main` agent |
| `subagent`, `subagent-spawn`, `subagent-fork` | subagent 提供方注册表加两个进程内后端:新子 agent以及用父 agent 已完成轮次前缀播种的子 agent |

View File

@@ -1,6 +1,11 @@
import type { Context } from 'cordis'
import type { GenerateOptions, LlmModelContext, LlmModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type {
GenerateOptions,
LlmModelInfo,
LlmResolvedModelInfo,
StreamChunk,
} from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
const CONTROL_PROBE = '\u001b]2;MODEL_CONTROLLED\u0007\u001b[999CMODEL_CURSOR\u009b31mMODEL_C1'
const INITIAL_TEXT = `I need one decision before I continue. ${CONTROL_PROBE}`
@@ -35,8 +40,28 @@ class ScriptedTuiAdapter extends LlmAdapter {
])
}
override resolveModelContext(_provider: string, _model: string): Promise<LlmModelContext> {
return Promise.resolve({ contextWindow: 128_000 })
override resolveModel(
provider: string,
model: string,
): Promise<LlmResolvedModelInfo> {
return Promise.resolve({
provider,
id: model,
name: model === 'tui-scripted-model-pro' ? 'Scripted Pro' : 'Scripted Base',
context: { contextWindow: 128_000 },
...model !== 'tui-scripted-model-pro'
? {}
: {
reasoning: {
efforts: [
{ id: ReasoningEffortId('off'), name: 'Off' },
{ id: ReasoningEffortId('high'), name: 'High' },
{ id: ReasoningEffortId('max'), name: 'Max' },
],
defaultEffort: ReasoningEffortId('high'),
},
},
})
}
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
@@ -47,8 +72,12 @@ class ScriptedTuiAdapter extends LlmAdapter {
for (const chunk of textChunks(TITLE_TEXT)) yield chunk
return
}
if (options.model !== 'tui-scripted-model-pro' || !options.system?.includes('tui-scripted-model-pro')) {
throw new Error('the scripted TUI request did not apply the selected model to routing and prompt variables')
if (
options.model !== 'tui-scripted-model-pro'
|| !options.system?.includes('tui-scripted-model-pro')
|| options.reasoningEffort !== ReasoningEffortId('max')
) {
throw new Error('the scripted TUI request did not apply the selected model and reasoning effort')
}
const lastMessage = options.messages.at(-1)
// The loop appends plugin-sourced context (the plan-mode notice, the

View File

@@ -103,7 +103,7 @@ function smoke(overrides: Partial<TuiPtySmokeOptions> & { label: string }): Prom
// other route (see fixtures/tui-scripted-llm.ts).
const SELECT_PRO_MODEL = [
{ waitFor: 'scripted TUI ready.', send: '/model\r' },
{ waitFor: 'Select model', send: '\x1b[B\r' },
{ waitFor: 'Select model', send: '\x1b[B\x1b[Z\r' },
] as const
describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
@@ -156,6 +156,7 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
],
})
expect(output).toContain('I need one decision before I continue.')
expect(output).toContain('Reasoning effort: Max.')
expect(output).toContain('Entering plan mode (applies from the next step). Use /plan off to leave.')
expect(output).toContain('Leaving plan mode (applies from the next step).')
expect(output).toContain('Default mode confirmed.')