Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,10 @@ dist-test/
# Desktop sub-package residual workspace files
packages/desktop/pnpm-lock.yaml
packages/desktop/pnpm-workspace.yaml

# Defensive: ignore any stray test temp dirs that may slip into the workspace.
.test-*
# OS / editor cruft
Thumbs.db
.idea/
.vscode/
7 changes: 4 additions & 3 deletions docs/subagent.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,17 @@ maxSteps: 180

### plan

只读代码研究 + 规划 Agent,可执行命令来验证环境:
只读代码研究 + 规划 Agent。**只允许只读工具**和 `submit_plan`(用于提交实现计划等待用户审批),不允许执行命令或写文件。计划提交后 session 会自动切换到 `build` profile。

```yaml
name: plan
description: 只读代码研究和规划
tools: [read_file, search_files, search_code, execute_command, fetch_url, tool_search]
readonly: true
tools: [read_file, search_files, search_code, fetch_url, tool_search, submit_plan, dispatch_agent]
maxSteps: 180
```

> 注意:`plan` profile 自身不设置 `permissionMode`。在 plan 模式下,写工具会被 `plan/planModeGateHook`(注册在 `tool.approval.pre`,priority -1000)拒绝,仅 `submit_plan` 与 `dispatch_agent` 放行。`dispatch_agent` 由 `plan/planSubagentWhitelistHook` 进一步限制为只能派发 `explore` 子代理。

---

## 执行流程
Expand Down
7 changes: 4 additions & 3 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ interface ToolVisibilityPolicy {
|------|------|------|
| 1 | **RuleEngine** | 规则引擎匹配,支持 glob 模式匹配工具名和参数,按优先级排序 |
| 2 | **ReadonlyWhitelist** | 只读工具自动放行(read_file, search_code, search_files, fetch_url, web_search, dispatch_agent, todo_write) |
| 3 | **PermissionMode** | 权限模式判断:`plan`(只允许只读)、`bypass`(全部放行)、`acceptEdits`(非破坏性工具放行)、`default`(继续下一层) |
| 3 | **PermissionMode** | 权限模式判断:`bypass`(全部放行)、`acceptEdits`(非破坏性工具放行)、`default`(继续下一层)。`plan` 模式由独立的 `plan/planModeGateHook` 在 Layer 4 强制,不在此层处理 |
| 4 | **HookPreToolUse** | 钩子决策,可返回 allow/deny/ask/continue,支持 `modifiedInput` 修改参数 |
| 5 | **UserConfirmation** | 异步用户确认,支持 allow/deny/always/never 四种响应,always/never 会持久化为规则 |
| 6 | **AuditLog** | 每一层决策后记录审计日志,通过 `tool.approval.post` 钩子发出 |
Expand All @@ -132,14 +132,15 @@ interface ToolVisibilityPolicy {
### 权限模式

```typescript
type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'bypass';
type PermissionMode = 'default' | 'acceptEdits' | 'bypass';
```

- `default`:逐层审批,危险操作需用户确认
- `acceptEdits`:非破坏性工具自动放行,减少确认弹窗
- `plan`:只允许只读工具,适合纯分析场景
- `bypass`:全部放行,跳过所有审批(慎用)

> `plan` 不再是 `PermissionMode` 的成员。plan 模式通过 `AgentProfile.name === 'plan'` 结构化识别,由 `plan/planModeGateHook` 在 `tool.approval.pre` 阶段(priority -1000)强制拒绝非白名单工具。白名单见 `plan/policy.ts` 的 `PLAN_MODE_ALLOWED_TOOLS`。

### OS 级沙箱(预留)

`packages/codingcode/src/sandbox/` 目前是 stub 实现(`SandboxService` 为空类),尚未集成实际的沙箱运行时。审批流水线已提供基本安全保障,OS 级沙箱将在未来版本中实现。
60 changes: 48 additions & 12 deletions packages/codingcode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
import { createDispatchAgentTool } from '../tools/domains/subagent/dispatch.js';
import { LLMFactoryService } from '../llm/factory.js';
import { getBuiltinTools } from '../tools/providers.js';
import { submitPlanTool } from '../tools/domains/subagent/submit-plan.js';
import { canonicalizeSchema } from '../tools/utils/canonicalize-schema.js';
import { normalizePath } from '../core/path.js';
import { isPlanProfile } from '../plan/index.js';

const REACTIVE_COMPACT_MAX_RETRIES = 3;
import { RulesService } from '../rules/index.js';
Expand Down Expand Up @@ -127,12 +129,12 @@
const hooks = yield* HookService;
const mcp = yield* McpService;
const checkpoint = yield* CheckpointService;
const approval = yield* ApprovalService;

Check warning on line 132 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'approval' is assigned a value but never used. Allowed unused vars must match /^_/u
const skills = yield* SkillService;
const runtime = yield* ProjectRuntimeService;
const todo = yield* TodoService;

Check warning on line 135 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'todo' is assigned a value but never used. Allowed unused vars must match /^_/u
const rules = yield* RulesService;
const context = yield* ContextService;

Check warning on line 137 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'context' is assigned a value but never used. Allowed unused vars must match /^_/u
const memory = yield* MemoryService;
const factory = yield* LLMFactoryService;

Expand All @@ -143,6 +145,9 @@
const state = sessionId
? yield* session.load(normalizedCwd, sessionId)
: yield* session.create(normalizedCwd, llm.modelInfo.model);
if (state.activeProfile) {
yield* runtime.restoreSessionProfile(normalizedCwd, state.sessionId, state.activeProfile);
}
state.model = llm.modelInfo.model;
state.memorySnapshot = memory.loadMemoryForPrompt(state.cwd);
const sid = state.sessionId;
Expand All @@ -160,9 +165,7 @@
}
}
const effectiveMaxSteps = profile?.maxSteps;
const effectiveApproval: any = profile?.readonly
? { permissionMode: 'bypass' }
: options?.approvalOverride;
const effectiveApproval: any = options?.approvalOverride;

if (profile?.hooks?.length) {
yield* hooks.attachSessionHooks(sid, profile.hooks);
Expand All @@ -187,6 +190,7 @@
const stream = agent.runStream({
state,
llm: activeLlm,
profile,
toolPolicy: policy,
maxStepsOverride: effectiveMaxSteps,
approvalOverride: effectiveApproval,
Expand Down Expand Up @@ -221,6 +225,7 @@
> {
const state = opts.state;
const llm = opts.llm;
const profile = opts.profile;
const sessionId = state.sessionId;
const projectPath = state.cwd;

Expand All @@ -234,9 +239,12 @@
const { skillInstruction, systemPromptVariant, rulesText } = opts;

const allAgentProfiles = runtime.listAgentProfiles(projectPath);
const agentProfiles = resolveSubagentEnabled(projectPath)
const enabledAgentProfiles = resolveSubagentEnabled(projectPath)
? allAgentProfiles.filter((p) => !resolveAgentDisabled(projectPath, p.name))
: [];
const visibleAgentProfiles = isPlanProfile(profile)
? enabledAgentProfiles.filter((p) => p.name === 'explore')
: enabledAgentProfiles;
const basePrompt =
opts.systemOverride ??
buildSystemPrompt({
Expand All @@ -245,8 +253,9 @@
shell: process.env.SHELL || process.env.ComSpec || 'bash',
variant: systemPromptVariant ?? 'default',
skillInstruction,
agentProfiles,
agentProfiles: visibleAgentProfiles,
rules: rulesText,
profileSystemPrompt: profile?.systemPrompt,
});

const memoryBlock = state.memorySnapshot;
Expand Down Expand Up @@ -281,6 +290,7 @@
let allToolDefs: ToolDefinition[] = [...builtinTools, ...(opts.mcpTools ?? [])];
if (opts.dispatchTool && resolveSubagentEnabled(projectPath))
allToolDefs = [...allToolDefs, opts.dispatchTool];
if (isPlanProfile(profile)) allToolDefs = [...allToolDefs, submitPlanTool];

const allowedByPolicy = opts.toolPolicy?.allowedTools;
let filteredDefs = allToolDefs;
Expand Down Expand Up @@ -459,7 +469,7 @@
}
}

const record = yield* session.recordAssistant(state, resp.content, toolCalls!, resp.usage);

Check warning on line 472 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'record' is assigned a value but never used. Allowed unused vars must match /^_/u
const allResults = yield* executor.executeBatch(toolCalls, state.sessionId, {
turnId: state.currentTurnId,
projectPath,
Expand Down Expand Up @@ -496,6 +506,31 @@
todoPrinted = true;
}
}

const submittedPlan = allResults.some(
(r) =>
r.type === 'ok' &&
r.name === 'submit_plan' &&
typeof r.output === 'string' &&
r.output.startsWith('Plan written to ')
);
if (submittedPlan) {
yield* q.offer({
_tag: 'Done',
content:
'Plan submitted and saved. Session switched to build mode — send a new message to execute.',
});
yield* hooks.emit('agent.turn.end', {
sessionId,
turnId: state.currentTurnId,
status: 'done',
});
yield* checkpoint.snapshotFinal(projectPath, state.sessionId, state.currentTurnId);
memory
.flushSessionToMemory(state.sessionId, llm, state.cwd)
.catch((e) => logger.error('memory flush failed:', e));
return Result.ok('Plan submitted');
}
}

if (overflow) continue;
Expand Down Expand Up @@ -530,18 +565,19 @@
}).pipe(
Effect.interruptible,
Effect.onInterrupt(() =>
Effect.sync(() => {
Effect.runSync(
q.offer({ _tag: 'Error', error: new AgentError('AGENT_ABORTED', 'cancelled') })
);
hooks
Effect.gen(function* () {
yield* Effect.sync(() => {
Effect.runSync(
q.offer({ _tag: 'Error', error: new AgentError('AGENT_ABORTED', 'cancelled') })
);
});
yield* hooks
.emit('agent.turn.end', {
sessionId,
turnId: state.currentTurnId,
status: 'aborted',
})
.pipe(Effect.runPromise)
.catch(() => {});
.pipe(Effect.ignore);
})
),
Effect.ensuring(
Expand Down
51 changes: 26 additions & 25 deletions packages/codingcode/src/agent/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,71 @@
import type { SystemPromptOptions } from './types.js';

const DEFAULT_SYSTEM_PROMPT = `You are a coding assistant — an AI agent that helps users with software engineering tasks.
const DEFAULT_BEHAVIOR_PROMPT = `You are a coding assistant —an AI agent that helps users with software engineering tasks.

## How you work
- Your text output is displayed to the user as formatted text. Tool calls and their results are shown separately — the user can see what tools you used and their outcomes.
- Tools run behind a permission system. If a tool call is denied, the user declined it — adjust your approach, do not retry the same call verbatim.
- Messages may contain <system-reminder> tags injected by the system, not by the user. They contain useful operational information — always read and follow them.
- Your text output is displayed to the user as formatted text. Tool calls and their results are shown separately —the user can see what tools you used and their outcomes.
- Tools run behind a permission system. If a tool call is denied, the user declined it —adjust your approach, do not retry the same call verbatim.
- Messages may contain <system-reminder> tags injected by the system, not by the user. They contain useful operational information —always read and follow them.

## Rules
1. Read files before modifying them — never guess file contents
2. Use search_code or search_files to locate code before reading — this is faster than reading entire files blindly
1. Read files before modifying them —never guess file contents
2. Use search_code or search_files to locate code before reading —this is faster than reading entire files blindly
3. Prefer editing existing files over creating new ones
4. Make small, focused changes — avoid large rewrites
4. Make small, focused changes —avoid large rewrites
5. Run tests or type-check after changes when applicable
6. If the user's request is ambiguous, ask for clarification
7. For complex or broad tasks (understanding a whole module, cross-file analysis, comprehensive search):
a. Briefly assess the task scope using your own reasoning — do not use tools for exploration at this stage, as that would consume your limited context window.
a. Briefly assess the task scope using your own reasoning —do not use tools for exploration at this stage, as that would consume your limited context window.
b. If you can clearly handle it without extensive file reading or searching, proceed yourself.
c. Otherwise, delegate to dispatch_agent with the original task and your assessment of what needs to be explored. The subagent handles discovery in its own separate context, keeping your main context clean for coordination.

## Using your tools
- **Prefer dedicated tools over shell commands.** Use read_file instead of cat, edit_file instead of sed, search_code instead of grep. Dedicated tools give the user better visibility into your work.
- **Call multiple tools in parallel** when they are independent — for example, reading several files at once, or searching with different patterns. Do NOT make sequential calls when the calls don't depend on each other.
- After editing a file, do NOT re-read it to verify — the edit tool already confirms success or reports failure. Only re-read if you suspect the edit did not apply correctly.
- **Call multiple tools in parallel** when they are independent —for example, reading several files at once, or searching with different patterns. Do NOT make sequential calls when the calls don't depend on each other.
- After editing a file, do NOT re-read it to verify —the edit tool already confirms success or reports failure. Only re-read if you suspect the edit did not apply correctly.
- Reserve execute_command for actual system commands and terminal operations (git, npm, build, test). Do not use it for file operations that dedicated tools can handle.

## Executing actions with care
Consider the reversibility and blast radius of actions before taking them:
- **Freely take** local, reversible actions: editing files, running tests, reading code.
- **Confirm with the user before** hard-to-reverse or outward-facing actions: pushing code, deleting files/branches, force-pushing, modifying CI/CD pipelines, sending messages to external services.
- **Never** use destructive commands (rm -rf /, sudo, git reset --hard, git push --force, git clean -f) unless explicitly requested and approved by the user.
- When you encounter unexpected state (unfamiliar files, branches, or configuration), investigate before deleting or overwriting — it may be the user's in-progress work. Never revert changes you did not make.
- When you encounter unexpected state (unfamiliar files, branches, or configuration), investigate before deleting or overwriting —it may be the user's in-progress work. Never revert changes you did not make.

## Git operations
- Do NOT commit changes unless the user explicitly asks you to.
- Do NOT push to remote unless the user explicitly asks you to.
- Do NOT use destructive git commands (git reset --hard, git push --force, git clean -f, git checkout -- .) unless explicitly requested and approved.
- If you notice unexpected changes in the working tree that you did not make, investigate before acting — they may be the user's in-progress work.
- If you notice unexpected changes in the working tree that you did not make, investigate before acting —they may be the user's in-progress work.

## Professional objectivity
Prioritize technical accuracy over validating the user's beliefs. When necessary, push back respectfully — honest guidance is more valuable than false agreement.
Prioritize technical accuracy over validating the user's beliefs. When necessary, push back respectfully —honest guidance is more valuable than false agreement.
- Do not begin responses with conversational interjections ("Got it", "Sure", "Great question")
- Do not apologize unnecessarily when results are unexpected

## Follow existing conventions
When modifying code, first look at the surrounding code's style (naming, frameworks, imports) and match it:
- **Never assume a library is available** — check imports in neighboring files, or check the dependency file (package.json, cargo.toml, requirements.txt, etc.) before using it.
- **Never assume a library is available** —check imports in neighboring files, or check the dependency file (package.json, cargo.toml, requirements.txt, etc.) before using it.
- **When creating a new component**, first look at existing components to understand naming conventions, typing patterns, and framework choices.
- **When editing code**, look at the surrounding context (especially imports) to understand the code's choice of frameworks and libraries, then make your change in the most idiomatic way.
- **Comments**: default to writing no comments. Only add one when the WHY is non-obvious — a hidden constraint, a subtle invariant, or a workaround for a specific bug. Do not explain WHAT the code does.
- **Comments**: default to writing no comments. Only add one when the WHY is non-obvious —a hidden constraint, a subtle invariant, or a workaround for a specific bug. Do not explain WHAT the code does.

## Code references
When referencing code, use the format \`file_path:line_number\` for easy navigation.

## Output efficiency
- Be concise. Lead with the answer or action, not with reasoning or preamble.
- Skip filler words and unnecessary transitions. Do not restate what the user said — just do it.
- Skip filler words and unnecessary transitions. Do not restate what the user said —just do it.
- When working on a multi-step task, give brief updates at key moments (when you find something, change direction, or hit a blocker). One sentence per update is enough.
- When the task is done, give a one-to-two sentence summary of what changed. Do not narrate your entire process.
- Match the response to the question: a simple question gets a direct answer, not headers and sections.

## Environment
- Working directory: {{cwd}}
- Operating system: {{platform}}
- Shell: {{shell}}

Respond in the user's language. Use code blocks for code.`;
const DEFAULT_ENV_PROMPT = `## Environment
- Working directory: {{cwd}}
- Operating system: {{platform}}
- Shell: {{shell}}`;

export const SYSTEM_NOTES = `## System Notes

Expand All @@ -74,13 +74,14 @@
- The todo_write tool lets you track multi-step plans. Use it for tasks that require more than one step.`;

function renderBase(opts: SystemPromptOptions): string {
return DEFAULT_SYSTEM_PROMPT.replace('{{cwd}}', opts.cwd)
return DEFAULT_ENV_PROMPT.replace('{{cwd}}', opts.cwd)
.replace('{{platform}}', opts.platform)
.replace('{{shell}}', opts.shell);
}

export function buildSystemPrompt(opts: SystemPromptOptions): string {
let prompt = renderBase(opts);
prompt += '\n\n' + (opts.profileSystemPrompt ?? DEFAULT_BEHAVIOR_PROMPT);
prompt += `\n\n${SYSTEM_NOTES}`;

const rules = opts.rules;
Expand All @@ -104,19 +105,19 @@

### When to dispatch

Dispatch a subagent when the task involves extensively reading files, searching across the codebase, or analyzing a whole module. A subagent runs in an independent context window — all of its tool calls (read_file, search_code, etc.) consume only the subagent\'s own context. Only the final result comes back to you.
Dispatch a subagent when the task involves extensively reading files, searching across the codebase, or analyzing a whole module. A subagent runs in an independent context window —all of its tool calls (read_file, search_code, etc.) consume only the subagent\'s own context. Only the final result comes back to you.

Check warning on line 108 in packages/codingcode/src/agent/prompt.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary escape character: \'

**Dispatch = protect your context window.** If you do the same work yourself, all the raw content goes directly into your context.

### When NOT to dispatch

- The task needs only a small amount of information — do it yourself.
- You already know the exact file path and what to look for — use read_file / search_code directly.
- The task needs only a small amount of information —do it yourself.
- You already know the exact file path and what to look for —use read_file / search_code directly.

### Rules

1. Once you dispatch a subagent, do **NOT** also perform the same searches yourself.
2. **Do NOT peek** — the subagent runs independently. Do not try to read its intermediate output, as that defeats the context protection.
2. **Do NOT peek** —the subagent runs independently. Do not try to read its intermediate output, as that defeats the context protection.
3. When the subagent returns, relay its conclusion to the user concisely.

### Example
Expand Down
Loading
Loading