Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pnpm add -g blade-code
blade
blade "Help me analyze this project"
blade --print "Write a quicksort"
blade --headless "Analyze this repo and propose a refactor"
blade --headless --output-format jsonl "Run the full agent loop in CI"

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

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

**Headless Mode**

- `blade --headless "..."` runs the full agent loop without the interactive Ink UI
- default permission mode is `yolo`, unless explicitly overridden with `--permission-mode`
- `--output-format jsonl` emits a stable machine-friendly event stream for CI, sandbox runs, and tests

---

## 📖 Documentation
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pnpm add -g blade-code
blade
blade "帮我分析这个项目"
blade --print "写一个快排算法"
blade --headless "分析这个仓库并给出重构建议"
blade --headless --output-format jsonl "在 CI 中运行完整 agent 循环"

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

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

**Headless 模式**

- `blade --headless "..."` 会运行完整 agent loop,但不启动交互式 Ink UI
- 默认权限模式为 `yolo`,除非显式传入 `--permission-mode`
- `--output-format jsonl` 会输出稳定的机器可消费事件流,适合 CI、sandbox 和测试场景

**交互式命令(会话内)**

- `/memory list` 列出所有记忆文件
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/acp/BladeAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@agentclientprotocol/sdk';
import { nanoid } from 'nanoid';
import { createLogger, LogCategory } from '../logging/Logger.js';
import { McpRegistry } from '../mcp/McpRegistry.js';
import { getConfig } from '../store/vanilla.js';
import { AcpSession } from './Session.js';

Expand Down Expand Up @@ -217,5 +218,6 @@ export class BladeAgent implements AcpAgentInterface {
await session.destroy();
}
this.sessions.clear();
await McpRegistry.getInstance().disconnectAll();
}
}
10 changes: 8 additions & 2 deletions packages/cli/src/acp/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
} from '@agentclientprotocol/sdk';
import { nanoid } from 'nanoid';
import { Agent } from '../agent/Agent.js';
import { SessionRuntime } from '../agent/runtime/SessionRuntime.js';
import type { ChatContext, LoopOptions } from '../agent/types.js';
import { PermissionMode } from '../config/types.js';
import { createLogger, LogCategory } from '../logging/Logger.js';
Expand Down Expand Up @@ -53,6 +54,7 @@ type AcpModeId = 'default' | 'auto-edit' | 'yolo' | 'plan';

export class AcpSession {
private agent: Agent | null = null;
private runtime: SessionRuntime | null = null;
private pendingPrompt: AbortController | null = null;
private messages: Message[] = [];
private mode: AcpModeId = 'default';
Expand Down Expand Up @@ -82,8 +84,8 @@ export class AcpSession {
);
logger.debug(`[AcpSession ${this.id}] ACP service context initialized`);

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

logger.debug(`[AcpSession ${this.id}] Agent created successfully`);
// 注意:available_commands_update 在 BladeAgent.newSession 响应后延迟发送
Expand Down Expand Up @@ -541,6 +543,10 @@ export class AcpSession {
await this.agent.destroy();
this.agent = null;
}
if (this.runtime) {
await this.runtime.dispose();
this.runtime = null;
}
// 销毁此会话的 ACP 服务(不影响其他会话)
AcpServiceContext.destroySession(this.id);
logger.debug(`[AcpSession ${this.id}] Destroyed`);
Expand Down
55 changes: 54 additions & 1 deletion packages/cli/src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { type Tool, ToolErrorType, type ToolResult } from '../tools/types/index.
import { getEnvironmentContext } from '../utils/environment.js';
import { isThinkingModel } from '../utils/modelDetection.js';
import { ExecutionEngine } from './ExecutionEngine.js';
import { SessionRuntime } from './runtime/SessionRuntime.js';
import { subagentRegistry } from './subagents/SubagentRegistry.js';
import type {
AgentOptions,
Expand Down Expand Up @@ -115,15 +116,18 @@ export class Agent {
// 当前模型的上下文窗口大小(用于 tokenUsage 上报)
private currentModelMaxContextTokens!: number;
private currentModelId?: string;
private sessionRuntime?: SessionRuntime;

constructor(
config: BladeConfig,
runtimeOptions: AgentOptions = {},
executionPipeline?: ExecutionPipeline
executionPipeline?: ExecutionPipeline,
sessionRuntime?: SessionRuntime
) {
this.config = config;
this.runtimeOptions = runtimeOptions;
this.executionPipeline = executionPipeline || this.createDefaultPipeline();
this.sessionRuntime = sessionRuntime;
// sessionId 不再存储在 Agent 内部,改为从 context 传入
}

Expand Down Expand Up @@ -190,6 +194,11 @@ export class Agent {
}

private async switchModelIfNeeded(modelId: string): Promise<void> {
if (this.sessionRuntime) {
await this.sessionRuntime.refresh({ modelId });
this.syncRuntimeState();
return;
}
if (!modelId || modelId === this.currentModelId) return;
const modelConfig = getModelById(modelId);
if (!modelConfig) {
Expand All @@ -204,6 +213,12 @@ export class Agent {
* 使用 Store 获取配置
*/
static async create(options: AgentOptions = {}): Promise<Agent> {
if (options.sessionId) {
throw new Error(
'Agent.create() does not accept sessionId. Create a SessionRuntime explicitly and use Agent.createWithRuntime().'
);
}

// 0. 确保 store 已初始化(防御性检查)
await ensureStoreInitialized();

Expand Down Expand Up @@ -242,6 +257,20 @@ export class Agent {
return agent;
}

static async createWithRuntime(
runtime: SessionRuntime,
options: AgentOptions = {}
): Promise<Agent> {
const agent = new Agent(
runtime.getConfig(),
options,
runtime.createExecutionPipeline(options),
runtime
);
await agent.initialize();
return agent;
}

/**
* 初始化Agent
*/
Expand All @@ -253,6 +282,17 @@ export class Agent {
try {
this.log('初始化Agent...');

if (this.sessionRuntime) {
await this.initializeSystemPrompt();
await this.sessionRuntime.refresh(this.runtimeOptions);
this.syncRuntimeState();
this.isInitialized = true;
this.log(
`Agent初始化完成,已加载 ${this.executionPipeline.getRegistry().getAll().length} 个工具`
);
return;
}

// 1. 初始化系统提示
await this.initializeSystemPrompt();

Expand Down Expand Up @@ -1903,6 +1943,19 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
}
}

private syncRuntimeState(): void {
if (!this.sessionRuntime) {
return;
}

this.chatService = this.sessionRuntime.getChatService();
this.executionEngine = this.sessionRuntime.getExecutionEngine();
this.attachmentCollector = this.sessionRuntime.getAttachmentCollector();
this.currentModelId = this.sessionRuntime.getCurrentModelId();
this.currentModelMaxContextTokens =
this.sessionRuntime.getCurrentModelMaxContextTokens();
}

/**
* 生成任务ID
*/
Expand Down
Loading
Loading