dsh-web-search-enhanced 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { WebError } from "@deepseek-ai/dsh-web";
2
2
  import z from "@deepseek-ai/schemastery";
3
+ import { createTypertRemoteInfo } from "@deepseek-ai/dsh-typert-protocol/remote-info";
3
4
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
4
5
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
5
6
  import { Remote, RemoteError, TypertRemoteService, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
6
- import { createTypertRemoteInfo } from "@deepseek-ai/dsh-typert-protocol/remote-info";
7
7
  import { scopeOf } from "@deepseek-ai/dsh-scope";
8
8
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
9
9
  import { DEEPSEEK_DEFAULT_API_VERSION, DEEPSEEK_DEFAULT_BASE_URL, DEEPSEEK_DEFAULT_MAX_TOKENS, DEEPSEEK_DEFAULT_MAX_USES, DEEPSEEK_DEFAULT_MODEL, DeepSeekSearchProvider } from "@deepseek-ai/dsh-web-search-deepseek";
10
10
  import { DEEPSEEK_DEFAULT_COUNT, DEEPSEEK_WEB_API_BASE_URL, DeepSeekSearchApiProvider } from "@deepseek-ai/dsh-web-deepseek-api";
11
+ import { WEB_SEARCH_MAX_QUERIES, WEB_SEARCH_MAX_RESULTS, formatSearchOutput, searchMetaFromValue } from "@deepseek-ai/dsh-tool-web";
12
+ import { defineTool } from "@deepseek-ai/dsh-tools";
11
13
  //#region \0rolldown/runtime.js
12
14
  var __defProp = Object.defineProperty;
13
15
  var __exportAll = (all, no_symbols) => {
@@ -241,8 +243,8 @@ function runAdapter(io, wire, target, input, signal) {
241
243
  async function adapterCredential(io, ref, signal) {
242
244
  try {
243
245
  return await io.errors.abortable(io.credential(ref), signal);
244
- } catch (error) {
245
- if (error instanceof WebError) throw error;
246
+ } catch {
247
+ io.errors.throwIfAborted(signal);
246
248
  throw new Error("Adapter credential resolution failed");
247
249
  }
248
250
  }
@@ -14146,6 +14148,10 @@ const TEXT_MAX_LENGTH = 2e3;
14146
14148
  const BUILTIN_PREFIX = "builtin:";
14147
14149
  /** Operator-defined connection ids: `custom:` plus a short, readable name. */
14148
14150
  const CUSTOM_CONNECTION_ID = /^custom:[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/u;
14151
+ /** Catalog connection ids: `builtin:` plus a lower-case, hyphenated catalog key. */
14152
+ const BUILTIN_CONNECTION_ID = /^builtin:[a-z0-9]{1,50}(?:-[a-z0-9]{1,50}){0,5}$/u;
14153
+ /** Whether a string, such as an untrusted tool argument, has the grammar of a connection id; it may still name nothing. */
14154
+ const isConnectionId = (value) => BUILTIN_CONNECTION_ID.test(value) || CUSTOM_CONNECTION_ID.test(value);
14149
14155
  /** A DSH Credentials reference, the only form a key is ever stored in. */
14150
14156
  const CREDENTIAL_REF = /^[A-Za-z_][A-Za-z0-9_]*$/u;
14151
14157
  const builtinId = (key) => `${BUILTIN_PREFIX}${key}`;
@@ -14196,6 +14202,7 @@ const SETTINGS_FIELDS = [
14196
14202
  "version",
14197
14203
  "defaultConnection",
14198
14204
  "showSelector",
14205
+ "enhancedTools",
14199
14206
  "defaults",
14200
14207
  "connections"
14201
14208
  ];
@@ -14238,16 +14245,25 @@ function migrateDocument(document, migrations = SETTINGS_MIGRATIONS) {
14238
14245
  }
14239
14246
  return result;
14240
14247
  }
14241
- const text$2 = string$1().trim().min(1).max(TEXT_MAX_LENGTH);
14242
- const label = string$1().trim().min(1).max(200);
14243
- const credentialRef$1 = text$2.regex(CREDENTIAL_REF, "must be a credential reference such as EXA_API_KEY");
14244
- const endpoint = text$2.refine(isEndpoint, "must be an absolute http(s) URL without credentials, query or fragment");
14248
+ /**
14249
+ * Operator text shown as one line (a label in menus, error messages and the
14250
+ * Agent's provider list): control characters and line or paragraph separators
14251
+ * become spaces, runs of whitespace one space, the ends trimmed.
14252
+ */
14253
+ function singleLine(value) {
14254
+ return value.replaceAll(/[\p{Cc}\p{Zl}\p{Zp}]/gu, " ").replaceAll(/\s+/gu, " ").trim();
14255
+ }
14256
+ const text$3 = string$1().trim().min(1).max(TEXT_MAX_LENGTH);
14257
+ const label = string$1().transform(singleLine).pipe(string$1().min(1).max(200));
14258
+ const credentialRef$1 = text$3.regex(CREDENTIAL_REF, "must be a credential reference such as EXA_API_KEY");
14259
+ const endpoint = text$3.refine(isEndpoint, "must be an absolute http(s) URL without credentials, query or fragment");
14245
14260
  const options = record(string$1(), unknown());
14246
14261
  function documentSchema(defaults, currentVersion = 2) {
14247
14262
  return strictObject({
14248
14263
  version: literal(currentVersion).optional(),
14249
14264
  defaultConnection: string$1().min(1).max(200).optional(),
14250
14265
  showSelector: boolean$1().optional(),
14266
+ enhancedTools: boolean$1().optional(),
14251
14267
  defaults: defaults.optional(),
14252
14268
  connections: record(string$1().max(200), unknown()).refine((value) => Object.keys(value).length <= 200, "must contain at most 200 connections").optional()
14253
14269
  });
@@ -14309,7 +14325,7 @@ function resolveDocument({ catalog, document, defaults, capabilityLabel, protoco
14309
14325
  const customSchema = customConnectionSchema(adapterKeys, protocolKeys);
14310
14326
  const connections = /* @__PURE__ */ new Map();
14311
14327
  const rejectId = (id, path) => fail(path, "unknown connection id \"" + id + "\"; this release does not read 0.1.x settings; remove the field or use Reset on the settings page");
14312
- const resolve = (id, key, override, customEndpoint) => {
14328
+ const resolve = (id, key, override, origin) => {
14313
14329
  const info = catalog[key];
14314
14330
  const path = "connections." + id;
14315
14331
  const access = info.kind === "model" ? "api-key" : override.access ?? defaultAccess(info.access);
@@ -14321,11 +14337,12 @@ function resolveDocument({ catalog, document, defaults, capabilityLabel, protoco
14321
14337
  if (problems.length > 0) fail(path + ".options", problems.join("; ") + "; this release does not read 0.1.x settings; remove the field or use Reset on the settings page");
14322
14338
  const trustedEndpoint = override.trustedEndpoint ?? false;
14323
14339
  for (const [name, rule] of Object.entries(info.options)) if (rule.kind === "url" && values[name] !== void 0 && values[name] !== info.endpoint && !trustedEndpoint) fail(path + ".trustedEndpoint", "a non-official " + name + " needs trustedEndpoint: true");
14340
+ const disabled = override.disabled ?? (origin.kind === "builtin" ? info.optIn : false);
14324
14341
  const base = {
14325
14342
  id,
14326
14343
  catalogKey: key,
14327
14344
  label: override.label ?? info.label,
14328
- disabled: override.disabled ?? false,
14345
+ disabled,
14329
14346
  remote: info.remote,
14330
14347
  trustedEndpoint,
14331
14348
  options: values
@@ -14342,7 +14359,7 @@ function resolveDocument({ catalog, document, defaults, capabilityLabel, protoco
14342
14359
  };
14343
14360
  if (info.kind !== "native" && info.kind !== "adapter") return fail(path + ".kind", "unsupported catalog kind");
14344
14361
  if (!info.access.apiKey || info.endpoint === void 0) return fail(path, "remote catalog entry needs credentials and an endpoint");
14345
- const url = customEndpoint ?? info.endpoint;
14362
+ const url = origin.kind === "custom" && origin.endpoint !== void 0 ? origin.endpoint : info.endpoint;
14346
14363
  if (url !== info.endpoint && !trustedEndpoint) fail(path + ".trustedEndpoint", "a custom endpoint needs trustedEndpoint: true");
14347
14364
  return {
14348
14365
  ...base,
@@ -14363,7 +14380,7 @@ function resolveDocument({ catalog, document, defaults, capabilityLabel, protoco
14363
14380
  ]) if (Object.hasOwn(raw, field)) fail("connections." + id + "." + field, "this connection takes its route from the Session model");
14364
14381
  }
14365
14382
  const override = raw === void 0 ? {} : parse(builtinOverrideSchema, raw, "connections." + id);
14366
- connections.set(id, resolve(id, key, override));
14383
+ connections.set(id, resolve(id, key, override, { kind: "builtin" }));
14367
14384
  }
14368
14385
  for (const [id, raw] of Object.entries(overrides)) {
14369
14386
  if (connections.has(id)) continue;
@@ -14373,7 +14390,10 @@ function resolveDocument({ catalog, document, defaults, capabilityLabel, protoco
14373
14390
  if (custom.kind === "adapter") connections.set(id, resolve(id, custom.adapter, {
14374
14391
  ...custom,
14375
14392
  access: "api-key"
14376
- }, custom.endpoint));
14393
+ }, {
14394
+ kind: "custom",
14395
+ endpoint: custom.endpoint
14396
+ }));
14377
14397
  else {
14378
14398
  const info = protocols[custom.protocol];
14379
14399
  const values = custom.options ?? {};
@@ -14404,6 +14424,7 @@ function resolveDocument({ catalog, document, defaults, capabilityLabel, protoco
14404
14424
  version: migrations.currentVersion,
14405
14425
  defaultConnection,
14406
14426
  showSelector: settings.showSelector ?? true,
14427
+ enhancedTools: settings.enhancedTools ?? false,
14407
14428
  defaults: settings.defaults ?? parse(defaults.schema, {}, "defaults"),
14408
14429
  connections
14409
14430
  };
@@ -14650,175 +14671,17 @@ function toolJson(value) {
14650
14671
  throw error;
14651
14672
  }
14652
14673
  }
14653
- function nextModel(ctx, agent) {
14654
- const pending = ctx.get("sessionProjections")?.stateOf(agent.session, "modelSelection")?.pending;
14655
- if (pending !== void 0 && pending !== null) return complete(pending);
14656
- const request = agent.session.requestHeader();
14657
- return complete(request === void 0 ? agent.options : request.config);
14658
- }
14659
- function complete(value) {
14660
- if (!value.provider || !value.model) return void 0;
14661
- return {
14662
- provider: value.provider,
14663
- model: value.model
14664
- };
14665
- }
14666
- /** The resolved route used for native account authentication; pending choices do not authenticate. */
14667
- function requestModel(agent) {
14668
- const request = agent.session.requestContext();
14669
- return request === void 0 ? void 0 : complete(request);
14670
- }
14671
- //#endregion
14672
- //#region ../core/src/host/credentials.ts
14673
- /** Credentials is authoritative; absence does not fall back to process.env. */
14674
- async function resolveApiKey(ctx, ref) {
14675
- const credentials = ctx.get("credentials");
14676
- if (credentials === void 0) return void 0;
14677
- const found = await credentials.resolve(credentialRef(ref));
14678
- return found === void 0 ? void 0 : found.value;
14679
- }
14680
- /** Route authentication retains DSH's launch-plane fallback only when Credentials is absent. */
14681
- async function routeApiKey(ctx, ref) {
14682
- if (ctx.get("credentials") !== void 0) return resolveApiKey(ctx, ref);
14683
- const ambient = launchEnvironmentOf(ctx).get(credentialRef(ref));
14684
- return ambient !== void 0 && ambient.value.length > 0 ? ambient.value : void 0;
14685
- }
14686
- /** Readiness follows the same source boundary as routeApiKey, without resolving secrets. */
14687
- async function routeKeyConfigured(ctx, ref, source) {
14688
- const credentials = ctx.get("credentials");
14689
- if (credentials !== void 0) return (await credentials.describe(credentialRef(ref))).configured;
14690
- if (source === "credentials") return false;
14691
- const ambient = launchEnvironmentOf(ctx).get(credentialRef(ref));
14692
- return ambient !== void 0 && ambient.value.length > 0;
14693
- }
14694
- /** Native providers use the same credential source as DSH model routes. */
14695
- function nativeAuth(ctx, ref, secrets = /* @__PURE__ */ new Set()) {
14696
- const apiKeyEnv = credentialRef(ref);
14697
- let resolverFailed = false;
14698
- const tracked = async (resolve) => {
14699
- try {
14700
- const value = await resolve();
14701
- if (value !== void 0) secrets.add(value);
14702
- return value;
14703
- } catch {
14704
- resolverFailed = true;
14705
- throw new Error("Native credential resolution failed");
14706
- }
14707
- };
14708
- return {
14709
- apiKeyEnv,
14710
- secrets,
14711
- resolverFailed: () => resolverFailed,
14712
- resolveAccountToken: (endpoint) => tracked(async () => {
14713
- const resolve = accountTokenFor(ctx, ctx.get("agents")?.currentInitiator());
14714
- return resolve === void 0 ? void 0 : await resolve(endpoint);
14715
- }),
14716
- resolveApiKey: () => tracked(() => routeApiKey(ctx, ref))
14717
- };
14718
- }
14719
- /** Account readiness uses the next selected model, while request authentication uses its frozen route. */
14720
- async function credentialReadiness(ctx, agent, options) {
14721
- const account = options.endpoint !== void 0 && nextModel(ctx, agent)?.provider === "deepseek-account" ? ctx.get("deepseekAccount") : void 0;
14722
- if (account !== void 0 && await account.resolveToken(options.endpoint) !== void 0) return {
14723
- access: "account",
14724
- status: "ready"
14725
- };
14726
- const credentials = ctx.get("credentials");
14727
- return {
14728
- access: "api-key",
14729
- status: (credentials === void 0 ? void 0 : await credentials.describe(credentialRef(options.credentialRef)))?.configured === true ? "ready" : "needs-credential"
14730
- };
14731
- }
14732
- /** Account tokens are available only on a resolved deepseek-account request route. */
14733
- function accountTokenFor(ctx, agent) {
14734
- if (agent === void 0 || requestModel(agent)?.provider !== "deepseek-account") return void 0;
14735
- const account = ctx.get("deepseekAccount");
14736
- return account === void 0 ? void 0 : (endpoint) => account.resolveToken(endpoint);
14737
- }
14738
- //#endregion
14739
- //#region ../core/src/host/pairings.ts
14740
- const keyOf = ({ provider, model }) => JSON.stringify([provider, model]);
14741
- const modelKey = tuple([string$1(), string$1()]);
14742
- var Pairings = class {
14743
- storage;
14744
- constructor(storage) {
14745
- this.storage = storage;
14746
- }
14747
- async get(model) {
14748
- const record = (await this.storage.domain()).table("pairings").get(keyOf(model));
14749
- return record === void 0 ? void 0 : record.connectionId;
14750
- }
14751
- async set(model, connectionId) {
14752
- await (await this.storage.domain()).table("pairings").put(keyOf(model), { connectionId });
14753
- }
14754
- async list() {
14755
- return [...(await this.storage.domain()).table("pairings").entries()].map(([key, value]) => {
14756
- const [provider, model] = modelKey.parse(JSON.parse(key));
14757
- return {
14758
- provider,
14759
- model,
14760
- connectionId: value.connectionId
14761
- };
14762
- });
14763
- }
14764
- async forget(model) {
14765
- await (await this.storage.domain()).table("pairings").delete(keyOf(model));
14766
- }
14767
- };
14768
- //#endregion
14769
- //#region ../core/src/host/plans.ts
14770
- function planFor(selection) {
14771
- const { connectionId, options } = selection;
14772
- if (connectionId === null) return { kind: "off" };
14773
- return {
14774
- kind: "ready",
14775
- connectionId,
14776
- options: structuredClone(options)
14777
- };
14778
- }
14779
- /** Concurrent capture is shared; only failed captures may retry in the same step. */
14780
- var Snapshots = class {
14781
- steps = /* @__PURE__ */ new WeakMap();
14782
- capture(agent, turn, step, plan) {
14783
- const previous = this.steps.get(agent);
14784
- if (previous !== void 0 && previous.turn === turn && previous.step === step) {
14785
- if (previous.kind === "capturing") return previous.pending;
14786
- if (previous.plan.kind !== "failed") return Promise.resolve();
14787
- }
14788
- const pending = plan().then((result) => {
14789
- if (this.steps.get(agent) === snapshot) this.steps.set(agent, {
14790
- turn,
14791
- step,
14792
- kind: "captured",
14793
- plan: result
14794
- });
14795
- }, (error) => {
14796
- if (this.steps.get(agent) === snapshot) this.steps.delete(agent);
14797
- throw error;
14798
- });
14799
- const snapshot = {
14800
- turn,
14801
- step,
14802
- kind: "capturing",
14803
- pending
14804
- };
14805
- this.steps.set(agent, snapshot);
14806
- return pending;
14807
- }
14808
- get(agent) {
14809
- const snapshot = this.steps.get(agent);
14810
- return snapshot?.kind === "captured" ? snapshot.plan : void 0;
14811
- }
14812
- forget(agent) {
14813
- this.steps.delete(agent);
14814
- }
14815
- };
14816
14674
  //#endregion
14817
14675
  //#region ../core/src/shared/ids.ts
14818
14676
  const SEARCH = {
14819
14677
  packageName: "dsh-web-search-enhanced",
14820
14678
  rowId: "web-search-enhanced",
14821
- providerId: "search-enhanced"
14679
+ providerId: "search-enhanced",
14680
+ tools: {
14681
+ action: "web_search_enhanced",
14682
+ list: "list_web_search_providers"
14683
+ },
14684
+ command: "setup-search"
14822
14685
  };
14823
14686
  //#endregion
14824
14687
  //#region ../core/src/shared/route.ts
@@ -15002,48 +14865,560 @@ function selectionContract(spec) {
15002
14865
  };
15003
14866
  }
15004
14867
  //#endregion
15005
- //#region ../core/src/host/selectable.ts
15006
- /** One eligibility rule for memories, Remote changes, plans and per-call policy. */
15007
- function isSelectable(connection) {
15008
- return connection !== void 0 && !connection.disabled;
14868
+ //#region ../core/src/host/enhanced-parts/wording.ts
14869
+ /** Why an id the operator added after the conversation started is unknown here (the list is fixed per conversation). */
14870
+ const LATER_PROVIDERS = ". Providers added or turned on after this conversation started are available only in new conversations.";
14871
+ /** At most this many ready ids are named in one message; the list tool shows the rest. */
14872
+ const READY_SHOWN = 20;
14873
+ /**
14874
+ * The tail of a message naming the ready providers the Agent can switch to:
14875
+ * `ready providers: <ids>` without `except`, at most twenty in list order, or
14876
+ * `no other provider is ready` in place of the whole clause. When readiness could
14877
+ * not be read, the list tool is named instead, so "none" is never claimed unread.
14878
+ */
14879
+ function readyClause(voice, ready, except) {
14880
+ if (ready.kind === "unread") return `${voice.identity.tools.list} shows the ready providers`;
14881
+ const others = ready.ids.filter((id) => id !== except);
14882
+ if (others.length === 0) return "no other provider is ready";
14883
+ const hidden = others.length - READY_SHOWN;
14884
+ const more = hidden > 0 ? ` and ${hidden} more (${voice.identity.tools.list})` : "";
14885
+ return `ready providers: ${others.slice(0, READY_SHOWN).join(", ")}${more}`;
14886
+ }
14887
+ /** Word a refusal core reported before any backend ran; `unavailable` passes core's own error on unchanged. */
14888
+ function rejectionError(voice, reason) {
14889
+ const { error } = capabilityErrors(voice.identity.packageName);
14890
+ switch (reason.kind) {
14891
+ case "unknown": return error("WEB_PROVIDER_CONFIGURED_MISSING", `unknown provider "${reason.connectionId}"; ${readyClause(voice, reason.ready, reason.connectionId)}${LATER_PROVIDERS}`);
14892
+ case "removed": return error("WEB_PROVIDER_CONFIGURED_MISSING", `provider "${reason.connectionId}" was removed by the user; ${readyClause(voice, reason.ready, reason.connectionId)}`);
14893
+ case "off": return error("WEB_PROVIDER_UNAVAILABLE", `${voice.capability} is off for this session; only the user can turn it back on (composer menu). Do not retry.`);
14894
+ case "withdrawn": return error("WEB_PROVIDER_UNAVAILABLE", `${voice.identity.tools.action} is unavailable now; use ${voice.nativeTool}.`);
14895
+ case "unavailable": return reason.error;
14896
+ }
14897
+ }
14898
+ /**
14899
+ * Point a backend failure at the list tool: the hint ends the first paragraph,
14900
+ * before any untrusted upstream explanation, after a full stop when that paragraph
14901
+ * has no sentence end; the code and the rest of the message stay as the execution
14902
+ * layer wrote them. Cancellation, and failures `unhinted` marks, pass on unchanged.
14903
+ */
14904
+ function providerFailure(voice, cause, connectionId, unhinted) {
14905
+ if (!(cause instanceof WebError) || cause.code === "WEB_ABORTED" || unhinted(cause)) return cause;
14906
+ const hint = ` Provider "${connectionId}" failed; ${voice.identity.tools.list} shows the others.`;
14907
+ const end = cause.message.indexOf("\n\n");
14908
+ const first = end === -1 ? cause.message : cause.message.slice(0, end);
14909
+ const rest = end === -1 ? "" : cause.message.slice(end);
14910
+ const stop = /[.!?]$/u.test(first) ? "" : ".";
14911
+ return new WebError(first + stop + hint + rest, cause.code);
14912
+ }
14913
+ /** An argument error in DSH's own form, without the package prefix. */
14914
+ function invalidArguments(problem) {
14915
+ return /* @__PURE__ */ new Error(`invalid arguments: ${problem}`);
14916
+ }
14917
+ /** DSH accepts extra properties on the open argument root; the enhanced tools refuse them without echoing the name. */
14918
+ function rejectUnknownProperties(args, known) {
14919
+ if (Object.keys(args).some((key) => !known.includes(key))) throw invalidArguments("unknown property");
14920
+ }
14921
+ /** The caller DSH reports for this execution; a call without an Agent never falls back to a default connection. */
14922
+ function explicitCall(tool, exec) {
14923
+ if (exec.agent === void 0) throw new Error(`${tool} can only be called by an Agent`);
14924
+ return {
14925
+ agent: exec.agent,
14926
+ signal: exec.signal
14927
+ };
14928
+ }
14929
+ /**
14930
+ * The model's `provider` argument, once it has the grammar of a connection id.
14931
+ * A malformed value is never echoed; the error names the ready providers instead.
14932
+ */
14933
+ async function providerArgument(voice, value, access, call) {
14934
+ if (value.length > 0 && value.length <= 128 && isConnectionId(value)) return value;
14935
+ throw invalidArguments(`provider must be an id such as builtin:<key> or custom:<name>; ${readyClause(voice, await access.ready(call))}`);
14936
+ }
14937
+ /**
14938
+ * Run one request on the named provider: a refusal is worded for the model,
14939
+ * a backend failure gets the switch-provider hint (docs/design.md, "增强工具的错误").
14940
+ */
14941
+ async function runOnProvider(voice, access, connectionId, request, call, unhinted = () => false) {
14942
+ let outcome;
14943
+ try {
14944
+ outcome = await access.run(connectionId, request, call);
14945
+ } catch (cause) {
14946
+ throw providerFailure(voice, cause, connectionId, unhinted);
14947
+ }
14948
+ if (outcome.kind === "rejected") throw rejectionError(voice, outcome.reason);
14949
+ return outcome.value;
15009
14950
  }
15010
14951
  //#endregion
15011
- //#region ../core/src/host/sessions.ts
15012
- var SelectionConflict = class extends Error {
15013
- constructor() {
15014
- super("the selection changed since it was read");
14952
+ //#region ../core/src/host/enhanced-parts/listing.ts
14953
+ /** The recipient `sendsTo` names for fetch `local`; the text says `fetches from this machine`. */
14954
+ const THIS_MACHINE = "this machine";
14955
+ const SESSION_MODEL = "the service behind the session model";
14956
+ const HOST_MAX = 253;
14957
+ /** Why the session model cannot search, in English; the execution layer's error and the listing share it. */
14958
+ const UNSUPPORTED_REASON_TEXT = {
14959
+ "no-model": "no session model is available",
14960
+ "unknown-route": "the model route is unknown",
14961
+ protocol: "the route does not declare a supported protocol",
14962
+ endpoint: "the route has no supported endpoint",
14963
+ headers: "the route uses custom request headers",
14964
+ credential: "the route has no supported credential reference"
14965
+ };
14966
+ const text$2 = {
14967
+ type: "string",
14968
+ required: true
14969
+ };
14970
+ function entrySchema(extra) {
14971
+ const common = {
14972
+ id: text$2,
14973
+ name: text$2,
14974
+ use: { type: "string" },
14975
+ sendsTo: text$2,
14976
+ ...extra
14977
+ };
14978
+ return { oneOf: [
14979
+ {
14980
+ type: "object",
14981
+ additionalProperties: false,
14982
+ properties: {
14983
+ ...common,
14984
+ status: {
14985
+ type: "string",
14986
+ required: true,
14987
+ const: "ready"
14988
+ }
14989
+ }
14990
+ },
14991
+ {
14992
+ type: "object",
14993
+ additionalProperties: false,
14994
+ properties: {
14995
+ ...common,
14996
+ status: {
14997
+ type: "string",
14998
+ required: true,
14999
+ const: "needs-key"
15000
+ }
15001
+ }
15002
+ },
15003
+ {
15004
+ type: "object",
15005
+ additionalProperties: false,
15006
+ properties: {
15007
+ ...common,
15008
+ status: {
15009
+ type: "string",
15010
+ required: true,
15011
+ const: "needs-sign-in"
15012
+ }
15013
+ }
15014
+ },
15015
+ {
15016
+ type: "object",
15017
+ additionalProperties: false,
15018
+ properties: {
15019
+ ...common,
15020
+ status: {
15021
+ type: "string",
15022
+ required: true,
15023
+ const: "unavailable"
15024
+ },
15025
+ reason: text$2
15026
+ }
15027
+ }
15028
+ ] };
15029
+ }
15030
+ const session = {
15031
+ sessionProvider: text$2,
15032
+ sessionProviderTurnedOff: {
15033
+ type: "boolean",
15034
+ required: true
15015
15035
  }
15016
15036
  };
15017
- var Sessions = class {
15018
- ctx;
15019
- settings;
15020
- storage;
15021
- pairings;
15022
- restriction;
15023
- describe;
15024
- queues = /* @__PURE__ */ new Map();
15025
- constructor(ctx, settings, storage, pairings, restriction, describe = async () => []) {
15026
- this.ctx = ctx;
15027
- this.settings = settings;
15028
- this.storage = storage;
15029
- this.pairings = pairings;
15030
- this.restriction = restriction;
15031
- this.describe = describe;
15037
+ function searchListingSchema() {
15038
+ return {
15039
+ type: "object",
15040
+ additionalProperties: false,
15041
+ properties: {
15042
+ ...session,
15043
+ freshness: {
15044
+ type: "string",
15045
+ required: true,
15046
+ enum: FRESHNESS
15047
+ },
15048
+ providers: {
15049
+ type: "array",
15050
+ required: true,
15051
+ items: entrySchema({ appliesFreshness: {
15052
+ type: "boolean",
15053
+ required: true
15054
+ } })
15055
+ }
15056
+ }
15057
+ };
15058
+ }
15059
+ /** This conversation's providers, or the worded refusal. */
15060
+ async function listProviders(voice, access, call) {
15061
+ const outcome = await access.list(call);
15062
+ if (outcome.kind === "rejected") throw rejectionError(voice, outcome.reason);
15063
+ return outcome.value;
15064
+ }
15065
+ function searchListingValue(listing) {
15066
+ return {
15067
+ ...sessionOf(listing),
15068
+ freshness: listing.options.freshness,
15069
+ providers: listing.providers.map((entry) => ({
15070
+ ...listedBase(entry),
15071
+ appliesFreshness: entry.facts.appliesFreshness === true,
15072
+ ...readiness(entry)
15073
+ }))
15074
+ };
15075
+ }
15076
+ /** One line per provider, `<id> — <name> · …`, without a heading (docs/design.md, "列出工具"). */
15077
+ function renderSearchListing(voice, value) {
15078
+ const uncached = value.freshness !== "auto";
15079
+ return renderLines(voice, value, uncached ? ["The user asked for uncached (fresh) page content; only providers marked \"uncached\" apply it."] : [], {
15080
+ marks: (entry) => uncached && entry.appliesFreshness ? ["uncached"] : [],
15081
+ notes: () => []
15082
+ });
15083
+ }
15084
+ function renderLines(voice, value, head, parts) {
15085
+ const entries = value.providers.map((entry) => {
15086
+ const line = [statusText$1(entry)];
15087
+ if (entry.id === value.sessionProvider) line.push("selected for this session");
15088
+ if (entry.use !== void 0) line.push(entry.use);
15089
+ line.push(...parts.marks(entry));
15090
+ if (entry.sendsTo === "this machine") line.push("fetches from this machine");
15091
+ else if (entry.sendsTo !== entry.name) line.push(`${voice.sends} go to ${entry.sendsTo}`);
15092
+ line.push(...parts.notes(entry));
15093
+ return `${entry.id} — ${entry.name} · ${line.join(" · ")}`;
15094
+ });
15095
+ const tail = value.sessionProviderTurnedOff ? [`This session's selection, ${value.sessionProvider}, is turned off; ${voice.nativeTool} will fail until the user changes it.`] : [];
15096
+ return [
15097
+ ...head,
15098
+ ...entries.length === 0 ? ["(no providers)"] : entries,
15099
+ ...tail
15100
+ ].join("\n");
15101
+ }
15102
+ function statusText$1(entry) {
15103
+ switch (entry.status) {
15104
+ case "ready": return "ready";
15105
+ case "needs-key": return "needs an API key (ask the user)";
15106
+ case "needs-sign-in": return "needs the user to sign in to DeepSeek again";
15107
+ case "unavailable": return `unavailable: ${entry.reason}`;
15032
15108
  }
15033
- current(agent) {
15034
- return this.exclusive(agent.session.id, async () => this.read(agent, await this.table()));
15109
+ }
15110
+ function sessionOf(listing) {
15111
+ return {
15112
+ sessionProvider: listing.session.connectionId,
15113
+ sessionProviderTurnedOff: !listing.session.selectable
15114
+ };
15115
+ }
15116
+ function listedBase(entry) {
15117
+ const { use, recipient } = entry.facts;
15118
+ return {
15119
+ id: entry.id,
15120
+ name: oneLine(entry.name, 200),
15121
+ ...use === void 0 ? {} : { use },
15122
+ sendsTo: recipientText(recipient)
15123
+ };
15124
+ }
15125
+ function recipientText(recipient) {
15126
+ switch (recipient.kind) {
15127
+ case "this-machine": return THIS_MACHINE;
15128
+ case "session-model": return recipient.provider === void 0 ? SESSION_MODEL : `${SESSION_MODEL} (${oneLine(recipient.provider, 200)})`;
15129
+ case "service": return recipient.host === void 0 ? oneLine(recipient.service, 200) : oneLine(recipient.host, HOST_MAX);
15130
+ }
15131
+ }
15132
+ function readiness(entry) {
15133
+ switch (entry.status) {
15134
+ case "ready": return { status: "ready" };
15135
+ case "needs-credential": return { status: entry.credential === "account" ? "needs-sign-in" : "needs-key" };
15136
+ case "unsupported": return {
15137
+ status: "unavailable",
15138
+ reason: UNSUPPORTED_REASON_TEXT[entry.reason]
15139
+ };
15140
+ }
15141
+ }
15142
+ /** Collapse line breaks, controls and runs of white space, then cut at `max` characters without splitting a surrogate pair. */
15143
+ function oneLine(value, max) {
15144
+ const flat = value.replace(/[\p{Cc}\p{Zl}\p{Zp}\s]+/gu, " ").trim();
15145
+ if (flat.length <= max) return flat;
15146
+ const last = flat.charCodeAt(max - 2);
15147
+ return flat.slice(0, last >= 55296 && last <= 56319 ? max - 2 : max - 1) + "…";
15148
+ }
15149
+ function nextModel(ctx, agent) {
15150
+ const pending = ctx.get("sessionProjections")?.stateOf(agent.session, "modelSelection")?.pending;
15151
+ if (pending !== void 0 && pending !== null) return complete(pending);
15152
+ const request = agent.session.requestHeader();
15153
+ return complete(request === void 0 ? agent.options : request.config);
15154
+ }
15155
+ function complete(value) {
15156
+ if (!value.provider || !value.model) return void 0;
15157
+ return {
15158
+ provider: value.provider,
15159
+ model: value.model
15160
+ };
15161
+ }
15162
+ /** The resolved route used for native account authentication; pending choices do not authenticate. */
15163
+ function requestModel(agent) {
15164
+ const request = agent.session.requestContext();
15165
+ return request === void 0 ? void 0 : complete(request);
15166
+ }
15167
+ //#endregion
15168
+ //#region ../core/src/host/credentials.ts
15169
+ /** Credentials is authoritative; absence does not fall back to process.env. */
15170
+ async function resolveApiKey(ctx, ref) {
15171
+ const credentials = ctx.get("credentials");
15172
+ if (credentials === void 0) return void 0;
15173
+ const found = await credentials.resolve(credentialRef(ref));
15174
+ return found === void 0 ? void 0 : found.value;
15175
+ }
15176
+ /** Route authentication retains DSH's launch-plane fallback only when Credentials is absent. */
15177
+ async function routeApiKey(ctx, ref) {
15178
+ if (ctx.get("credentials") !== void 0) return resolveApiKey(ctx, ref);
15179
+ const ambient = launchEnvironmentOf(ctx).get(credentialRef(ref));
15180
+ return ambient !== void 0 && ambient.value.length > 0 ? ambient.value : void 0;
15181
+ }
15182
+ /** Readiness follows the same source boundary as routeApiKey, without resolving secrets. */
15183
+ async function routeKeyConfigured(ctx, ref, source) {
15184
+ const credentials = ctx.get("credentials");
15185
+ if (credentials !== void 0) return (await credentials.describe(credentialRef(ref))).configured;
15186
+ if (source === "credentials") return false;
15187
+ const ambient = launchEnvironmentOf(ctx).get(credentialRef(ref));
15188
+ return ambient !== void 0 && ambient.value.length > 0;
15189
+ }
15190
+ /** Native providers use the same credential source as DSH model routes. */
15191
+ function nativeAuth(ctx, ref, secrets = /* @__PURE__ */ new Set()) {
15192
+ const apiKeyEnv = credentialRef(ref);
15193
+ let resolverFailed = false;
15194
+ const tracked = async (resolve) => {
15195
+ try {
15196
+ const value = await resolve();
15197
+ if (value !== void 0) secrets.add(value);
15198
+ return value;
15199
+ } catch {
15200
+ resolverFailed = true;
15201
+ throw new Error("Native credential resolution failed");
15202
+ }
15203
+ };
15204
+ return {
15205
+ apiKeyEnv,
15206
+ secrets,
15207
+ resolverFailed: () => resolverFailed,
15208
+ resolveAccountToken: (endpoint) => tracked(async () => {
15209
+ const resolve = accountTokenFor(ctx, ctx.get("agents")?.currentInitiator());
15210
+ return resolve === void 0 ? void 0 : await resolve(endpoint);
15211
+ }),
15212
+ resolveApiKey: () => tracked(() => routeApiKey(ctx, ref))
15213
+ };
15214
+ }
15215
+ /** Account readiness uses the next selected model, while request authentication uses its frozen route. */
15216
+ async function credentialReadiness(ctx, model, options) {
15217
+ const account = options.endpoint !== void 0 && model?.provider === "deepseek-account" ? ctx.get("deepseekAccount") : void 0;
15218
+ if (account !== void 0 && await account.resolveToken(options.endpoint) !== void 0) return {
15219
+ access: "account",
15220
+ status: "ready"
15221
+ };
15222
+ const credentials = ctx.get("credentials");
15223
+ return {
15224
+ access: "api-key",
15225
+ status: (credentials === void 0 ? void 0 : await credentials.describe(credentialRef(options.credentialRef)))?.configured === true ? "ready" : "needs-credential"
15226
+ };
15227
+ }
15228
+ /** Account tokens are available only on a resolved deepseek-account request route. */
15229
+ function accountTokenFor(ctx, agent) {
15230
+ if (agent === void 0 || requestModel(agent)?.provider !== "deepseek-account") return void 0;
15231
+ const account = ctx.get("deepseekAccount");
15232
+ return account === void 0 ? void 0 : (endpoint) => account.resolveToken(endpoint);
15233
+ }
15234
+ //#endregion
15235
+ //#region ../core/src/host/pairings.ts
15236
+ const keyOf = ({ provider, model }) => JSON.stringify([provider, model]);
15237
+ const modelKey = tuple([string$1(), string$1()]);
15238
+ var Pairings = class {
15239
+ storage;
15240
+ constructor(storage) {
15241
+ this.storage = storage;
15242
+ }
15243
+ async get(model) {
15244
+ const record = (await this.storage.domain()).table("pairings").get(keyOf(model));
15245
+ return record === void 0 ? void 0 : record.connectionId;
15246
+ }
15247
+ async set(model, connectionId) {
15248
+ await (await this.storage.domain()).table("pairings").put(keyOf(model), { connectionId });
15249
+ }
15250
+ async list() {
15251
+ return [...(await this.storage.domain()).table("pairings").entries()].map(([key, value]) => {
15252
+ const [provider, model] = modelKey.parse(JSON.parse(key));
15253
+ return {
15254
+ provider,
15255
+ model,
15256
+ connectionId: value.connectionId
15257
+ };
15258
+ });
15259
+ }
15260
+ async forget(model) {
15261
+ await (await this.storage.domain()).table("pairings").delete(keyOf(model));
15262
+ }
15263
+ };
15264
+ //#endregion
15265
+ //#region ../core/src/host/plans.ts
15266
+ function planFor(selection) {
15267
+ const { connectionId, options } = selection;
15268
+ if (connectionId === null) return { kind: "off" };
15269
+ return {
15270
+ kind: "ready",
15271
+ connectionId,
15272
+ options: structuredClone(options)
15273
+ };
15274
+ }
15275
+ /** Concurrent capture is shared; only failed captures may retry in the same step. */
15276
+ var Snapshots = class {
15277
+ steps = /* @__PURE__ */ new WeakMap();
15278
+ capture(agent, turn, step, plan) {
15279
+ const previous = this.steps.get(agent);
15280
+ if (previous !== void 0 && previous.turn === turn && previous.step === step) {
15281
+ if (previous.kind === "capturing") return previous.pending;
15282
+ if (previous.plan.kind !== "failed") return Promise.resolve();
15283
+ }
15284
+ const pending = plan().then((result) => {
15285
+ if (this.steps.get(agent) === snapshot) this.steps.set(agent, {
15286
+ turn,
15287
+ step,
15288
+ kind: "captured",
15289
+ plan: result
15290
+ });
15291
+ }, (error) => {
15292
+ if (this.steps.get(agent) === snapshot) this.steps.delete(agent);
15293
+ throw error;
15294
+ });
15295
+ const snapshot = {
15296
+ turn,
15297
+ step,
15298
+ kind: "capturing",
15299
+ pending
15300
+ };
15301
+ this.steps.set(agent, snapshot);
15302
+ return pending;
15303
+ }
15304
+ get(agent) {
15305
+ const snapshot = this.steps.get(agent);
15306
+ return snapshot?.kind === "captured" ? snapshot.plan : void 0;
15307
+ }
15308
+ forget(agent) {
15309
+ this.steps.delete(agent);
15310
+ }
15311
+ };
15312
+ //#endregion
15313
+ //#region ../core/src/host/session-origin.ts
15314
+ function sessionOrigin(header) {
15315
+ const { parentSession: parent, isSeeded: seeded, origin } = header;
15316
+ if (origin === "subagent") return {
15317
+ kind: "subagent",
15318
+ parent,
15319
+ seeded
15320
+ };
15321
+ return parent === void 0 ? {
15322
+ kind: "top-level",
15323
+ seeded
15324
+ } : {
15325
+ kind: "fork",
15326
+ parent,
15327
+ seeded
15328
+ };
15329
+ }
15330
+ function parentOf(origin) {
15331
+ return origin.kind === "top-level" ? void 0 : origin.parent;
15332
+ }
15333
+ //#endregion
15334
+ //#region ../core/src/host/provider-lists.ts
15335
+ /** Live setup/created events do not make a new conversation a restored one. */
15336
+ function isNewConversation(session) {
15337
+ const origin = sessionOrigin(session.header);
15338
+ return parentOf(origin) === void 0 && !origin.seeded && session.firstLiveSeq === 0 && session.firstLifecycleSeq === 0;
15339
+ }
15340
+ /** Immutable conversation authorization, sharing the selection queue and storage lifetime. */
15341
+ var ProviderLists = class {
15342
+ storage;
15343
+ constructor(storage) {
15344
+ this.storage = storage;
15345
+ }
15346
+ read(agent) {
15347
+ return this.storage.exclusive(agent.session.id, async () => {
15348
+ const table = (await this.storage.domain()).table("provider_lists");
15349
+ return this.nonempty(table.get(agent.session.id));
15350
+ });
15351
+ }
15352
+ create(agent, initial) {
15353
+ return this.storage.exclusive(agent.session.id, async () => {
15354
+ const table = (await this.storage.domain()).table("provider_lists");
15355
+ const stored = table.get(agent.session.id);
15356
+ if (stored !== void 0) return this.nonempty(stored);
15357
+ const parent = parentOf(sessionOrigin(agent.session.header));
15358
+ let ids;
15359
+ if (parent !== void 0) {
15360
+ await this.storage.settled(parent);
15361
+ ids = table.get(parent);
15362
+ } else ids = initial;
15363
+ if (ids === void 0) return void 0;
15364
+ await table.put(agent.session.id, [...ids]);
15365
+ return this.nonempty(ids);
15366
+ });
15367
+ }
15368
+ nonempty(ids) {
15369
+ return ids === void 0 || ids.length === 0 ? void 0 : [...ids];
15370
+ }
15371
+ };
15372
+ //#endregion
15373
+ //#region ../core/src/host/selectable.ts
15374
+ /** One eligibility rule for memories, Remote changes, plans and per-call policy. */
15375
+ function isSelectable(connection) {
15376
+ return connection !== void 0 && !connection.disabled;
15377
+ }
15378
+ //#endregion
15379
+ //#region ../core/src/host/sessions.ts
15380
+ function liveSession(agent) {
15381
+ return {
15382
+ kind: "live",
15383
+ id: agent.session.id,
15384
+ header: agent.session.header,
15385
+ agent
15386
+ };
15387
+ }
15388
+ var SelectionConflict = class extends Error {
15389
+ constructor() {
15390
+ super("the selection changed since it was read");
15391
+ }
15392
+ };
15393
+ var Sessions = class {
15394
+ ctx;
15395
+ settings;
15396
+ storage;
15397
+ pairings;
15398
+ restriction;
15399
+ describe;
15400
+ constructor(ctx, settings, storage, pairings, restriction, describe = async () => []) {
15401
+ this.ctx = ctx;
15402
+ this.settings = settings;
15403
+ this.storage = storage;
15404
+ this.pairings = pairings;
15405
+ this.restriction = restriction;
15406
+ this.describe = describe;
15407
+ }
15408
+ current(target) {
15409
+ return this.exclusive(target.id, async () => this.read(target, await this.table()));
15035
15410
  }
15036
15411
  /** Resolve visibility without creating records or materializing inherited/default options. */
15037
- peek(agent) {
15038
- return this.exclusive(agent.session.id, async () => this.read(agent, await this.table(), false));
15412
+ peek(target) {
15413
+ return this.exclusive(target.id, async () => this.read(target, await this.table(), false));
15039
15414
  }
15040
- change(agent, change, expectedRevision) {
15041
- return this.exclusive(agent.session.id, async () => {
15415
+ change(target, change, expectedRevision) {
15416
+ return this.exclusive(target.id, async () => {
15042
15417
  const table = await this.table();
15043
- await this.read(agent, table);
15418
+ await this.read(target, table);
15044
15419
  const { connectionId, ...options } = change;
15045
15420
  const record = this.storage.optionsCodec.toRecord(options);
15046
- const updated = await table.update(agent.session.id, (current) => {
15421
+ const updated = await table.update(target.id, (current) => {
15047
15422
  if (current.revision !== expectedRevision) throw new SelectionConflict();
15048
15423
  return {
15049
15424
  ...current,
@@ -15052,9 +15427,10 @@ var Sessions = class {
15052
15427
  revision: current.revision + 1
15053
15428
  };
15054
15429
  });
15055
- this.restriction.sync(agent, updated.connectionId);
15056
- if (connectionId !== void 0) {
15057
- const model = nextModel(this.ctx, agent);
15430
+ const agent = this.ctx.get("agents")?.get(target.id);
15431
+ if (agent !== void 0) this.restriction.sync(agent, updated.connectionId);
15432
+ if (connectionId !== void 0 && sessionOrigin(target.header).kind !== "subagent") {
15433
+ const model = this.model(target);
15058
15434
  if (model !== void 0) {
15059
15435
  const memory = await this.pairings.set(model, updated.connectionId).then(() => ({ kind: "stored" }), (error) => ({
15060
15436
  kind: "failed",
@@ -15069,37 +15445,34 @@ var Sessions = class {
15069
15445
  };
15070
15446
  });
15071
15447
  }
15072
- connections(agent) {
15073
- return this.describe(this.settings(), agent);
15448
+ connections(target) {
15449
+ return this.describe(this.settings(), this.model(target));
15074
15450
  }
15075
15451
  selectable(connectionId) {
15076
15452
  return isSelectable(this.settings().connections.get(connectionId));
15077
15453
  }
15454
+ model(target) {
15455
+ return target.kind === "live" ? nextModel(this.ctx, target.agent) : void 0;
15456
+ }
15078
15457
  async table() {
15079
15458
  return (await this.storage.domain()).table("selections");
15080
15459
  }
15081
15460
  exclusive(id, work) {
15082
- const previous = this.queues.get(id);
15083
- const run = previous === void 0 ? work() : previous.then(work);
15084
- const tail = run.then(() => {}, () => {});
15085
- this.queues.set(id, tail);
15086
- tail.then(() => {
15087
- if (this.queues.get(id) === tail) this.queues.delete(id);
15088
- });
15089
- return run;
15461
+ return this.storage.exclusive(id, work);
15090
15462
  }
15091
- async read(agent, table, persist = true) {
15463
+ async read(target, table, persist = true) {
15092
15464
  const settings = this.settings();
15093
15465
  const codec = this.storage.optionsCodec;
15094
- const id = agent.session.id;
15466
+ const id = target.id;
15095
15467
  let stored = table.get(id);
15096
- const { parentSession, isSeeded } = agent.session.header;
15097
- const seeded = parentSession !== void 0 && isSeeded;
15468
+ const origin = sessionOrigin(target.header);
15469
+ const parentSession = parentOf(origin);
15470
+ const seeded = parentSession !== void 0 && origin.seeded;
15098
15471
  let inherited = stored !== void 0 && seeded && (stored.inherited ?? table.get(parentSession) !== void 0);
15099
15472
  if (stored === void 0 && seeded) {
15100
15473
  const parentAgent = this.ctx.get("agents")?.get(parentSession);
15101
- if (parentAgent !== void 0 && persist) await this.current(parentAgent);
15102
- else await this.queues.get(parentSession);
15474
+ if (parentAgent !== void 0 && persist) await this.current(liveSession(parentAgent));
15475
+ else await this.storage.settled(parentSession);
15103
15476
  const parent = table.get(parentSession);
15104
15477
  if (parent !== void 0) {
15105
15478
  stored = {
@@ -15122,7 +15495,7 @@ var Sessions = class {
15122
15495
  source: next.revision > 0 ? "session" : "inherited"
15123
15496
  };
15124
15497
  }
15125
- const fallback = await this.defaultFor(agent, settings);
15498
+ const fallback = await this.defaultFor(target, settings);
15126
15499
  const options = stored === void 0 ? codec.initial(settings) : codec.fromRecord(stored, settings);
15127
15500
  const next = {
15128
15501
  ...codec.toRecord(options),
@@ -15136,8 +15509,8 @@ var Sessions = class {
15136
15509
  source: fallback.source
15137
15510
  };
15138
15511
  }
15139
- async defaultFor(agent, settings) {
15140
- const model = nextModel(this.ctx, agent);
15512
+ async defaultFor(target, settings) {
15513
+ const model = this.model(target);
15141
15514
  const paired = model === void 0 ? void 0 : await this.pairings.get(model);
15142
15515
  if (paired === null) return {
15143
15516
  connectionId: null,
@@ -15164,6 +15537,24 @@ function sameRecord(left, right) {
15164
15537
  return left !== void 0 && JSON.stringify(left) === JSON.stringify(right);
15165
15538
  }
15166
15539
  //#endregion
15540
+ //#region ../core/src/host/selection-write.ts
15541
+ /** The shared eligibility check and revisioned write for commands and Remote set. */
15542
+ async function writeSelection(sessions, target, change, expectedRevision) {
15543
+ if (typeof change.connectionId === "string" && !sessions.selectable(change.connectionId)) return {
15544
+ kind: "unavailable",
15545
+ connectionId: change.connectionId
15546
+ };
15547
+ try {
15548
+ return {
15549
+ kind: "written",
15550
+ current: await sessions.change(target, change, expectedRevision)
15551
+ };
15552
+ } catch (error) {
15553
+ if (error instanceof SelectionConflict) return { kind: "conflict" };
15554
+ throw error;
15555
+ }
15556
+ }
15557
+ //#endregion
15167
15558
  //#region ../core/src/host/remote.ts
15168
15559
  var __runInitializers = function(thisArg, initializers, value) {
15169
15560
  var useValue = arguments.length > 2;
@@ -15311,27 +15702,22 @@ function createConnectionsService(ctx, spec) {
15311
15702
  async get(request) {
15312
15703
  return boundary("get", async () => {
15313
15704
  const { sessionId } = parse(selectionRequest, request);
15314
- const agent = await this.agent(sessionId);
15315
- return this.view(agent, await spec.sessions.current(agent));
15705
+ const target = await this.target(sessionId);
15706
+ return this.view(target, await spec.sessions.current(target));
15316
15707
  });
15317
15708
  }
15318
15709
  async set(request) {
15319
15710
  return boundary("set", async () => {
15320
15711
  const { sessionId, connectionId, options, expectedRevision } = parse(spec.contract.selectionChange, request);
15321
- const agent = await this.agent(sessionId);
15322
- if (typeof connectionId === "string" && !spec.sessions.selectable(connectionId)) throw new RemoteError(spec.namespace + "/unavailable", "the connection is missing, disabled or unsupported", { connectionId });
15323
- let current;
15324
- try {
15325
- const change = {
15326
- ...options,
15327
- ...connectionId === void 0 ? {} : { connectionId }
15328
- };
15329
- current = await spec.sessions.change(agent, change, expectedRevision);
15330
- } catch (error) {
15331
- if (error instanceof SelectionConflict) throw new RemoteError(spec.namespace + "/conflict", "the selection changed elsewhere; re-read it and retry", {});
15332
- throw error;
15333
- }
15334
- return this.view(agent, current);
15712
+ const target = await this.target(sessionId);
15713
+ const change = {
15714
+ ...options,
15715
+ ...connectionId === void 0 ? {} : { connectionId }
15716
+ };
15717
+ const result = await writeSelection(spec.sessions, target, change, expectedRevision);
15718
+ if (result.kind === "unavailable") throw new RemoteError(spec.namespace + "/unavailable", "the connection is missing, disabled or unsupported", { connectionId: result.connectionId });
15719
+ if (result.kind === "conflict") throw new RemoteError(spec.namespace + "/conflict", "the selection changed elsewhere; re-read it and retry", {});
15720
+ return this.view(target, result.current);
15335
15721
  });
15336
15722
  }
15337
15723
  async provider(request) {
@@ -15353,15 +15739,27 @@ function createConnectionsService(ctx, spec) {
15353
15739
  return { pairings: await spec.pairings.list() };
15354
15740
  });
15355
15741
  }
15356
- async agent(sessionId) {
15357
- const found = await this.ctx.sessionController.resolveAgent(sessionId);
15742
+ async target(sessionId) {
15743
+ const id = sessionId;
15744
+ const agent = this.ctx.agents.get(id);
15745
+ if (agent !== void 0) return liveSession(agent);
15746
+ const persistence = this.ctx.get("sessionPersistence");
15747
+ if (persistence !== void 0) {
15748
+ const stored = await persistence.stat(id);
15749
+ if (stored !== void 0 && sessionOrigin(stored.header).kind === "subagent") return {
15750
+ kind: "cold",
15751
+ id,
15752
+ header: stored.header
15753
+ };
15754
+ }
15755
+ const found = await this.ctx.sessionController.resolveAgent(id);
15358
15756
  if ("error" in found) throw found.error;
15359
- return found.agent;
15757
+ return liveSession(found.agent);
15360
15758
  }
15361
- async view(agent, current) {
15759
+ async view(target, current) {
15362
15760
  return {
15363
15761
  ...current,
15364
- connections: await spec.sessions.connections(agent)
15762
+ connections: await spec.sessions.connections(target)
15365
15763
  };
15366
15764
  }
15367
15765
  };
@@ -15369,55 +15767,210 @@ function createConnectionsService(ctx, spec) {
15369
15767
  }
15370
15768
  //#endregion
15371
15769
  //#region ../core/src/host/restriction.ts
15372
- /** An instance owns only its own restrictions; disposed work can never install new ones. */
15770
+ /** Owns registration and both masks, including their single ordered teardown. */
15373
15771
  var ToolRestriction = class {
15772
+ ctx;
15374
15773
  options;
15375
15774
  lifts = /* @__PURE__ */ new Map();
15775
+ enhancedLifts = /* @__PURE__ */ new Map();
15376
15776
  known = /* @__PURE__ */ new Map();
15777
+ states = /* @__PURE__ */ new Map();
15377
15778
  forgotten = /* @__PURE__ */ new WeakSet();
15779
+ removes = [];
15780
+ definitions;
15781
+ generation = 0;
15378
15782
  disposed = false;
15783
+ enhancedContext;
15379
15784
  constructor(ctx, options) {
15785
+ this.ctx = ctx;
15380
15786
  this.options = options;
15381
15787
  ctx.on("settings/document-updated", async (ns) => {
15382
- if (ns !== "web" || this.disposed) return;
15383
- const agents = [.../* @__PURE__ */ new Set([...this.known.keys(), ...this.lifts.keys()])];
15384
- const selection = options.routed() ? options.selection : void 0;
15385
- const results = await Promise.allSettled(agents.map(async (agent) => {
15386
- const connectionId = selection === void 0 ? this.known.get(agent) : await selection(agent);
15387
- if (connectionId !== void 0) this.sync(agent, connectionId);
15388
- }));
15389
- for (const result of results) if (result.status === "rejected") ctx.logger.warn("%s: refreshing tool restriction failed: %s", options.tool, result.reason);
15788
+ if (this.disposed) return;
15789
+ const enhanced = options.enhanced;
15790
+ if (ns !== "web" && (enhanced === void 0 || ns !== enhanced.rowId)) return;
15791
+ const newlyRegistered = this.reconcile();
15792
+ const nativeAgents = ns === "web" ? [...this.known.keys()] : [];
15793
+ if (!options.routed()) for (const agent of nativeAgents) this.native(agent, this.known.get(agent));
15794
+ const agents = new Set(newlyRegistered);
15795
+ if (this.registered) for (const agent of this.states.keys()) agents.add(agent);
15796
+ if (options.routed()) for (const agent of nativeAgents) agents.add(agent);
15797
+ await Promise.all([...agents].map((agent) => this.refresh(agent, nativeAgents.includes(agent))));
15390
15798
  });
15391
15799
  ctx.effect(() => () => {
15392
15800
  this.disposed = true;
15393
15801
  for (const lift of this.lifts.values()) lift();
15394
15802
  this.lifts.clear();
15395
15803
  this.known.clear();
15396
- }, options.tool + " tool restriction");
15804
+ this.states.clear();
15805
+ }, options.tool + " visibility");
15397
15806
  }
15398
15807
  get active() {
15399
15808
  return !this.disposed;
15400
15809
  }
15810
+ get registered() {
15811
+ return this.removes.length !== 0;
15812
+ }
15813
+ offered() {
15814
+ return this.active && this.enhancedContext !== void 0 && this.options.routed() && this.options.enhanced !== void 0 && this.options.enhanced.enabled();
15815
+ }
15816
+ /** Call only after all runtime closures exist. Hot-start reads never create records. */
15817
+ async start() {
15818
+ if (this.options.enhanced === void 0) return;
15819
+ await this.ctx.inject(["tools", "agents"], async (scope) => {
15820
+ this.enhancedContext = scope;
15821
+ scope.effect(() => () => {
15822
+ this.withdraw();
15823
+ this.enhancedContext = void 0;
15824
+ }, this.options.tool + " enhanced visibility");
15825
+ const agents = this.reconcile();
15826
+ await Promise.all(agents.map((agent) => this.refresh(agent, false)));
15827
+ });
15828
+ }
15829
+ created(agent) {
15830
+ if (!this.active || this.forgotten.has(agent)) return;
15831
+ this.states.set(agent, {
15832
+ version: 0,
15833
+ choice: "unknown",
15834
+ providers: "unknown"
15835
+ });
15836
+ this.apply(agent);
15837
+ }
15838
+ begin(agent) {
15839
+ const state = this.state(agent);
15840
+ state.version++;
15841
+ return {
15842
+ generation: this.generation,
15843
+ version: state.version
15844
+ };
15845
+ }
15846
+ captured(agent, read, connectionId, providers) {
15847
+ this.commit(agent, read, connectionId === null ? "off" : "on", this.providerState(providers), connectionId);
15848
+ }
15849
+ providerState(read) {
15850
+ if (read.kind === "unread") return "unknown";
15851
+ return read.ids === void 0 ? "absent" : "present";
15852
+ }
15853
+ failed(agent, read) {
15854
+ this.commit(agent, read, "unknown", "unknown");
15855
+ }
15401
15856
  sync(agent, connectionId) {
15402
- if (this.disposed || this.forgotten.has(agent)) return;
15403
- this.known.set(agent, connectionId);
15404
- const lift = this.lifts.get(agent);
15857
+ if (!this.active || this.forgotten.has(agent)) return;
15858
+ const read = this.begin(agent);
15859
+ this.commit(agent, read, connectionId === null ? "off" : "on", this.state(agent).providers, connectionId);
15860
+ }
15861
+ forget(agent) {
15862
+ this.forgotten.add(agent);
15863
+ this.known.delete(agent);
15864
+ this.states.delete(agent);
15865
+ this.lift(this.lifts, agent);
15866
+ this.lift(this.enhancedLifts, agent);
15867
+ }
15868
+ state(agent) {
15869
+ let state = this.states.get(agent);
15870
+ if (state === void 0) {
15871
+ state = {
15872
+ version: 0,
15873
+ choice: "unknown",
15874
+ providers: "unknown"
15875
+ };
15876
+ this.states.set(agent, state);
15877
+ }
15878
+ return state;
15879
+ }
15880
+ reconcile() {
15881
+ if (!this.offered()) {
15882
+ if (this.registered) this.withdraw();
15883
+ return [];
15884
+ }
15885
+ if (this.registered) return [];
15886
+ const enhanced = this.options.enhanced;
15887
+ if (this.definitions === void 0) this.definitions = enhanced.tools();
15888
+ this.generation++;
15889
+ const scope = this.enhancedContext;
15890
+ const removes = [];
15891
+ try {
15892
+ removes.push(scope.tools.register(this.definitions.action));
15893
+ removes.push(scope.tools.register(this.definitions.list));
15894
+ } catch {
15895
+ for (const remove of removes.toReversed()) remove();
15896
+ this.ctx.logger.warn("%s: enhanced tool registration failed", this.options.tool);
15897
+ return [];
15898
+ }
15899
+ this.removes = removes;
15900
+ const agents = scope.agents.list();
15901
+ for (const agent of agents) {
15902
+ this.state(agent);
15903
+ this.apply(agent);
15904
+ }
15905
+ return agents;
15906
+ }
15907
+ withdraw() {
15908
+ this.generation++;
15909
+ for (const remove of this.removes.splice(0)) remove();
15910
+ for (const lift of this.enhancedLifts.values()) lift();
15911
+ this.enhancedLifts.clear();
15912
+ }
15913
+ async refresh(agent, native) {
15914
+ if (!this.active || this.forgotten.has(agent)) return;
15915
+ const read = this.begin(agent);
15916
+ try {
15917
+ const selection = this.options.selection;
15918
+ const connectionId = selection === void 0 ? this.known.get(agent) : await selection(agent);
15919
+ if (connectionId === void 0) return;
15920
+ const enhanced = this.options.enhanced;
15921
+ let providers = { kind: "unread" };
15922
+ if (this.registered && this.offered() && enhanced !== void 0) try {
15923
+ providers = {
15924
+ kind: "read",
15925
+ ids: await enhanced.providers(agent)
15926
+ };
15927
+ } catch {
15928
+ this.ctx.logger.warn("%s: conversation provider list could not be read", this.options.tool);
15929
+ }
15930
+ this.commit(agent, read, connectionId === null ? "off" : "on", this.providerState(providers), native ? connectionId : void 0);
15931
+ } catch (error) {
15932
+ this.failed(agent, read);
15933
+ this.ctx.logger.warn("%s: refreshing tool restriction failed: %s", this.options.tool, error);
15934
+ }
15935
+ }
15936
+ commit(agent, read, choice, providers, native) {
15937
+ const state = this.states.get(agent);
15938
+ if (!this.active || this.forgotten.has(agent) || state === void 0 || read.generation !== this.generation || read.version !== state.version) return;
15939
+ if (this.registered && !this.offered()) {
15940
+ this.withdraw();
15941
+ return;
15942
+ }
15943
+ state.choice = choice;
15944
+ state.providers = providers;
15945
+ if (native !== void 0) {
15946
+ this.known.set(agent, native);
15947
+ this.native(agent, native);
15948
+ }
15949
+ this.apply(agent);
15950
+ }
15951
+ apply(agent) {
15952
+ if (!this.registered) return;
15953
+ const state = this.state(agent);
15954
+ if (state.choice === "on" && state.providers === "present") this.lift(this.enhancedLifts, agent);
15955
+ else if (!this.enhancedLifts.has(agent)) {
15956
+ const names = this.options.enhanced.names;
15957
+ this.enhancedLifts.set(agent, agent.ctx.tools.restrict({ deny: [names.action, names.list] }));
15958
+ }
15959
+ }
15960
+ native(agent, connectionId) {
15405
15961
  if (connectionId !== null || !this.options.routed()) {
15406
- this.lifts.delete(agent);
15407
- if (lift !== void 0) lift();
15962
+ this.lift(this.lifts, agent);
15408
15963
  return;
15409
15964
  }
15410
- if (lift !== void 0) return;
15965
+ if (this.lifts.has(agent)) return;
15411
15966
  const tools = agent.ctx.get("tools");
15412
15967
  const scope = scopeOf(agent.ctx);
15413
15968
  if (scope === void 0 || tools === void 0 || tools.get(this.options.tool, scope) === void 0) return;
15414
15969
  this.lifts.set(agent, tools.restrict({ deny: [this.options.tool] }));
15415
15970
  }
15416
- forget(agent) {
15417
- this.forgotten.add(agent);
15418
- this.known.delete(agent);
15419
- const lift = this.lifts.get(agent);
15420
- this.lifts.delete(agent);
15971
+ lift(lifts, agent) {
15972
+ const lift = lifts.get(agent);
15973
+ lifts.delete(agent);
15421
15974
  if (lift !== void 0) lift();
15422
15975
  }
15423
15976
  };
@@ -15438,6 +15991,207 @@ function routeFacts(ctx, field, providerId) {
15438
15991
  return routeOf(value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0, field, environment(field), providerId);
15439
15992
  }
15440
15993
  //#endregion
15994
+ //#region ../core/src/host/explicit.ts
15995
+ const rejected = (reason) => ({
15996
+ kind: "rejected",
15997
+ reason
15998
+ });
15999
+ function explicitAccess(ctx, capability, runtime, errors, unavailable) {
16000
+ const availabilityError = () => errors.error("WEB_PROVIDER_ERROR", "provider availability could not be read; retry the request");
16001
+ const diagnostic = () => {
16002
+ ctx.logger.warn("%s: provider availability could not be read", capability.identity.rowId);
16003
+ };
16004
+ function check() {
16005
+ if (!runtime.active()) return rejected({
16006
+ kind: "unavailable",
16007
+ error: unavailable()
16008
+ });
16009
+ let settings;
16010
+ try {
16011
+ settings = capability.settings();
16012
+ } catch (error) {
16013
+ return rejected({
16014
+ kind: "unavailable",
16015
+ error: error instanceof WebError ? error : unavailable()
16016
+ });
16017
+ }
16018
+ if (!runtime.offered()) return rejected({ kind: "withdrawn" });
16019
+ return {
16020
+ kind: "done",
16021
+ value: settings
16022
+ };
16023
+ }
16024
+ async function prepare(call) {
16025
+ errors.throwIfAborted(call.signal);
16026
+ const checked = check();
16027
+ if (checked.kind === "rejected") return checked;
16028
+ const plan = runtime.plan(call.agent);
16029
+ if (plan === void 0) return rejected({
16030
+ kind: "unavailable",
16031
+ error: unavailable()
16032
+ });
16033
+ if (plan.kind === "failed") return rejected({
16034
+ kind: "unavailable",
16035
+ error: plan.error
16036
+ });
16037
+ if (plan.kind === "off") return rejected({ kind: "off" });
16038
+ let ids;
16039
+ try {
16040
+ ids = await errors.abortable(runtime.providers(call.agent), call.signal);
16041
+ } catch {
16042
+ errors.throwIfAborted(call.signal);
16043
+ ctx.logger.warn("%s: conversation provider list could not be read", capability.identity.rowId);
16044
+ return rejected({
16045
+ kind: "unavailable",
16046
+ error: unavailable()
16047
+ });
16048
+ }
16049
+ errors.throwIfAborted(call.signal);
16050
+ const current = check();
16051
+ if (current.kind === "rejected") return current;
16052
+ if (ids === void 0) return rejected({ kind: "withdrawn" });
16053
+ return {
16054
+ kind: "done",
16055
+ value: {
16056
+ settings: current.value,
16057
+ plan,
16058
+ ids
16059
+ }
16060
+ };
16061
+ }
16062
+ async function views(prepared, call) {
16063
+ errors.throwIfAborted(call.signal);
16064
+ const connections = /* @__PURE__ */ new Map();
16065
+ for (const id of prepared.ids) {
16066
+ const connection = prepared.settings.connections.get(id);
16067
+ if (connection !== void 0) connections.set(id, {
16068
+ ...connection,
16069
+ disabled: false
16070
+ });
16071
+ }
16072
+ try {
16073
+ const described = await errors.abortable(capability.describe({
16074
+ ...prepared.settings,
16075
+ connections
16076
+ }, nextModel(ctx, call.agent)), call.signal);
16077
+ errors.throwIfAborted(call.signal);
16078
+ return new Map(described.map((view) => [view.id, view]));
16079
+ } catch {
16080
+ errors.throwIfAborted(call.signal);
16081
+ diagnostic();
16082
+ throw availabilityError();
16083
+ }
16084
+ }
16085
+ function readiness(view) {
16086
+ if (view === void 0) throw availabilityError();
16087
+ switch (view.status) {
16088
+ case "ready": return { status: "ready" };
16089
+ case "needs-credential": return {
16090
+ status: "needs-credential",
16091
+ credential: view.access === "account" ? "account" : "api-key"
16092
+ };
16093
+ case "unsupported":
16094
+ if (view.reason !== void 0) return {
16095
+ status: "unsupported",
16096
+ reason: view.reason
16097
+ };
16098
+ throw availabilityError();
16099
+ case "disabled": throw availabilityError();
16100
+ }
16101
+ }
16102
+ async function readyPrepared(prepared, call) {
16103
+ try {
16104
+ const described = await views(prepared, call);
16105
+ errors.throwIfAborted(call.signal);
16106
+ return {
16107
+ kind: "read",
16108
+ ids: prepared.ids.filter((id) => prepared.settings.connections.has(id) && described.get(id)?.status === "ready")
16109
+ };
16110
+ } catch {
16111
+ return { kind: "unread" };
16112
+ }
16113
+ }
16114
+ return {
16115
+ async run(connectionId, request, call) {
16116
+ const result = await prepare(call);
16117
+ errors.throwIfAborted(call.signal);
16118
+ if (result.kind === "rejected") return result;
16119
+ const prepared = result.value;
16120
+ const connection = prepared.settings.connections.get(connectionId);
16121
+ const refusal = !prepared.ids.includes(connectionId) ? "unknown" : connection === void 0 ? "removed" : void 0;
16122
+ if (refusal !== void 0) {
16123
+ const ready = await readyPrepared(prepared, call);
16124
+ errors.throwIfAborted(call.signal);
16125
+ return rejected({
16126
+ kind: refusal,
16127
+ connectionId,
16128
+ ready
16129
+ });
16130
+ }
16131
+ return ctx.get("agents").withInitiator(call.agent, async () => {
16132
+ errors.throwIfAborted(call.signal);
16133
+ const execution = {
16134
+ ctx,
16135
+ agent: call.agent,
16136
+ signal: call.signal
16137
+ };
16138
+ const target = capability.target(connection, prepared.plan.options, execution);
16139
+ return {
16140
+ kind: "done",
16141
+ value: await capability.execute(target, request, execution)
16142
+ };
16143
+ });
16144
+ },
16145
+ async list(call) {
16146
+ const result = await prepare(call);
16147
+ errors.throwIfAborted(call.signal);
16148
+ if (result.kind === "rejected") return result;
16149
+ const prepared = result.value;
16150
+ const described = await views(prepared, call);
16151
+ errors.throwIfAborted(call.signal);
16152
+ try {
16153
+ const providers = prepared.ids.flatMap((id) => {
16154
+ const connection = prepared.settings.connections.get(id);
16155
+ if (connection === void 0) return [];
16156
+ const view = described.get(id);
16157
+ const state = readiness(view);
16158
+ return [{
16159
+ id,
16160
+ name: view.label,
16161
+ facts: capability.enhanced.listing(connection, call),
16162
+ ...state
16163
+ }];
16164
+ });
16165
+ errors.throwIfAborted(call.signal);
16166
+ return {
16167
+ kind: "done",
16168
+ value: {
16169
+ session: {
16170
+ connectionId: prepared.plan.connectionId,
16171
+ selectable: isSelectable(prepared.settings.connections.get(prepared.plan.connectionId))
16172
+ },
16173
+ options: prepared.plan.options,
16174
+ providers
16175
+ }
16176
+ };
16177
+ } catch {
16178
+ errors.throwIfAborted(call.signal);
16179
+ diagnostic();
16180
+ throw availabilityError();
16181
+ }
16182
+ },
16183
+ async ready(call) {
16184
+ try {
16185
+ const prepared = await prepare(call);
16186
+ errors.throwIfAborted(call.signal);
16187
+ return prepared.kind === "rejected" ? { kind: "unread" } : await readyPrepared(prepared.value, call);
16188
+ } catch {
16189
+ return { kind: "unread" };
16190
+ }
16191
+ }
16192
+ };
16193
+ }
16194
+ //#endregion
15441
16195
  //#region ../core/src/host/storage.ts
15442
16196
  function selectionDomain(name) {
15443
16197
  const selection = looseObject({
@@ -15451,7 +16205,8 @@ function selectionDomain(name) {
15451
16205
  version: 1,
15452
16206
  tables: {
15453
16207
  selections: domainTable(selection),
15454
- pairings: domainTable(pairing)
16208
+ pairings: domainTable(pairing),
16209
+ provider_lists: domainTable(array(string$1()))
15455
16210
  }
15456
16211
  });
15457
16212
  }
@@ -15492,6 +16247,7 @@ function openDomain(ctx, name, unavailable = () => capabilityErrors(name).error(
15492
16247
  var SessionStorage = class {
15493
16248
  optionsCodec;
15494
16249
  access;
16250
+ queues = /* @__PURE__ */ new Map();
15495
16251
  constructor(ctx, name, optionsCodec, unavailable) {
15496
16252
  this.optionsCodec = optionsCodec;
15497
16253
  this.access = openDomain(ctx, name, unavailable);
@@ -15499,7 +16255,202 @@ var SessionStorage = class {
15499
16255
  domain() {
15500
16256
  return this.access.domain();
15501
16257
  }
16258
+ exclusive(id, work) {
16259
+ const previous = this.queues.get(id);
16260
+ const run = previous === void 0 ? work() : previous.then(work);
16261
+ const tail = run.then(() => {}, () => {});
16262
+ this.queues.set(id, tail);
16263
+ tail.then(() => {
16264
+ if (this.queues.get(id) === tail) this.queues.delete(id);
16265
+ });
16266
+ return run;
16267
+ }
16268
+ settled(id) {
16269
+ return this.queues.get(id);
16270
+ }
16271
+ };
16272
+ //#endregion
16273
+ //#region ../core/src/host/command.ts
16274
+ var SelectionOperationError = class extends Error {};
16275
+ const SOURCES = {
16276
+ session: "chosen in this session",
16277
+ inherited: "copied from the parent session",
16278
+ model: "remembered for this model",
16279
+ default: "the default connection"
15502
16280
  };
16281
+ function registerCommand(ctx, capability, sessions) {
16282
+ const { command, providerId, rowId } = capability.identity;
16283
+ const noun = capability.tool === "web_search" ? "web search" : "web fetch";
16284
+ const capital = capability.tool === "web_search" ? "Web search" : "Web fetch";
16285
+ const options = capability.commandOptions;
16286
+ const hint = options === void 0 ? "[off | <connection id>]" : "[off | <connection id> | freshness auto|fresh|realtime]";
16287
+ const failure = "The " + noun + " selection could not be read or saved; retry, or check the Host logs.";
16288
+ const error = (text) => ({
16289
+ kind: "error",
16290
+ text
16291
+ });
16292
+ const selectionOperation = async (work) => {
16293
+ try {
16294
+ return await work();
16295
+ } catch (cause) {
16296
+ if (cause instanceof InvalidSettingsError) throw cause;
16297
+ ctx.logger.warn("%s command selection read or write failed: %s", rowId, cause);
16298
+ throw new SelectionOperationError("selection read or write failed");
16299
+ }
16300
+ };
16301
+ const route = () => routeFacts(ctx, capability.field, providerId);
16302
+ const routing = () => routingText(route(), capability.tool, providerId);
16303
+ const unavailable = (id) => error(id + " is missing or turned off in the settings. Run /" + command + " to list the connections.");
16304
+ const success = (text) => ({
16305
+ kind: "success",
16306
+ text: text + (route().kind === "this" ? "" : "\n" + routing())
16307
+ });
16308
+ const run = async (invocation) => {
16309
+ const raw = invocation.rawInput.trim();
16310
+ const args = raw === "" ? [] : raw.split(/\s+/u);
16311
+ const usage = () => error("Unknown argument \"" + oneLine(raw, 64) + "\". Usage: /" + command + " " + hint);
16312
+ const keyword = args.length === 0 ? "" : args[0].toLowerCase();
16313
+ const freshnessValue = args.length === 2 ? args[1].toLowerCase() : "";
16314
+ const freshness = options !== void 0 && args.length === 2 && keyword === "freshness";
16315
+ if (args.length > 0 && !freshness && !(args.length === 1 && (keyword === "off" || isConnectionId(args[0])))) return usage();
16316
+ if (freshness && !FRESHNESS.some((value) => value === freshnessValue)) return error("Unknown freshness \"" + oneLine(args[1], 64) + "\". Use auto, fresh or realtime.");
16317
+ const target = liveSession(invocation.agent);
16318
+ const settings = capability.settings();
16319
+ const current = await selectionOperation(() => sessions.current(target));
16320
+ if (args.length === 0) {
16321
+ const views = await sessions.connections(target);
16322
+ const id = current.selection.connectionId;
16323
+ const selected = id === null ? void 0 : views.find((view) => view.id === id);
16324
+ let summary = id === null ? "Off" : selected === void 0 || !sessions.selectable(id) ? id : oneLine(selected.label, 200) + " (" + id + ")";
16325
+ summary += " · " + SOURCES[current.source];
16326
+ if (id !== null && !sessions.selectable(id)) summary += " · missing or turned off in the settings; " + capability.tool + " fails until another connection is chosen";
16327
+ if (options !== void 0) summary += " · freshness: " + options.freshness(current.selection.options);
16328
+ const entries = [];
16329
+ for (const view of views) {
16330
+ const connection = settings.connections.get(view.id);
16331
+ if (!isSelectable(connection)) continue;
16332
+ const name = oneLine(view.label, 200);
16333
+ const parts = [statusText(view)];
16334
+ if (view.id === id) parts.push("current");
16335
+ if (view.freshness === true) parts.push("applies freshness");
16336
+ const recipient = recipientText(capability.enhanced.listing(connection, {
16337
+ agent: invocation.agent,
16338
+ signal: invocation.signal
16339
+ }).recipient);
16340
+ if (recipient === "this machine") parts.push("fetches from this machine");
16341
+ else if (recipient !== name) parts.push((capability.tool === "web_search" ? "queries" : "URLs") + " go to " + recipient);
16342
+ entries.push("- " + view.id + " — " + name + " · " + parts.join(" · "));
16343
+ }
16344
+ if (entries.length === 0) entries.push("- (none; turn connections on in the row settings)");
16345
+ const tail = "Turn off: /" + command + " off" + (options === void 0 ? "" : " · Freshness: /" + command + " freshness auto|fresh|realtime (connections marked \"applies freshness\")");
16346
+ return {
16347
+ kind: "success",
16348
+ text: [
16349
+ summary,
16350
+ routing(),
16351
+ "Connections (/" + command + " <id> chooses one):",
16352
+ ...entries,
16353
+ tail
16354
+ ].join("\n")
16355
+ };
16356
+ }
16357
+ let change;
16358
+ let message;
16359
+ if (freshness && options !== void 0) {
16360
+ const selected = (await sessions.connections(target)).find((view) => view.id === current.selection.connectionId);
16361
+ if (selected === void 0 || selected.freshness !== true || !sessions.selectable(selected.id)) return error("The current connection does not apply freshness. Choose one marked \"applies freshness\" first.");
16362
+ const value = FRESHNESS.find((mode) => mode === freshnessValue);
16363
+ change = options.changeFreshness(value);
16364
+ message = "Freshness is " + value + " from the next model request.";
16365
+ } else if (keyword === "off") {
16366
+ change = { connectionId: null };
16367
+ message = capital + " is off for this session from the next model request.";
16368
+ } else {
16369
+ const id = args[0];
16370
+ if (!sessions.selectable(id)) return unavailable(id);
16371
+ const selected = (await sessions.connections(target)).find((view) => view.id === id);
16372
+ change = { connectionId: id };
16373
+ message = "Now using " + oneLine(selected.label, 200) + " (" + id + ") for " + noun + " from the next model request.";
16374
+ }
16375
+ const result = await selectionOperation(() => writeSelection(sessions, target, change, current.selection.revision));
16376
+ if (result.kind === "unavailable") return unavailable(result.connectionId);
16377
+ if (result.kind === "conflict") return error("The selection changed while this command ran. Run /" + command + " to see it, then try again.");
16378
+ return success(message);
16379
+ };
16380
+ ctx.effect(() => ctx.commands.register({
16381
+ name: command,
16382
+ description: "Show or change this session's " + noun + " connection",
16383
+ input: { hint },
16384
+ async handler(invocation) {
16385
+ try {
16386
+ return await run(invocation);
16387
+ } catch (cause) {
16388
+ if (cause instanceof SelectionOperationError) return error(failure);
16389
+ ctx.logger.warn("%s command failed: %s", rowId, cause);
16390
+ if (cause instanceof InvalidSettingsError) return error("The " + noun + " settings are invalid; fix them in the Enhanced " + noun + " row settings.");
16391
+ throw new Error("The " + noun + " command failed; retry, or check the Host logs.");
16392
+ }
16393
+ }
16394
+ }));
16395
+ }
16396
+ function routingText(route, tool, providerId) {
16397
+ switch (route.kind) {
16398
+ case "this": return "Routing: " + tool + " goes to this plugin.";
16399
+ case "other": return "Routing: " + tool + " goes to " + oneLine(route.provider, 200) + ", so this selection has no effect until the web row routes it to " + providerId + ".";
16400
+ case "auto": return "Routing: the web row pins no " + tool + " provider, so this selection has no effect until it routes " + tool + " to " + providerId + ".";
16401
+ case "unknown": return "Routing: the web row could not be read, so this selection may have no effect.";
16402
+ }
16403
+ }
16404
+ function statusText(view) {
16405
+ switch (view.status) {
16406
+ case "ready": return "ready";
16407
+ case "needs-credential": return view.access === "account" ? "needs DeepSeek sign-in" : "needs an API key";
16408
+ case "unsupported": return "unavailable: " + UNSUPPORTED_REASON_TEXT[view.reason];
16409
+ case "disabled": return "missing or turned off in the settings";
16410
+ }
16411
+ }
16412
+ //#endregion
16413
+ //#region ../core/src/host/terminal.ts
16414
+ function registerTerminal(ctx, root, noun, settings, freshness) {
16415
+ ctx.effect(() => ctx.tuiCommandTrees.register({
16416
+ root,
16417
+ children(path) {
16418
+ if (path[0] !== root) return [];
16419
+ if (freshness && path.length === 2 && path[1] === "freshness") return [
16420
+ {
16421
+ name: "auto",
16422
+ description: "Auto"
16423
+ },
16424
+ {
16425
+ name: "fresh",
16426
+ description: "Within 24 hours"
16427
+ },
16428
+ {
16429
+ name: "realtime",
16430
+ description: "Live"
16431
+ }
16432
+ ];
16433
+ if (path.length !== 1) return [];
16434
+ const nodes = [{
16435
+ name: "off",
16436
+ description: "Turn " + noun + " off for this session"
16437
+ }];
16438
+ try {
16439
+ for (const connection of settings().connections.values()) if (isSelectable(connection)) nodes.push({
16440
+ name: connection.id,
16441
+ description: oneLine(connection.label, 200)
16442
+ });
16443
+ } catch (error) {
16444
+ if (!(error instanceof InvalidSettingsError)) throw error;
16445
+ }
16446
+ if (freshness) nodes.push({
16447
+ name: "freshness",
16448
+ description: "Content freshness, for connections that apply it"
16449
+ });
16450
+ return nodes;
16451
+ }
16452
+ }));
16453
+ }
15503
16454
  //#endregion
15504
16455
  //#region ../core/src/host/runtime.ts
15505
16456
  /** Install the sole Seat, session lifecycle, route-guarded restrictions and Remote service. */
@@ -15510,24 +16461,56 @@ function installCapability(ctx, capability) {
15510
16461
  const invalid = () => errors.error("WEB_PROVIDER_CONFIGURED_UNAVAILABLE", "the selected " + kind + " connection is unavailable; ask the user to enable it or choose another connection");
15511
16462
  const storage = new SessionStorage(ctx, capability.storageDomain, capability.optionsCodec, () => errors.error("WEB_PROVIDER_ERROR", kind + " selection storage is unavailable; retry or ask the user to check the DSH storage backend"));
15512
16463
  const pairings = new Pairings(storage);
16464
+ const lists = new ProviderLists(storage);
16465
+ const snapshots = new Snapshots();
16466
+ const access = explicitAccess(ctx, capability, {
16467
+ active: () => restriction.active,
16468
+ offered: () => restriction.offered(),
16469
+ plan: (agent) => snapshots.get(agent),
16470
+ providers: (agent) => lists.read(agent)
16471
+ }, errors, contextUnavailable);
15513
16472
  const restriction = new ToolRestriction(ctx, {
15514
16473
  tool: capability.tool,
15515
16474
  routed: () => routeFacts(ctx, capability.field, capability.identity.providerId).kind === "this",
15516
- selection: async (agent) => (await sessions.peek(agent)).selection.connectionId
16475
+ selection: async (agent) => (await sessions.peek(liveSession(agent))).selection.connectionId,
16476
+ enhanced: {
16477
+ rowId: capability.identity.rowId,
16478
+ names: capability.enhanced.names,
16479
+ enabled: () => {
16480
+ try {
16481
+ return capability.settings().enhancedTools;
16482
+ } catch (error) {
16483
+ if (error instanceof InvalidSettingsError) return false;
16484
+ throw error;
16485
+ }
16486
+ },
16487
+ tools: () => capability.enhanced.tools(access),
16488
+ providers: (agent) => lists.read(agent)
16489
+ }
15517
16490
  });
15518
- const sessions = new Sessions(ctx, () => capability.settings(), storage, pairings, restriction, (settings, agent) => capability.describe(settings, agent));
15519
- const snapshots = new Snapshots();
16491
+ const sessions = new Sessions(ctx, () => capability.settings(), storage, pairings, restriction, (settings, model) => capability.describe(settings, model));
15520
16492
  const capture = async (agent, signal) => {
16493
+ const operation = restriction.begin(agent);
15521
16494
  try {
15522
- const read = sessions.current(agent);
16495
+ const read = sessions.current(liveSession(agent));
15523
16496
  const { selection } = signal === void 0 ? await read : await errors.abortable(read, signal);
15524
16497
  if (!restriction.active) return {
15525
16498
  kind: "failed",
15526
16499
  error: contextUnavailable()
15527
16500
  };
15528
- restriction.sync(agent, selection.connectionId);
16501
+ let providers = { kind: "unread" };
16502
+ if (restriction.registered && restriction.offered()) try {
16503
+ providers = {
16504
+ kind: "read",
16505
+ ids: await lists.read(agent)
16506
+ };
16507
+ } catch {
16508
+ ctx.logger.warn("%s: conversation provider list could not be read", capability.identity.rowId);
16509
+ }
16510
+ restriction.captured(agent, operation, selection.connectionId, providers);
15529
16511
  return planFor(selection);
15530
16512
  } catch (error) {
16513
+ restriction.failed(agent, operation);
15531
16514
  ctx.logger.warn("%s: capturing %s selection failed: %s", capability.identity.rowId, kind, error);
15532
16515
  return {
15533
16516
  kind: "failed",
@@ -15540,13 +16523,33 @@ function installCapability(ctx, capability) {
15540
16523
  return next();
15541
16524
  });
15542
16525
  ctx.on("agent/created", async ({ agent }) => {
16526
+ restriction.created(agent);
16527
+ const initial = isNewConversation(agent.session) ? restriction.offered() ? [...capability.settings().connections.values()].filter(isSelectable).map((connection) => connection.id) : [] : void 0;
16528
+ try {
16529
+ await lists.create(agent, initial);
16530
+ } catch {
16531
+ ctx.logger.warn("%s: conversation provider list could not be read", capability.identity.rowId);
16532
+ }
15543
16533
  await snapshots.capture(agent, -1, -1, () => capture(agent));
15544
16534
  });
16535
+ restriction.start().catch((error) => {
16536
+ ctx.logger.warn("%s: starting tool visibility failed: %s", capability.identity.rowId, error);
16537
+ });
15545
16538
  ctx.on("agent/disposed", ({ agent }) => {
15546
16539
  snapshots.forget(agent);
15547
16540
  restriction.forget(agent);
15548
16541
  });
15549
- ctx.inject(["sessionController", "typert"], (scope) => {
16542
+ ctx.inject(["commands"], (scope) => {
16543
+ registerCommand(scope, capability, sessions);
16544
+ });
16545
+ ctx.inject(["tuiCommandTrees"], (scope) => {
16546
+ registerTerminal(scope, capability.identity.command, "web " + kind, () => capability.settings(), capability.commandOptions !== void 0);
16547
+ });
16548
+ ctx.inject([
16549
+ "sessionController",
16550
+ "typert",
16551
+ "agents"
16552
+ ], (scope) => {
15550
16553
  createConnectionsService(scope, {
15551
16554
  namespace: capability.remoteNamespace,
15552
16555
  contract: capability.contract,
@@ -16428,7 +17431,7 @@ const { withDeadline, readJson, requestJson, invalidResponse } = transport;
16428
17431
  const mcp = createMcpClient(transport, searchErrors, {
16429
17432
  clientInfo: {
16430
17433
  name: SEARCH.packageName,
16431
- version: "0.2.0"
17434
+ version: "0.2.2"
16432
17435
  },
16433
17436
  messages: {
16434
17437
  toolFailed: (label, detail) => label + " could not complete the search; retry later or choose another search connection" + upstreamSuffix(label, detail),
@@ -16534,6 +17537,7 @@ const location = {
16534
17537
  const MODEL_PROTOCOLS = {
16535
17538
  "anthropic-messages": {
16536
17539
  label: "Anthropic Messages",
17540
+ use: "the model searches and answers",
16537
17541
  options: {
16538
17542
  maxTokens,
16539
17543
  anthropicTool,
@@ -16549,6 +17553,7 @@ const MODEL_PROTOCOLS = {
16549
17553
  },
16550
17554
  "openai-responses": {
16551
17555
  label: "OpenAI Responses",
17556
+ use: "the model searches and answers",
16552
17557
  options: {
16553
17558
  maxTokens,
16554
17559
  responsesTool,
@@ -16562,6 +17567,7 @@ const MODEL_PROTOCOLS = {
16562
17567
  },
16563
17568
  "openai-chat-completions": {
16564
17569
  label: "OpenAI Chat Completions",
17570
+ use: "the model searches and answers",
16565
17571
  options: {
16566
17572
  maxTokens,
16567
17573
  searchContextSize,
@@ -16593,6 +17599,8 @@ const CATALOG = {
16593
17599
  label: "DeepSeek Messages",
16594
17600
  kind: "native",
16595
17601
  remote: true,
17602
+ optIn: false,
17603
+ use: "answers with a summary and sources",
16596
17604
  endpoint: "https://api.deepseek.com/anthropic/v1",
16597
17605
  homepage: "https://platform.deepseek.com",
16598
17606
  access: nativeAccess,
@@ -16623,6 +17631,7 @@ const CATALOG = {
16623
17631
  label: "DeepSeek Search API",
16624
17632
  kind: "native",
16625
17633
  remote: true,
17634
+ optIn: true,
16626
17635
  endpoint: "https://api.deepseek.com/v1/tools",
16627
17636
  homepage: "https://platform.deepseek.com",
16628
17637
  access: nativeAccess,
@@ -16641,6 +17650,8 @@ const CATALOG = {
16641
17650
  label: "Session model",
16642
17651
  kind: "model",
16643
17652
  remote: true,
17653
+ optIn: false,
17654
+ use: "the session model searches and answers",
16644
17655
  access: {
16645
17656
  keyless: false,
16646
17657
  apiKey: false,
@@ -16660,6 +17671,7 @@ const CATALOG = {
16660
17671
  },
16661
17672
  kind: "adapter",
16662
17673
  remote: true,
17674
+ optIn: true,
16663
17675
  freshness: ["api-key"],
16664
17676
  homepage: "https://exa.ai",
16665
17677
  options: { type: {
@@ -16682,6 +17694,7 @@ const CATALOG = {
16682
17694
  },
16683
17695
  kind: "adapter",
16684
17696
  remote: true,
17697
+ optIn: true,
16685
17698
  freshness: ["keyless", "api-key"],
16686
17699
  homepage: "https://www.firecrawl.dev",
16687
17700
  options: {
@@ -16707,6 +17720,7 @@ const CATALOG = {
16707
17720
  },
16708
17721
  kind: "adapter",
16709
17722
  remote: true,
17723
+ optIn: true,
16710
17724
  freshness: [],
16711
17725
  homepage: "https://www.tavily.com",
16712
17726
  options: {
@@ -16739,6 +17753,7 @@ const CATALOG = {
16739
17753
  },
16740
17754
  kind: "adapter",
16741
17755
  remote: true,
17756
+ optIn: true,
16742
17757
  freshness: [],
16743
17758
  homepage: "https://www.tinyfish.ai",
16744
17759
  options: {
@@ -16775,8 +17790,10 @@ const CATALOG = {
16775
17790
  },
16776
17791
  kind: "adapter",
16777
17792
  remote: true,
17793
+ optIn: true,
16778
17794
  freshness: [],
16779
17795
  homepage: "https://openalex.org",
17796
+ use: "scholarly works (papers, citations); keyword queries",
16780
17797
  options: {
16781
17798
  sort: {
16782
17799
  kind: "text",
@@ -16805,8 +17822,10 @@ const CATALOG = {
16805
17822
  },
16806
17823
  kind: "adapter",
16807
17824
  remote: true,
17825
+ optIn: true,
16808
17826
  freshness: [],
16809
17827
  homepage: "https://www.semanticscholar.org/product/api",
17828
+ use: "academic papers (citations, TL;DR); keyword queries",
16810
17829
  options: {
16811
17830
  year: {
16812
17831
  kind: "text",
@@ -17066,17 +18085,9 @@ function sourceFields(source) {
17066
18085
  }
17067
18086
  //#endregion
17068
18087
  //#region src/model/execute.ts
17069
- const UNSUPPORTED = {
17070
- "no-model": "no session model is available",
17071
- "unknown-route": "the model route is unknown",
17072
- protocol: "the route does not declare a supported protocol",
17073
- endpoint: "the route has no supported endpoint",
17074
- headers: "the route uses custom request headers",
17075
- credential: "the route has no supported credential reference"
17076
- };
17077
18088
  async function executeModelSearch(target, request, deps, signal) {
17078
18089
  throwIfAborted(signal);
17079
- if (target.route.kind === "unsupported") throw searchError("WEB_PROVIDER_CONFIGURED_UNAVAILABLE", "the session model cannot run web search: " + UNSUPPORTED[target.route.reason] + "; choose another search connection");
18090
+ if (target.route.kind === "unsupported") throw searchError("WEB_PROVIDER_CONFIGURED_UNAVAILABLE", "the session model cannot run web search: " + UNSUPPORTED_REASON_TEXT[target.route.reason] + "; choose another search connection");
17080
18091
  const route = target.route.route;
17081
18092
  let matcher;
17082
18093
  try {
@@ -17114,15 +18125,24 @@ async function executeModelSearch(target, request, deps, signal) {
17114
18125
  async function credential(route, deps, signal) {
17115
18126
  const { auth } = route;
17116
18127
  if (auth.kind === "account") {
17117
- const token = await abortable(deps.accountToken(modelUrl(route.protocol, route.baseURL)), signal);
18128
+ const endpoint = modelUrl(route.protocol, route.baseURL);
18129
+ const token = await readCredential(() => deps.accountToken(endpoint), signal);
17118
18130
  if (token === void 0 || !/^[\x20-\x7e]+$/u.test(token)) throw searchError("WEB_PROVIDER_CREDENTIAL_MISSING", "the DeepSeek account sign-in is unavailable for this search; ask the user to sign in again or choose another search connection");
17119
18131
  return token;
17120
18132
  }
17121
- const stored = storedApiKey(await abortable(deps.apiKey(auth.credentialRef, auth.source), signal));
18133
+ const stored = storedApiKey(await readCredential(() => deps.apiKey(auth.credentialRef, auth.source), signal));
17122
18134
  if (stored.kind === "missing") throw searchError("WEB_PROVIDER_CREDENTIAL_MISSING", credentialMissing(auth.credentialRef, auth.source));
17123
18135
  if (stored.kind === "malformed") throw searchError("WEB_PROVIDER_CREDENTIAL_MISSING", credentialInvalid(auth.credentialRef, auth.source));
17124
18136
  return stored.key;
17125
18137
  }
18138
+ async function readCredential(resolve, signal) {
18139
+ try {
18140
+ return await abortable(resolve(), signal);
18141
+ } catch {
18142
+ throwIfAborted(signal);
18143
+ throw new Error("Model credential resolution failed");
18144
+ }
18145
+ }
17126
18146
  //#endregion
17127
18147
  //#region src/contract.ts
17128
18148
  const contract = selectionContract({
@@ -17216,15 +18236,287 @@ function createSearchSeat(id, select) {
17216
18236
  };
17217
18237
  }
17218
18238
  //#endregion
18239
+ //#region src/host/listing.ts
18240
+ /**
18241
+ * What list_web_search_providers says about one connection (docs/design.md, "列出工具"):
18242
+ * its catalog use, who receives the query and whether it applies the Session's
18243
+ * freshness. Reads only settings and public model facts; no I/O, no secrets.
18244
+ */
18245
+ function searchListing(ctx, connection, call) {
18246
+ switch (connection.kind) {
18247
+ case "native": {
18248
+ const info = CATALOG[connection.native];
18249
+ const { baseURL } = connection.options;
18250
+ return {
18251
+ use: info.use,
18252
+ recipient: service(info.label, typeof baseURL === "string" ? baseURL : info.endpoint, info.endpoint),
18253
+ appliesFreshness: false
18254
+ };
18255
+ }
18256
+ case "adapter": {
18257
+ const info = CATALOG[connection.adapter];
18258
+ const endpoint = connection.access === "keyless" ? info.endpoint : connection.endpoint;
18259
+ return {
18260
+ use: info.use,
18261
+ recipient: service(info.label, endpoint, info.endpoint),
18262
+ appliesFreshness: honorsFreshness(connection.adapter, connection.access)
18263
+ };
18264
+ }
18265
+ case "model": {
18266
+ if (connection.route.kind === "session") {
18267
+ const model = nextModel(ctx, call.agent);
18268
+ return {
18269
+ use: CATALOG["session-model"].use,
18270
+ recipient: {
18271
+ kind: "session-model",
18272
+ provider: model === void 0 ? void 0 : model.provider
18273
+ },
18274
+ appliesFreshness: false
18275
+ };
18276
+ }
18277
+ const protocol = MODEL_PROTOCOLS[connection.route.protocol];
18278
+ return {
18279
+ use: protocol.use,
18280
+ recipient: {
18281
+ kind: "service",
18282
+ service: protocol.label,
18283
+ host: new URL(connection.route.endpoint).hostname
18284
+ },
18285
+ appliesFreshness: false
18286
+ };
18287
+ }
18288
+ }
18289
+ }
18290
+ /** The catalog service; an operator endpoint that differs from the catalog's is named by its hostname. */
18291
+ function service(label, endpoint, catalogEndpoint) {
18292
+ return {
18293
+ kind: "service",
18294
+ service: label,
18295
+ host: endpoint === catalogEndpoint ? void 0 : new URL(endpoint).hostname
18296
+ };
18297
+ }
18298
+ //#endregion
18299
+ //#region src/tools/queries.ts
18300
+ /**
18301
+ * The multi-query part of `web_search`, copied from `@deepseek-ai/dsh-tool-web`
18302
+ * 0.2.1-rc.2 (packages/web/tool-web/src/search.ts: `parseSearchArgs`,
18303
+ * `runSearchQueries`, `mergeSearchResults`, `projectSource`) and
18304
+ * `@deepseek-ai/dsh-web` (packages/web/web/src/index.ts: `capSources`), which do
18305
+ * not export them. The enhanced tool bypasses `ctx.web`, so it applies the
18306
+ * seam's `capSources` itself. Keep in step with DSH on baseline upgrades;
18307
+ * tests/tools-search.spec.ts compares the result with the real tool-web.
18308
+ */
18309
+ /** tool-web's `parseSearchArgs`: 1–4 non-blank queries, exact duplicates collapsed after the bound check. */
18310
+ function parseQueries(queries) {
18311
+ if (queries.length === 0) throw invalidArguments("queries must contain at least one query");
18312
+ if (queries.length > WEB_SEARCH_MAX_QUERIES) throw invalidArguments(`queries must contain at most ${WEB_SEARCH_MAX_QUERIES} queries`);
18313
+ if (queries.some((query) => query.trim().length === 0)) throw invalidArguments("each query must be a non-empty string");
18314
+ return [...new Set(queries)];
18315
+ }
18316
+ /**
18317
+ * tool-web's `runSearchQueries`: one query keeps its (capped) result; several
18318
+ * run concurrently and merge. A failed search aborts its siblings, and the
18319
+ * first failure is rethrown once every search has settled.
18320
+ */
18321
+ async function searchQueries(queries, run, signal) {
18322
+ const search = async (query, querySignal) => capSources(await run({
18323
+ query,
18324
+ maxResults: WEB_SEARCH_MAX_RESULTS
18325
+ }, querySignal), WEB_SEARCH_MAX_RESULTS);
18326
+ const [only] = queries;
18327
+ if (queries.length === 1 && only !== void 0) return search(only, signal);
18328
+ const controller = new AbortController();
18329
+ const batchSignal = AbortSignal.any([signal, controller.signal]);
18330
+ let firstFailure;
18331
+ const results = [];
18332
+ const searches = queries.map(async (query, index) => {
18333
+ try {
18334
+ results[index] = await search(query, batchSignal);
18335
+ } catch (error) {
18336
+ if (firstFailure === void 0) firstFailure = { error };
18337
+ controller.abort(error);
18338
+ throw error;
18339
+ }
18340
+ });
18341
+ await Promise.allSettled(searches);
18342
+ if (firstFailure !== void 0) throw firstFailure.error;
18343
+ return mergeSearchResults(queries, results, WEB_SEARCH_MAX_RESULTS);
18344
+ }
18345
+ /** tool-web's `mergeSearchResults`: round-robin by rank, deduplicated by URL, capped; answers joined under `### <query>`. */
18346
+ function mergeSearchResults(queries, results, maxResults) {
18347
+ const seen = /* @__PURE__ */ new Set();
18348
+ const sources = [];
18349
+ let sourceRanks = 0;
18350
+ for (const result of results) sourceRanks = Math.max(sourceRanks, result.sources.length);
18351
+ let droppedSource = false;
18352
+ merge: for (let rank = 0; rank < sourceRanks; rank++) for (const result of results) {
18353
+ const source = result.sources[rank];
18354
+ if (source !== void 0 && !seen.has(source.url)) {
18355
+ seen.add(source.url);
18356
+ if (sources.length === maxResults) {
18357
+ droppedSource = true;
18358
+ break merge;
18359
+ }
18360
+ sources.push(source);
18361
+ }
18362
+ }
18363
+ const contents = results.flatMap((result, index) => {
18364
+ if (result.content === void 0 || result.content.length === 0) return [];
18365
+ return [`### ${queries[index]}\n\n${result.content}`];
18366
+ });
18367
+ return {
18368
+ ...contents.length > 0 ? { content: contents.join("\n\n") } : {},
18369
+ sources,
18370
+ truncated: results.some((result) => result.truncated) || droppedSource
18371
+ };
18372
+ }
18373
+ /** dsh-web's `capSources`: the seam's cut of an over-returning provider. */
18374
+ function capSources(result, maxResults) {
18375
+ if (result.sources.length <= maxResults) return result;
18376
+ return {
18377
+ ...result,
18378
+ sources: result.sources.slice(0, maxResults),
18379
+ truncated: true
18380
+ };
18381
+ }
18382
+ /** tool-web's `projectSource`: `{ url }` plus each present optional field. */
18383
+ function projectSource(source) {
18384
+ return {
18385
+ url: source.url,
18386
+ ...source.title !== void 0 ? { title: source.title } : {},
18387
+ ...source.snippet !== void 0 ? { snippet: source.snippet } : {},
18388
+ ...source.publishedAt !== void 0 ? { publishedAt: source.publishedAt } : {}
18389
+ };
18390
+ }
18391
+ //#endregion
18392
+ //#region src/tools/definitions.ts
18393
+ const { action, list } = SEARCH.tools;
18394
+ const VOICE = {
18395
+ identity: SEARCH,
18396
+ capability: "web search",
18397
+ nativeTool: "web_search",
18398
+ sends: "queries"
18399
+ };
18400
+ /** Slightly longer than the remote backends' 90 s deadline, so their worded timeout arrives first (docs/design.md, "工具与输出"). */
18401
+ const SEARCH_TIMEOUT_MS = 1e5;
18402
+ /** `web_search`'s output schema plus the provider that served the call. */
18403
+ const SEARCH_RESULT_SCHEMA = {
18404
+ type: "object",
18405
+ additionalProperties: false,
18406
+ properties: {
18407
+ provider: {
18408
+ type: "string",
18409
+ required: true
18410
+ },
18411
+ content: { type: "string" },
18412
+ sources: {
18413
+ type: "array",
18414
+ required: true,
18415
+ items: {
18416
+ type: "object",
18417
+ additionalProperties: false,
18418
+ properties: {
18419
+ url: {
18420
+ type: "string",
18421
+ required: true
18422
+ },
18423
+ title: { type: "string" },
18424
+ snippet: { type: "string" },
18425
+ publishedAt: { type: "string" }
18426
+ }
18427
+ }
18428
+ },
18429
+ truncated: {
18430
+ type: "boolean",
18431
+ required: true
18432
+ }
18433
+ }
18434
+ };
18435
+ /** The two static search definitions (docs/design.md, "工具与输出"); available providers never enter them. */
18436
+ function searchTools(access) {
18437
+ return {
18438
+ action: defineTool({
18439
+ name: action,
18440
+ description: `Search the web through a provider you choose. Use web_search unless you need a specific provider: one the user named, a specialized index, or another after web_search fails.`,
18441
+ parameters: {
18442
+ queries: {
18443
+ type: "array",
18444
+ required: true,
18445
+ items: { type: "string" },
18446
+ description: `1–${WEB_SEARCH_MAX_QUERIES} search queries; their results are merged.`
18447
+ },
18448
+ provider: {
18449
+ type: "string",
18450
+ required: true,
18451
+ description: `Provider id exactly as listed by ${list}.`
18452
+ }
18453
+ },
18454
+ output: {
18455
+ schema: SEARCH_RESULT_SCHEMA,
18456
+ render: (_args, value) => [{
18457
+ type: "text",
18458
+ text: `Provider: ${value.provider}\n\n${formatSearchOutput(value)}`
18459
+ }],
18460
+ presentationMeta: (_args, value) => searchMetaFromValue(value)
18461
+ },
18462
+ timeoutMs: SEARCH_TIMEOUT_MS,
18463
+ isConcurrencySafe: () => true,
18464
+ async execute(args, exec) {
18465
+ rejectUnknownProperties(args, ["queries", "provider"]);
18466
+ const queries = parseQueries(args.queries);
18467
+ const call = explicitCall(action, exec);
18468
+ const provider = await providerArgument(VOICE, args.provider, access, call);
18469
+ const result = await searchQueries(queries, (request, signal) => runOnProvider(VOICE, access, provider, request, {
18470
+ agent: call.agent,
18471
+ signal
18472
+ }), call.signal);
18473
+ return {
18474
+ provider,
18475
+ ...result.content !== void 0 ? { content: result.content } : {},
18476
+ sources: result.sources.map(projectSource),
18477
+ truncated: result.truncated
18478
+ };
18479
+ }
18480
+ }),
18481
+ list: defineTool({
18482
+ name: list,
18483
+ description: `List the providers ${action} accepts, with what each is for and whether it is ready.`,
18484
+ parameters: {},
18485
+ output: {
18486
+ schema: searchListingSchema(),
18487
+ render: (_args, value) => [{
18488
+ type: "text",
18489
+ text: renderSearchListing(VOICE, value)
18490
+ }]
18491
+ },
18492
+ isConcurrencySafe: () => true,
18493
+ async execute(args, exec) {
18494
+ rejectUnknownProperties(args, []);
18495
+ return searchListingValue(await listProviders(VOICE, access, explicitCall(list, exec)));
18496
+ }
18497
+ })
18498
+ };
18499
+ }
18500
+ //#endregion
18501
+ //#region src/tools/index.ts
18502
+ /** The search side of the enhanced tools (docs/design.md, "增强工具"). */
18503
+ function searchEnhanced(ctx) {
18504
+ return {
18505
+ names: SEARCH.tools,
18506
+ tools: searchTools,
18507
+ listing: (connection, call) => searchListing(ctx, connection, call)
18508
+ };
18509
+ }
18510
+ //#endregion
17219
18511
  //#region src/host/connections.ts
17220
- async function describeConnections(ctx, settings, agent) {
18512
+ async function describeConnections(ctx, settings, model) {
17221
18513
  return Promise.all([...settings.connections.values()].map(async (connection) => {
17222
18514
  const base = baseOf(connection);
17223
18515
  if (connection.kind === "model") {
17224
18516
  const target = connection.route.kind === "fixed" ? {
17225
18517
  kind: "route",
17226
18518
  route: fixedRoute(connection.route)
17227
- } : sessionRoute(ctx, nextModel(ctx, agent));
18519
+ } : sessionRoute(ctx, model);
17228
18520
  const access = target.kind === "route" ? target.route.auth.kind : "api-key";
17229
18521
  if (connection.disabled) return {
17230
18522
  ...base,
@@ -17257,7 +18549,7 @@ async function describeConnections(ctx, settings, agent) {
17257
18549
  const endpoint = connection.kind === "native" ? typeof connection.options.baseURL === "string" ? connection.options.baseURL : CATALOG[connection.native].endpoint : void 0;
17258
18550
  return {
17259
18551
  ...base,
17260
- ...await credentialReadiness(ctx, agent, {
18552
+ ...await credentialReadiness(ctx, model, {
17261
18553
  endpoint,
17262
18554
  credentialRef: connection.credentialRef
17263
18555
  })
@@ -17302,7 +18594,11 @@ function searchCapability(ctx, settings) {
17302
18594
  contract,
17303
18595
  optionsCodec,
17304
18596
  settings,
17305
- describe: (current, agent) => describeConnections(ctx, current, agent),
18597
+ commandOptions: {
18598
+ freshness: (options) => options.freshness,
18599
+ changeFreshness: (freshness) => ({ freshness })
18600
+ },
18601
+ describe: (current, model) => describeConnections(ctx, current, model),
17306
18602
  target: (connection, options, call) => {
17307
18603
  switch (connection.kind) {
17308
18604
  case "adapter": return {
@@ -17349,7 +18645,8 @@ function searchCapability(ctx, settings) {
17349
18645
  available: () => true,
17350
18646
  search: invoke
17351
18647
  })));
17352
- }
18648
+ },
18649
+ enhanced: searchEnhanced(ctx)
17353
18650
  };
17354
18651
  }
17355
18652
  function installSearch(ctx, settings) {