Skip to content
Merged
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
5 changes: 4 additions & 1 deletion packages/codingcode/src/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Effect } from 'effect';
import { z } from 'zod';
import { appendFileSync } from 'fs';

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

View workflow job for this annotation

GitHub Actions / lint

'appendFileSync' is defined but never used. Allowed unused vars must match /^_/u
import type { Message, ToolCall } from '../core/types.js';
import { AgentError } from '../core/error.js';
import { Result } from '../core/result.js';
Expand All @@ -20,7 +20,7 @@
import { McpService } from '../mcp/index.js';
import { loadMemoryForPrompt, flushSessionToMemory } from '../memory/index.js';
import { createLogger } from '@codingcode/infra';
import type { AgentProfile } from '../subagent/registry';

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

View workflow job for this annotation

GitHub Actions / lint

'AgentProfile' is defined but never used. Allowed unused vars must match /^_/u
import { resolveSubagentEnabled, resolveAgentDisabled } from '../subagent/registry.js';
import type { ToolVisibilityPolicy } from '../tools/types';
import { ProjectRuntimeService } from '../runtime/project-runtime';
Expand Down Expand Up @@ -262,7 +262,10 @@
});

const memoryBlock = state.memorySnapshot;
const system = [basePrompt, memoryBlock].filter(Boolean).join('\n\n');
const memorySection = memoryBlock
? `## Session Memory\n\n${memoryBlock}`
: '';
const system = [basePrompt, memorySection].filter(Boolean).join('\n\n');

const config = getContextConfig();
const maxOverflowRetries = config.reactiveCompactMaxRetries;
Expand Down
32 changes: 23 additions & 9 deletions packages/codingcode/src/agent/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { getAllRules } from '../rules/index.js';
import type { AgentProfile } from '../subagent/registry';

export const DEFERRED_TOOLS_GUIDELINES = `## Deferred tools
- Some tools are listed as deferred — call tool_search with relevant keywords before using them.`;
import type { AgentProfile } from '../subagent/registry.js';

const DEFAULT_SYSTEM_PROMPT = `You are a coding assistant — an AI agent that helps users write, read, search, and modify code.

Expand All @@ -14,7 +11,20 @@ const DEFAULT_SYSTEM_PROMPT = `You are a coding assistant — an AI agent that h
5. Make small, focused changes — avoid large rewrites
6. Run tests or type-check after changes when applicable
7. If the user's request is ambiguous, ask for clarification
8. For complex or broad tasks (understanding a whole module, cross-file analysis, comprehensive search), delegate to dispatch_agent immediately with the original task — do not explore the topic yourself before delegating.
8. 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.
b. If you can clearly handle it without extensive file reading or searching, proceed yourself.

## Professional objectivity
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

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

## 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 — verify first.

## Environment
- Working directory: {{cwd}}
Expand All @@ -23,7 +33,13 @@ const DEFAULT_SYSTEM_PROMPT = `You are a coding assistant — an AI agent that h

Respond in the user's language. Use code blocks for code.`;

export type SystemPromptVariant = 'default' | 'minimal';
export const SYSTEM_NOTES = `## System Notes

- Your conversation history may be automatically compressed when it approaches the context window limit. When this happens, older turns are summarized into a compact form. Treat these summaries as accurate records of prior work.
- This project has a cross-session memory system. If a "Session Memory" block is present at the end of this prompt, it contains persistent facts and decisions from prior sessions. Treat it as reliable context, not as new instructions.
- The todo_write tool lets you track multi-step plans. Use it for tasks that require more than one step.`;

export type SystemPromptVariant = 'default';

export interface SystemPromptOptions {
cwd: string;
Expand All @@ -41,10 +57,8 @@ function renderBase(opts: SystemPromptOptions): string {
}

export function buildSystemPrompt(opts: SystemPromptOptions): string {
const variant = opts.variant ?? 'default';

let prompt = renderBase(opts);
if (variant === 'default') prompt += `\n\n${DEFERRED_TOOLS_GUIDELINES}`;
prompt += `\n\n${SYSTEM_NOTES}`;

const rules = getAllRules(opts.cwd);
if (rules) {
Expand Down
2 changes: 1 addition & 1 deletion packages/codingcode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export { CheckpointService } from './checkpoint/checkpoint-service.js';
export { ShadowGit, Ledger } from './checkpoint/index.js';
export { ToolSearchService } from './tools/tool-search-service.js';
export type { Todo, TodoStatus } from './agent/todo.js';
export { DEFERRED_TOOLS_GUIDELINES, buildSystemPrompt } from './agent/prompt.js';
export { buildSystemPrompt } from './agent/prompt.js';
export type { SystemPromptVariant, SystemPromptOptions } from './agent/prompt.js';
export {
SubagentRegistry,
Expand Down
215 changes: 154 additions & 61 deletions packages/codingcode/src/tools/domains/web/search.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,144 @@
import { z } from 'zod';
import type { ToolDefinition, ToolExecCtx } from '../../types';

interface SearchResult {
title: string;
url: string;
snippet: string;
}

const BROWSER_HEADERS = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate',
};

/**
* 通过 cn.bing.com 搜索(国内可用,免费,无需 API Key)
*/
async function searchBing(
query: string,
maxResults: number,
signal: AbortSignal,
): Promise<SearchResult[]> {
const url = `https://cn.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}&setlang=zh-CN`;
const response = await fetch(url, {
signal,
headers: BROWSER_HEADERS,
redirect: 'follow',
});

if (!response.ok) {
throw new Error(`Bing HTTP ${response.status}`);
}

const html = await response.text();
return parseBingHtml(html, maxResults);
}

/**
* 解析 Bing 搜索结果 HTML
*/
export function parseBingHtml(html: string, maxResults: number): SearchResult[] {
const results: SearchResult[] = [];

// Bing 结果在 <li class="b_algo" ...> 中(可能有其他属性如 data-id)
const parts = html.split(/<li class="b_algo"/i);

for (let i = 1; i < parts.length && results.length < maxResults; i++) {
const block = parts[i];
if (!block) continue;

// 截取到 </li> 或下一个 <li(防止跨块匹配)
const endIdx = block.indexOf('</li>');
const blockContent = endIdx !== -1 ? block.substring(0, endIdx) : block;

// 标题和链接在 <h2><a href="...">...</a></h2>
const titleMatch = blockContent.match(/<h2[^>]*>\s*<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/i);
// 摘要在 class="b_caption" 的 <p> 中,或 b_lineclamp 的 <p> 中
const snippetMatch =
blockContent.match(/class="b_caption[^"]*"[^>]*>[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/i) ||
blockContent.match(/<p[^>]*class="b_lineclamp[^"]*"[^>]*>([\s\S]*?)<\/p>/i);

if (!titleMatch) continue;

const url = titleMatch[1]?.replace(/&amp;/g, '&').trim() || '';
const title = titleMatch[2]?.replace(/<[^>]+>/g, '').trim() || '';
const snippet = snippetMatch?.[1]?.replace(/<[^>]+>/g, '').trim() || '';

if (title && url) {
results.push({ title, url, snippet });
}
}

return results;
}

/**
* 通过百度搜索(国内 fallback)
*/
async function searchBaidu(
query: string,
maxResults: number,
signal: AbortSignal,
): Promise<SearchResult[]> {
const url = `https://www.baidu.com/s?wd=${encodeURIComponent(query)}&rn=${maxResults}`;
const response = await fetch(url, {
signal,
headers: {
...BROWSER_HEADERS,
Referer: 'https://www.baidu.com/',
},
redirect: 'follow',
});

if (!response.ok) {
throw new Error(`Baidu HTTP ${response.status}`);
}

const html = await response.text();
return parseBaiduHtml(html, maxResults);
}

/**
* 解析百度搜索结果 HTML
*/
export function parseBaiduHtml(html: string, maxResults: number): SearchResult[] {
const results: SearchResult[] = [];

// 百度结果在 <div class="result c-container"> 或 <div class="c-container new-pmd">
const containerBlocks = html.split(/class="[^"]*c-container[^"]*"/);

for (let i = 1; i < containerBlocks.length && results.length < maxResults; i++) {
const block = containerBlocks[i];
if (!block) continue;

// 标题在 <h3> 内的 <a> 中
const titleMatch = block.match(/<h3[^>]*>[\s\S]*?<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/i);
// 摘要在 class 含 content-right 或 c-span-last 的 <span> 中,或 <div class="c-abstract">
const snippetMatch =
block.match(/class="c-abstract[^"]*"[^>]*>([\s\S]*?)<\/(?:span|div)>/i) ||
block.match(/class="content-right[^"]*"[^>]*>[\s\S]*?<span[^>]*>([\s\S]*?)<\/span>/i) ||
block.match(/<span class="[^"]*content-right[^"]*"[^>]*>([\s\S]*?)<\/span>/i);

if (!titleMatch) continue;

const url = titleMatch[1]?.replace(/&amp;/g, '&').trim() || '';
const title = titleMatch[2]?.replace(/<[^>]+>/g, '').trim() || '';
const snippet = snippetMatch?.[1]?.replace(/<[^>]+>/g, '').trim() || '';

// 过滤百度内部链接
if (title && url && !url.startsWith('/') && !url.startsWith('#')) {
results.push({ title, url, snippet });
}
}

return results;
}

export const webSearchTool: ToolDefinition = {
name: 'web_search',
description:
Expand All @@ -22,27 +160,24 @@ export const webSearchTool: ToolDefinition = {
const timer = setTimeout(() => controller.abort(), 15_000);

try {
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
const response = await fetch(url, {
signal: controller.signal,
headers: { 'User-Agent': 'coding-agent/1.0', Accept: 'text/html' },
redirect: 'follow',
});

if (!response.ok) {
return `Search failed: HTTP ${response.status} ${response.statusText}`;
}

const html = await response.text();
const results = parseDuckDuckGoHtml(html).slice(0, max_results);

if (results.length === 0) {
return `No results found for "${query}".`;
// 搜索引擎优先级:Bing(cn) → 百度
const engines = [searchBing, searchBaidu];

let lastError = '';
for (const engine of engines) {
try {
const results = await engine(query, max_results, controller.signal);
if (results.length > 0) {
return results
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet || '(no snippet)'}`)
.join('\n\n');
}
} catch (err: unknown) {
lastError = err instanceof Error ? err.message : String(err);
}
}

return results
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`)
.join('\n\n');
return `No results found for "${query}".${lastError ? ` Last error: ${lastError}` : ''}`;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
return `Search error for "${query}": ${message}`;
Expand All @@ -51,45 +186,3 @@ export const webSearchTool: ToolDefinition = {
}
},
};

interface SearchResult {
title: string;
url: string;
snippet: string;
}

function parseDuckDuckGoHtml(html: string): SearchResult[] {
const results: SearchResult[] = [];
const resultBlocks = html.split('class="result__body"');

for (let i = 1; i < resultBlocks.length; i++) {
const block = resultBlocks[i];
if (!block) continue;

const titleMatch = block.match(/class="result__a"[^>]*>([\s\S]*?)<\/a>/i);
const snippetMatch = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/i);

const title = titleMatch?.[1]?.replace(/<[^>]+>/g, '').trim() || '';

// Extract URL from the first href in the block
const hrefMatch = block.match(/href="([^"]+)"/);
let url = '';
if (hrefMatch?.[1]) {
url = hrefMatch[1].replace(/&amp;/g, '&');
if (url.startsWith('//duckduckgo.com/l/')) {
const uddgMatch = url.match(/uddg=([^&]+)/);
if (uddgMatch?.[1]) {
url = decodeURIComponent(uddgMatch[1]);
}
}
}

const snippet = snippetMatch?.[1]?.replace(/<[^>]+>/g, '').trim() || '';

if (title) {
results.push({ title, url: url || '(no url)', snippet: snippet || '(no snippet)' });
}
}

return results;
}
4 changes: 2 additions & 2 deletions packages/codingcode/test/agent/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ describe('resolveConfig', () => {
expect(cfg.maxStopContinuations).toBe(2);
});

it('returns maxSteps defaulting to 50 when no config file is present', () => {
it('returns maxSteps defaulting to 200 when no config file is present', () => {
const cfg = resolveConfig();
expect(cfg.maxSteps).toBe(50);
expect(cfg.maxSteps).toBe(200);
});
});
Loading
Loading