2026-07-27 12:41:36 +08:00
import { describe , expect , it , vi } from 'vitest'
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
import { Context } from 'cordis'
2026-07-27 12:41:36 +08:00
import TurndownService from 'turndown'
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
2026-07-21 03:08:35 +08:00
import ToolRegistry , { type ToolExecutionResult } from '@deepseek-ai/dsh-tools'
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
import WebService from '@deepseek-ai/dsh-web'
2026-07-14 04:17:38 +08:00
import type { WebSearchProvider , WebSearchResult } from '@deepseek-ai/dsh-web'
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
import {
formatSearchOutput ,
formatFetchOutput ,
parseSearchArgs ,
parseFetchArgs ,
presentSearchCall ,
presentFetchCall ,
2026-07-30 17:04:09 +08:00
presentSearchResult ,
presentFetchResult ,
searchMetaFromValue ,
searchMetaFromResult ,
fetchMetaFromValue ,
fetchMetaFromResult ,
Expose audited hardcoded tunables as plugin config
The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.
- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
read-render already documented that the consumer applies the caps, so
they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
schemastery default). Also fixes the stale GREP_LIMIT references in
search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
RunInternals.graceMs test seam is gone: graceMs is now a required
SpawnSpec field filled from config, so tests exercise the real
config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
rollback-journal modes serve filesystems where WAL's shared-memory
files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
hook/result stderr summary. The duplicated summarize() helpers merge
into hook-protocol's summarizeStderr(stderr, maxChars), beside the
HookResultRecord field it feeds, with the bound parameterized the
same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
fires far too late). Also corrects the BasicCompactService class doc,
which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
FsIoInternals.streamMinSize seam — the read-routing bound lives in
the consumer (tool-fs), where it is now config. This is item 1 of
the proposed prune-write-only-fs-surface RFC, annotated accordingly.
Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
2026-07-04 17:37:23 +08:00
WEB_SEARCH_MAX_RESULTS ,
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
} from '@deepseek-ai/dsh-tool-web'
2026-07-30 17:04:09 +08:00
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { ToolResult } from '@deepseek-ai/dsh-tools'
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
2026-07-19 23:38:54 +08:00
const testToolSignal = new AbortController ( ) . signal
2026-07-14 04:17:38 +08:00
const available = true
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
2026-07-14 04:17:38 +08:00
function searchProvider ( result : WebSearchResult , isAvailable = available ) : WebSearchProvider {
return { id : 'stub-search' , available : ( ) = > isAvailable , search : ( ) = > Promise . resolve ( result ) }
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
}
/** Mount the real registry, seam, and tool-web; return an executor helper. */
async function mountTools ( opts : {
config? : ToolWeb.Config
webConfig? : ConstructorParameters < typeof WebService > [ 1 ]
search? : WebSearchProvider
fetchProvider? : import ( '@deepseek-ai/dsh-web' ) . WebFetchProvider
2026-07-21 03:08:35 +08:00
} = { } ) : Promise < { ctx : Context ; fiber : Awaited < ReturnType < Context [ 'plugin' ] > > ; call : ( name : string , args : unknown ) = > Promise < ToolExecutionResult > } > {
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
const ctx = new Context ( )
await ctx . plugin ( SystemPrompt )
await ctx . plugin ( ToolRegistry )
await ctx . plugin ( WebService , opts . webConfig ? ? { } )
if ( opts . search ) ctx . web . registerSearchProvider ( opts . search )
if ( opts . fetchProvider ) ctx . web . registerFetchProvider ( opts . fetchProvider )
const fiber = await ctx . plugin ( ToolWeb , opts . config ? ? { } )
let counter = 0
2026-07-21 23:39:03 +08:00
const call = ( name : string , args : unknown ) = > ctx . tools . execute ( { signal : testToolSignal , callId : CallId ( ` call- ${ ++ counter } ` ) , name , arguments : args } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
return { ctx , fiber , call }
}
describe ( 'search formatting' , ( ) = > {
it ( 'renders content, sources with titles/hostnames, snippets, and a citation reminder' , ( ) = > {
const out = formatSearchOutput ( {
2026-07-14 04:17:38 +08:00
content : 'an answer' , truncated : false ,
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
sources : [
{ url : 'https://a.test/x' , title : 'A' , snippet : 'about a' , publishedAt : '2026-01-01' } ,
{ url : 'https://b.test/y' } ,
] ,
} )
expect ( out ) . toContain ( 'an answer' )
expect ( out ) . toContain ( '[A](https://a.test/x) — about a (2026-01-01)' )
expect ( out ) . toContain ( '[b.test](https://b.test/y)' )
expect ( out ) . toContain ( 'Cite the relevant URLs' )
} )
it ( 'reports no results when there is neither content nor sources' , ( ) = > {
2026-07-14 04:17:38 +08:00
expect ( formatSearchOutput ( { sources : [ ] , truncated : false } ) )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
. toContain ( 'No results found.' )
} )
it ( 'renders content alone when there are no sources' , ( ) = > {
2026-07-14 04:17:38 +08:00
const out = formatSearchOutput ( { content : 'just an answer' , sources : [ ] , truncated : false } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
expect ( out ) . toContain ( 'just an answer' )
expect ( out ) . not . toContain ( 'No results found.' )
expect ( out ) . not . toContain ( 'Sources:' )
} )
it ( 'notes truncation' , ( ) = > {
2026-07-14 04:17:38 +08:00
const out = formatSearchOutput ( { sources : [ { url : 'https://a.test' } ] , truncated : true } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
expect ( out ) . toContain ( 'Showing the first 1 sources' )
} )
it ( 'validates the query' , ( ) = > {
expect ( ( ) = > parseSearchArgs ( { query : ' ' } ) ) . toThrow ( 'non-empty' )
expect ( parseSearchArgs ( { query : 'hi' } ) ) . toEqual ( { query : 'hi' } )
} )
feat(tool-web): replace the regex HTML-to-markdown converter with turndown
Implements the turndown Agent Note from the NIH dependency audit (full
variant, not the minimal entities-only fallback): dsh-tool-web's fetch
rendering now converts HTML through turndown + @joplin/turndown-plugin-gfm
(atx headings, fenced code, dash bullets, GFM tables/strikethrough) over
the real domino DOM, with script/style/noscript removed wholesale. The
hand-rolled ~86-line regex converter html.ts and its entity tables are
deleted; renderBody wraps the conversion in try/catch falling back to
the raw HTML body, because turndown's recursive DOM walk overflows with
a RangeError on pathological nesting (measured: 4k levels on the main
thread, 8k in a worker) where the regex version could never throw.
Closure weight, measured: tool-web IS in the single-exe runtime closure,
and the exe asset globs would pack ~7.9 MB of the three new packages —
but ~6 MB of that is domino's test corpus, with runtime lib/ at ~550 KB
against a ~174 MB artifact (<0.5% either way), so the swap wins.
Per testing policy the previously-missing keyless web_fetch snapshot
ships in the same change: the acp-agent `web-fetch` scenario boots a new
web.cordis.yml overlay (web seam + real dsh-web-fetch-local provider +
tool-web fetch-only + a loopback HTTP fixture server on a fixed port
serving deterministic HTML with entities, a GFM table, and nesting), so
recording and keyless replay both drive the real HTTP fetch and real
conversion end to end; the scenario pins the new `web` header class.
The Agent Note moves proposed -> implemented and is rewritten per the
lifecycle contract (Decision/Consequences/Testing, closure verdict and
alternatives recorded); tool-web and acp-agent READMEs updated in both
languages and pairs re-recorded.
2026-07-27 00:22:13 +08:00
it ( 'falls back to the raw URL as a source label when the URL is unparseable' , ( ) = > {
const out = formatSearchOutput ( { truncated : false , sources : [ { url : 'not a url' } ] } )
expect ( out ) . toContain ( '[not a url](not a url)' )
} )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
it ( 'presents a search call as a search-kind card titled by the query' , ( ) = > {
2026-07-03 22:52:18 +08:00
expect ( presentSearchCall ( { query : 'find me' } ) ) . toEqual ( { card : 'generic' , title : 'find me' , kind : 'search' , rawInput : 'find me' } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
} )
} )
2026-07-30 17:04:09 +08:00
/** Build a completed non-error tool result with the given meta and text content. */
function toolResult ( meta : unknown , text = 'body' , isError = false ) : ToolResult {
const content : ContentBlock [ ] = [ { type : 'text' , text } ]
return { content , isError , . . . meta !== undefined ? { meta : meta as never } : { } }
}
describe ( 'web_search presentation meta and result view' , ( ) = > {
it ( 'projects sources, answer, and truncation into meta, omitting absent optional fields' , ( ) = > {
const meta = searchMetaFromValue ( {
content : 'an answer' , truncated : true ,
sources : [
{ url : 'https://a.test/x' , title : 'A' , snippet : 'about a' , publishedAt : '2026-01-01' } ,
{ url : 'https://b.test/y' } ,
] ,
} )
expect ( meta ) . toEqual ( {
answer : 'an answer' ,
truncated : true ,
sources : [
{ url : 'https://a.test/x' , title : 'A' , snippet : 'about a' , publishedAt : '2026-01-01' } ,
{ url : 'https://b.test/y' } ,
] ,
} )
} )
it ( 'omits answer from meta when the provider returned none' , ( ) = > {
const meta = searchMetaFromValue ( { truncated : false , sources : [ { url : 'https://a.test' } ] } )
expect ( meta ) . toEqual ( { truncated : false , sources : [ { url : 'https://a.test' } ] } )
} )
it ( 'round-trips projected meta back to a typed search meta' , ( ) = > {
const value = {
content : 'ans' , truncated : false ,
sources : [ { url : 'https://a.test' , title : 'A' , snippet : 's' , publishedAt : '2026-01-01' } ] ,
}
expect ( searchMetaFromResult ( searchMetaFromValue ( value ) ) ) . toEqual ( {
answer : 'ans' , truncated : false ,
sources : [ { url : 'https://a.test' , title : 'A' , snippet : 's' , publishedAt : '2026-01-01' } ] ,
} )
} )
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
it ( 'presents a completed search as a web/search card carrying the structured sources, titled by the query' , ( ) = > {
2026-07-30 17:04:09 +08:00
const meta = searchMetaFromValue ( {
content : 'an answer' , truncated : true ,
sources : [ { url : 'https://a.test' , title : 'A' , snippet : 'snip' , publishedAt : '2026-07-20' } ] ,
} )
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
expect ( presentSearchResult ( { query : 'q' } , toolResult ( meta , 'rendered' ) ) ) . toEqual ( {
2026-07-30 17:04:09 +08:00
card : 'web' ,
kind : 'search' ,
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
title : 'q' ,
2026-07-30 17:04:09 +08:00
answer : 'an answer' ,
truncated : true ,
sources : [ { url : 'https://a.test' , title : 'A' , snippet : 'snip' , publishedAt : '2026-07-20' } ] ,
} )
} )
it ( 'omits the answer from the view when meta carries none' , ( ) = > {
const meta = searchMetaFromValue ( { truncated : false , sources : [ { url : 'https://a.test' } ] } )
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
const view = presentSearchResult ( { query : 'q' } , toolResult ( meta ) )
2026-07-30 17:04:09 +08:00
expect ( view ) . toBeDefined ( )
expect ( view && 'answer' in view ) . toBe ( false )
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
expect ( view && 'content' in view ) . toBe ( false )
2026-07-30 17:04:09 +08:00
} )
it ( 'falls back to the generic card on an error result' , ( ) = > {
const meta = searchMetaFromValue ( { truncated : false , sources : [ { url : 'https://a.test' } ] } )
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
expect ( presentSearchResult ( { query : 'q' } , toolResult ( meta , 'body' , true ) ) ) . toBeUndefined ( )
2026-07-30 17:04:09 +08:00
} )
it ( 'falls back to the generic card on absent or malformed meta' , ( ) = > {
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
expect ( presentSearchResult ( { query : 'q' } , toolResult ( undefined ) ) ) . toBeUndefined ( )
2026-07-30 17:04:09 +08:00
expect ( searchMetaFromResult ( undefined ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( null ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( 'nope' ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( [ ] ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( { } ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( { sources : 'x' , truncated : false } ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( { sources : [ ] , truncated : 'no' } ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( { sources : [ ] , truncated : false , answer : 1 } ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( { sources : [ null ] , truncated : false } ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( { sources : [ { url : 1 } ] , truncated : false } ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( { sources : [ { url : 'u' , title : 2 } ] , truncated : false } ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( { sources : [ { url : 'u' , snippet : 2 } ] , truncated : false } ) ) . toBeUndefined ( )
expect ( searchMetaFromResult ( { sources : [ { url : 'u' , publishedAt : 2 } ] , truncated : false } ) ) . toBeUndefined ( )
} )
it ( 'accepts an empty source list as valid meta' , ( ) = > {
expect ( searchMetaFromResult ( { sources : [ ] , truncated : false } ) ) . toEqual ( { sources : [ ] , truncated : false } )
} )
} )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
describe ( 'fetch formatting' , ( ) = > {
2026-07-27 12:41:36 +08:00
const NO_CAP = 1 _000_000
2026-07-27 16:40:02 +08:00
const HEADER = 'Fetched https://a.test (HTTP 200)\n\n'
const renderHtml = ( content : string ) = > formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' , content } ,
} , NO_CAP ) . slice ( HEADER . length )
2026-07-27 12:41:36 +08:00
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
it ( 'renders an html body to markdown text with a status header' , ( ) = > {
const out = formatFetchOutput ( {
2026-07-14 04:17:38 +08:00
url : 'https://a.test' , statusCode : 200 , truncated : false ,
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
body : { kind : 'html' , content : '<h1>Title</h1><p>Body text</p>' } ,
2026-07-27 12:41:36 +08:00
} , NO_CAP )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
expect ( out ) . toContain ( 'Fetched https://a.test (HTTP 200)' )
expect ( out ) . toContain ( '# Title' )
expect ( out ) . toContain ( 'Body text' )
} )
it ( 'passes a text body through and notes truncation' , ( ) = > {
const out = formatFetchOutput ( {
2026-07-14 04:17:38 +08:00
url : 'https://a.test' , statusCode : 200 , truncated : true ,
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
body : { kind : 'text' , content : 'plain' } ,
2026-07-27 12:41:36 +08:00
} , NO_CAP )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
expect ( out ) . toContain ( 'plain' )
expect ( out ) . toContain ( 'Content truncated' )
} )
2026-07-27 12:41:36 +08:00
it ( 'caps the complete output and notes truncation, even when markdown escaping expands the body' , ( ) = > {
// 1,000 underscores render as 2,000 escaped characters — conversion can
// outgrow a provider-side body cap, so the bound applies to the output.
const out = formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' , content : ` <p> ${ '_' . repeat ( 1000 ) } </p> ` } ,
} , 500 )
expect ( out . length ) . toBeLessThanOrEqual ( 500 )
expect ( out ) . toContain ( 'Fetched https://a.test (HTTP 200)' )
expect ( out ) . toContain ( '\\_\\_' )
expect ( out ) . toContain ( 'Content truncated' )
// Exact and tiny caps: the complete result is bounded, header and footer included.
const exact = formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'text' , content : 'abc' } ,
} , 'Fetched https://a.test (HTTP 200)\n\nabc' . length )
expect ( exact ) . toBe ( 'Fetched https://a.test (HTTP 200)\n\nabc' )
const tiny = formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : true ,
body : { kind : 'text' , content : 'abcdef' } ,
} , 10 )
2026-07-27 16:40:02 +08:00
expect ( tiny . length ) . toBeLessThanOrEqual ( 10 )
expect ( tiny ) . toBe ( 'Fetched ht' )
2026-07-27 12:41:36 +08:00
} )
2026-07-27 16:40:02 +08:00
it ( 'dispatches text and html bodies' , ( ) = > {
expect ( formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'text' , content : 'x' } ,
} , NO_CAP ) ) . toBe ( ` ${ HEADER } x ` )
expect ( renderHtml ( '<p>y</p>' ) ) . toBe ( 'y' )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
} )
feat(tool-web): replace the regex HTML-to-markdown converter with turndown
Implements the turndown Agent Note from the NIH dependency audit (full
variant, not the minimal entities-only fallback): dsh-tool-web's fetch
rendering now converts HTML through turndown + @joplin/turndown-plugin-gfm
(atx headings, fenced code, dash bullets, GFM tables/strikethrough) over
the real domino DOM, with script/style/noscript removed wholesale. The
hand-rolled ~86-line regex converter html.ts and its entity tables are
deleted; renderBody wraps the conversion in try/catch falling back to
the raw HTML body, because turndown's recursive DOM walk overflows with
a RangeError on pathological nesting (measured: 4k levels on the main
thread, 8k in a worker) where the regex version could never throw.
Closure weight, measured: tool-web IS in the single-exe runtime closure,
and the exe asset globs would pack ~7.9 MB of the three new packages —
but ~6 MB of that is domino's test corpus, with runtime lib/ at ~550 KB
against a ~174 MB artifact (<0.5% either way), so the swap wins.
Per testing policy the previously-missing keyless web_fetch snapshot
ships in the same change: the acp-agent `web-fetch` scenario boots a new
web.cordis.yml overlay (web seam + real dsh-web-fetch-local provider +
tool-web fetch-only + a loopback HTTP fixture server on a fixed port
serving deterministic HTML with entities, a GFM table, and nesting), so
recording and keyless replay both drive the real HTTP fetch and real
conversion end to end; the scenario pins the new `web` header class.
The Agent Note moves proposed -> implemented and is rewritten per the
lifecycle contract (Decision/Consequences/Testing, closure verdict and
alternatives recorded); tool-web and acp-agent READMEs updated in both
languages and pairs re-recorded.
2026-07-27 00:22:13 +08:00
it ( 'converts html via turndown: entities, links, tables, nesting; drops script/style/noscript' , ( ) = > {
2026-07-27 16:40:02 +08:00
expect ( renderHtml ( '<style>.x{}</style><script>bad()</script><noscript>ns</noscript><p>Tom & Jerry © Résumé</p><a href="https://a.test">link</a>' ) )
. toBe ( 'Tom & Jerry © Résumé\n\n[link](https://a.test)' )
expect ( renderHtml ( '<h2>Heading</h2><ul><li>one</li><li>two</li></ul>' ) )
feat(tool-web): replace the regex HTML-to-markdown converter with turndown
Implements the turndown Agent Note from the NIH dependency audit (full
variant, not the minimal entities-only fallback): dsh-tool-web's fetch
rendering now converts HTML through turndown + @joplin/turndown-plugin-gfm
(atx headings, fenced code, dash bullets, GFM tables/strikethrough) over
the real domino DOM, with script/style/noscript removed wholesale. The
hand-rolled ~86-line regex converter html.ts and its entity tables are
deleted; renderBody wraps the conversion in try/catch falling back to
the raw HTML body, because turndown's recursive DOM walk overflows with
a RangeError on pathological nesting (measured: 4k levels on the main
thread, 8k in a worker) where the regex version could never throw.
Closure weight, measured: tool-web IS in the single-exe runtime closure,
and the exe asset globs would pack ~7.9 MB of the three new packages —
but ~6 MB of that is domino's test corpus, with runtime lib/ at ~550 KB
against a ~174 MB artifact (<0.5% either way), so the swap wins.
Per testing policy the previously-missing keyless web_fetch snapshot
ships in the same change: the acp-agent `web-fetch` scenario boots a new
web.cordis.yml overlay (web seam + real dsh-web-fetch-local provider +
tool-web fetch-only + a loopback HTTP fixture server on a fixed port
serving deterministic HTML with entities, a GFM table, and nesting), so
recording and keyless replay both drive the real HTTP fetch and real
conversion end to end; the scenario pins the new `web` header class.
The Agent Note moves proposed -> implemented and is rewritten per the
lifecycle contract (Decision/Consequences/Testing, closure verdict and
alternatives recorded); tool-web and acp-agent READMEs updated in both
languages and pairs re-recorded.
2026-07-27 00:22:13 +08:00
. toBe ( '## Heading\n\n- one\n- two' )
2026-07-27 16:40:02 +08:00
expect ( renderHtml ( '<table><tr><th>A</th><th>B</th></tr><tr><td>1</td><td>2</td></tr></table>' ) )
feat(tool-web): replace the regex HTML-to-markdown converter with turndown
Implements the turndown Agent Note from the NIH dependency audit (full
variant, not the minimal entities-only fallback): dsh-tool-web's fetch
rendering now converts HTML through turndown + @joplin/turndown-plugin-gfm
(atx headings, fenced code, dash bullets, GFM tables/strikethrough) over
the real domino DOM, with script/style/noscript removed wholesale. The
hand-rolled ~86-line regex converter html.ts and its entity tables are
deleted; renderBody wraps the conversion in try/catch falling back to
the raw HTML body, because turndown's recursive DOM walk overflows with
a RangeError on pathological nesting (measured: 4k levels on the main
thread, 8k in a worker) where the regex version could never throw.
Closure weight, measured: tool-web IS in the single-exe runtime closure,
and the exe asset globs would pack ~7.9 MB of the three new packages —
but ~6 MB of that is domino's test corpus, with runtime lib/ at ~550 KB
against a ~174 MB artifact (<0.5% either way), so the swap wins.
Per testing policy the previously-missing keyless web_fetch snapshot
ships in the same change: the acp-agent `web-fetch` scenario boots a new
web.cordis.yml overlay (web seam + real dsh-web-fetch-local provider +
tool-web fetch-only + a loopback HTTP fixture server on a fixed port
serving deterministic HTML with entities, a GFM table, and nesting), so
recording and keyless replay both drive the real HTTP fetch and real
conversion end to end; the scenario pins the new `web` header class.
The Agent Note moves proposed -> implemented and is rewritten per the
lifecycle contract (Decision/Consequences/Testing, closure verdict and
alternatives recorded); tool-web and acp-agent READMEs updated in both
languages and pairs re-recorded.
2026-07-27 00:22:13 +08:00
. toBe ( '| A | B |\n| --- | --- |\n| 1 | 2 |' )
2026-07-27 16:40:02 +08:00
expect ( renderHtml ( '<table><thead><tr><th align="left">L</th><th align="right">R</th><th style="text-align:center">C</th></tr></thead><tbody><tr><td>1</td><td>2</td><td>3</td></tr></tbody></table>' ) )
. toBe ( '| L | R | C |\n| :--- | ---: | :---: |\n| 1 | 2 | 3 |' )
expect ( renderHtml ( '<p><strong>bold <em>italic</em></strong></p><blockquote><p>quoted</p></blockquote>' ) )
feat(tool-web): replace the regex HTML-to-markdown converter with turndown
Implements the turndown Agent Note from the NIH dependency audit (full
variant, not the minimal entities-only fallback): dsh-tool-web's fetch
rendering now converts HTML through turndown + @joplin/turndown-plugin-gfm
(atx headings, fenced code, dash bullets, GFM tables/strikethrough) over
the real domino DOM, with script/style/noscript removed wholesale. The
hand-rolled ~86-line regex converter html.ts and its entity tables are
deleted; renderBody wraps the conversion in try/catch falling back to
the raw HTML body, because turndown's recursive DOM walk overflows with
a RangeError on pathological nesting (measured: 4k levels on the main
thread, 8k in a worker) where the regex version could never throw.
Closure weight, measured: tool-web IS in the single-exe runtime closure,
and the exe asset globs would pack ~7.9 MB of the three new packages —
but ~6 MB of that is domino's test corpus, with runtime lib/ at ~550 KB
against a ~174 MB artifact (<0.5% either way), so the swap wins.
Per testing policy the previously-missing keyless web_fetch snapshot
ships in the same change: the acp-agent `web-fetch` scenario boots a new
web.cordis.yml overlay (web seam + real dsh-web-fetch-local provider +
tool-web fetch-only + a loopback HTTP fixture server on a fixed port
serving deterministic HTML with entities, a GFM table, and nesting), so
recording and keyless replay both drive the real HTTP fetch and real
conversion end to end; the scenario pins the new `web` header class.
The Agent Note moves proposed -> implemented and is rewritten per the
lifecycle contract (Decision/Consequences/Testing, closure verdict and
alternatives recorded); tool-web and acp-agent READMEs updated in both
languages and pairs re-recorded.
2026-07-27 00:22:13 +08:00
. toBe ( '**bold _italic_**\n\n> quoted' )
} )
2026-07-27 16:40:02 +08:00
it ( 'does not expand numeric colspan attributes into unbounded output' , ( ) = > {
const table = '<table><thead><tr><th colspan="1000000">A</th></tr></thead><tbody><tr><td>B</td></tr></tbody></table>'
expect ( renderHtml ( table ) ) . toBe ( '| A |\n| --- |\n| B |' )
} )
2026-07-27 12:41:36 +08:00
it ( 'passes deeply nested html through raw without attempting conversion' , ( ) = > {
// Unclosed-tag nesting makes the synchronous conversion superlinear
// (seconds at 20k levels, during which the cooperative timeout cannot
// fire), so the depth preflight skips conversion entirely; this must
// return fast, not merely not-throw.
feat(tool-web): replace the regex HTML-to-markdown converter with turndown
Implements the turndown Agent Note from the NIH dependency audit (full
variant, not the minimal entities-only fallback): dsh-tool-web's fetch
rendering now converts HTML through turndown + @joplin/turndown-plugin-gfm
(atx headings, fenced code, dash bullets, GFM tables/strikethrough) over
the real domino DOM, with script/style/noscript removed wholesale. The
hand-rolled ~86-line regex converter html.ts and its entity tables are
deleted; renderBody wraps the conversion in try/catch falling back to
the raw HTML body, because turndown's recursive DOM walk overflows with
a RangeError on pathological nesting (measured: 4k levels on the main
thread, 8k in a worker) where the regex version could never throw.
Closure weight, measured: tool-web IS in the single-exe runtime closure,
and the exe asset globs would pack ~7.9 MB of the three new packages —
but ~6 MB of that is domino's test corpus, with runtime lib/ at ~550 KB
against a ~174 MB artifact (<0.5% either way), so the swap wins.
Per testing policy the previously-missing keyless web_fetch snapshot
ships in the same change: the acp-agent `web-fetch` scenario boots a new
web.cordis.yml overlay (web seam + real dsh-web-fetch-local provider +
tool-web fetch-only + a loopback HTTP fixture server on a fixed port
serving deterministic HTML with entities, a GFM table, and nesting), so
recording and keyless replay both drive the real HTTP fetch and real
conversion end to end; the scenario pins the new `web` header class.
The Agent Note moves proposed -> implemented and is rewritten per the
lifecycle contract (Decision/Consequences/Testing, closure verdict and
alternatives recorded); tool-web and acp-agent READMEs updated in both
languages and pairs re-recorded.
2026-07-27 00:22:13 +08:00
const depth = 20 _000
const pathological = '<div>' . repeat ( depth ) + 'x' + '</div>' . repeat ( depth )
2026-07-27 12:41:36 +08:00
const started = Date . now ( )
2026-07-27 16:40:02 +08:00
expect ( formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' , content : pathological } ,
} , NO_CAP ) ) . toBe ( ` ${ HEADER } ${ pathological } ` )
2026-07-27 12:41:36 +08:00
expect ( Date . now ( ) - started ) . toBeLessThan ( 2 _000 )
} )
2026-07-27 16:40:02 +08:00
it ( 'comments and mismatched closing tags cannot hide deep nesting from the preflight' , ( ) = > {
const pathological = '<div><!-- </div> --></span>' . repeat ( 600 ) + 'x'
expect ( formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' , content : pathological } ,
} , NO_CAP ) ) . toBe ( ` ${ HEADER } ${ pathological } ` )
const abruptlyClosedComments = '<div><!-->' . repeat ( 600 ) + 'x'
expect ( formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' , content : abruptlyClosedComments } ,
} , NO_CAP ) ) . toBe ( ` ${ HEADER } ${ abruptlyClosedComments } ` )
} )
it ( 'the preflight accepts ordinary closed, void, self-closing, quoted, and raw-text markup' , ( ) = > {
const paragraphs = '<p title=\'>\'>x<br ><img src="x"><input/></p>' . repeat ( 600 )
const script = ` <script>const invalid = '</scriptx>'; const template = ' ${ '<div>' . repeat ( 600 ) } '</script > `
expect ( renderHtml ( ` <!doctype html><?pi><1bad> ${ paragraphs } ${ script } ` ) )
. not . toContain ( '<p' )
expect ( renderHtml ( 'plain text' ) ) . toBe ( 'plain text' )
expect ( renderHtml ( '<p>x</p><!-- unfinished' ) ) . toBe ( 'x' )
expect ( renderHtml ( '<script>unclosed' ) ) . toBe ( '' )
expect ( renderHtml ( '<script>closed by slash</script/>' ) ) . toBe ( '' )
expect ( renderHtml ( '<script>closed at end</script' ) ) . toBe ( '' )
} )
it ( 'scans malformed unterminated tags in bounded time' , ( ) = > {
const malformed = '<a' . repeat ( 100 _000 )
const started = Date . now ( )
const out = formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' , content : malformed } ,
} , 200 _000 )
expect ( out . length ) . toBeLessThanOrEqual ( 200 _000 )
expect ( Date . now ( ) - started ) . toBeLessThan ( 2 _000 )
2026-07-27 12:41:36 +08:00
} )
it ( 'falls back to the raw html when turndown throws despite a shallow depth scan' , ( ) = > {
const spy = vi . spyOn ( TurndownService . prototype , 'turndown' ) . mockImplementation ( ( ) = > {
throw new RangeError ( 'Maximum call stack size exceeded' )
} )
try {
2026-07-27 16:40:02 +08:00
expect ( formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' , content : '<p>x</p>' } ,
} , NO_CAP ) ) . toBe ( ` ${ HEADER } <p>x</p> ` )
} finally {
spy . mockRestore ( )
}
} )
it ( 'bounds source conversion work before rendering a custom provider body' , ( ) = > {
const spy = vi . spyOn ( TurndownService . prototype , 'turndown' ) . mockReturnValue ( 'converted' )
try {
const out = formatFetchOutput ( {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' , content : ` <p> ${ 'x' . repeat ( 10 _000 ) } </p> ` } ,
} , 500 )
expect ( spy ) . toHaveBeenCalledWith ( ` <p> ${ 'x' . repeat ( 497 ) } ` )
expect ( out . length ) . toBeLessThanOrEqual ( 500 )
expect ( out ) . toContain ( 'Content truncated' )
2026-07-27 12:41:36 +08:00
} finally {
spy . mockRestore ( )
}
feat(tool-web): replace the regex HTML-to-markdown converter with turndown
Implements the turndown Agent Note from the NIH dependency audit (full
variant, not the minimal entities-only fallback): dsh-tool-web's fetch
rendering now converts HTML through turndown + @joplin/turndown-plugin-gfm
(atx headings, fenced code, dash bullets, GFM tables/strikethrough) over
the real domino DOM, with script/style/noscript removed wholesale. The
hand-rolled ~86-line regex converter html.ts and its entity tables are
deleted; renderBody wraps the conversion in try/catch falling back to
the raw HTML body, because turndown's recursive DOM walk overflows with
a RangeError on pathological nesting (measured: 4k levels on the main
thread, 8k in a worker) where the regex version could never throw.
Closure weight, measured: tool-web IS in the single-exe runtime closure,
and the exe asset globs would pack ~7.9 MB of the three new packages —
but ~6 MB of that is domino's test corpus, with runtime lib/ at ~550 KB
against a ~174 MB artifact (<0.5% either way), so the swap wins.
Per testing policy the previously-missing keyless web_fetch snapshot
ships in the same change: the acp-agent `web-fetch` scenario boots a new
web.cordis.yml overlay (web seam + real dsh-web-fetch-local provider +
tool-web fetch-only + a loopback HTTP fixture server on a fixed port
serving deterministic HTML with entities, a GFM table, and nesting), so
recording and keyless replay both drive the real HTTP fetch and real
conversion end to end; the scenario pins the new `web` header class.
The Agent Note moves proposed -> implemented and is rewritten per the
lifecycle contract (Decision/Consequences/Testing, closure verdict and
alternatives recorded); tool-web and acp-agent READMEs updated in both
languages and pairs re-recorded.
2026-07-27 00:22:13 +08:00
} )
2026-07-08 10:06:07 +08:00
it ( 'validates url (non-empty), no timeout parameter' , ( ) = > {
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
expect ( ( ) = > parseFetchArgs ( { url : ' ' } ) ) . toThrow ( 'non-empty' )
2026-07-08 10:06:07 +08:00
expect ( parseFetchArgs ( { url : 'https://a.test' } ) ) . toEqual ( { url : 'https://a.test' } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
} )
it ( 'presents a fetch call as a fetch-kind card titled by the url' , ( ) = > {
2026-07-03 22:52:18 +08:00
expect ( presentFetchCall ( { url : 'https://a.test' } ) ) . toEqual ( { card : 'generic' , title : 'https://a.test' , kind : 'fetch' , rawInput : 'https://a.test' } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
} )
} )
2026-07-30 17:04:09 +08:00
describe ( 'web_fetch presentation meta and result view' , ( ) = > {
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
const NO_CAP = 1 _000_000
it ( 'projects url, status, and the provider truncation into meta' , ( ) = > {
expect ( fetchMetaFromValue ( { url : 'https://a.test' , statusCode : 404 , truncated : true , body : { kind : 'text' , content : 'x' } } , NO_CAP ) )
2026-07-30 17:04:09 +08:00
. toEqual ( { url : 'https://a.test' , statusCode : 404 , truncated : true } )
} )
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
it ( 'projects truncated: true when the output cap cut a body the provider did not, matching the render footer' , ( ) = > {
// The provider reports truncated: false, but conversion outgrows the cap, so
// the render text carries the truncation footer. The meta must agree.
const value = {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' as const , content : ` <p> ${ '_' . repeat ( 1000 ) } </p> ` } ,
}
const meta = fetchMetaFromValue ( value , 500 ) as { truncated : boolean }
expect ( meta . truncated ) . toBe ( true )
expect ( formatFetchOutput ( value , 500 ) ) . toContain ( 'Content truncated' )
} )
it ( 'projects truncated: false when neither the provider nor the cap cut the body' , ( ) = > {
const value = {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'text' as const , content : 'short' } ,
}
const meta = fetchMetaFromValue ( value , NO_CAP ) as { truncated : boolean }
expect ( meta . truncated ) . toBe ( false )
expect ( formatFetchOutput ( value , NO_CAP ) ) . not . toContain ( 'Content truncated' )
} )
2026-07-30 20:48:19 +08:00
it ( 'converts one HTML body once across the render and meta projections of the same result' , ( ) = > {
// The registry calls output.render and output.presentationMeta with the same
// frozen result value; the memo must collapse them into one turndown walk so
// a large or deeply nested page is not parsed and converted twice. A second
// cap on the same result is a distinct entry, so it converts again.
const spy = vi . spyOn ( TurndownService . prototype , 'turndown' )
const value = {
url : 'https://a.test' , statusCode : 200 , truncated : false ,
body : { kind : 'html' as const , content : '<p>hello</p>' } ,
}
try {
formatFetchOutput ( value , NO_CAP )
fetchMetaFromValue ( value , NO_CAP )
expect ( spy ) . toHaveBeenCalledTimes ( 1 )
formatFetchOutput ( value , NO_CAP - 1 )
expect ( spy ) . toHaveBeenCalledTimes ( 2 )
} finally {
spy . mockRestore ( )
}
} )
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
it ( 'presents a completed fetch as a web/fetch card carrying the summary, titled by the url, without content' , ( ) = > {
const meta = fetchMetaFromValue ( { url : 'https://a.test' , statusCode : 200 , truncated : false , body : { kind : 'text' , content : '# Title' } } , NO_CAP )
expect ( presentFetchResult ( { url : 'https://a.test' } , toolResult ( meta , '# Title' ) ) ) . toEqual ( {
2026-07-30 17:04:09 +08:00
card : 'web' ,
kind : 'fetch' ,
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
title : 'https://a.test' ,
2026-07-30 17:04:09 +08:00
url : 'https://a.test' ,
statusCode : 200 ,
truncated : false ,
} )
} )
it ( 'falls back to the generic card on an error result' , ( ) = > {
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
const meta = fetchMetaFromValue ( { url : 'https://a.test' , statusCode : 200 , truncated : false , body : { kind : 'text' , content : 'ok' } } , NO_CAP )
expect ( presentFetchResult ( { url : 'https://a.test' } , toolResult ( meta , 'body' , true ) ) ) . toBeUndefined ( )
2026-07-30 17:04:09 +08:00
} )
it ( 'falls back to the generic card on absent or malformed meta' , ( ) = > {
fix(tool-web): align fetch card truncation, drop view content copies, sync card docs
Address the code-review bot findings on the web result card:
- web_fetch's card truncated now derives from the shared renderFetchOutput
helper, matching the effective truncation the model-facing text reflects
(provider cap, source cut, or output cap), instead of the provider-only flag.
- Drop the redundant content copy from both web result views; a UI without the
web capability falls back to the raw tool/result content. Narrow the TUI
transcript view.content access accordingly.
- Set the result-state title from the call args (query/url) so a window-
truncated replay keeps a title.
- Project meta from the seam result types rather than hand-rolled value types.
- Sync the card vocabulary across core tools README, docs/core-data-structures,
the adding-a-tool cookbook, and the tool-web package README (both languages,
re-recorded pairings); regenerate the cordis api-catalog and cordis-inspect
snapshot; revise the Agent Note.
2026-07-30 20:01:41 +08:00
expect ( presentFetchResult ( { url : 'https://a.test' } , toolResult ( undefined ) ) ) . toBeUndefined ( )
2026-07-30 17:04:09 +08:00
expect ( fetchMetaFromResult ( undefined ) ) . toBeUndefined ( )
expect ( fetchMetaFromResult ( null ) ) . toBeUndefined ( )
expect ( fetchMetaFromResult ( 'nope' ) ) . toBeUndefined ( )
expect ( fetchMetaFromResult ( [ ] ) ) . toBeUndefined ( )
expect ( fetchMetaFromResult ( { } ) ) . toBeUndefined ( )
expect ( fetchMetaFromResult ( { url : 1 , statusCode : 200 , truncated : false } ) ) . toBeUndefined ( )
expect ( fetchMetaFromResult ( { url : 'u' , statusCode : 'x' , truncated : false } ) ) . toBeUndefined ( )
expect ( fetchMetaFromResult ( { url : 'u' , statusCode : 200 , truncated : 'no' } ) ) . toBeUndefined ( )
} )
} )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
describe ( 'tool-web registration' , ( ) = > {
it ( 'registers both tools by default' , async ( ) = > {
const { fiber , ctx } = await mountTools ( )
const names = ctx . tools . schemas ( ) . map ( s = > s . name )
expect ( names ) . toContain ( 'web_search' )
expect ( names ) . toContain ( 'web_fetch' )
2026-07-19 23:38:54 +08:00
expect ( ctx . tools . executionMode ( { signal : testToolSignal , callId : CallId ( 'search-safe' ) , name : 'web_search' , arguments : { query : 'q' } } ) )
2026-07-13 11:02:21 +08:00
. toEqual ( { kind : 'parallel' } )
2026-07-19 23:38:54 +08:00
expect ( ctx . tools . executionMode ( { signal : testToolSignal , callId : CallId ( 'fetch-safe' ) , name : 'web_fetch' , arguments : { url : 'https://a.test' } } ) )
2026-07-13 11:02:21 +08:00
. toEqual ( { kind : 'parallel' } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
await fiber . dispose ( )
expect ( ctx . tools . schemas ( ) . map ( s = > s . name ) ) . not . toContain ( 'web_search' )
} )
it ( 'registers only enabled tools' , async ( ) = > {
const { fiber , ctx } = await mountTools ( { config : { search : true , fetch : false } } )
const names = ctx . tools . schemas ( ) . map ( s = > s . name )
expect ( names ) . toContain ( 'web_search' )
expect ( names ) . not . toContain ( 'web_fetch' )
await fiber . dispose ( )
} )
it ( 'registers only web_fetch when search is disabled' , async ( ) = > {
const { fiber , ctx } = await mountTools ( { config : { search : false , fetch : true } } )
const names = ctx . tools . schemas ( ) . map ( s = > s . name )
expect ( names ) . not . toContain ( 'web_search' )
expect ( names ) . toContain ( 'web_fetch' )
await fiber . dispose ( )
} )
it ( 'registers web_search even when no provider is available (schema follows enablement, not availability)' , async ( ) = > {
refactor(web): drop the unconsumed observation surface
WebService exposed an observation surface nothing in production observes:
the web/providers-change event (declared, emitted on every provider
registration/disposal, rollback-yield ordered before the emit solely so a
throwing change listener unwinds the registration) and the aggregated
searchStatus()/fetchStatus() query with its WebCapabilityStatus union.
dsh-tool-web executes through ctx.web.search()/fetch() and routes on the
structured WebError codes selection throws at execution time; tool
registration follows product enablement, not provider availability. The
only listeners/callers were the web packages' own tests, and the
tool-web README / architecture.md prose claiming the tool 'reads only the
aggregated searchStatus()/fetchStatus()' had drifted from the call sites.
Remove the event declaration, both emits, and the rollback-before-emit
machinery (the plain ctx.effect disposer keeps HMR cleanup, matching
LlmService.registerAdapter). Remove searchStatus()/fetchStatus(),
resolveStatus(), and WebCapabilityStatus; the provider-private status()
stays as the execution-time selection input. Delete the listener-throw
rollback test, and rewrite every event/status assertion across the web
packages' tests onto caller-observable behavior: a successful
search()/fetch() or the structured WEB_PROVIDER_* codes. Regenerate the
cordis catalog; update the web/tool-web READMEs, the architecture.md web
paragraph, core-data-structures/web.md, and the type-equiv manifest; amend
the web capability seam RFC's facts to the shipped surface. This follows
the llm/adapter-change precedent: a boot-time backend-registry signal and
an availability probe distinct from executing both sit on the cut side of
its keep/cut criterion.
RFC: docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
2026-07-04 15:42:56 +08:00
const { fiber , ctx , call } = await mountTools ( )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
expect ( ctx . tools . schemas ( ) . map ( s = > s . name ) ) . toContain ( 'web_search' )
refactor(web): drop the unconsumed observation surface
WebService exposed an observation surface nothing in production observes:
the web/providers-change event (declared, emitted on every provider
registration/disposal, rollback-yield ordered before the emit solely so a
throwing change listener unwinds the registration) and the aggregated
searchStatus()/fetchStatus() query with its WebCapabilityStatus union.
dsh-tool-web executes through ctx.web.search()/fetch() and routes on the
structured WebError codes selection throws at execution time; tool
registration follows product enablement, not provider availability. The
only listeners/callers were the web packages' own tests, and the
tool-web README / architecture.md prose claiming the tool 'reads only the
aggregated searchStatus()/fetchStatus()' had drifted from the call sites.
Remove the event declaration, both emits, and the rollback-before-emit
machinery (the plain ctx.effect disposer keeps HMR cleanup, matching
LlmService.registerAdapter). Remove searchStatus()/fetchStatus(),
resolveStatus(), and WebCapabilityStatus; the provider-private status()
stays as the execution-time selection input. Delete the listener-throw
rollback test, and rewrite every event/status assertion across the web
packages' tests onto caller-observable behavior: a successful
search()/fetch() or the structured WEB_PROVIDER_* codes. Regenerate the
cordis catalog; update the web/tool-web READMEs, the architecture.md web
paragraph, core-data-structures/web.md, and the type-equiv manifest; amend
the web capability seam RFC's facts to the shipped surface. This follows
the llm/adapter-change precedent: a boot-time backend-registry signal and
an availability probe distinct from executing both sit on the cut side of
its keep/cut criterion.
RFC: docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
2026-07-04 15:42:56 +08:00
// No provider is registered: the schema stays visible and execution reports
// the structured unavailability instead.
const out = await call ( 'web_search' , { query : 'q' } )
2026-07-21 03:08:35 +08:00
expect ( out . error ? . info ? . code ) . toBe ( 'WEB_PROVIDER_UNAVAILABLE' )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
await fiber . dispose ( )
} )
it ( 'contributes prompt sections for the enabled tools' , async ( ) = > {
const { fiber , ctx } = await mountTools ( )
const prompt = await ctx . systemPrompt . assemble ( )
2026-07-05 01:54:46 +08:00
const text = prompt . sections . map ( s = > s . text ) . join ( '\n' )
2026-07-31 12:46:18 +08:00
expect ( text ) . toContain ( 'Use the web_search tool to discover current information on the web. It returns an optional answer plus a list of source URLs. Follow up with web_fetch when you need the full content of a specific result, and cite the relevant URLs as markdown links.' )
expect ( text ) . toContain ( 'Use the web_fetch tool to retrieve the content of a specific HTTP(S) URL' )
await fiber . dispose ( )
} )
it ( 'does not advertise web_fetch in search-only prompt guidance' , async ( ) = > {
const { fiber , ctx } = await mountTools ( { config : { search : true , fetch : false } } )
const prompt = await ctx . systemPrompt . assemble ( )
const text = prompt . sections . map ( s = > s . text ) . join ( '\n' )
expect ( text ) . toContain ( 'Use the returned source snippets when available' )
expect ( text ) . not . toContain ( 'web_fetch' )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
await fiber . dispose ( )
} )
} )
describe ( 'tool-web execution through the real registry' , ( ) = > {
it ( 'executes web_search and formats the result' , async ( ) = > {
const result : WebSearchResult = {
2026-07-14 04:17:38 +08:00
content : 'answer' , truncated : false ,
2026-07-21 03:08:35 +08:00
sources : [ { url : 'https://a.test' , title : 'A' , snippet : 'snip' , publishedAt : '2026-07-20' } ] ,
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
}
const { fiber , call } = await mountTools ( { webConfig : { searchProvider : 'stub-search' } , search : searchProvider ( result ) } )
const out = await call ( 'web_search' , { query : 'q' } )
expect ( out . isError ) . toBe ( false )
2026-07-21 03:08:35 +08:00
expect ( out . value ) . toEqual ( result )
expect ( out . content . map ( b = > b . type === 'text' ? b . text : '' ) . join ( '' ) ) . toContain ( '[A](https://a.test)' )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
await fiber . dispose ( )
} )
2026-07-30 17:04:09 +08:00
it ( 'projects the search sources into the tool result meta and derives its web/search view' , async ( ) = > {
const result : WebSearchResult = {
content : 'answer' , truncated : true ,
sources : [ { url : 'https://a.test' , title : 'A' , snippet : 'snip' , publishedAt : '2026-07-20' } ] ,
}
const { ctx , fiber , call } = await mountTools ( { webConfig : { searchProvider : 'stub-search' } , search : searchProvider ( result ) } )
const out = await call ( 'web_search' , { query : 'q' } )
expect ( out . meta ) . toEqual ( {
answer : 'answer' , truncated : true ,
sources : [ { url : 'https://a.test' , title : 'A' , snippet : 'snip' , publishedAt : '2026-07-20' } ] ,
} )
const view = ctx . tools . get ( 'web_search' ) ? . presentResult ? . ( { query : 'q' } , { content : out.content , isError : out.isError , . . . out . meta !== undefined ? { meta : out.meta } : { } } )
expect ( view ) . toMatchObject ( { card : 'web' , kind : 'search' , truncated : true , answer : 'answer' } )
await fiber . dispose ( )
} )
it ( 'projects the fetch summary into the tool result meta and derives its web/fetch view' , async ( ) = > {
const fetchProvider = {
id : 'stub-fetch' ,
available : ( ) = > available ,
fetch : ( request : { url : string } ) = > Promise . resolve ( {
url : request.url , statusCode : 200 , body : { kind : 'text' as const , content : 'ok' } , truncated : true ,
} ) ,
}
const { ctx , fiber , call } = await mountTools ( { webConfig : { fetchProvider : 'stub-fetch' } , fetchProvider } )
const out = await call ( 'web_fetch' , { url : 'https://a.test' } )
expect ( out . meta ) . toEqual ( { url : 'https://a.test' , statusCode : 200 , truncated : true } )
const view = ctx . tools . get ( 'web_fetch' ) ? . presentResult ? . ( { url : 'https://a.test' } , { content : out.content , isError : out.isError , . . . out . meta !== undefined ? { meta : out.meta } : { } } )
expect ( view ) . toMatchObject ( { card : 'web' , kind : 'fetch' , url : 'https://a.test' , statusCode : 200 , truncated : true } )
await fiber . dispose ( )
} )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
it ( 'surfaces a structured WebError when no provider is available' , async ( ) = > {
const { fiber , call } = await mountTools ( )
const out = await call ( 'web_search' , { query : 'q' } )
expect ( out . isError ) . toBe ( true )
2026-07-21 03:08:35 +08:00
expect ( out . error ? . info ? . code ) . toBe ( 'WEB_PROVIDER_UNAVAILABLE' )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
await fiber . dispose ( )
} )
it ( 'surfaces WEB_PROVIDER_AMBIGUOUS for multiple unconfigured providers' , async ( ) = > {
2026-07-14 04:17:38 +08:00
const { ctx , fiber , call } = await mountTools ( { search : searchProvider ( { sources : [ ] , truncated : false } ) } )
ctx . web . registerSearchProvider ( { id : 'other' , available : ( ) = > available , search : ( ) = > Promise . resolve ( { sources : [ ] , truncated : false } ) } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
const out = await call ( 'web_search' , { query : 'q' } )
expect ( out . isError ) . toBe ( true )
2026-07-21 03:08:35 +08:00
expect ( out . error ? . info ? . code ) . toBe ( 'WEB_PROVIDER_AMBIGUOUS' )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
await fiber . dispose ( )
} )
it ( 'rejects invalid arguments with a structured INVALID_ARGS error' , async ( ) = > {
2026-07-14 04:17:38 +08:00
const { fiber , call } = await mountTools ( { webConfig : { searchProvider : 'stub-search' } , search : searchProvider ( { sources : [ ] , truncated : false } ) } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
const out = await call ( 'web_search' , { query : 123 } )
expect ( out . isError ) . toBe ( true )
2026-07-21 03:08:35 +08:00
expect ( out . error ? . info ? . code ) . toBe ( 'INVALID_ARGS' )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
await fiber . dispose ( )
} )
it ( 'has no default export (namespace plugin export shape)' , ( ) = > {
expect ( 'default' in ToolWeb ) . toBe ( false )
} )
2026-07-08 10:06:07 +08:00
it ( 'executes web_fetch, forwarding the url (no timeout param) and the abort signal to the seam' , async ( ) = > {
2026-07-14 04:17:38 +08:00
const seen : { request ? : { url : string } ; signal? : AbortSignal | undefined } = { }
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
const fetchProvider = {
id : 'stub-fetch' ,
2026-07-14 04:17:38 +08:00
available : ( ) = > available ,
fetch : ( request : { url : string } , signal? : AbortSignal ) = > {
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
seen . request = request
2026-07-14 04:17:38 +08:00
seen . signal = signal
return Promise . resolve ( { url : request.url , statusCode : 200 , body : { kind : 'text' as const , content : 'ok' } , truncated : false } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
} ,
}
const { ctx , fiber } = await mountTools ( { webConfig : { fetchProvider : 'stub-fetch' } , fetchProvider } )
const controller = new AbortController ( )
2026-07-08 10:06:07 +08:00
const out = await ctx . tools . execute ( { callId : CallId ( 'fetch-1' ) , name : 'web_fetch' , arguments : { url : 'https://a.test' } , signal : controller.signal } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
expect ( out . isError ) . toBe ( false )
2026-07-21 03:08:35 +08:00
expect ( out . value ) . toEqual ( {
url : 'https://a.test' ,
statusCode : 200 ,
body : { kind : 'text' , content : 'ok' } ,
truncated : false ,
} )
2026-07-08 10:06:07 +08:00
// The model schema exposes no timeout: the tool forwards only the url; the
// tool-call budget is owned by dsh-timeout-policy over exec.signal.
expect ( seen . request ) . toEqual ( { url : 'https://a.test' } )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
expect ( seen . signal ) . toBe ( controller . signal )
await fiber . dispose ( )
} )
2026-07-19 23:38:54 +08:00
it ( 'forwards the required caller signal to web_fetch' , async ( ) = > {
2026-07-14 04:17:38 +08:00
const seen : { signal? : AbortSignal | undefined ; passedSignal? : boolean } = { }
2026-07-08 10:06:07 +08:00
const fetchProvider = {
id : 'stub-fetch' ,
2026-07-14 04:17:38 +08:00
available : ( ) = > available ,
fetch : ( request : { url : string } , signal? : AbortSignal ) = > {
seen . passedSignal = signal !== undefined
seen . signal = signal
return Promise . resolve ( { url : request.url , statusCode : 200 , body : { kind : 'text' as const , content : 'ok' } , truncated : false } )
2026-07-08 10:06:07 +08:00
} ,
}
const { ctx , fiber } = await mountTools ( { webConfig : { fetchProvider : 'stub-fetch' } , fetchProvider } )
2026-07-19 23:38:54 +08:00
const out = await ctx . tools . execute ( { signal : testToolSignal , callId : CallId ( 'fetch-2' ) , name : 'web_fetch' , arguments : { url : 'https://a.test' } } )
2026-07-08 10:06:07 +08:00
expect ( out . isError ) . toBe ( false )
2026-07-21 03:08:35 +08:00
expect ( out . value ) . toEqual ( {
url : 'https://a.test' ,
statusCode : 200 ,
body : { kind : 'text' , content : 'ok' } ,
truncated : false ,
} )
2026-07-19 23:38:54 +08:00
expect ( seen . passedSignal ) . toBe ( true )
expect ( seen . signal ) . toBe ( testToolSignal )
2026-07-08 10:06:07 +08:00
await fiber . dispose ( )
} )
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
it ( 'executes web_search, forwarding the abort signal to the seam' , async ( ) = > {
const seen : { signal? : AbortSignal | undefined } = { }
const provider : WebSearchProvider = {
id : 'stub-search' ,
2026-07-14 04:17:38 +08:00
available : ( ) = > available ,
search : ( _request , signal ) = > { seen . signal = signal ; return Promise . resolve ( { sources : [ ] , truncated : false } ) } ,
Add web capability seam: ctx.web, search/fetch providers, web tools
Introduce web access as a first-class capability seam so the model-facing
web tools stay stable while backends change. dsh-web owns ctx.web as a
provider registry with registration-order-independent selection and the
WebError taxonomy; dsh-web-search-exa, dsh-web-search-perplexity, and
dsh-web-fetch-local register capabilities into it; dsh-tool-web is the sole
owner of the model-facing web_search/web_fetch schemas, prompt sections, and
HTML-to-markdown presentation. Search and fetch are deliberately one seam.
Providers ship as namespace plugins that register into ctx.web (like an
LlmAdapter into ctx.llm), not key-owning services, since multiple search
providers cannot each own the key. Tool registration follows product
enablement, not backend availability, so load order/credentials never enter
the model contract; the seam resolves the provider at execution time and
surfaces a structured WebError otherwise.
Moves the RFC to implemented/ amended to match what shipped. Example/app
configs are intentionally not wired yet (RFC migration step 6).
2026-06-25 15:04:12 +08:00
}
const { ctx , fiber } = await mountTools ( { webConfig : { searchProvider : 'stub-search' } , search : provider } )
const controller = new AbortController ( )
await ctx . tools . execute ( { callId : CallId ( 'search-1' ) , name : 'web_search' , arguments : { query : 'q' } , signal : controller.signal } )
expect ( seen . signal ) . toBe ( controller . signal )
await fiber . dispose ( )
} )
} )
Expose audited hardcoded tunables as plugin config
The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.
- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
read-render already documented that the consumer applies the caps, so
they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
schemastery default). Also fixes the stale GREP_LIMIT references in
search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
RunInternals.graceMs test seam is gone: graceMs is now a required
SpawnSpec field filled from config, so tests exercise the real
config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
rollback-journal modes serve filesystems where WAL's shared-memory
files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
hook/result stderr summary. The duplicated summarize() helpers merge
into hook-protocol's summarizeStderr(stderr, maxChars), beside the
HookResultRecord field it feeds, with the bound parameterized the
same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
fires far too late). Also corrects the BasicCompactService class doc,
which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
FsIoInternals.streamMinSize seam — the read-routing bound lives in
the consumer (tool-fs), where it is now config. This is item 1 of
the proposed prune-write-only-fs-surface RFC, annotated accordingly.
Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
2026-07-04 17:37:23 +08:00
describe ( 'searchMaxResults is plugin config' , ( ) = > {
it ( 'forwards the default cap to the seam when unconfigured' , async ( ) = > {
const seen : { maxResults? : number | undefined } = { }
const provider : WebSearchProvider = {
id : 'stub-search' ,
2026-07-14 04:17:38 +08:00
available : ( ) = > available ,
search : ( request ) = > { seen . maxResults = request . maxResults ; return Promise . resolve ( { sources : [ ] , truncated : false } ) } ,
Expose audited hardcoded tunables as plugin config
The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.
- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
read-render already documented that the consumer applies the caps, so
they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
schemastery default). Also fixes the stale GREP_LIMIT references in
search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
RunInternals.graceMs test seam is gone: graceMs is now a required
SpawnSpec field filled from config, so tests exercise the real
config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
rollback-journal modes serve filesystems where WAL's shared-memory
files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
hook/result stderr summary. The duplicated summarize() helpers merge
into hook-protocol's summarizeStderr(stderr, maxChars), beside the
HookResultRecord field it feeds, with the bound parameterized the
same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
fires far too late). Also corrects the BasicCompactService class doc,
which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
FsIoInternals.streamMinSize seam — the read-routing bound lives in
the consumer (tool-fs), where it is now config. This is item 1 of
the proposed prune-write-only-fs-surface RFC, annotated accordingly.
Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
2026-07-04 17:37:23 +08:00
}
const { fiber , call } = await mountTools ( { webConfig : { searchProvider : 'stub-search' } , search : provider } )
await call ( 'web_search' , { query : 'q' } )
expect ( seen . maxResults ) . toBe ( WEB_SEARCH_MAX_RESULTS )
await fiber . dispose ( )
} )
it ( 'forwards a configured cap to the seam, which enforces it' , async ( ) = > {
const sources = Array . from ( { length : 5 } , ( _ , i ) = > ( { url : ` https://s ${ i } .test ` } ) )
const provider : WebSearchProvider = {
id : 'stub-search' ,
2026-07-14 04:17:38 +08:00
available : ( ) = > available ,
search : ( ) = > Promise . resolve ( { sources , truncated : false } ) ,
Expose audited hardcoded tunables as plugin config
The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.
- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
read-render already documented that the consumer applies the caps, so
they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
schemastery default). Also fixes the stale GREP_LIMIT references in
search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
RunInternals.graceMs test seam is gone: graceMs is now a required
SpawnSpec field filled from config, so tests exercise the real
config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
rollback-journal modes serve filesystems where WAL's shared-memory
files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
hook/result stderr summary. The duplicated summarize() helpers merge
into hook-protocol's summarizeStderr(stderr, maxChars), beside the
HookResultRecord field it feeds, with the bound parameterized the
same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
fires far too late). Also corrects the BasicCompactService class doc,
which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
FsIoInternals.streamMinSize seam — the read-routing bound lives in
the consumer (tool-fs), where it is now config. This is item 1 of
the proposed prune-write-only-fs-surface RFC, annotated accordingly.
Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
2026-07-04 17:37:23 +08:00
}
const { fiber , call } = await mountTools ( { config : { searchMaxResults : 2 } , webConfig : { searchProvider : 'stub-search' } , search : provider } )
const out = await call ( 'web_search' , { query : 'q' } )
expect ( out . isError ) . toBe ( false )
2026-07-21 03:08:35 +08:00
const body = out . content . map ( b = > b . type === 'text' ? b . text : '' ) . join ( '' )
Expose audited hardcoded tunables as plugin config
The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.
- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
read-render already documented that the consumer applies the caps, so
they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
schemastery default). Also fixes the stale GREP_LIMIT references in
search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
RunInternals.graceMs test seam is gone: graceMs is now a required
SpawnSpec field filled from config, so tests exercise the real
config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
rollback-journal modes serve filesystems where WAL's shared-memory
files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
hook/result stderr summary. The duplicated summarize() helpers merge
into hook-protocol's summarizeStderr(stderr, maxChars), beside the
HookResultRecord field it feeds, with the bound parameterized the
same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
fires far too late). Also corrects the BasicCompactService class doc,
which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
FsIoInternals.streamMinSize seam — the read-routing bound lives in
the consumer (tool-fs), where it is now config. This is item 1 of
the proposed prune-write-only-fs-surface RFC, annotated accordingly.
Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
2026-07-04 17:37:23 +08:00
expect ( body ) . toContain ( 'https://s1.test' )
expect ( body ) . not . toContain ( 'https://s2.test' )
expect ( body ) . toContain ( 'Showing the first 2 sources.' )
await fiber . dispose ( )
} )
it . each ( [
[ 'zero' , 0 ] ,
[ 'negative' , - 3 ] ,
[ 'fractional' , 1.5 ] ,
] ) ( 'rejects a %s searchMaxResults at load' , async ( _label , value ) = > {
const ctx = new Context ( )
await ctx . plugin ( SystemPrompt )
await ctx . plugin ( ToolRegistry )
await ctx . plugin ( WebService , { } )
await expect ( ctx . plugin ( ToolWeb , { searchMaxResults : value } ) )
. rejects . toThrow ( /tool-web: searchMaxResults must be a positive integer/ )
} )
} )
2026-07-08 14:40:14 +08:00
describe ( 'tool-call timeout budget is plugin config' , ( ) = > {
it ( 'attaches the default 30s budget to web_fetch and web_search' , async ( ) = > {
const { fiber , ctx } = await mountTools ( )
expect ( ctx . tools . get ( 'web_fetch' ) ? . timeoutMs ) . toBe ( 30 _000 )
expect ( ctx . tools . get ( 'web_search' ) ? . timeoutMs ) . toBe ( 30 _000 )
await fiber . dispose ( )
} )
it ( 'honors per-tool timeout overrides from config' , async ( ) = > {
const { fiber , ctx } = await mountTools ( { config : { fetchTimeoutMs : 60_000 , searchTimeoutMs : 10_000 } } )
expect ( ctx . tools . get ( 'web_fetch' ) ? . timeoutMs ) . toBe ( 60 _000 )
expect ( ctx . tools . get ( 'web_search' ) ? . timeoutMs ) . toBe ( 10 _000 )
await fiber . dispose ( )
} )
it . each ( [
[ 'fetchTimeoutMs' , { fetchTimeoutMs : 0 } ] ,
[ 'searchTimeoutMs' , { searchTimeoutMs : - 5 } ] ,
] ) ( 'rejects a non-positive-integer %s at load' , async ( key , config ) = > {
const ctx = new Context ( )
await ctx . plugin ( SystemPrompt )
await ctx . plugin ( ToolRegistry )
await ctx . plugin ( WebService , { } )
await expect ( ctx . plugin ( ToolWeb , config ) )
. rejects . toThrow ( new RegExp ( ` tool-web: ${ key } must be a positive integer ` ) )
} )
} )
2026-07-27 16:40:02 +08:00
describe ( 'fetchMaxOutputChars is plugin config' , ( ) = > {
it ( 'bounds the rendered output of the registered web_fetch tool' , async ( ) = > {
const fetchProvider = {
id : 'stub-fetch' ,
available : ( ) = > available ,
fetch : ( request : { url : string } ) = > Promise . resolve ( {
url : request.url ,
statusCode : 200 ,
body : { kind : 'html' as const , content : ` <p> ${ '_' . repeat ( 1 _000 ) } </p> ` } ,
truncated : false ,
} ) ,
}
const { fiber , call } = await mountTools ( {
config : { fetchMaxOutputChars : 100 } ,
webConfig : { fetchProvider : 'stub-fetch' } ,
fetchProvider ,
} )
const out = await call ( 'web_fetch' , { url : 'https://a.test' } )
expect ( out . content . map ( block = > block . type === 'text' ? block . text : '' ) . join ( '' ) ) . toHaveLength ( 100 )
await fiber . dispose ( )
} )
it . each ( [ 0 , - 1 , 1.5 ] ) ( 'rejects an invalid fetchMaxOutputChars value %s at load' , async ( value ) = > {
const ctx = new Context ( )
await ctx . plugin ( SystemPrompt )
await ctx . plugin ( ToolRegistry )
await ctx . plugin ( WebService , { } )
await expect ( ctx . plugin ( ToolWeb , { fetchMaxOutputChars : value } ) )
. rejects . toThrow ( /tool-web: fetchMaxOutputChars must be a positive integer/ )
} )
} )