Skip to content

Commit ea2ad74

Browse files
authored
Merge pull request #12 from echoVic/codex/session-runtime
feat(cli): add session runtime reuse and headless runner
2 parents 810ab66 + bd7089e commit ea2ad74

34 files changed

+2026
-92
lines changed

README.en.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ pnpm add -g blade-code
5757
blade
5858
blade "Help me analyze this project"
5959
blade --print "Write a quicksort"
60+
blade --headless "Analyze this repo and propose a refactor"
61+
blade --headless --output-format jsonl "Run the full agent loop in CI"
6062

6163
# Web UI mode (new in 0.2.0)
6264
blade web # Start and open browser
@@ -97,10 +99,17 @@ See docs for the full schema.
9799
**Common Options**
98100

99101
- `--print/-p` print mode (pipe-friendly)
100-
- `--output-format` output: text/json/stream-json
102+
- `--headless` full agent mode without Ink UI, prints streamed events to the terminal
103+
- `--output-format` output: text/json/stream-json/jsonl
101104
- `--permission-mode` permission mode
102105
- `--resume/-r` resume session / `--session-id` set session
103106

107+
**Headless Mode**
108+
109+
- `blade --headless "..."` runs the full agent loop without the interactive Ink UI
110+
- default permission mode is `yolo`, unless explicitly overridden with `--permission-mode`
111+
- `--output-format jsonl` emits a stable machine-friendly event stream for CI, sandbox runs, and tests
112+
104113
---
105114

106115
## 📖 Documentation

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ pnpm add -g blade-code
5757
blade
5858
blade "帮我分析这个项目"
5959
blade --print "写一个快排算法"
60+
blade --headless "分析这个仓库并给出重构建议"
61+
blade --headless --output-format jsonl "在 CI 中运行完整 agent 循环"
6062

6163
# Web UI 模式(0.2.0 新增)
6264
blade web # 启动并打开浏览器
@@ -97,10 +99,17 @@ blade serve --port 3000 # 无头服务器模式
9799
**常用选项**
98100

99101
- `--print/-p` 打印模式(适合管道)
100-
- `--output-format` 输出格式(text/json/stream-json)
102+
- `--headless` 无 Ink UI 的完整 agent 模式,按终端事件流输出
103+
- `--output-format` 输出格式(text/json/stream-json/jsonl)
101104
- `--permission-mode` 权限模式
102105
- `--resume/-r` 恢复会话 / `--session-id` 指定会话
103106

107+
**Headless 模式**
108+
109+
- `blade --headless "..."` 会运行完整 agent loop,但不启动交互式 Ink UI
110+
- 默认权限模式为 `yolo`,除非显式传入 `--permission-mode`
111+
- `--output-format jsonl` 会输出稳定的机器可消费事件流,适合 CI、sandbox 和测试场景
112+
104113
**交互式命令(会话内)**
105114

106115
- `/memory list` 列出所有记忆文件

packages/cli/src/acp/BladeAgent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '@agentclientprotocol/sdk';
1414
import { nanoid } from 'nanoid';
1515
import { createLogger, LogCategory } from '../logging/Logger.js';
16+
import { McpRegistry } from '../mcp/McpRegistry.js';
1617
import { getConfig } from '../store/vanilla.js';
1718
import { AcpSession } from './Session.js';
1819

@@ -217,5 +218,6 @@ export class BladeAgent implements AcpAgentInterface {
217218
await session.destroy();
218219
}
219220
this.sessions.clear();
221+
await McpRegistry.getInstance().disconnectAll();
220222
}
221223
}

packages/cli/src/acp/Session.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
} from '@agentclientprotocol/sdk';
2323
import { nanoid } from 'nanoid';
2424
import { Agent } from '../agent/Agent.js';
25+
import { SessionRuntime } from '../agent/runtime/SessionRuntime.js';
2526
import type { ChatContext, LoopOptions } from '../agent/types.js';
2627
import { PermissionMode } from '../config/types.js';
2728
import { createLogger, LogCategory } from '../logging/Logger.js';
@@ -53,6 +54,7 @@ type AcpModeId = 'default' | 'auto-edit' | 'yolo' | 'plan';
5354

5455
export class AcpSession {
5556
private agent: Agent | null = null;
57+
private runtime: SessionRuntime | null = null;
5658
private pendingPrompt: AbortController | null = null;
5759
private messages: Message[] = [];
5860
private mode: AcpModeId = 'default';
@@ -82,8 +84,8 @@ export class AcpSession {
8284
);
8385
logger.debug(`[AcpSession ${this.id}] ACP service context initialized`);
8486

85-
// 创建 Agent(cwd 通过 ChatContext.workspaceRoot 传递,不修改全局工作目录)
86-
this.agent = await Agent.create({});
87+
this.runtime = await SessionRuntime.create({ sessionId: this.id });
88+
this.agent = await Agent.createWithRuntime(this.runtime, { sessionId: this.id });
8789

8890
logger.debug(`[AcpSession ${this.id}] Agent created successfully`);
8991
// 注意:available_commands_update 在 BladeAgent.newSession 响应后延迟发送
@@ -541,6 +543,10 @@ export class AcpSession {
541543
await this.agent.destroy();
542544
this.agent = null;
543545
}
546+
if (this.runtime) {
547+
await this.runtime.dispose();
548+
this.runtime = null;
549+
}
544550
// 销毁此会话的 ACP 服务(不影响其他会话)
545551
AcpServiceContext.destroySession(this.id);
546552
logger.debug(`[AcpSession ${this.id}] Destroyed`);

packages/cli/src/agent/Agent.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import { type Tool, ToolErrorType, type ToolResult } from '../tools/types/index.
7171
import { getEnvironmentContext } from '../utils/environment.js';
7272
import { isThinkingModel } from '../utils/modelDetection.js';
7373
import { ExecutionEngine } from './ExecutionEngine.js';
74+
import { SessionRuntime } from './runtime/SessionRuntime.js';
7475
import { subagentRegistry } from './subagents/SubagentRegistry.js';
7576
import type {
7677
AgentOptions,
@@ -115,15 +116,18 @@ export class Agent {
115116
// 当前模型的上下文窗口大小(用于 tokenUsage 上报)
116117
private currentModelMaxContextTokens!: number;
117118
private currentModelId?: string;
119+
private sessionRuntime?: SessionRuntime;
118120

119121
constructor(
120122
config: BladeConfig,
121123
runtimeOptions: AgentOptions = {},
122-
executionPipeline?: ExecutionPipeline
124+
executionPipeline?: ExecutionPipeline,
125+
sessionRuntime?: SessionRuntime
123126
) {
124127
this.config = config;
125128
this.runtimeOptions = runtimeOptions;
126129
this.executionPipeline = executionPipeline || this.createDefaultPipeline();
130+
this.sessionRuntime = sessionRuntime;
127131
// sessionId 不再存储在 Agent 内部,改为从 context 传入
128132
}
129133

@@ -190,6 +194,11 @@ export class Agent {
190194
}
191195

192196
private async switchModelIfNeeded(modelId: string): Promise<void> {
197+
if (this.sessionRuntime) {
198+
await this.sessionRuntime.refresh({ modelId });
199+
this.syncRuntimeState();
200+
return;
201+
}
193202
if (!modelId || modelId === this.currentModelId) return;
194203
const modelConfig = getModelById(modelId);
195204
if (!modelConfig) {
@@ -204,6 +213,12 @@ export class Agent {
204213
* 使用 Store 获取配置
205214
*/
206215
static async create(options: AgentOptions = {}): Promise<Agent> {
216+
if (options.sessionId) {
217+
throw new Error(
218+
'Agent.create() does not accept sessionId. Create a SessionRuntime explicitly and use Agent.createWithRuntime().'
219+
);
220+
}
221+
207222
// 0. 确保 store 已初始化(防御性检查)
208223
await ensureStoreInitialized();
209224

@@ -242,6 +257,20 @@ export class Agent {
242257
return agent;
243258
}
244259

260+
static async createWithRuntime(
261+
runtime: SessionRuntime,
262+
options: AgentOptions = {}
263+
): Promise<Agent> {
264+
const agent = new Agent(
265+
runtime.getConfig(),
266+
options,
267+
runtime.createExecutionPipeline(options),
268+
runtime
269+
);
270+
await agent.initialize();
271+
return agent;
272+
}
273+
245274
/**
246275
* 初始化Agent
247276
*/
@@ -253,6 +282,17 @@ export class Agent {
253282
try {
254283
this.log('初始化Agent...');
255284

285+
if (this.sessionRuntime) {
286+
await this.initializeSystemPrompt();
287+
await this.sessionRuntime.refresh(this.runtimeOptions);
288+
this.syncRuntimeState();
289+
this.isInitialized = true;
290+
this.log(
291+
`Agent初始化完成,已加载 ${this.executionPipeline.getRegistry().getAll().length} 个工具`
292+
);
293+
return;
294+
}
295+
256296
// 1. 初始化系统提示
257297
await this.initializeSystemPrompt();
258298

@@ -1903,6 +1943,19 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
19031943
}
19041944
}
19051945

1946+
private syncRuntimeState(): void {
1947+
if (!this.sessionRuntime) {
1948+
return;
1949+
}
1950+
1951+
this.chatService = this.sessionRuntime.getChatService();
1952+
this.executionEngine = this.sessionRuntime.getExecutionEngine();
1953+
this.attachmentCollector = this.sessionRuntime.getAttachmentCollector();
1954+
this.currentModelId = this.sessionRuntime.getCurrentModelId();
1955+
this.currentModelMaxContextTokens =
1956+
this.sessionRuntime.getCurrentModelMaxContextTokens();
1957+
}
1958+
19061959
/**
19071960
* 生成任务ID
19081961
*/

0 commit comments

Comments
 (0)