From 3287b8fc0f964cbcffb2012f45416f7f69dbeef3 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Fri, 3 Jul 2026 14:49:56 +0800 Subject: [PATCH] refactor(agent-core-v2): move web to app, split WebSearch into auth - Move the `web` domain from src/agent/web to src/app/web; it now owns only the auth-independent FetchURL (tool, UrlFetcher contract, and the local and Moonshot fetch providers). - Move WebSearch (tool, WebSearchProvider contract, MoonshotWebSearchProvider) into the KimiOAuth auth domain under src/app/auth/webSearch, since it needs an authenticated backend. - Replace the Agent-scope IAgentWebService registrar with registerTool contributions fed by new App-scope seams (IWebFetchService, IWebSearchProviderService), because the tool registry is Agent-scoped. - Add documented auth>tool / auth>toolRegistry layer exceptions and update the DI scope-domain graph. --- .../agent-core-v2/docs/di-scope-domains.puml | 5 +- .../scripts/check-domain-layers.mjs | 28 +++++----- packages/agent-core-v2/src/agent/web/index.ts | 8 --- packages/agent-core-v2/src/agent/web/web.ts | 30 ----------- .../agent-core-v2/src/agent/web/webService.ts | 53 ------------------- packages/agent-core-v2/src/app/auth/index.ts | 15 ++++-- .../providers/moonshot-web-search.ts | 2 +- .../auth/webSearch}/tools/web-search.md | 0 .../auth/webSearch}/tools/web-search.ts | 26 +++++++-- .../src/app/auth/webSearch/webSearch.ts | 31 +++++++++++ .../app/auth/webSearch/webSearchService.ts | 37 +++++++++++++ packages/agent-core-v2/src/app/web/index.ts | 14 +++++ .../web/providers/local-fetch-url.ts | 2 +- .../web/providers/moonshot-fetch-url.ts | 2 +- .../src/{agent => app}/web/tools/fetch-url.md | 0 .../src/{agent => app}/web/tools/fetch-url.ts | 17 ++++-- packages/agent-core-v2/src/app/web/web.ts | 30 +++++++++++ .../agent-core-v2/src/app/web/webService.ts | 37 +++++++++++++ packages/agent-core-v2/src/index.ts | 1 + .../test/{agent => app}/web/fetch-url.test.ts | 6 +-- 20 files changed, 220 insertions(+), 124 deletions(-) delete mode 100644 packages/agent-core-v2/src/agent/web/index.ts delete mode 100644 packages/agent-core-v2/src/agent/web/web.ts delete mode 100644 packages/agent-core-v2/src/agent/web/webService.ts rename packages/agent-core-v2/src/{agent/web => app/auth/webSearch}/providers/moonshot-web-search.ts (97%) rename packages/agent-core-v2/src/{agent/web => app/auth/webSearch}/tools/web-search.md (100%) rename packages/agent-core-v2/src/{agent/web => app/auth/webSearch}/tools/web-search.ts (84%) create mode 100644 packages/agent-core-v2/src/app/auth/webSearch/webSearch.ts create mode 100644 packages/agent-core-v2/src/app/auth/webSearch/webSearchService.ts create mode 100644 packages/agent-core-v2/src/app/web/index.ts rename packages/agent-core-v2/src/{agent => app}/web/providers/local-fetch-url.ts (99%) rename packages/agent-core-v2/src/{agent => app}/web/providers/moonshot-fetch-url.ts (99%) rename packages/agent-core-v2/src/{agent => app}/web/tools/fetch-url.md (100%) rename packages/agent-core-v2/src/{agent => app}/web/tools/fetch-url.ts (87%) create mode 100644 packages/agent-core-v2/src/app/web/web.ts create mode 100644 packages/agent-core-v2/src/app/web/webService.ts rename packages/agent-core-v2/test/{agent => app}/web/fetch-url.test.ts (94%) diff --git a/packages/agent-core-v2/docs/di-scope-domains.puml b/packages/agent-core-v2/docs/di-scope-domains.puml index 10609fa29..c4b44b23b 100644 --- a/packages/agent-core-v2/docs/di-scope-domains.puml +++ b/packages/agent-core-v2/docs/di-scope-domains.puml @@ -32,7 +32,8 @@ package "App scope (process-wide)" #EAF3FB { rectangle "workspaceRegistry\nApp\n IWorkspaceRegistry" as workspaceRegistry #D6EAF8 rectangle "hostFolderBrowser\nApp\n IHostFolderBrowser" as hostFolderBrowser #D6EAF8 rectangle "hostEnvironment\nApp\n IHostEnvironment" as hostEnvironment #D6EAF8 - rectangle "auth\nApp\n IOAuthService\n IAuthSummaryService" as auth #D6EAF8 + rectangle "auth\nApp\n IOAuthService\n IAuthSummaryService\n IWebSearchProviderService" as auth #D6EAF8 + rectangle "web\nApp\n IWebFetchService" as web #D6EAF8 rectangle "provider\nApp\n IProviderService" as provider #D6EAF8 rectangle "flag\nApp\n IFlagService\n IFlagRegistry" as flag #D6EAF8 rectangle "config\nApp\n IConfigRegistry\n IConfigService" as config #D6EAF8 @@ -337,6 +338,8 @@ shellTools --> process #34495E shellTools --> hostEnvironment shellTools --> execContext #34495E shellTools --> background #34495E +web --> toolRegistry #34495E +auth --> toolRegistry #34495E ' ---- event-driven (dashed) ---- flag ..> config #16A085 : onDidChangeConfiguration diff --git a/packages/agent-core-v2/scripts/check-domain-layers.mjs b/packages/agent-core-v2/scripts/check-domain-layers.mjs index c8623fb12..2a6644c08 100644 --- a/packages/agent-core-v2/scripts/check-domain-layers.mjs +++ b/packages/agent-core-v2/scripts/check-domain-layers.mjs @@ -56,16 +56,13 @@ const DOMAIN_LAYER = new Map([ ['bootstrap', 1], // `environment` is the App-scope resolved startup snapshot: host facts, the // app path layout, and the env bag; low-level substrate that any domain may - // read for paths/facts, so it sits in L1 beside `bootstrap` and - // `hostEnvironment`. + // read for paths/facts, so it sits in L1 beside `bootstrap` and the + // `os/interface` host facts. ['environment', 1], // `event` is the App-scope pub/sub bus, a thin wrapper over the // `_base/event` `Emitter`. Foundational substrate that any domain may // publish/subscribe through, so it sits in L1 (not the edge boundary). ['event', 1], - // `hostEnvironment` is the App-scope OS/shell/path/home probe snapshot; - // low-level substrate that any Session/Agent domain may read synchronously. - ['hostEnvironment', 1], // `execContext` is the Session-scope seeded immutable value (`cwd`, // `envLayers`); same layer as the other low-level bridges. ['execContext', 1], @@ -73,15 +70,15 @@ const DOMAIN_LAYER = new Map([ // (`sessionId`/`workspaceId`/`sessionDir`/`metaScope`); like `execContext` // it is a pure seed with no IO, so it sits in L1. ['sessionContext', 1], - ['hostFs', 1], // `git` is the App-scope `IGitService` that runs `git status` / `git diff` - // against a local repo via `node:child_process`; it depends only on `_base` + // against a local repo. Process spawning goes through `os/interface` + // (`IHostProcessService`) and the lone path-existence probe through + // `IHostFileSystem`; besides those host bridges it depends only on `_base` // and the `errors` facade, so it sits in L1 beside the other host bridges. ['git', 1], ['workspaceContext', 1], ['protocol', 1], ['hooks', 1], - ['storage', 1], // `task` is the managed-concurrent-execution primitive (run + defer). // Depends only on `_base`; sits in L1 beside the other program-control // layer substrates. @@ -263,10 +260,17 @@ const ALLOWED_EXCEPTIONS = new Set([ 'bootstrap>globalSkillCatalog', // bootstrap is the composition root — it wires backends by design. 'bootstrap>persistence/backends', + // `auth` (KimiOAuth, L2) owns the OAuth-backed `WebSearch` tool and registers + // it through the tool contribution API, so it reaches up to the L3 tool + // contract and registry. Surfaced for review: the tool needs an authenticated + // backend, which is why it lives beside the OAuth toolkit rather than in the + // auth-independent `web` domain. + 'auth>tool', + 'auth>toolRegistry', // path-access (base tool policy) needs the `IHostEnvironment` type to stay // host-aware (path class, home dir). Structural type dependency only — // path-access does not construct or resolve the service. - '_base>hostEnvironment', + '_base>os/interface', 'permissionGate>approval', 'userTool>interaction', 'permissionPolicy>plan', @@ -310,13 +314,7 @@ const ALLOWED_EXCEPTIONS = new Set([ 'wireRecord>contextMemory', 'wireRecord>loop', 'wireRecord>tool', - // Compatibility barrels: old domain barrels (L1-L2) re-export from the new - // persistence/os backends directories. These are pure re-export shims and - // will be removed once all consumers migrate to canonical paths. - 'hostEnvironment>os/backends', - 'hostFs>os/backends', 'hostFolderBrowser>os/backends', - 'storage>persistence/backends', 'filestore>persistence/backends', 'process>os/backends', 'terminal>os/backends', diff --git a/packages/agent-core-v2/src/agent/web/index.ts b/packages/agent-core-v2/src/agent/web/index.ts deleted file mode 100644 index 6678aad5e..000000000 --- a/packages/agent-core-v2/src/agent/web/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * `web` domain barrel — re-exports the web contract (`web`) and its scoped - * service (`webService`). Importing this barrel registers the `IAgentWebService` - * binding into the scope registry. - */ - -export * from './web'; -export * from './webService'; diff --git a/packages/agent-core-v2/src/agent/web/web.ts b/packages/agent-core-v2/src/agent/web/web.ts deleted file mode 100644 index 40e1f5960..000000000 --- a/packages/agent-core-v2/src/agent/web/web.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * `web` domain (L4) — web tool registration contract and provider options. - * - * `IAgentWebService` is a marker: its implementation registers the built-in - * `FetchURL` tool (always, falling back to `LocalFetchURLProvider`) and the - * `WebSearch` tool (only when a `WebSearchProvider` is supplied) into the - * agent `IAgentToolRegistryService` on construction. Bound at Agent scope. - * - * The actual fetch/search backends are host-injected through - * `WebServiceOptions` so this domain stays independent of config and OAuth. - */ - -import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; - -import type { UrlFetcher } from '#/agent/web/tools/fetch-url'; -import type { WebSearchProvider } from '#/agent/web/tools/web-search'; - -export interface WebServiceOptions { - /** URL fetch backend. Defaults to the built-in `LocalFetchURLProvider`. */ - readonly urlFetcher?: UrlFetcher; - /** Web search backend. When omitted, `WebSearch` is not registered. */ - readonly webSearcher?: WebSearchProvider; -} - -export interface IAgentWebService { - readonly _serviceBrand: undefined; -} - -export const IAgentWebService: ServiceIdentifier = - createDecorator('agentWebService'); diff --git a/packages/agent-core-v2/src/agent/web/webService.ts b/packages/agent-core-v2/src/agent/web/webService.ts deleted file mode 100644 index 1f404531b..000000000 --- a/packages/agent-core-v2/src/agent/web/webService.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * `web` domain (L4) — `IAgentWebService` implementation. - * - * Eager Agent-scope registration service for the built-in web tools: `FetchURL` - * is always registered (using the host-injected `UrlFetcher` or the built-in - * `LocalFetchURLProvider` fallback); `WebSearch` is registered only when a - * `WebSearchProvider` is supplied via options. Each tool is created via - * `IInstantiationService.createInstance` (the provider is passed as a leading - * static argument) and registered into the agent `IAgentToolRegistryService`. - * Eager so the tools are registered when the Agent scope is created. - */ - -import { InstantiationType } from '#/_base/di/extensions'; -import { IInstantiationService } from '#/_base/di/instantiation'; -import { Disposable } from '#/_base/di/lifecycle'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { IAgentToolRegistryService } from '#/agent/toolRegistry'; - -import { LocalFetchURLProvider } from '#/agent/web/providers/local-fetch-url'; -import { FetchURLTool } from '#/agent/web/tools/fetch-url'; -import { WebSearchTool } from '#/agent/web/tools/web-search'; -import { IAgentWebService, type WebServiceOptions } from './web'; - -export class AgentWebService extends Disposable implements IAgentWebService { - declare readonly _serviceBrand: undefined; - - constructor( - private readonly options: WebServiceOptions = {}, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAgentToolRegistryService toolRegistry: IAgentToolRegistryService, - ) { - super(); - const fetcher = options.urlFetcher ?? new LocalFetchURLProvider(); - this._register( - toolRegistry.register(instantiationService.createInstance(FetchURLTool, fetcher)), - ); - if (options.webSearcher !== undefined) { - this._register( - toolRegistry.register( - instantiationService.createInstance(WebSearchTool, options.webSearcher), - ), - ); - } - } -} - -registerScopedService( - LifecycleScope.Agent, - IAgentWebService, - AgentWebService, - InstantiationType.Eager, - 'web', -); diff --git a/packages/agent-core-v2/src/app/auth/index.ts b/packages/agent-core-v2/src/app/auth/index.ts index 7e0989751..e5f317f9a 100644 --- a/packages/agent-core-v2/src/app/auth/index.ts +++ b/packages/agent-core-v2/src/app/auth/index.ts @@ -1,8 +1,17 @@ /** - * `auth` domain barrel — re-exports the auth contract (`auth`) and its scoped - * services (`authService`). Importing this barrel registers the `IOAuthService` - * and `IAuthSummaryService` bindings into the scope registry. + * `auth` domain barrel — re-exports the auth contract (`auth`), its scoped + * services (`authService`), the OAuth-backed web-search seam (`webSearch`), + * and a side-effect import of the `WebSearch` tool so its `registerTool(...)` + * call runs at module load. Importing this barrel registers the + * `IOAuthService`, `IAuthSummaryService`, and `IWebSearchProviderService` + * bindings into the scope registry and conditionally adds `WebSearch` to the + * tool contribution list. */ +import './webSearch/tools/web-search'; + export * from './auth'; export * from './authService'; +export * from './webSearch/webSearch'; +export * from './webSearch/webSearchService'; +export * from './webSearch/providers/moonshot-web-search'; diff --git a/packages/agent-core-v2/src/agent/web/providers/moonshot-web-search.ts b/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts similarity index 97% rename from packages/agent-core-v2/src/agent/web/providers/moonshot-web-search.ts rename to packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts index b062eaf0e..8b76a77f2 100644 --- a/packages/agent-core-v2/src/agent/web/providers/moonshot-web-search.ts +++ b/packages/agent-core-v2/src/app/auth/webSearch/providers/moonshot-web-search.ts @@ -1,4 +1,4 @@ -import type { WebSearchProvider, WebSearchResult } from '#/agent/web/tools/web-search'; +import type { WebSearchProvider, WebSearchResult } from '../tools/web-search'; export interface BearerTokenProvider { getAccessToken(options?: { readonly force?: boolean | undefined }): Promise; diff --git a/packages/agent-core-v2/src/agent/web/tools/web-search.md b/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.md similarity index 100% rename from packages/agent-core-v2/src/agent/web/tools/web-search.md rename to packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.md diff --git a/packages/agent-core-v2/src/agent/web/tools/web-search.ts b/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts similarity index 84% rename from packages/agent-core-v2/src/agent/web/tools/web-search.ts rename to packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts index 893ef4980..90846cda5 100644 --- a/packages/agent-core-v2/src/agent/web/tools/web-search.ts +++ b/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts @@ -1,9 +1,14 @@ /** - * WebSearchTool — host-injected web search. + * `auth` domain (cross-cutting) — `WebSearch` builtin tool and its + * `WebSearchProvider` contract. * - * agent-core-v2 defines the interface; the host provides the real search - * implementation via `WebSearchProvider`. If no provider is supplied, - * the tool is not registered (there is no local search backend). + * Defines the `WebSearch` tool and the host-injected `WebSearchProvider` + * interface (plus `WebSearchResult`). Web search needs an authenticated + * Moonshot backend, so the tool lives in the KimiOAuth `auth` domain: it reads + * its provider from the App-scope `IWebSearchProviderService` at + * registry-construction time and self-registers via `registerTool(...)` at + * module load, but only when a provider is configured (there is no local + * search backend). */ import { z } from 'zod'; @@ -18,7 +23,9 @@ import type { } from '#/agent/tool'; import { ToolAccesses } from '#/agent/tool'; import { ToolResultBuilder } from '#/agent/tool/result-builder'; +import { registerTool } from '#/agent/toolRegistry'; +import { IWebSearchProviderService } from '../webSearch'; import DESCRIPTION from './web-search.md?raw'; // ── Provider interface (host-injected) ─────────────────────────────── @@ -172,3 +179,14 @@ function classifySearchError(error: unknown): string { } return `Search failed: ${message}`; } + +registerTool(WebSearchTool, { + when: (accessor) => accessor.get(IWebSearchProviderService).getWebSearchProvider() !== undefined, + staticArgs: (accessor) => { + const provider = accessor.get(IWebSearchProviderService).getWebSearchProvider(); + if (provider === undefined) { + throw new Error('WebSearchProviderService returned no provider during tool registration.'); + } + return [provider]; + }, +}); diff --git a/packages/agent-core-v2/src/app/auth/webSearch/webSearch.ts b/packages/agent-core-v2/src/app/auth/webSearch/webSearch.ts new file mode 100644 index 000000000..8f78cde16 --- /dev/null +++ b/packages/agent-core-v2/src/app/auth/webSearch/webSearch.ts @@ -0,0 +1,31 @@ +/** + * `auth` domain (cross-cutting) — OAuth-backed web search seam. + * + * Owns the host-injection seam for the `WebSearch` backend. `WebSearch` needs + * an authenticated Moonshot search provider, so it lives here beside the OAuth + * toolkit rather than in the auth-independent `web` domain. + * `IWebSearchProviderService` yields the configured `WebSearchProvider` (or + * `undefined` when search is not configured, in which case the `WebSearch` tool + * is not registered). The host builds a `MoonshotWebSearchProvider` from the + * OAuth token (resolved through `IOAuthService`) and binds it here. Bound at + * App scope. + */ + +import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; + +import type { WebSearchProvider } from './tools/web-search'; + +export type { WebSearchProvider, WebSearchResult } from './tools/web-search'; + +export interface WebSearchProviderOptions { + /** Search backend. When omitted, `WebSearch` is not registered. */ + readonly provider?: WebSearchProvider; +} + +export interface IWebSearchProviderService { + readonly _serviceBrand: undefined; + getWebSearchProvider(): WebSearchProvider | undefined; +} + +export const IWebSearchProviderService: ServiceIdentifier = + createDecorator('webSearchProviderService'); diff --git a/packages/agent-core-v2/src/app/auth/webSearch/webSearchService.ts b/packages/agent-core-v2/src/app/auth/webSearch/webSearchService.ts new file mode 100644 index 000000000..a9424a2d9 --- /dev/null +++ b/packages/agent-core-v2/src/app/auth/webSearch/webSearchService.ts @@ -0,0 +1,37 @@ +/** + * `auth` domain (cross-cutting) — `IWebSearchProviderService` implementation. + * + * Holds the host-injected `WebSearchProvider` (a `MoonshotWebSearchProvider` + * the host builds from the OAuth token) and exposes it to the `WebSearch` tool + * through `IWebSearchProviderService`. Returns `undefined` when no provider is + * bound so the `WebSearch` tool is skipped. Owns no tool registration — the + * `WebSearch` tool self-registers via `registerTool(...)` and reads this + * service from the Agent-scope accessor. Bound at App scope. + */ + +import { InstantiationType } from '#/_base/di/extensions'; +import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; + +import type { WebSearchProvider } from './tools/web-search'; +import { IWebSearchProviderService, type WebSearchProviderOptions } from './webSearch'; + +export class WebSearchProviderService implements IWebSearchProviderService { + declare readonly _serviceBrand: undefined; + private readonly provider: WebSearchProvider | undefined; + + constructor(options: WebSearchProviderOptions = {}) { + this.provider = options.provider; + } + + getWebSearchProvider(): WebSearchProvider | undefined { + return this.provider; + } +} + +registerScopedService( + LifecycleScope.App, + IWebSearchProviderService, + WebSearchProviderService, + InstantiationType.Delayed, + 'auth', +); diff --git a/packages/agent-core-v2/src/app/web/index.ts b/packages/agent-core-v2/src/app/web/index.ts new file mode 100644 index 000000000..e5b5d1856 --- /dev/null +++ b/packages/agent-core-v2/src/app/web/index.ts @@ -0,0 +1,14 @@ +/** + * `web` domain barrel — re-exports the web contract (`web`), its scoped + * service (`webService`), the fetch providers, and a side-effect import of the + * `FetchURL` tool so its `registerTool(...)` call runs at module load. + * Importing this barrel registers the `IWebFetchService` binding into the scope + * registry and adds `FetchURL` to the tool contribution list. + */ + +import './tools/fetch-url'; + +export * from './web'; +export * from './webService'; +export * from './providers/local-fetch-url'; +export * from './providers/moonshot-fetch-url'; diff --git a/packages/agent-core-v2/src/agent/web/providers/local-fetch-url.ts b/packages/agent-core-v2/src/app/web/providers/local-fetch-url.ts similarity index 99% rename from packages/agent-core-v2/src/agent/web/providers/local-fetch-url.ts rename to packages/agent-core-v2/src/app/web/providers/local-fetch-url.ts index dcec75de8..a2cb3cefe 100644 --- a/packages/agent-core-v2/src/agent/web/providers/local-fetch-url.ts +++ b/packages/agent-core-v2/src/app/web/providers/local-fetch-url.ts @@ -1,7 +1,7 @@ import { Readability } from '@mozilla/readability'; import { parseHTML as rawParseHTML } from 'linkedom'; -import { HttpFetchError, type UrlFetcher, type UrlFetchResult } from '#/agent/web/tools/fetch-url'; +import { HttpFetchError, type UrlFetcher, type UrlFetchResult } from '../tools/fetch-url'; // Readability's .d.ts references the global `Document` type, but this // package compiles with `lib: ES2023` (no DOM). Extracting the diff --git a/packages/agent-core-v2/src/agent/web/providers/moonshot-fetch-url.ts b/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts similarity index 99% rename from packages/agent-core-v2/src/agent/web/providers/moonshot-fetch-url.ts rename to packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts index b738ba37e..340ee41c3 100644 --- a/packages/agent-core-v2/src/agent/web/providers/moonshot-fetch-url.ts +++ b/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts @@ -1,4 +1,4 @@ -import { HttpFetchError, type UrlFetcher, type UrlFetchResult } from '#/agent/web/tools/fetch-url'; +import { HttpFetchError, type UrlFetcher, type UrlFetchResult } from '../tools/fetch-url'; export interface BearerTokenProvider { getAccessToken(options?: { readonly force?: boolean | undefined }): Promise; diff --git a/packages/agent-core-v2/src/agent/web/tools/fetch-url.md b/packages/agent-core-v2/src/app/web/tools/fetch-url.md similarity index 100% rename from packages/agent-core-v2/src/agent/web/tools/fetch-url.md rename to packages/agent-core-v2/src/app/web/tools/fetch-url.md diff --git a/packages/agent-core-v2/src/agent/web/tools/fetch-url.ts b/packages/agent-core-v2/src/app/web/tools/fetch-url.ts similarity index 87% rename from packages/agent-core-v2/src/agent/web/tools/fetch-url.ts rename to packages/agent-core-v2/src/app/web/tools/fetch-url.ts index 50775edac..9fc91ed1b 100644 --- a/packages/agent-core-v2/src/agent/web/tools/fetch-url.ts +++ b/packages/agent-core-v2/src/app/web/tools/fetch-url.ts @@ -1,9 +1,12 @@ /** - * FetchURLTool — host-injected URL fetcher. + * `web` domain (L4) — `FetchURL` builtin tool and its `UrlFetcher` contract. * - * agent-core-v2 defines the interface; the host provides the real fetch - * implementation via `UrlFetcher`. If no fetcher is supplied, the tool - * falls back to the built-in `LocalFetchURLProvider`. + * Defines the `FetchURL` tool and the host-injected `UrlFetcher` interface + * (plus `UrlFetchResult` / `HttpFetchError`). The tool reads its fetcher from + * the App-scope `IWebFetchService` at registry-construction time and + * self-registers via `registerTool(...)` at module load; the default service + * falls back to the built-in `LocalFetchURLProvider`, so `FetchURL` is always + * available without OAuth. */ import { z } from 'zod'; @@ -18,7 +21,9 @@ import type { } from '#/agent/tool'; import { ToolAccesses } from '#/agent/tool'; import { ToolResultBuilder } from '#/agent/tool/result-builder'; +import { registerTool } from '#/agent/toolRegistry'; +import { IWebFetchService } from '../web'; import DESCRIPTION from './fetch-url.md?raw'; // ── Provider interface (host-injected) ─────────────────────────────── @@ -135,3 +140,7 @@ export class FetchURLTool implements BuiltinTool { } } } + +registerTool(FetchURLTool, { + staticArgs: (accessor) => [accessor.get(IWebFetchService).getUrlFetcher()], +}); diff --git a/packages/agent-core-v2/src/app/web/web.ts b/packages/agent-core-v2/src/app/web/web.ts new file mode 100644 index 000000000..a3cee1b4b --- /dev/null +++ b/packages/agent-core-v2/src/app/web/web.ts @@ -0,0 +1,30 @@ +/** + * `web` domain (L4) — auth-independent URL fetching. + * + * Owns the built-in `FetchURL` tool and the host-injection seam for its fetch + * backend. `IWebFetchService` yields the `UrlFetcher` the `FetchURL` tool uses; + * the default implementation falls back to the built-in `LocalFetchURLProvider`, + * so `FetchURL` works without any OAuth configuration. The `MoonshotFetchURLProvider` + * is exported as a building block for hosts that want to route fetches through + * the Moonshot fetch service. Bound at App scope. + */ + +import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; + +import type { UrlFetcher } from './tools/fetch-url'; + +export type { UrlFetcher, UrlFetchKind, UrlFetchResult } from './tools/fetch-url'; +export { HttpFetchError } from './tools/fetch-url'; + +export interface WebFetchServiceOptions { + /** URL fetch backend. Defaults to the built-in `LocalFetchURLProvider`. */ + readonly urlFetcher?: UrlFetcher; +} + +export interface IWebFetchService { + readonly _serviceBrand: undefined; + getUrlFetcher(): UrlFetcher; +} + +export const IWebFetchService: ServiceIdentifier = + createDecorator('webFetchService'); diff --git a/packages/agent-core-v2/src/app/web/webService.ts b/packages/agent-core-v2/src/app/web/webService.ts new file mode 100644 index 000000000..b3512b470 --- /dev/null +++ b/packages/agent-core-v2/src/app/web/webService.ts @@ -0,0 +1,37 @@ +/** + * `web` domain (L4) — `IWebFetchService` implementation. + * + * Holds the host-injected `UrlFetcher` (defaulting to the built-in + * `LocalFetchURLProvider`) and hands it to the `FetchURL` tool through + * `IWebFetchService`. Owns no tool registration of its own — the `FetchURL` + * tool self-registers via `registerTool(...)` and reads this service from the + * Agent-scope accessor. Bound at App scope. + */ + +import { InstantiationType } from '#/_base/di/extensions'; +import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; + +import { LocalFetchURLProvider } from './providers/local-fetch-url'; +import type { UrlFetcher } from './tools/fetch-url'; +import { IWebFetchService, type WebFetchServiceOptions } from './web'; + +export class WebFetchService implements IWebFetchService { + declare readonly _serviceBrand: undefined; + private readonly urlFetcher: UrlFetcher; + + constructor(options: WebFetchServiceOptions = {}) { + this.urlFetcher = options.urlFetcher ?? new LocalFetchURLProvider(); + } + + getUrlFetcher(): UrlFetcher { + return this.urlFetcher; + } +} + +registerScopedService( + LifecycleScope.App, + IWebFetchService, + WebFetchService, + InstantiationType.Delayed, + 'web', +); diff --git a/packages/agent-core-v2/src/index.ts b/packages/agent-core-v2/src/index.ts index 43d3224d1..902024095 100644 --- a/packages/agent-core-v2/src/index.ts +++ b/packages/agent-core-v2/src/index.ts @@ -74,6 +74,7 @@ export * from '#/persistence/backends/memory'; export * from '#/app/auth'; export * from '#/app/authLegacy'; export * from '#/app/file'; +export * from '#/app/web'; // Ported agent services. These keep the current service boundaries during the migration. export * from '#/agent/blob'; diff --git a/packages/agent-core-v2/test/agent/web/fetch-url.test.ts b/packages/agent-core-v2/test/app/web/fetch-url.test.ts similarity index 94% rename from packages/agent-core-v2/test/agent/web/fetch-url.test.ts rename to packages/agent-core-v2/test/app/web/fetch-url.test.ts index 613a07959..03aebeb90 100644 --- a/packages/agent-core-v2/test/agent/web/fetch-url.test.ts +++ b/packages/agent-core-v2/test/app/web/fetch-url.test.ts @@ -10,8 +10,8 @@ import { describe, expect, it, vi } from 'vitest'; import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '#/agent/tool'; -import { LocalFetchURLProvider } from '#/agent/web/providers/local-fetch-url'; -import { FetchURLTool, type UrlFetcher, type UrlFetchResult } from '#/agent/web/tools/fetch-url'; +import { LocalFetchURLProvider } from '#/app/web/providers/local-fetch-url'; +import { FetchURLTool, type UrlFetcher, type UrlFetchResult } from '#/app/web/tools/fetch-url'; function isPromiseLike(value: ToolExecution | Promise): value is Promise { return typeof (value as Promise).then === 'function'; @@ -25,7 +25,7 @@ async function execute( const resolved = tool.resolveExecution({ url }); const execution = isPromiseLike(resolved) ? await resolved : resolved; if (execution.isError === true) return execution; - const ctx: ExecutableToolContext = { turnId: '0', toolCallId: 'call_fetch', signal }; + const ctx: ExecutableToolContext = { turnId: 0, toolCallId: 'call_fetch', signal }; return execution.execute(ctx); }