From 64d8427ca71e4b63d72926574587e4563390a4b9 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 11 Feb 2026 18:30:20 +0800 Subject: [PATCH 1/9] fix(agent): enforce data-web evidence fusion --- packages/core/src/agent/system-prompt/sections.test.ts | 8 ++++++++ packages/core/src/agent/system-prompt/sections.ts | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/core/src/agent/system-prompt/sections.test.ts b/packages/core/src/agent/system-prompt/sections.test.ts index 1830f9fc..cb792de5 100644 --- a/packages/core/src/agent/system-prompt/sections.test.ts +++ b/packages/core/src/agent/system-prompt/sections.test.ts @@ -179,6 +179,14 @@ describe("buildConditionalToolSections", () => { expect(result.join("\n")).toContain("## Web Access"); }); + it("adds data-web fusion guidance when both data and web tools are present", () => { + const result = buildConditionalToolSections(["data", "web_search"], "full"); + const text = result.join("\n"); + expect(text).toContain("## Data Access"); + expect(text).toContain("combine them"); + expect(text).toContain("macro, policy, and breaking-news context"); + }); + it("returns empty when no conditional tools match", () => { const result = buildConditionalToolSections(["read", "write"], "full"); expect(result).toEqual([]); diff --git a/packages/core/src/agent/system-prompt/sections.ts b/packages/core/src/agent/system-prompt/sections.ts index 6d696e7d..0a864709 100644 --- a/packages/core/src/agent/system-prompt/sections.ts +++ b/packages/core/src/agent/system-prompt/sections.ts @@ -240,6 +240,7 @@ export function buildConditionalToolSections( if (mode === "none" || !tools || tools.length === 0) return []; const toolSet = new Set(tools.map((t) => t.toLowerCase())); + const hasWebTools = toolSet.has("web_search") || toolSet.has("web_fetch"); const lines: string[] = []; // Memory tools @@ -275,12 +276,15 @@ export function buildConditionalToolSections( "You have access to structured financial and market data via the `data` tool.", 'Use domain="finance" with specific actions to retrieve stock prices, financial statements, SEC filings, metrics, and more.', "Always specify dates in YYYY-MM-DD format. Use period='annual' or 'quarterly' or 'ttm' for financial statements.", + hasWebTools + ? "When both data and web tools are available, combine them: use `data` for structured fundamentals, and web sources for macro, policy, and breaking-news context." + : "Use tool outputs as evidence, and clearly state assumptions when data is incomplete.", "", ); } // Web tools - if (toolSet.has("web_search") || toolSet.has("web_fetch")) { + if (hasWebTools) { lines.push( "## Web Access", "You have web access. Use it when the user asks about current events, needs up-to-date information, or requests content from URLs.", From f2adddcde75e76751855ec4f4eab5219eb8458fa Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 11 Feb 2026 18:30:42 +0800 Subject: [PATCH 2/9] feat(skills): upgrade finance analysis workflows --- skills/dcf-valuation/SKILL.md | 14 +++- skills/finance-research/SKILL.md | 128 +++++++++++++++++++++++-------- 2 files changed, 108 insertions(+), 34 deletions(-) diff --git a/skills/dcf-valuation/SKILL.md b/skills/dcf-valuation/SKILL.md index f2f6dc7b..5fb9b299 100644 --- a/skills/dcf-valuation/SKILL.md +++ b/skills/dcf-valuation/SKILL.md @@ -1,7 +1,7 @@ --- name: DCF Valuation description: Perform Discounted Cash Flow (DCF) valuation analysis for public companies. Use when the user asks to value a stock, calculate intrinsic value, fair value, perform DCF analysis, determine if a stock is undervalued or overvalued, or estimate a price target. -version: 1.0.0 +version: 1.1.0 metadata: emoji: "\U0001F9EE" requires: @@ -17,7 +17,7 @@ disableModelInvocation: false ## Instructions -Perform a rigorous Discounted Cash Flow (DCF) valuation. Follow all steps and show your work. +Perform a rigorous Discounted Cash Flow (DCF) valuation. Follow all steps and show your work. Use external macro context when assumptions are time-sensitive (for example, risk-free rate regime shifts). ### Progress Checklist @@ -86,6 +86,14 @@ Use `data` tool with `domain="finance"` for all calls: ``` Extract: `sector` — use to determine WACC range from [sector-wacc.md](references/sector-wacc.md) +8. **Recent Event Context**: +- Pull company-specific headlines with: + ``` + action: "get_news" + params: { ticker: "[TICKER]", limit: 10 } + ``` +- Use this to flag event risk (guidance reset, litigation, regulation, one-off gains/losses) that may distort near-term FCF extrapolation. + ### Step 2: Calculate Historical FCF and Growth - Compute FCF for each of the last 5 years @@ -112,7 +120,7 @@ Where: ``` **Default assumptions:** -- Risk-free rate: ~4.0-4.5% (10-year Treasury) +- Risk-free rate: pull latest 10-year Treasury yield using `web_search` (preferred) and cite date/source. Fallback range: ~4.0-4.5%. - Equity risk premium: ~5.5% - If beta unavailable, use sector average diff --git a/skills/finance-research/SKILL.md b/skills/finance-research/SKILL.md index acf225d7..1cf68ad0 100644 --- a/skills/finance-research/SKILL.md +++ b/skills/finance-research/SKILL.md @@ -1,7 +1,7 @@ --- name: Finance Research -description: Conduct financial research and analysis including stock analysis, company fundamentals, SEC filings review, and market data retrieval. Use when the user asks about stocks, financial statements, company performance, market data, or investment analysis. -version: 1.0.0 +description: Conduct analyst-grade financial research across primary and secondary markets using structured financial data plus macro and public-information cross-checks. +version: 1.1.0 metadata: emoji: "\U0001F4CA" requires: @@ -12,13 +12,15 @@ metadata: - research - stocks - data + - macro + - sentiment userInvocable: true disableModelInvocation: false --- ## Instructions -You are conducting financial research using real market data. Use the `data` tool with `domain="finance"` and the appropriate action. +You are conducting financial research with an analyst-grade standard. Do not rely on a single source. Combine structured company data with external macro/policy/news context whenever the conclusion can be affected by market regime or recent events. ### Available Data Actions @@ -48,44 +50,108 @@ Actions: #### Company Info - `get_company_facts` — Sector, industry, employees, exchange, website. Params: `{ ticker }` -- `get_news` — Recent news articles. Params: `{ ticker, start_date?, end_date?, limit? }` +- `get_news` — Recent company news articles. Params: `{ ticker, start_date?, end_date?, limit? }` - `get_insider_trades` — Insider buying/selling (SEC Form 4). Params: `{ ticker, limit?, filing_date*? }` - `get_segmented_revenues` — Revenue by segment/geography. Params: `{ ticker, period, limit? }` #### SEC Filings - `get_filings` — List filings metadata. Params: `{ ticker, filing_type?, limit? }` - - filing_type: "10-K", "10-Q", "8-K" -- `get_filing_items` — Read specific filing sections. Params: `{ ticker, filing_type, accession_number?, item? }` - - item: array of section names (e.g. ["Item-1A", "Item-7"] for 10-K) +- `get_filing_items` — Read filing sections. Params: `{ ticker, filing_type, accession_number?, item? }` -### Research Workflow +### Mandatory Multi-Source Framework -1. **Understand** what financial data is needed -2. **Get context** — start with `get_price_snapshot` and `get_company_facts` for orientation -3. **Gather data** — use the appropriate actions for the analysis -4. **Analyze** — interpret data with proper financial reasoning -5. **Present** — clear findings with data tables and key takeaways +Use this structure by default for finance analysis tasks. -### Best Practices +1. **Scope & Market Type** +- Identify if this is primary market (IPO, pre-IPO, follow-on, placement) or secondary market (listed stock/sector/index). +- State region and analysis horizon (event-driven, 3-6 months, 1-3 years). -- Use `get_all_financial_statements` when you need multiple statement types (saves API calls) -- Use annual data for trend analysis, quarterly for recent performance, TTM for current state -- Cross-reference metrics: revenue growth vs cash flow growth, margins vs peers -- Always note the time period and currency when presenting financial data -- For SEC filing analysis: first `get_filings` to find relevant filings, then `get_filing_items` to read specific sections -- Common 10-K items: Item-1 (Business), Item-1A (Risk Factors), Item-7 (MD&A), Item-8 (Financial Statements) -- Common 10-Q items: Part-1,Item-1 (Financial Statements), Part-1,Item-2 (MD&A) +2. **Core Company Data (Structured)** +- Start with: `get_price_snapshot`, `get_company_facts`, `get_financial_metrics_snapshot`. +- Pull statements (`get_all_financial_statements`) and estimates as needed. -### Example: Company Analysis +3. **Macro & Policy Context (External)** +- Use `web_search` for current policy/rates/inflation/liquidity context relevant to the asset. +- Use `web_fetch` to read high-signal primary sources (central bank, regulator, official releases). +- For time-sensitive analysis, include at least 2 external macro/policy signals with dates. -For "Analyze Apple's financial health": +4. **News & Sentiment Context (Hybrid)** +- Pull `get_news` for company-linked coverage. +- Use `web_search` to cross-check major events, management guidance changes, supply-chain/regulatory headlines, and consensus narrative. -``` -1. data(domain="finance", action="get_price_snapshot", params={ticker: "AAPL"}) -2. data(domain="finance", action="get_company_facts", params={ticker: "AAPL"}) -3. data(domain="finance", action="get_all_financial_statements", params={ticker: "AAPL", period: "annual", limit: 3}) -4. data(domain="finance", action="get_financial_metrics_snapshot", params={ticker: "AAPL"}) -5. data(domain="finance", action="get_analyst_estimates", params={ticker: "AAPL"}) -``` +5. **Synthesis & Decision** +- Separate **facts**, **inference**, and **assumptions**. +- Build bull/base/bear scenarios with explicit trigger conditions. +- Provide confidence level and explain the main uncertainty drivers. -Then analyze trends, margins, growth rates, and present findings. +### Primary Market (一级市场) Workflow + +When asked about IPOs, pre-IPO, or new issuance: + +1. **Deal Basics** +- Identify issuer, listing venue, offering structure (primary/secondary shares), expected timeline. + +2. **Filing/Prospectus Review** +- Prefer official documents (e.g., S-1/F-1/prospectus) via `web_search` + `web_fetch`. +- Extract: use of proceeds, customer concentration, related-party transactions, share classes, lock-up, dilution risks. + +3. **Valuation & Comparable Set** +- Build peer set from listed comps (secondary market tickers) and compare growth, margin, and valuation multiples. +- Flag gaps between issuer narrative and peer reality. + +4. **Deal Risk Map** +- Highlight red flags: weak FCF quality, aggressive non-GAAP adjustments, concentrated revenue, regulatory overhang. +- Provide post-listing watch items: lock-up expiry, first earnings, guidance revisions. + +### Secondary Market (二级市场) Workflow + +When asked about listed equities: + +1. **Trend & Positioning** +- Pull 1y price history (`get_prices`) and identify regime (uptrend/range/downtrend) with volatility context. + +2. **Fundamentals** +- Analyze growth quality (revenue vs FCF), margin durability, leverage, and capital allocation. + +3. **Valuation** +- Compare current multiples to historical bands and peers (when peer data is available). +- Connect valuation premium/discount to expected growth and risk profile. + +4. **Catalysts & Risks** +- Earnings, guidance, product cycle, policy changes, rates/FX/commodity sensitivity, insider activity. + +### Output Standard + +Always include: + +1. **Executive Summary** (thesis + stance + confidence) +2. **Evidence Table** with columns: +- Signal +- Direction (Bull/Bear/Neutral) +- Why it matters +- Source +- Date +3. **Scenario Table** (bull/base/bear with probabilities or relative weights) +4. **Key Monitoring Triggers** (what would invalidate current thesis) + +### Guardrails + +- Always state data cutoff dates. +- If data is missing, explicitly mark it and show the impact on confidence. +- Do not present assumptions as facts. +- Prefer source diversity: structured finance data + at least one external source for event-driven conclusions. + +### Example: Secondary Market Analysis + +For "Analyze Apple's investment outlook": + +1. `data(domain="finance", action="get_price_snapshot", params={ticker: "AAPL"})` +2. `data(domain="finance", action="get_company_facts", params={ticker: "AAPL"})` +3. `data(domain="finance", action="get_all_financial_statements", params={ticker: "AAPL", period: "annual", limit: 3})` +4. `data(domain="finance", action="get_financial_metrics", params={ticker: "AAPL", period: "quarterly", limit: 8})` +5. `data(domain="finance", action="get_analyst_estimates", params={ticker: "AAPL", period: "annual"})` +6. `data(domain="finance", action="get_news", params={ticker: "AAPL", limit: 10})` +7. `web_search(query="latest Fed policy decision impact on US mega-cap tech valuations")` +8. `web_search(query="Apple supply chain or regulatory news latest quarter")` + +Then synthesize fundamental trend, macro regime, and event sentiment into a scenario-based conclusion. From c5db9bf232ca2dc78d32065c75ed15ef335eb02c Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 11 Feb 2026 18:48:31 +0800 Subject: [PATCH 3/9] refactor(agent): make finance web usage dynamic --- .../core/src/agent/system-prompt/sections.test.ts | 11 +++++++---- packages/core/src/agent/system-prompt/sections.ts | 13 +++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/core/src/agent/system-prompt/sections.test.ts b/packages/core/src/agent/system-prompt/sections.test.ts index cb792de5..55daae5c 100644 --- a/packages/core/src/agent/system-prompt/sections.test.ts +++ b/packages/core/src/agent/system-prompt/sections.test.ts @@ -176,15 +176,18 @@ describe("buildConditionalToolSections", () => { it("includes web access section when web tools present", () => { const result = buildConditionalToolSections(["web_search"], "full"); - expect(result.join("\n")).toContain("## Web Access"); + const text = result.join("\n"); + expect(text).toContain("## Web Access"); + expect(text).toContain("Web usage is conditional, not mandatory"); }); - it("adds data-web fusion guidance when both data and web tools are present", () => { + it("adds dynamic evidence decision guidance when both data and web tools are present", () => { const result = buildConditionalToolSections(["data", "web_search"], "full"); const text = result.join("\n"); expect(text).toContain("## Data Access"); - expect(text).toContain("combine them"); - expect(text).toContain("macro, policy, and breaking-news context"); + expect(text).toContain("dynamic evidence decision"); + expect(text).toContain("Tool Decision"); + expect(text).toContain("plan (`data_only` | `hybrid` | `web_first`)"); }); it("returns empty when no conditional tools match", () => { diff --git a/packages/core/src/agent/system-prompt/sections.ts b/packages/core/src/agent/system-prompt/sections.ts index 0a864709..2fe2cf8b 100644 --- a/packages/core/src/agent/system-prompt/sections.ts +++ b/packages/core/src/agent/system-prompt/sections.ts @@ -271,15 +271,23 @@ export function buildConditionalToolSections( // Data tools if (toolSet.has("data")) { - lines.push( + const dataLines = [ "## Data Access", "You have access to structured financial and market data via the `data` tool.", 'Use domain="finance" with specific actions to retrieve stock prices, financial statements, SEC filings, metrics, and more.', "Always specify dates in YYYY-MM-DD format. Use period='annual' or 'quarterly' or 'ttm' for financial statements.", hasWebTools - ? "When both data and web tools are available, combine them: use `data` for structured fundamentals, and web sources for macro, policy, and breaking-news context." + ? "When both data and web tools are available, make a dynamic evidence decision: start from structured data, and use web tools only when external validation is needed (for example: event-driven, time-sensitive, or conflicting/incomplete evidence)." : "Use tool outputs as evidence, and clearly state assumptions when data is incomplete.", + ...(hasWebTools + ? [ + "Before final conclusions, include a short `Tool Decision` summary with: plan (`data_only` | `hybrid` | `web_first`), reason, and missing evidence impact.", + ] + : []), "", + ]; + lines.push( + ...dataLines, ); } @@ -289,6 +297,7 @@ export function buildConditionalToolSections( "## Web Access", "You have web access. Use it when the user asks about current events, needs up-to-date information, or requests content from URLs.", "Prefer web_search for discovery and web_fetch for specific URLs.", + "Web usage is conditional, not mandatory: call web tools when they materially improve evidence quality.", "", ); } From bbda13d0059fab2b0160ae5641f5d1e60453c7ca Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 11 Feb 2026 18:48:44 +0800 Subject: [PATCH 4/9] feat(skills): add finance evidence sufficiency gate --- skills/finance-research/SKILL.md | 60 ++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/skills/finance-research/SKILL.md b/skills/finance-research/SKILL.md index 1cf68ad0..22fba8a5 100644 --- a/skills/finance-research/SKILL.md +++ b/skills/finance-research/SKILL.md @@ -20,7 +20,7 @@ disableModelInvocation: false ## Instructions -You are conducting financial research with an analyst-grade standard. Do not rely on a single source. Combine structured company data with external macro/policy/news context whenever the conclusion can be affected by market regime or recent events. +You are conducting financial research with an analyst-grade standard. Tool usage is a dynamic decision. Do not force tool combinations. Choose tools based on evidence sufficiency for the specific question. ### Available Data Actions @@ -58,9 +58,26 @@ Actions: - `get_filings` — List filings metadata. Params: `{ ticker, filing_type?, limit? }` - `get_filing_items` — Read filing sections. Params: `{ ticker, filing_type, accession_number?, item? }` -### Mandatory Multi-Source Framework +### Evidence Sufficiency Gate (Dynamic Tool Decision) -Use this structure by default for finance analysis tasks. +Before deep analysis, output a short `Tool Decision` block: + +```text +Tool Decision +- plan: data_only | hybrid | web_first +- reason: why this plan is sufficient +- missing_evidence: what is still unknown +- confidence_impact: low | medium | high +``` + +Decision policy: + +- Start with `data_only` when structured data can support the requested conclusion. +- Escalate to `hybrid` when the task is event-driven, time-sensitive, or requires causal explanation not visible in structured data alone. +- Use `web_first` only when the task is mainly document/news/policy driven (common in pre-IPO without stable ticker coverage). +- If a tool is unavailable, continue with available tools and explicitly downgrade confidence. + +### Core Analysis Framework 1. **Scope & Market Type** - Identify if this is primary market (IPO, pre-IPO, follow-on, placement) or secondary market (listed stock/sector/index). @@ -70,14 +87,14 @@ Use this structure by default for finance analysis tasks. - Start with: `get_price_snapshot`, `get_company_facts`, `get_financial_metrics_snapshot`. - Pull statements (`get_all_financial_statements`) and estimates as needed. -3. **Macro & Policy Context (External)** -- Use `web_search` for current policy/rates/inflation/liquidity context relevant to the asset. -- Use `web_fetch` to read high-signal primary sources (central bank, regulator, official releases). -- For time-sensitive analysis, include at least 2 external macro/policy signals with dates. +3. **Macro & Policy Context (Conditional)** +- Use `web_search` / `web_fetch` only if required by your `Tool Decision`. +- If used, prefer high-signal primary sources (central bank, regulator, official releases). +- For time-sensitive conclusions, include source dates explicitly. -4. **News & Sentiment Context (Hybrid)** -- Pull `get_news` for company-linked coverage. -- Use `web_search` to cross-check major events, management guidance changes, supply-chain/regulatory headlines, and consensus narrative. +4. **News & Sentiment Context (Conditional)** +- Use `get_news` for company-linked coverage when available. +- Add web cross-checks only when event validation materially affects the conclusion. 5. **Synthesis & Decision** - Separate **facts**, **inference**, and **assumptions**. @@ -90,11 +107,16 @@ When asked about IPOs, pre-IPO, or new issuance: 1. **Deal Basics** - Identify issuer, listing venue, offering structure (primary/secondary shares), expected timeline. +- Determine whether a reliable ticker exists in current data coverage. 2. **Filing/Prospectus Review** - Prefer official documents (e.g., S-1/F-1/prospectus) via `web_search` + `web_fetch`. - Extract: use of proceeds, customer concentration, related-party transactions, share classes, lock-up, dilution risks. +Primary-market capability boundary: +- If `ticker` is available and filings are retrievable, run hybrid analysis (structured + document evidence). +- If `ticker` is unavailable or structured filing fields are limited, run web-led analysis and clearly label it as partial-coverage with reduced confidence. + 3. **Valuation & Comparable Set** - Build peer set from listed comps (secondary market tickers) and compare growth, margin, and valuation multiples. - Flag gaps between issuer narrative and peer reality. @@ -124,24 +146,26 @@ When asked about listed equities: Always include: -1. **Executive Summary** (thesis + stance + confidence) -2. **Evidence Table** with columns: +1. **Tool Decision** (plan + reason + evidence gap impact) +2. **Executive Summary** (thesis + stance + confidence) +3. **Evidence Table** with columns: - Signal - Direction (Bull/Bear/Neutral) - Why it matters - Source - Date -3. **Scenario Table** (bull/base/bear with probabilities or relative weights) -4. **Key Monitoring Triggers** (what would invalidate current thesis) +4. **Scenario Table** (bull/base/bear with probabilities or relative weights) +5. **Key Monitoring Triggers** (what would invalidate current thesis) ### Guardrails - Always state data cutoff dates. - If data is missing, explicitly mark it and show the impact on confidence. - Do not present assumptions as facts. -- Prefer source diversity: structured finance data + at least one external source for event-driven conclusions. +- For event-driven conclusions, if you skip web validation, explicitly explain why structured evidence is still sufficient. -### Example: Secondary Market Analysis + +### Example: Secondary Market Analysis (Tool Decision = `hybrid`) For "Analyze Apple's investment outlook": @@ -151,7 +175,7 @@ For "Analyze Apple's investment outlook": 4. `data(domain="finance", action="get_financial_metrics", params={ticker: "AAPL", period: "quarterly", limit: 8})` 5. `data(domain="finance", action="get_analyst_estimates", params={ticker: "AAPL", period: "annual"})` 6. `data(domain="finance", action="get_news", params={ticker: "AAPL", limit: 10})` -7. `web_search(query="latest Fed policy decision impact on US mega-cap tech valuations")` -8. `web_search(query="Apple supply chain or regulatory news latest quarter")` +7. `web_search(query="latest Fed policy decision impact on US mega-cap tech valuations")` (only because plan=`hybrid`) +8. `web_search(query="Apple supply chain or regulatory news latest quarter")` (only because plan=`hybrid`) Then synthesize fundamental trend, macro regime, and event sentiment into a scenario-based conclusion. From 5cccb066f10696e3e2763eaa0565dbce92271ed3 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 11 Feb 2026 18:56:31 +0800 Subject: [PATCH 5/9] refactor(agent): keep finance tool decisions internal --- packages/core/src/agent/system-prompt/sections.test.ts | 4 ++-- packages/core/src/agent/system-prompt/sections.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/agent/system-prompt/sections.test.ts b/packages/core/src/agent/system-prompt/sections.test.ts index 55daae5c..00549efe 100644 --- a/packages/core/src/agent/system-prompt/sections.test.ts +++ b/packages/core/src/agent/system-prompt/sections.test.ts @@ -186,8 +186,8 @@ describe("buildConditionalToolSections", () => { const text = result.join("\n"); expect(text).toContain("## Data Access"); expect(text).toContain("dynamic evidence decision"); - expect(text).toContain("Tool Decision"); - expect(text).toContain("plan (`data_only` | `hybrid` | `web_first`)"); + expect(text).toContain("Make this evidence decision internally"); + expect(text).toContain("user-facing research rationale"); }); it("returns empty when no conditional tools match", () => { diff --git a/packages/core/src/agent/system-prompt/sections.ts b/packages/core/src/agent/system-prompt/sections.ts index 2fe2cf8b..52809aad 100644 --- a/packages/core/src/agent/system-prompt/sections.ts +++ b/packages/core/src/agent/system-prompt/sections.ts @@ -281,7 +281,7 @@ export function buildConditionalToolSections( : "Use tool outputs as evidence, and clearly state assumptions when data is incomplete.", ...(hasWebTools ? [ - "Before final conclusions, include a short `Tool Decision` summary with: plan (`data_only` | `hybrid` | `web_first`), reason, and missing evidence impact.", + "Make this evidence decision internally. In final answers, present concise user-facing research rationale instead of technical decision labels unless the user asks for methodology details.", ] : []), "", From 695d001f9f4f6a1b79603df49ff21be76ead2bbe Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 11 Feb 2026 18:56:35 +0800 Subject: [PATCH 6/9] refactor(skills): hide technical finance decision block --- skills/finance-research/SKILL.md | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/skills/finance-research/SKILL.md b/skills/finance-research/SKILL.md index 22fba8a5..18bee569 100644 --- a/skills/finance-research/SKILL.md +++ b/skills/finance-research/SKILL.md @@ -58,17 +58,11 @@ Actions: - `get_filings` — List filings metadata. Params: `{ ticker, filing_type?, limit? }` - `get_filing_items` — Read filing sections. Params: `{ ticker, filing_type, accession_number?, item? }` -### Evidence Sufficiency Gate (Dynamic Tool Decision) +### Evidence Sufficiency Gate (Internal Decision) -Before deep analysis, output a short `Tool Decision` block: +Before deep analysis, make an internal evidence decision. Do not output a technical decision block by default. -```text -Tool Decision -- plan: data_only | hybrid | web_first -- reason: why this plan is sufficient -- missing_evidence: what is still unknown -- confidence_impact: low | medium | high -``` +If the user explicitly asks for methodology or reasoning transparency, provide a concise plain-language explanation of your research approach. Decision policy: @@ -88,7 +82,7 @@ Decision policy: - Pull statements (`get_all_financial_statements`) and estimates as needed. 3. **Macro & Policy Context (Conditional)** -- Use `web_search` / `web_fetch` only if required by your `Tool Decision`. +- Use `web_search` / `web_fetch` only if required by your internal evidence decision. - If used, prefer high-signal primary sources (central bank, regulator, official releases). - For time-sensitive conclusions, include source dates explicitly. @@ -146,16 +140,15 @@ When asked about listed equities: Always include: -1. **Tool Decision** (plan + reason + evidence gap impact) -2. **Executive Summary** (thesis + stance + confidence) -3. **Evidence Table** with columns: +1. **Executive Summary** (thesis + stance + confidence) +2. **Evidence Table** with columns: - Signal - Direction (Bull/Bear/Neutral) - Why it matters - Source - Date -4. **Scenario Table** (bull/base/bear with probabilities or relative weights) -5. **Key Monitoring Triggers** (what would invalidate current thesis) +3. **Scenario Table** (bull/base/bear with probabilities or relative weights) +4. **Key Monitoring Triggers** (what would invalidate current thesis) ### Guardrails @@ -165,7 +158,7 @@ Always include: - For event-driven conclusions, if you skip web validation, explicitly explain why structured evidence is still sufficient. -### Example: Secondary Market Analysis (Tool Decision = `hybrid`) +### Example: Secondary Market Analysis For "Analyze Apple's investment outlook": @@ -175,7 +168,7 @@ For "Analyze Apple's investment outlook": 4. `data(domain="finance", action="get_financial_metrics", params={ticker: "AAPL", period: "quarterly", limit: 8})` 5. `data(domain="finance", action="get_analyst_estimates", params={ticker: "AAPL", period: "annual"})` 6. `data(domain="finance", action="get_news", params={ticker: "AAPL", limit: 10})` -7. `web_search(query="latest Fed policy decision impact on US mega-cap tech valuations")` (only because plan=`hybrid`) -8. `web_search(query="Apple supply chain or regulatory news latest quarter")` (only because plan=`hybrid`) +7. `web_search(query="latest Fed policy decision impact on US mega-cap tech valuations")` +8. `web_search(query="Apple supply chain or regulatory news latest quarter")` Then synthesize fundamental trend, macro regime, and event sentiment into a scenario-based conclusion. From 4c4f8989ca20350bb74435c765bb1f88f4d2f247 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 11 Feb 2026 20:02:30 +0800 Subject: [PATCH 7/9] feat(agent): add internal finance evidence decisioner --- .../agent/research/finance-decisioner.test.ts | 68 ++++ .../src/agent/research/finance-decisioner.ts | 304 ++++++++++++++++++ packages/core/src/agent/runner.ts | 48 ++- packages/core/src/agent/session/types.ts | 9 + 4 files changed, 428 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/agent/research/finance-decisioner.test.ts create mode 100644 packages/core/src/agent/research/finance-decisioner.ts diff --git a/packages/core/src/agent/research/finance-decisioner.test.ts b/packages/core/src/agent/research/finance-decisioner.test.ts new file mode 100644 index 00000000..af7daec4 --- /dev/null +++ b/packages/core/src/agent/research/finance-decisioner.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import { buildInternalFinanceGuidance, decideFinanceEvidencePlan } from "./finance-decisioner.js"; + +describe("decideFinanceEvidencePlan", () => { + it("returns undefined for non-finance prompts", () => { + const result = decideFinanceEvidencePlan({ + prompt: "Write a TypeScript utility to parse CSV files.", + tools: ["read", "write", "exec"], + }); + expect(result).toBeUndefined(); + }); + + it("prefers data_only for secondary market non-event tasks", () => { + const result = decideFinanceEvidencePlan({ + prompt: "Analyze AAPL valuation based on 5-year financial statements.", + tools: ["data", "web_search", "web_fetch"], + }); + expect(result).toBeDefined(); + expect(result?.plan).toBe("data_only"); + expect(result?.marketRoute).toBe("secondary"); + expect(result?.reasons).toContain("secondary_market_task"); + }); + + it("prefers hybrid for event-driven secondary tasks", () => { + const result = decideFinanceEvidencePlan({ + prompt: "Why did AAPL drop after latest earnings and guidance update?", + tools: ["data", "web_search", "web_fetch"], + }); + expect(result).toBeDefined(); + expect(result?.plan).toBe("hybrid"); + expect(result?.reasons).toContain("event_driven"); + expect(result?.reasons).toContain("causal_explanation_needed"); + }); + + it("prefers web_first for primary market tasks without ticker", () => { + const result = decideFinanceEvidencePlan({ + prompt: "Review this pre-IPO issuance structure and lock-up risks.", + tools: ["data", "web_search", "web_fetch"], + }); + expect(result).toBeDefined(); + expect(result?.marketRoute).toBe("primary_no_ticker"); + expect(result?.plan).toBe("web_first"); + }); + + it("degrades when web tools are unavailable for event-driven tasks", () => { + const result = decideFinanceEvidencePlan({ + prompt: "Analyze latest earnings surprise drivers for TSLA stock.", + tools: ["data"], + }); + expect(result).toBeDefined(); + expect(result?.reasons).toContain("web_tools_unavailable"); + expect(result?.confidencePenalty).toBe("high"); + }); +}); + +describe("buildInternalFinanceGuidance", () => { + it("formats internal guidance text", () => { + const decision = decideFinanceEvidencePlan({ + prompt: "Analyze latest AAPL earnings impact on valuation.", + tools: ["data", "web_search"], + }); + expect(decision).toBeDefined(); + const guidance = buildInternalFinanceGuidance(decision!); + expect(guidance).toContain("Internal Finance Research Guidance"); + expect(guidance).toContain("Preferred evidence plan:"); + expect(guidance).toContain("Do not expose technical labels"); + }); +}); diff --git a/packages/core/src/agent/research/finance-decisioner.ts b/packages/core/src/agent/research/finance-decisioner.ts new file mode 100644 index 00000000..7df1be8d --- /dev/null +++ b/packages/core/src/agent/research/finance-decisioner.ts @@ -0,0 +1,304 @@ +/** + * Finance evidence decisioner. + * + * Produces an internal research plan for finance tasks: + * - data_only + * - hybrid (data + web validation) + * - web_first + * + * The output is intended for internal orchestration only. + */ + +export type FinanceEvidencePlan = "data_only" | "hybrid" | "web_first"; + +export type FinanceMarketRoute = "secondary" | "primary_with_ticker" | "primary_no_ticker"; + +export type FinanceConfidencePenalty = "low" | "medium" | "high"; + +export interface FinanceDecisionInput { + prompt: string; + tools: string[]; +} + +export interface FinanceDecision { + plan: FinanceEvidencePlan; + marketRoute: FinanceMarketRoute; + confidencePenalty: FinanceConfidencePenalty; + reasons: string[]; + score: Record; +} + +const FINANCE_KEYWORDS = [ + "stock", + "stocks", + "equity", + "equities", + "valuation", + "financial", + "finance", + "earnings", + "revenue", + "eps", + "cash flow", + "balance sheet", + "income statement", + "pe ratio", + "market cap", + "ipo", + "pre-ipo", + "listing", + "ticker", + "一级市场", + "二级市场", + "财报", + "股票", + "估值", + "市值", + "募资", + "锁定期", + "稀释", +]; + +const PRIMARY_MARKET_KEYWORDS = [ + "ipo", + "pre-ipo", + "prospectus", + "s-1", + "f-1", + "roadshow", + "listing", + "follow-on", + "new issuance", + "lock-up", + "dilution", + "一级市场", + "募资", + "锁定期", + "稀释", +]; + +const EVENT_DRIVEN_KEYWORDS = [ + "latest", + "recent", + "today", + "yesterday", + "breaking", + "earnings call", + "guidance", + "surprise", + "selloff", + "policy", + "fed", + "fomc", + "news", + "headline", + "突发", + "最新", + "消息", + "政策", + "财报后", +]; + +const CAUSAL_KEYWORDS = [ + "why", + "reason", + "driver", + "impact", + "because", + "attribution", + "explain", + "原因", + "驱动", + "影响", + "为什么", +]; + +const TIME_SENSITIVE_KEYWORDS = [ + "latest", + "today", + "this week", + "this month", + "current", + "now", + "最新", + "当前", + "近期", +]; + +const COMMON_UPPERCASE_NON_TICKERS = new Set([ + "IPO", + "SEC", + "USD", + "CNY", + "HKD", + "GDP", + "CPI", + "PPI", + "FED", + "FOMC", + "EPS", + "FCF", + "PE", + "TTM", + "DCF", +]); + +function includesAny(text: string, keywords: string[]): boolean { + return keywords.some((keyword) => text.includes(keyword)); +} + +function normalizeTools(tools: string[]): Set { + return new Set(tools.map((tool) => tool.toLowerCase())); +} + +function hasTickerSignal(prompt: string): boolean { + const explicit = /(?:\$|ticker\s*[:=]\s*)([A-Za-z]{1,6})/g; + if (explicit.test(prompt)) return true; + + const upperWords = prompt.match(/\b[A-Z]{1,6}\b/g) ?? []; + const candidates = upperWords.filter((word) => !COMMON_UPPERCASE_NON_TICKERS.has(word)); + return candidates.length > 0; +} + +function isFinanceTask(prompt: string): boolean { + const normalized = prompt.toLowerCase(); + return includesAny(normalized, FINANCE_KEYWORDS); +} + +function resolveMarketRoute(prompt: string): FinanceMarketRoute { + const normalized = prompt.toLowerCase(); + const primary = includesAny(normalized, PRIMARY_MARKET_KEYWORDS); + if (!primary) return "secondary"; + return hasTickerSignal(prompt) ? "primary_with_ticker" : "primary_no_ticker"; +} + +function choosePlan(score: Record): FinanceEvidencePlan { + const order: FinanceEvidencePlan[] = ["data_only", "hybrid", "web_first"]; + let best: FinanceEvidencePlan = order[0]; + for (const plan of order) { + if (score[plan] > score[best]) best = plan; + } + return best; +} + +function resolveConfidencePenalty(params: { + plan: FinanceEvidencePlan; + hasData: boolean; + hasWeb: boolean; + route: FinanceMarketRoute; + eventDriven: boolean; + timeSensitive: boolean; +}): FinanceConfidencePenalty { + const { plan, hasData, hasWeb, route, eventDriven, timeSensitive } = params; + + if (!hasData && !hasWeb) return "high"; + if ((plan === "hybrid" || plan === "web_first") && !hasWeb) return "high"; + if (plan === "data_only" && (eventDriven || timeSensitive) && !hasWeb) return "high"; + if (route === "primary_no_ticker") return "medium"; + if (plan === "data_only" && (eventDriven || timeSensitive)) return "medium"; + return "low"; +} + +export function decideFinanceEvidencePlan(input: FinanceDecisionInput): FinanceDecision | undefined { + const { prompt } = input; + if (!isFinanceTask(prompt)) return undefined; + + const normalized = prompt.toLowerCase(); + const toolSet = normalizeTools(input.tools); + const hasData = toolSet.has("data"); + const hasWebSearch = toolSet.has("web_search"); + const hasWebFetch = toolSet.has("web_fetch"); + const hasWeb = hasWebSearch || hasWebFetch; + + const route = resolveMarketRoute(prompt); + const eventDriven = includesAny(normalized, EVENT_DRIVEN_KEYWORDS); + const causal = includesAny(normalized, CAUSAL_KEYWORDS); + const timeSensitive = includesAny(normalized, TIME_SENSITIVE_KEYWORDS); + + const score: Record = { + data_only: hasData ? 1.0 : -3.0, + hybrid: hasData && hasWeb ? 1.0 : -2.0, + web_first: hasWeb ? 0.6 : -3.0, + }; + + const reasons: string[] = []; + + if (route === "secondary") { + score.data_only += 0.7; + score.hybrid += 0.4; + reasons.push("secondary_market_task"); + } else if (route === "primary_with_ticker") { + score.hybrid += 0.9; + score.web_first += 0.3; + score.data_only -= 0.2; + reasons.push("primary_market_with_ticker"); + } else { + score.web_first += 1.3; + score.hybrid += 0.7; + score.data_only -= 1.0; + reasons.push("primary_market_without_ticker"); + } + + if (eventDriven) { + score.hybrid += 0.9; + score.web_first += 0.4; + score.data_only -= 0.5; + reasons.push("event_driven"); + } + + if (timeSensitive) { + score.hybrid += 0.6; + score.web_first += 0.3; + score.data_only -= 0.4; + reasons.push("time_sensitive"); + } + + if (causal) { + score.hybrid += 0.4; + score.web_first += 0.2; + score.data_only -= 0.2; + reasons.push("causal_explanation_needed"); + } + + if (!hasWeb) { + score.hybrid -= 2.0; + score.web_first -= 3.0; + reasons.push("web_tools_unavailable"); + } + if (!hasData) { + score.data_only -= 2.5; + score.hybrid -= 1.5; + score.web_first += 0.5; + reasons.push("data_tool_unavailable"); + } + + const plan = choosePlan(score); + const confidencePenalty = resolveConfidencePenalty({ + plan, + hasData, + hasWeb, + route, + eventDriven, + timeSensitive, + }); + + return { + plan, + marketRoute: route, + confidencePenalty, + reasons, + score, + }; +} + +export function buildInternalFinanceGuidance(decision: FinanceDecision): string { + return [ + "## Internal Finance Research Guidance", + "This section is internal orchestration guidance. Do not expose technical labels directly to the user unless they explicitly request methodology details.", + `Preferred evidence plan: ${decision.plan}`, + `Market route: ${decision.marketRoute}`, + `Confidence penalty if evidence gaps remain: ${decision.confidencePenalty}`, + `Decision factors: ${decision.reasons.join(", ") || "none"}`, + "Execution policy: start with the preferred plan, then escalate evidence collection if signals conflict or causality remains unresolved.", + ].join("\n"); +} diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index beaa4faf..8ea675f3 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -35,8 +35,12 @@ import { import { buildSystemPrompt as buildStructuredSystemPrompt, collectRuntimeInfo, - type SystemPromptMode, } from "./system-prompt/index.js"; +import { + buildInternalFinanceGuidance, + decideFinanceEvidencePlan, + type FinanceDecision, +} from "./research/finance-decisioner.js"; import type { AuthProfileFailureReason } from "./auth-profiles/index.js"; import { sanitizeToolCallInputs, @@ -425,6 +429,7 @@ export class Agent { ): Promise { await this.ensureInitialized(); this.refreshAuthState(); + this.applyFinanceResearchGuidance(prompt); this.output.state.lastAssistantText = ""; this.currentUserDisplayPrompt = options?.displayPrompt; @@ -968,6 +973,13 @@ export class Agent { * Shared by constructor (via buildFullSystemPrompt) and reloadSystemPrompt. */ private rebuildSystemPrompt(toolNames: string[]): string | undefined { + return this.rebuildSystemPromptWithExtra(toolNames); + } + + private rebuildSystemPromptWithExtra( + toolNames: string[], + extraSystemPrompt?: string | undefined, + ): string | undefined { const profile = this.profile?.getProfile(); if (!profile) return undefined; @@ -993,6 +1005,40 @@ export class Agent { tools: toolNames, skillsPrompt, runtime, + extraSystemPrompt, + }); + } + + private applyFinanceResearchGuidance(prompt: string): void { + const toolNames = (this.agent.state.tools ?? []).map((t: { name: string }) => t.name); + const decision = decideFinanceEvidencePlan({ prompt, tools: toolNames }); + + const guidance = decision ? buildInternalFinanceGuidance(decision) : undefined; + const systemPrompt = this.rebuildSystemPromptWithExtra(toolNames, guidance); + if (systemPrompt) { + this.agent.setSystemPrompt(systemPrompt); + this.session.setSystemPrompt(systemPrompt); + } + + this.saveFinanceDecisionMeta(decision); + } + + private saveFinanceDecisionMeta(decision: FinanceDecision | undefined): void { + const currentMeta = this.session.getMeta() ?? {}; + if (!decision) { + // Keep previously saved decisions for non-finance turns. + return; + } + this.session.saveMeta({ + ...currentMeta, + researchDecision: { + domain: "finance", + plan: decision.plan, + marketRoute: decision.marketRoute, + confidencePenalty: decision.confidencePenalty, + reasons: decision.reasons, + timestamp: Date.now(), + }, }); } } diff --git a/packages/core/src/agent/session/types.ts b/packages/core/src/agent/session/types.ts index ec734424..48c037a9 100644 --- a/packages/core/src/agent/session/types.ts +++ b/packages/core/src/agent/session/types.ts @@ -9,6 +9,15 @@ export type SessionMeta = { reasoningMode?: string; /** Context window token 数 */ contextWindowTokens?: number; + /** Internal finance evidence decision from the latest run */ + researchDecision?: { + domain: "finance"; + plan: "data_only" | "hybrid" | "web_first"; + marketRoute: "secondary" | "primary_with_ticker" | "primary_no_ticker"; + confidencePenalty: "low" | "medium" | "high"; + reasons: string[]; + timestamp: number; + } | undefined; }; export type SessionEntry = From 0330b8b7ca50d4672dd9069c635e967ba6965302 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 11 Feb 2026 20:29:14 +0800 Subject: [PATCH 8/9] refactor(agent): remove keyword-based finance decisioner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The finance-decisioner used hardcoded keyword matching and arbitrary scoring weights to decide evidence plans — the LLM reading the improved SKILL.md already makes this decision more intelligently. Removes: finance-decisioner.ts, its tests, runner.ts integration (applyFinanceResearchGuidance, saveFinanceDecisionMeta, rebuildSystemPromptWithExtra shim), and researchDecision from session meta. Keeps: SKILL.md improvements, sections.ts dynamic data/web guidance. Co-Authored-By: Claude Opus 4.6 --- .../agent/research/finance-decisioner.test.ts | 68 ---- .../src/agent/research/finance-decisioner.ts | 304 ------------------ packages/core/src/agent/runner.ts | 48 +-- packages/core/src/agent/session/types.ts | 9 - 4 files changed, 1 insertion(+), 428 deletions(-) delete mode 100644 packages/core/src/agent/research/finance-decisioner.test.ts delete mode 100644 packages/core/src/agent/research/finance-decisioner.ts diff --git a/packages/core/src/agent/research/finance-decisioner.test.ts b/packages/core/src/agent/research/finance-decisioner.test.ts deleted file mode 100644 index af7daec4..00000000 --- a/packages/core/src/agent/research/finance-decisioner.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { buildInternalFinanceGuidance, decideFinanceEvidencePlan } from "./finance-decisioner.js"; - -describe("decideFinanceEvidencePlan", () => { - it("returns undefined for non-finance prompts", () => { - const result = decideFinanceEvidencePlan({ - prompt: "Write a TypeScript utility to parse CSV files.", - tools: ["read", "write", "exec"], - }); - expect(result).toBeUndefined(); - }); - - it("prefers data_only for secondary market non-event tasks", () => { - const result = decideFinanceEvidencePlan({ - prompt: "Analyze AAPL valuation based on 5-year financial statements.", - tools: ["data", "web_search", "web_fetch"], - }); - expect(result).toBeDefined(); - expect(result?.plan).toBe("data_only"); - expect(result?.marketRoute).toBe("secondary"); - expect(result?.reasons).toContain("secondary_market_task"); - }); - - it("prefers hybrid for event-driven secondary tasks", () => { - const result = decideFinanceEvidencePlan({ - prompt: "Why did AAPL drop after latest earnings and guidance update?", - tools: ["data", "web_search", "web_fetch"], - }); - expect(result).toBeDefined(); - expect(result?.plan).toBe("hybrid"); - expect(result?.reasons).toContain("event_driven"); - expect(result?.reasons).toContain("causal_explanation_needed"); - }); - - it("prefers web_first for primary market tasks without ticker", () => { - const result = decideFinanceEvidencePlan({ - prompt: "Review this pre-IPO issuance structure and lock-up risks.", - tools: ["data", "web_search", "web_fetch"], - }); - expect(result).toBeDefined(); - expect(result?.marketRoute).toBe("primary_no_ticker"); - expect(result?.plan).toBe("web_first"); - }); - - it("degrades when web tools are unavailable for event-driven tasks", () => { - const result = decideFinanceEvidencePlan({ - prompt: "Analyze latest earnings surprise drivers for TSLA stock.", - tools: ["data"], - }); - expect(result).toBeDefined(); - expect(result?.reasons).toContain("web_tools_unavailable"); - expect(result?.confidencePenalty).toBe("high"); - }); -}); - -describe("buildInternalFinanceGuidance", () => { - it("formats internal guidance text", () => { - const decision = decideFinanceEvidencePlan({ - prompt: "Analyze latest AAPL earnings impact on valuation.", - tools: ["data", "web_search"], - }); - expect(decision).toBeDefined(); - const guidance = buildInternalFinanceGuidance(decision!); - expect(guidance).toContain("Internal Finance Research Guidance"); - expect(guidance).toContain("Preferred evidence plan:"); - expect(guidance).toContain("Do not expose technical labels"); - }); -}); diff --git a/packages/core/src/agent/research/finance-decisioner.ts b/packages/core/src/agent/research/finance-decisioner.ts deleted file mode 100644 index 7df1be8d..00000000 --- a/packages/core/src/agent/research/finance-decisioner.ts +++ /dev/null @@ -1,304 +0,0 @@ -/** - * Finance evidence decisioner. - * - * Produces an internal research plan for finance tasks: - * - data_only - * - hybrid (data + web validation) - * - web_first - * - * The output is intended for internal orchestration only. - */ - -export type FinanceEvidencePlan = "data_only" | "hybrid" | "web_first"; - -export type FinanceMarketRoute = "secondary" | "primary_with_ticker" | "primary_no_ticker"; - -export type FinanceConfidencePenalty = "low" | "medium" | "high"; - -export interface FinanceDecisionInput { - prompt: string; - tools: string[]; -} - -export interface FinanceDecision { - plan: FinanceEvidencePlan; - marketRoute: FinanceMarketRoute; - confidencePenalty: FinanceConfidencePenalty; - reasons: string[]; - score: Record; -} - -const FINANCE_KEYWORDS = [ - "stock", - "stocks", - "equity", - "equities", - "valuation", - "financial", - "finance", - "earnings", - "revenue", - "eps", - "cash flow", - "balance sheet", - "income statement", - "pe ratio", - "market cap", - "ipo", - "pre-ipo", - "listing", - "ticker", - "一级市场", - "二级市场", - "财报", - "股票", - "估值", - "市值", - "募资", - "锁定期", - "稀释", -]; - -const PRIMARY_MARKET_KEYWORDS = [ - "ipo", - "pre-ipo", - "prospectus", - "s-1", - "f-1", - "roadshow", - "listing", - "follow-on", - "new issuance", - "lock-up", - "dilution", - "一级市场", - "募资", - "锁定期", - "稀释", -]; - -const EVENT_DRIVEN_KEYWORDS = [ - "latest", - "recent", - "today", - "yesterday", - "breaking", - "earnings call", - "guidance", - "surprise", - "selloff", - "policy", - "fed", - "fomc", - "news", - "headline", - "突发", - "最新", - "消息", - "政策", - "财报后", -]; - -const CAUSAL_KEYWORDS = [ - "why", - "reason", - "driver", - "impact", - "because", - "attribution", - "explain", - "原因", - "驱动", - "影响", - "为什么", -]; - -const TIME_SENSITIVE_KEYWORDS = [ - "latest", - "today", - "this week", - "this month", - "current", - "now", - "最新", - "当前", - "近期", -]; - -const COMMON_UPPERCASE_NON_TICKERS = new Set([ - "IPO", - "SEC", - "USD", - "CNY", - "HKD", - "GDP", - "CPI", - "PPI", - "FED", - "FOMC", - "EPS", - "FCF", - "PE", - "TTM", - "DCF", -]); - -function includesAny(text: string, keywords: string[]): boolean { - return keywords.some((keyword) => text.includes(keyword)); -} - -function normalizeTools(tools: string[]): Set { - return new Set(tools.map((tool) => tool.toLowerCase())); -} - -function hasTickerSignal(prompt: string): boolean { - const explicit = /(?:\$|ticker\s*[:=]\s*)([A-Za-z]{1,6})/g; - if (explicit.test(prompt)) return true; - - const upperWords = prompt.match(/\b[A-Z]{1,6}\b/g) ?? []; - const candidates = upperWords.filter((word) => !COMMON_UPPERCASE_NON_TICKERS.has(word)); - return candidates.length > 0; -} - -function isFinanceTask(prompt: string): boolean { - const normalized = prompt.toLowerCase(); - return includesAny(normalized, FINANCE_KEYWORDS); -} - -function resolveMarketRoute(prompt: string): FinanceMarketRoute { - const normalized = prompt.toLowerCase(); - const primary = includesAny(normalized, PRIMARY_MARKET_KEYWORDS); - if (!primary) return "secondary"; - return hasTickerSignal(prompt) ? "primary_with_ticker" : "primary_no_ticker"; -} - -function choosePlan(score: Record): FinanceEvidencePlan { - const order: FinanceEvidencePlan[] = ["data_only", "hybrid", "web_first"]; - let best: FinanceEvidencePlan = order[0]; - for (const plan of order) { - if (score[plan] > score[best]) best = plan; - } - return best; -} - -function resolveConfidencePenalty(params: { - plan: FinanceEvidencePlan; - hasData: boolean; - hasWeb: boolean; - route: FinanceMarketRoute; - eventDriven: boolean; - timeSensitive: boolean; -}): FinanceConfidencePenalty { - const { plan, hasData, hasWeb, route, eventDriven, timeSensitive } = params; - - if (!hasData && !hasWeb) return "high"; - if ((plan === "hybrid" || plan === "web_first") && !hasWeb) return "high"; - if (plan === "data_only" && (eventDriven || timeSensitive) && !hasWeb) return "high"; - if (route === "primary_no_ticker") return "medium"; - if (plan === "data_only" && (eventDriven || timeSensitive)) return "medium"; - return "low"; -} - -export function decideFinanceEvidencePlan(input: FinanceDecisionInput): FinanceDecision | undefined { - const { prompt } = input; - if (!isFinanceTask(prompt)) return undefined; - - const normalized = prompt.toLowerCase(); - const toolSet = normalizeTools(input.tools); - const hasData = toolSet.has("data"); - const hasWebSearch = toolSet.has("web_search"); - const hasWebFetch = toolSet.has("web_fetch"); - const hasWeb = hasWebSearch || hasWebFetch; - - const route = resolveMarketRoute(prompt); - const eventDriven = includesAny(normalized, EVENT_DRIVEN_KEYWORDS); - const causal = includesAny(normalized, CAUSAL_KEYWORDS); - const timeSensitive = includesAny(normalized, TIME_SENSITIVE_KEYWORDS); - - const score: Record = { - data_only: hasData ? 1.0 : -3.0, - hybrid: hasData && hasWeb ? 1.0 : -2.0, - web_first: hasWeb ? 0.6 : -3.0, - }; - - const reasons: string[] = []; - - if (route === "secondary") { - score.data_only += 0.7; - score.hybrid += 0.4; - reasons.push("secondary_market_task"); - } else if (route === "primary_with_ticker") { - score.hybrid += 0.9; - score.web_first += 0.3; - score.data_only -= 0.2; - reasons.push("primary_market_with_ticker"); - } else { - score.web_first += 1.3; - score.hybrid += 0.7; - score.data_only -= 1.0; - reasons.push("primary_market_without_ticker"); - } - - if (eventDriven) { - score.hybrid += 0.9; - score.web_first += 0.4; - score.data_only -= 0.5; - reasons.push("event_driven"); - } - - if (timeSensitive) { - score.hybrid += 0.6; - score.web_first += 0.3; - score.data_only -= 0.4; - reasons.push("time_sensitive"); - } - - if (causal) { - score.hybrid += 0.4; - score.web_first += 0.2; - score.data_only -= 0.2; - reasons.push("causal_explanation_needed"); - } - - if (!hasWeb) { - score.hybrid -= 2.0; - score.web_first -= 3.0; - reasons.push("web_tools_unavailable"); - } - if (!hasData) { - score.data_only -= 2.5; - score.hybrid -= 1.5; - score.web_first += 0.5; - reasons.push("data_tool_unavailable"); - } - - const plan = choosePlan(score); - const confidencePenalty = resolveConfidencePenalty({ - plan, - hasData, - hasWeb, - route, - eventDriven, - timeSensitive, - }); - - return { - plan, - marketRoute: route, - confidencePenalty, - reasons, - score, - }; -} - -export function buildInternalFinanceGuidance(decision: FinanceDecision): string { - return [ - "## Internal Finance Research Guidance", - "This section is internal orchestration guidance. Do not expose technical labels directly to the user unless they explicitly request methodology details.", - `Preferred evidence plan: ${decision.plan}`, - `Market route: ${decision.marketRoute}`, - `Confidence penalty if evidence gaps remain: ${decision.confidencePenalty}`, - `Decision factors: ${decision.reasons.join(", ") || "none"}`, - "Execution policy: start with the preferred plan, then escalate evidence collection if signals conflict or causality remains unresolved.", - ].join("\n"); -} diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index 8ea675f3..beaa4faf 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -35,12 +35,8 @@ import { import { buildSystemPrompt as buildStructuredSystemPrompt, collectRuntimeInfo, + type SystemPromptMode, } from "./system-prompt/index.js"; -import { - buildInternalFinanceGuidance, - decideFinanceEvidencePlan, - type FinanceDecision, -} from "./research/finance-decisioner.js"; import type { AuthProfileFailureReason } from "./auth-profiles/index.js"; import { sanitizeToolCallInputs, @@ -429,7 +425,6 @@ export class Agent { ): Promise { await this.ensureInitialized(); this.refreshAuthState(); - this.applyFinanceResearchGuidance(prompt); this.output.state.lastAssistantText = ""; this.currentUserDisplayPrompt = options?.displayPrompt; @@ -973,13 +968,6 @@ export class Agent { * Shared by constructor (via buildFullSystemPrompt) and reloadSystemPrompt. */ private rebuildSystemPrompt(toolNames: string[]): string | undefined { - return this.rebuildSystemPromptWithExtra(toolNames); - } - - private rebuildSystemPromptWithExtra( - toolNames: string[], - extraSystemPrompt?: string | undefined, - ): string | undefined { const profile = this.profile?.getProfile(); if (!profile) return undefined; @@ -1005,40 +993,6 @@ export class Agent { tools: toolNames, skillsPrompt, runtime, - extraSystemPrompt, - }); - } - - private applyFinanceResearchGuidance(prompt: string): void { - const toolNames = (this.agent.state.tools ?? []).map((t: { name: string }) => t.name); - const decision = decideFinanceEvidencePlan({ prompt, tools: toolNames }); - - const guidance = decision ? buildInternalFinanceGuidance(decision) : undefined; - const systemPrompt = this.rebuildSystemPromptWithExtra(toolNames, guidance); - if (systemPrompt) { - this.agent.setSystemPrompt(systemPrompt); - this.session.setSystemPrompt(systemPrompt); - } - - this.saveFinanceDecisionMeta(decision); - } - - private saveFinanceDecisionMeta(decision: FinanceDecision | undefined): void { - const currentMeta = this.session.getMeta() ?? {}; - if (!decision) { - // Keep previously saved decisions for non-finance turns. - return; - } - this.session.saveMeta({ - ...currentMeta, - researchDecision: { - domain: "finance", - plan: decision.plan, - marketRoute: decision.marketRoute, - confidencePenalty: decision.confidencePenalty, - reasons: decision.reasons, - timestamp: Date.now(), - }, }); } } diff --git a/packages/core/src/agent/session/types.ts b/packages/core/src/agent/session/types.ts index 48c037a9..ec734424 100644 --- a/packages/core/src/agent/session/types.ts +++ b/packages/core/src/agent/session/types.ts @@ -9,15 +9,6 @@ export type SessionMeta = { reasoningMode?: string; /** Context window token 数 */ contextWindowTokens?: number; - /** Internal finance evidence decision from the latest run */ - researchDecision?: { - domain: "finance"; - plan: "data_only" | "hybrid" | "web_first"; - marketRoute: "secondary" | "primary_with_ticker" | "primary_no_ticker"; - confidencePenalty: "low" | "medium" | "high"; - reasons: string[]; - timestamp: number; - } | undefined; }; export type SessionEntry = From 24435cdba4f2fa7519c2272fc29691ec6604fd39 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 11 Feb 2026 20:31:40 +0800 Subject: [PATCH 9/9] refactor(agent): remove hasWebTools conditional in data section Web tools are always available, so the conditional branching was unnecessary. Data section now always includes the dynamic evidence decision guidance. Co-Authored-By: Claude Opus 4.6 --- .../src/agent/system-prompt/sections.test.ts | 4 ++-- .../core/src/agent/system-prompt/sections.ts | 18 ++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/core/src/agent/system-prompt/sections.test.ts b/packages/core/src/agent/system-prompt/sections.test.ts index 00549efe..df446356 100644 --- a/packages/core/src/agent/system-prompt/sections.test.ts +++ b/packages/core/src/agent/system-prompt/sections.test.ts @@ -181,8 +181,8 @@ describe("buildConditionalToolSections", () => { expect(text).toContain("Web usage is conditional, not mandatory"); }); - it("adds dynamic evidence decision guidance when both data and web tools are present", () => { - const result = buildConditionalToolSections(["data", "web_search"], "full"); + it("adds dynamic evidence decision guidance when data tool is present", () => { + const result = buildConditionalToolSections(["data"], "full"); const text = result.join("\n"); expect(text).toContain("## Data Access"); expect(text).toContain("dynamic evidence decision"); diff --git a/packages/core/src/agent/system-prompt/sections.ts b/packages/core/src/agent/system-prompt/sections.ts index 52809aad..ce8bcc67 100644 --- a/packages/core/src/agent/system-prompt/sections.ts +++ b/packages/core/src/agent/system-prompt/sections.ts @@ -240,7 +240,6 @@ export function buildConditionalToolSections( if (mode === "none" || !tools || tools.length === 0) return []; const toolSet = new Set(tools.map((t) => t.toLowerCase())); - const hasWebTools = toolSet.has("web_search") || toolSet.has("web_fetch"); const lines: string[] = []; // Memory tools @@ -271,28 +270,19 @@ export function buildConditionalToolSections( // Data tools if (toolSet.has("data")) { - const dataLines = [ + lines.push( "## Data Access", "You have access to structured financial and market data via the `data` tool.", 'Use domain="finance" with specific actions to retrieve stock prices, financial statements, SEC filings, metrics, and more.', "Always specify dates in YYYY-MM-DD format. Use period='annual' or 'quarterly' or 'ttm' for financial statements.", - hasWebTools - ? "When both data and web tools are available, make a dynamic evidence decision: start from structured data, and use web tools only when external validation is needed (for example: event-driven, time-sensitive, or conflicting/incomplete evidence)." - : "Use tool outputs as evidence, and clearly state assumptions when data is incomplete.", - ...(hasWebTools - ? [ - "Make this evidence decision internally. In final answers, present concise user-facing research rationale instead of technical decision labels unless the user asks for methodology details.", - ] - : []), + "When both data and web tools are available, make a dynamic evidence decision: start from structured data, and use web tools only when external validation is needed (for example: event-driven, time-sensitive, or conflicting/incomplete evidence).", + "Make this evidence decision internally. In final answers, present concise user-facing research rationale instead of technical decision labels unless the user asks for methodology details.", "", - ]; - lines.push( - ...dataLines, ); } // Web tools - if (hasWebTools) { + if (toolSet.has("web_search") || toolSet.has("web_fetch")) { lines.push( "## Web Access", "You have web access. Use it when the user asks about current events, needs up-to-date information, or requests content from URLs.",