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);
}