@agent-surface/core 0.2.0 → 0.4.0

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/README.md CHANGED
@@ -40,7 +40,8 @@ registry.register(
40
40
 
41
41
  const toolset = createAgentToolset(registry, {
42
42
  consumer: { id: "copilot", kind: "embedded" },
43
- });
43
+ topology: "embedded", // required (D26): embedded → confirmations "wait",
44
+ }); // remote → "two-phase". No global default.
44
45
  ```
45
46
 
46
47
  React apps should use [`@agent-surface/react`](https://www.npmjs.com/package/@agent-surface/react) instead of calling `register` directly. Full specification: [docs](https://github.com/Wiseair-srl/agent-surface/tree/main/docs).
package/dist/index.d.ts CHANGED
@@ -136,13 +136,41 @@ declare function encodeWireName(id: string): string;
136
136
  * Wire name disambiguated per instance: providers require UNIQUE tool names,
137
137
  * so when several live instances expose the same capability the adapter
138
138
  * appends `_at_<instanceId>` (docs/09 rule 7 — the id↔name map stays
139
- * authoritative; instance ids never contain "." so decoding stays possible
140
- * by stripping the suffix).
139
+ * authoritative).
140
+ *
141
+ * The result is ALWAYS ≤ 64 characters (`AS-WIRE-004`) and deterministic for a
142
+ * given `(id, instanceId, level)` (`AS-WIRE-005`). `level` escalates the hash
143
+ * when a catalog would otherwise emit the same name twice — see
144
+ * {@link assignWireNames}, which owns that check; callers with a whole catalog
145
+ * in hand should use it rather than this function directly.
146
+ */
147
+ declare function encodeWireNameForInstance(id: string, instanceId?: string, level?: number): string;
148
+ interface WireNameEntry {
149
+ /** Canonical capability id. */
150
+ id: string;
151
+ /** Instance disambiguator, when several live instances share the id. */
152
+ instanceId?: string;
153
+ }
154
+ interface WireNameAssignment {
155
+ /** Emitted names, positionally aligned with the input entries. */
156
+ names: string[];
157
+ /** wireName → canonical id. Authoritative; shortened names are not decodable. */
158
+ byName: ReadonlyMap<string, string>;
159
+ }
160
+ /**
161
+ * Assigns wire names to a whole catalog, guaranteeing uniqueness within it
162
+ * (`AS-WIRE-006`). Two distinct entries that collide are BOTH re-encoded at the
163
+ * next hash level, so the outcome depends on the set of entries and not on
164
+ * their order (`AS-WIRE-005`). Escalation is bounded; the last level appends
165
+ * the entry's rank among the colliding keys, which terminates by construction.
141
166
  */
142
- declare function encodeWireNameForInstance(id: string, instanceId?: string): string;
167
+ declare function assignWireNames(entries: readonly WireNameEntry[]): WireNameAssignment;
143
168
  /**
144
- * Reverses `encodeWireName` for non-truncated names. Truncated names cannot
145
- * be decoded the adapter's id↔name map is authoritative (docs/09 rule 7).
169
+ * Reverses `encodeWireName` for names that were encoded faithfully, and returns
170
+ * `undefined` for every name that was not shortened names and per-instance
171
+ * names among them (`AS-WIRE-007`: consult `toolset.wireNameMap()` instead).
172
+ * Returning a plausible-but-wrong canonical id would take the audit identity
173
+ * with it, so this refuses anything it cannot re-encode byte-identically.
146
174
  */
147
175
  declare function decodeWireName(name: string): string | undefined;
148
176
 
@@ -671,6 +699,15 @@ interface AgentSurfaceSnapshot {
671
699
  truncated?: {
672
700
  droppedComponents: number;
673
701
  };
702
+ /**
703
+ * [Experimental] Present iff a configured scope floor refused part of a
704
+ * requested scope (D27) — set by the adapter, never by `snapshot()`, which
705
+ * has no floor to intersect against. Empty `components` alongside this marker
706
+ * means the request fell outside the floor, not that the surface is empty.
707
+ */
708
+ scopeRejected?: {
709
+ prefixes: string[];
710
+ };
674
711
  }
675
712
  interface AgentComponentDescriptor {
676
713
  type: string;
@@ -710,7 +747,20 @@ interface AgentActionDescriptor {
710
747
  }
711
748
  interface AgentProcedureDescriptor {
712
749
  procedureId: string;
750
+ /**
751
+ * The manifest description. Stable across snapshots — the contextual
752
+ * `describe()` output is `contextualNote`, not part of this string, unless
753
+ * the registry was created with `snapshotMergesContextualNote: true`
754
+ * (the 0.2 default, removed in a later minor — D28).
755
+ */
713
756
  description: string;
757
+ /**
758
+ * Volatile: this snapshot's contextual `describe()` output, if any. Always
759
+ * populated, in both merge modes, so a host can migrate before the default
760
+ * moves. Use {@link stableDescriptionOf} to recover the note-free
761
+ * description without parsing.
762
+ */
763
+ contextualNote?: string;
714
764
  /** Agent-facing (reduced) input schema per binding rule 1 (docs/05). */
715
765
  inputSchema: JsonSchema;
716
766
  outputSchema?: JsonSchema;
@@ -733,6 +783,15 @@ interface AgentProcedureDescriptor {
733
783
  meta?: Record<string, JsonValue>;
734
784
  }
735
785
  type AgentCapabilityDescriptorUnion = AgentObservationDescriptor | AgentActionDescriptor | AgentProcedureDescriptor;
786
+ /**
787
+ * The note-free description of a descriptor, whichever way the registry was
788
+ * configured to compose it (D28). The merge rule is `${description} ${note}`,
789
+ * so the split is exact — no host ever has to parse a prefix it did not write.
790
+ */
791
+ declare function stableDescriptionOf(descriptor: {
792
+ description: string;
793
+ contextualNote?: string;
794
+ }): string;
736
795
 
737
796
  interface RegistrationCandidate {
738
797
  definition: AgentComponentDefinition;
@@ -756,6 +815,15 @@ interface RegistryOptions {
756
815
  /** Route descriptor for snapshots (host wires its router here). */
757
816
  route?: () => AgentRouteInfo | undefined;
758
817
  limits?: Partial<AgentSurfaceLimits>;
818
+ /**
819
+ * D28 compatibility flag. `true` (default, for one minor) folds a procedure
820
+ * reference's contextual `describe()` output into
821
+ * `AgentProcedureDescriptor.description`, as 0.1 did. `false` keeps the two
822
+ * apart, so `description` is stable across snapshots and the live text is
823
+ * read from `contextualNote`. Populated either way; the default flips in a
824
+ * later minor and the flag is then removed.
825
+ */
826
+ snapshotMergesContextualNote?: boolean;
759
827
  /** Injectable clock (docs/08 determinism); default Date.now. */
760
828
  now?: () => number;
761
829
  }
@@ -791,7 +859,12 @@ declare function createAgentSurfaceRegistry(options?: RegistryOptions): AgentSur
791
859
 
792
860
  interface AgentToolsetOptions {
793
861
  consumer: AgentConsumer;
794
- /** "direct": one tool per capability. "meta": 3 generic tools. [meta: Experimental] */
862
+ /**
863
+ * "direct": one tool per capability — provider-native input typing, catalog
864
+ * size linear in the surface. "meta": three fixed tools with lazy discovery —
865
+ * constant tool-block size, one extra round trip before the first act.
866
+ * Default "direct"; see the selection guide in docs/09 §choosing-a-mode.
867
+ */
795
868
  mode?: "direct" | "meta";
796
869
  /**
797
870
  * Loop topology (D26). Sets the confirmation-mode default: "embedded" →
@@ -824,18 +897,50 @@ interface AgentToolsetOptions {
824
897
  maxComponents?: number;
825
898
  maxBytes?: number;
826
899
  };
900
+ /**
901
+ * D28 compatibility flag. `true` (default, for one minor) composes
902
+ * availability and the contextual note into `description`, as 0.1 did.
903
+ * `false` keeps `description` free of live state, so the provider tool block
904
+ * is byte-stable across steps and prompt-prefix caching survives; the host
905
+ * renders `AgentTool.state` outside the tool definitions (docs/09
906
+ * §rendering-capability-state). `state` is populated either way.
907
+ */
908
+ descriptionIncludesState?: boolean;
827
909
  }
828
910
  interface AgentTool {
829
- /** Wire-safe name (docs/09 §wire-names), ≤ 64 chars. */
911
+ /** Wire-safe name (docs/09 §wire-names), ≤ 64 chars, unique in this catalog. */
830
912
  name: string;
913
+ /**
914
+ * Plane + effect + confirmation prefix, then the authored description.
915
+ * With `descriptionIncludesState: false` this contains NO live state — it is
916
+ * safe in a provider tool block with prompt-prefix caching across steps.
917
+ */
831
918
  description: string;
832
919
  inputSchema: JsonSchema;
920
+ /**
921
+ * Volatile: re-derived on every snapshot. Hosts render this OUTSIDE the tool
922
+ * block (e.g. a trailing system message) so availability stays honest without
923
+ * invalidating the cached prefix (D28).
924
+ */
925
+ state: {
926
+ available: boolean;
927
+ unavailableReason?: string;
928
+ /** Live text contributed by a contextual binding's `describe()`. */
929
+ note?: string;
930
+ };
833
931
  execute(input: JsonValue, call: {
834
932
  toolCallId?: string;
835
933
  }): Promise<AgentInvocationResult>;
836
934
  }
837
935
  interface AgentToolset {
838
936
  tools(): AgentTool[];
937
+ /**
938
+ * wireName → canonical capability id, for the catalog `tools()` last built.
939
+ * Authoritative: shortened names are not decodable by string surgery, so a
940
+ * host MUST consult this rather than reversing names itself (D30). Empty in
941
+ * "meta" mode, whose three tool names are not capability ids.
942
+ */
943
+ wireNameMap(): ReadonlyMap<string, string>;
839
944
  /** Fires when tools() would return a different catalog. */
840
945
  subscribe(listener: (tools: AgentTool[]) => void): Unsubscribe;
841
946
  dispose(): void;
@@ -845,4 +950,4 @@ declare function createAgentToolset(registry: AgentSurfaceRegistry, options: Age
845
950
  /** Deep equality over JsonValue (order-sensitive for arrays, docs/06 rule 2). */
846
951
  declare function jsonDeepEqual(a: JsonValue | undefined, b: JsonValue | undefined): boolean;
847
952
 
848
- export { AGENT_CAPABILITY_ERROR_CODES, type AgentActionContext, type AgentActionDefinition, type AgentActionDescriptor, type AgentAuthorizationContext, type AgentCapabilityDescriptorUnion, type AgentCapabilityErrorCode, type AgentCapabilityErrorPayload, type AgentComponentDefinition, type AgentComponentDescriptor, type AgentConcurrency, type AgentConsumer, type AgentEffect, type AgentEnvironment, type AgentErrorRetry, type AgentInvocation, type AgentInvocationPolicyContext, type AgentInvocationResult, type AgentObservationDefinition, type AgentObservationDescriptor, type AgentPlane, type AgentPolicy, type AgentPolicyContext, type AgentProcedureBinding, type AgentProcedureBindingRuntimeConfig, type AgentProcedureDescriptor, type AgentProcedureEffect, type AgentProcedureExecutor, type AgentProcedureRefDescriptor, type AgentReadContext, type AgentRegistrationHandle, type AgentRouteInfo, type AgentSchema, AgentSchemaError, type AgentSchemaIssue, AgentSurfaceDefinitionError, type AgentSurfaceDefinitionErrorCode, AgentSurfaceError, type AgentSurfaceEvent, type AgentSurfaceLimits, type AgentSurfaceRegistry, type AgentSurfaceSnapshot, type AgentTool, type AgentToolset, type AgentToolsetOptions, type AuditEvent, type AuditSink, CONFIRMATION_ESCALATION, type ConfirmationController, type ConfirmationEscalation, DEFAULT_LIMITS, type DiscoveryDecision, type InvokeOptions, type JsonSchema, type JsonValue, MAX_ID_LENGTH, MAX_WIRE_NAME_LENGTH, type ParsedCapabilityId, type PendingConfirmation, type PreconditionFailure, type ProcedureCallInfo, type RegistrationCandidate, type RegistryOptions, type SnapshotContext, type StandardSchemaV1, type Unsubscribe, action, audit, authenticated, composeInvokeChain, consoleAuditSink, createAgentSurfaceRegistry, createAgentToolset, decodeWireName, defineAgentComponent, emptyObjectSchema, encodeWireName, encodeWireNameForInstance, environment, evaluateDiscovery, formatDomainCapabilityId, formatViewCapabilityId, fromJsonSchema, fromStandardSchema, hasPermission, isAgentSurfaceError, isValidCapabilityName, isValidComponentType, isValidInstanceId, jsonDeepEqual, memoryAuditSink, observation, parseCapabilityId, rateLimit, requireConfirmation, tenantBoundary, validateComponentDefinition, validateJsonSchemaDocument, validateValueAgainstSchema };
953
+ export { AGENT_CAPABILITY_ERROR_CODES, type AgentActionContext, type AgentActionDefinition, type AgentActionDescriptor, type AgentAuthorizationContext, type AgentCapabilityDescriptorUnion, type AgentCapabilityErrorCode, type AgentCapabilityErrorPayload, type AgentComponentDefinition, type AgentComponentDescriptor, type AgentConcurrency, type AgentConsumer, type AgentEffect, type AgentEnvironment, type AgentErrorRetry, type AgentInvocation, type AgentInvocationPolicyContext, type AgentInvocationResult, type AgentObservationDefinition, type AgentObservationDescriptor, type AgentPlane, type AgentPolicy, type AgentPolicyContext, type AgentProcedureBinding, type AgentProcedureBindingRuntimeConfig, type AgentProcedureDescriptor, type AgentProcedureEffect, type AgentProcedureExecutor, type AgentProcedureRefDescriptor, type AgentReadContext, type AgentRegistrationHandle, type AgentRouteInfo, type AgentSchema, AgentSchemaError, type AgentSchemaIssue, AgentSurfaceDefinitionError, type AgentSurfaceDefinitionErrorCode, AgentSurfaceError, type AgentSurfaceEvent, type AgentSurfaceLimits, type AgentSurfaceRegistry, type AgentSurfaceSnapshot, type AgentTool, type AgentToolset, type AgentToolsetOptions, type AuditEvent, type AuditSink, CONFIRMATION_ESCALATION, type ConfirmationController, type ConfirmationEscalation, DEFAULT_LIMITS, type DiscoveryDecision, type InvokeOptions, type JsonSchema, type JsonValue, MAX_ID_LENGTH, MAX_WIRE_NAME_LENGTH, type ParsedCapabilityId, type PendingConfirmation, type PreconditionFailure, type ProcedureCallInfo, type RegistrationCandidate, type RegistryOptions, type SnapshotContext, type StandardSchemaV1, type Unsubscribe, type WireNameAssignment, type WireNameEntry, action, assignWireNames, audit, authenticated, composeInvokeChain, consoleAuditSink, createAgentSurfaceRegistry, createAgentToolset, decodeWireName, defineAgentComponent, emptyObjectSchema, encodeWireName, encodeWireNameForInstance, environment, evaluateDiscovery, formatDomainCapabilityId, formatViewCapabilityId, fromJsonSchema, fromStandardSchema, hasPermission, isAgentSurfaceError, isValidCapabilityName, isValidComponentType, isValidInstanceId, jsonDeepEqual, memoryAuditSink, observation, parseCapabilityId, rateLimit, requireConfirmation, stableDescriptionOf, tenantBoundary, validateComponentDefinition, validateJsonSchemaDocument, validateValueAgainstSchema };
package/dist/index.js CHANGED
@@ -102,29 +102,81 @@ function parseCapabilityId(id) {
102
102
  return void 0;
103
103
  }
104
104
  var MAX_WIRE_NAME_LENGTH = 64;
105
- function fnv1aHash36(input) {
106
- let hash = 2166136261;
107
- for (let i = 0; i < input.length; i++) {
108
- hash ^= input.charCodeAt(i);
109
- hash = Math.imul(hash, 16777619) >>> 0;
105
+ var SHORTENED_MARKER = "_0_";
106
+ var INSTANCE_MARKER = "_at_";
107
+ function hash36(input, length) {
108
+ let out = "";
109
+ for (let round = 0; out.length < length; round++) {
110
+ let hash = (2166136261 ^ round) >>> 0;
111
+ for (let i = 0; i < input.length; i++) {
112
+ hash ^= input.charCodeAt(i);
113
+ hash = Math.imul(hash, 16777619) >>> 0;
114
+ }
115
+ out += hash.toString(36).padStart(7, "0");
110
116
  }
111
- return hash.toString(36).padStart(7, "0").slice(0, 7);
117
+ return out.slice(0, length);
118
+ }
119
+ function rawWireName(id, instanceId) {
120
+ const encoded = id.replace(":", "_").replaceAll(".", "__");
121
+ return instanceId ? `${encoded}${INSTANCE_MARKER}${instanceId}` : encoded;
112
122
  }
113
123
  function encodeWireName(id) {
114
124
  return encodeWireNameForInstance(id);
115
125
  }
116
- function encodeWireNameForInstance(id, instanceId) {
117
- const raw = id.replace(":", "_").replaceAll(".", "__") + (instanceId ? `_at_${instanceId}` : "");
118
- if (raw.length <= MAX_WIRE_NAME_LENGTH) return raw;
119
- return `${raw.slice(0, 56)}_${fnv1aHash36(`${id}#${instanceId ?? ""}`)}`;
126
+ function encodeWireNameForInstance(id, instanceId, level = 0) {
127
+ const raw = rawWireName(id, instanceId);
128
+ if (level === 0 && raw.length <= MAX_WIRE_NAME_LENGTH) return raw;
129
+ const hashLength = 7 + level * 2;
130
+ const keep = MAX_WIRE_NAME_LENGTH - SHORTENED_MARKER.length - hashLength;
131
+ const hash = hash36(`${id}#${instanceId ?? ""}#${level}`, hashLength);
132
+ return `${raw.slice(0, keep)}${SHORTENED_MARKER}${hash}`;
133
+ }
134
+ function assignWireNames(entries) {
135
+ const keyOf = (e) => `${e.id}#${e.instanceId ?? ""}`;
136
+ const level = /* @__PURE__ */ new Map();
137
+ const MAX_LEVEL = 3;
138
+ let names = entries.map((e) => encodeWireNameForInstance(e.id, e.instanceId));
139
+ for (let round = 0; round <= MAX_LEVEL; round++) {
140
+ const byName2 = /* @__PURE__ */ new Map();
141
+ entries.forEach((entry, i) => {
142
+ const set = byName2.get(names[i]) ?? /* @__PURE__ */ new Set();
143
+ set.add(keyOf(entry));
144
+ byName2.set(names[i], set);
145
+ });
146
+ const colliding = /* @__PURE__ */ new Set();
147
+ for (const [, keys] of byName2) {
148
+ if (keys.size > 1) for (const key of keys) colliding.add(key);
149
+ }
150
+ if (colliding.size === 0) break;
151
+ if (round === MAX_LEVEL) {
152
+ const ranked = [...colliding].sort();
153
+ names = entries.map((entry, i) => {
154
+ const rank = ranked.indexOf(keyOf(entry));
155
+ if (rank < 0) return names[i];
156
+ const suffix = `${SHORTENED_MARKER}${rank}`;
157
+ const base = encodeWireNameForInstance(entry.id, entry.instanceId, MAX_LEVEL);
158
+ return `${base.slice(0, MAX_WIRE_NAME_LENGTH - suffix.length)}${suffix}`;
159
+ });
160
+ break;
161
+ }
162
+ for (const key of colliding) level.set(key, (level.get(key) ?? 0) + 1);
163
+ names = entries.map(
164
+ (entry) => encodeWireNameForInstance(entry.id, entry.instanceId, level.get(keyOf(entry)) ?? 0)
165
+ );
166
+ }
167
+ const byName = /* @__PURE__ */ new Map();
168
+ entries.forEach((entry, i) => byName.set(names[i], entry.id));
169
+ return { names, byName };
120
170
  }
121
171
  function decodeWireName(name) {
172
+ if (name.includes(SHORTENED_MARKER) || name.includes(INSTANCE_MARKER)) return void 0;
122
173
  const planeEnd = name.indexOf("_");
123
174
  if (planeEnd <= 0) return void 0;
124
175
  const plane = name.slice(0, planeEnd);
125
176
  if (plane !== "view" && plane !== "domain") return void 0;
126
- const rest = name.slice(planeEnd + 1).replaceAll("__", ".");
127
- return `${plane}:${rest}`;
177
+ const id = `${plane}:${name.slice(planeEnd + 1).replaceAll("__", ".")}`;
178
+ if (id.includes("_") || !parseCapabilityId(id) || encodeWireName(id) !== name) return void 0;
179
+ return id;
128
180
  }
129
181
 
130
182
  // src/utils.ts
@@ -2520,6 +2572,12 @@ function drainObservationQueues(internals) {
2520
2572
 
2521
2573
  // src/snapshot.ts
2522
2574
  var DEFAULT_CONSUMER2 = { id: "anonymous", kind: "embedded" };
2575
+ function stableDescriptionOf(descriptor) {
2576
+ const note = descriptor.contextualNote;
2577
+ if (!note) return descriptor.description;
2578
+ if (descriptor.description === note) return "";
2579
+ return descriptor.description.endsWith(` ${note}`) ? descriptor.description.slice(0, -(note.length + 1)) : descriptor.description;
2580
+ }
2523
2581
  function matchesScope(type, scope) {
2524
2582
  if (!scope || scope.length === 0) return true;
2525
2583
  return scope.some((prefix) => type === prefix || type.startsWith(`${prefix}.`));
@@ -2625,18 +2683,20 @@ function createSnapshot(internals, ctx) {
2625
2683
  const available = availability.available && decision.decision === "expose";
2626
2684
  const reason = decision.decision === "disable" ? decision.reason : availability.reason;
2627
2685
  if (!available && !includeUnavailable) continue;
2628
- let description = proc.baseDescription;
2686
+ let contextualNote;
2629
2687
  const describe = proc.binding.config.describe;
2630
2688
  if (describe) {
2631
2689
  try {
2632
2690
  const contextual = describe();
2633
- if (contextual) description = `${description} ${contextual}`.trim();
2691
+ if (contextual) contextualNote = contextual;
2634
2692
  } catch {
2635
2693
  }
2636
2694
  }
2695
+ const description = contextualNote && internals.mergesContextualNote ? `${proc.baseDescription} ${contextualNote}`.trim() : proc.baseDescription;
2637
2696
  procedures.push({
2638
2697
  procedureId: proc.capabilityId,
2639
2698
  description,
2699
+ ...contextualNote !== void 0 ? { contextualNote } : {},
2640
2700
  inputSchema: proc.reducedInputSchema,
2641
2701
  ...proc.outputJsonSchema ? { outputSchema: proc.outputJsonSchema } : {},
2642
2702
  effect: proc.effect,
@@ -2703,6 +2763,7 @@ function createAgentSurfaceRegistry(options) {
2703
2763
  const internals = {
2704
2764
  environment: environment2,
2705
2765
  limits,
2766
+ mergesContextualNote: options?.snapshotMergesContextualNote ?? true,
2706
2767
  surfaceId: `srf_${randomBase62(22)}`,
2707
2768
  version: 0,
2708
2769
  registrations: /* @__PURE__ */ new Map(),
@@ -2994,14 +3055,22 @@ var EMPTY_INPUT_SCHEMA = {
2994
3055
  properties: {},
2995
3056
  additionalProperties: false
2996
3057
  };
2997
- function describePrefix(plane, effect, confirmation, available, unavailableReason) {
3058
+ function describePrefix(plane, effect, confirmation) {
2998
3059
  const parts = [plane, effect];
2999
3060
  if (confirmation === "required") parts.push("requires confirmation");
3000
- let prefix = `[${parts.join(" \xB7 ")}]`;
3001
- if (!available) {
3002
- prefix += ` [currently unavailable${unavailableReason ? `: ${unavailableReason}` : ""}]`;
3003
- }
3004
- return prefix;
3061
+ return `[${parts.join(" \xB7 ")}]`;
3062
+ }
3063
+ function legacyDescription(prefix, description, state) {
3064
+ const unavailable = state.available ? "" : ` [currently unavailable${state.unavailableReason ? `: ${state.unavailableReason}` : ""}]`;
3065
+ const note = state.note ? ` ${state.note}` : "";
3066
+ return `${prefix}${unavailable} ${description}${note}`;
3067
+ }
3068
+ function availabilityState(descriptor) {
3069
+ return {
3070
+ available: descriptor.available,
3071
+ ...descriptor.unavailableReason !== void 0 ? { unavailableReason: descriptor.unavailableReason } : {},
3072
+ ...descriptor.contextualNote !== void 0 ? { note: descriptor.contextualNote } : {}
3073
+ };
3005
3074
  }
3006
3075
  function createAgentToolset(registry, options) {
3007
3076
  const mode = options.mode ?? "direct";
@@ -3016,11 +3085,13 @@ function createAgentToolset(registry, options) {
3016
3085
  );
3017
3086
  }
3018
3087
  const confirmationsMode = options.confirmations ?? (options.topology === "remote" ? "two-phase" : "wait");
3088
+ const descriptionIncludesState = options.descriptionIncludesState ?? true;
3019
3089
  const listeners = /* @__PURE__ */ new Set();
3020
3090
  const pendingWaits = /* @__PURE__ */ new Set();
3021
3091
  let disposed = false;
3022
3092
  let cachedVersion;
3023
3093
  let cachedTools;
3094
+ let cachedWireNames = /* @__PURE__ */ new Map();
3024
3095
  let cachedSignature;
3025
3096
  async function waitForConfirmation(confirmationId) {
3026
3097
  if (disposed) return;
@@ -3063,26 +3134,32 @@ function createAgentToolset(registry, options) {
3063
3134
  ...options.scope ? { scope: options.scope } : {},
3064
3135
  includeUnavailable: true
3065
3136
  });
3066
- const tools = [];
3067
- const push = (capabilityId, kind, registrationId, instanceId, description, inputSchema, nameSuffix) => {
3068
- const entry = {
3069
- capabilityId,
3070
- registrationId,
3071
- ...instanceId !== void 0 ? { instanceId } : {},
3072
- surfaceVersion: snapshot.surfaceVersion,
3073
- kind
3074
- };
3075
- tools.push({
3137
+ const pending = [];
3138
+ const push = (capabilityId, kind, registrationId, instanceId, prefix, description, inputSchema, state, nameSuffix) => {
3139
+ const suffix = nameSuffix ?? instanceId;
3140
+ pending.push({
3076
3141
  // Providers require unique tool names: multi-instance capabilities
3077
3142
  // are disambiguated with an `_at_<instance>` suffix (docs/09).
3078
- name: encodeWireNameForInstance(capabilityId, nameSuffix ?? instanceId),
3143
+ wire: { id: capabilityId, ...suffix !== void 0 ? { instanceId: suffix } : {} },
3144
+ entry: {
3145
+ capabilityId,
3146
+ registrationId,
3147
+ ...instanceId !== void 0 ? { instanceId } : {},
3148
+ surfaceVersion: snapshot.surfaceVersion,
3149
+ kind
3150
+ },
3151
+ prefix,
3079
3152
  description,
3080
3153
  inputSchema,
3081
- execute: (input, call) => invokeThroughSurface(entry, input, call.toolCallId)
3154
+ state
3082
3155
  });
3083
3156
  };
3157
+ const typeCounts = /* @__PURE__ */ new Map();
3158
+ for (const component of snapshot.components) {
3159
+ typeCounts.set(component.type, (typeCounts.get(component.type) ?? 0) + 1);
3160
+ }
3084
3161
  for (const component of snapshot.components) {
3085
- const multiInstance = snapshot.components.filter((c) => c.type === component.type).length > 1;
3162
+ const multiInstance = (typeCounts.get(component.type) ?? 0) > 1;
3086
3163
  const instanceId = multiInstance ? component.instanceId : void 0;
3087
3164
  for (const obs of component.observations) {
3088
3165
  push(
@@ -3090,8 +3167,10 @@ function createAgentToolset(registry, options) {
3090
3167
  "observation",
3091
3168
  component.registrationId,
3092
3169
  instanceId,
3093
- `${describePrefix("view", "read", "never", obs.available, obs.unavailableReason)} ${obs.description}`,
3094
- EMPTY_INPUT_SCHEMA
3170
+ describePrefix("view", "read", "never"),
3171
+ obs.description,
3172
+ EMPTY_INPUT_SCHEMA,
3173
+ availabilityState(obs)
3095
3174
  );
3096
3175
  }
3097
3176
  for (const act of component.actions) {
@@ -3100,8 +3179,10 @@ function createAgentToolset(registry, options) {
3100
3179
  "action",
3101
3180
  component.registrationId,
3102
3181
  instanceId,
3103
- `${describePrefix("view", act.effect, act.confirmation, act.available, act.unavailableReason)} ${act.description}`,
3104
- act.inputSchema
3182
+ describePrefix("view", act.effect, act.confirmation),
3183
+ act.description,
3184
+ act.inputSchema,
3185
+ availabilityState(act)
3105
3186
  );
3106
3187
  }
3107
3188
  }
@@ -3116,25 +3197,45 @@ function createAgentToolset(registry, options) {
3116
3197
  "procedure",
3117
3198
  proc.registrationId,
3118
3199
  void 0,
3119
- `${describePrefix("domain", proc.effect, proc.confirmation, proc.available, proc.unavailableReason)} ${proc.description}`,
3200
+ describePrefix("domain", proc.effect, proc.confirmation),
3201
+ // The stable half only: a contextual note travels in `state.note`.
3202
+ stableDescriptionOf(proc),
3120
3203
  proc.inputSchema,
3204
+ availabilityState(proc),
3121
3205
  needsSuffix ? proc.context?.instanceId ?? proc.registrationId.replace(/[^A-Za-z0-9_-]/g, "") : void 0
3122
3206
  );
3123
3207
  }
3124
- return tools;
3208
+ const assignment = assignWireNames(pending.map((p) => p.wire));
3209
+ const tools = pending.map((p, i) => ({
3210
+ name: assignment.names[i],
3211
+ description: descriptionIncludesState ? legacyDescription(p.prefix, p.description, p.state) : `${p.prefix} ${p.description}`,
3212
+ inputSchema: p.inputSchema,
3213
+ state: p.state,
3214
+ execute: (input, call) => invokeThroughSurface(p.entry, input, call.toolCallId)
3215
+ }));
3216
+ return { tools, wireNames: assignment.byName };
3125
3217
  }
3126
3218
  function buildMetaTools() {
3127
3219
  const snapshotFor = () => registry.snapshot({
3128
3220
  consumer: options.consumer,
3129
3221
  ...options.scope ? { scope: options.scope } : {}
3130
3222
  });
3131
- return [
3223
+ const verbs = [
3132
3224
  {
3133
3225
  name: "surface_discover",
3134
3226
  description: "[meta] Discover the current agent surface: components, capabilities, procedures, availability, schemas.",
3135
3227
  inputSchema: {
3136
3228
  type: "object",
3137
- properties: { scope: { type: "array", items: { type: "string" } } },
3229
+ properties: {
3230
+ scope: {
3231
+ type: "array",
3232
+ items: { type: "string" },
3233
+ // No enum: valid tokens are live component types, and inlining
3234
+ // them would make this tool block churn on every mount —
3235
+ // the churn AS-META-005 and D28 exist to prevent.
3236
+ description: 'Component-type prefixes to narrow the result, e.g. ["devices.table"], taken from `components[].type` of an earlier call \u2014 omit on the first. Narrows only: prefixes outside this host\'s configured scope match nothing and come back in `scopeRejected`.'
3237
+ }
3238
+ },
3138
3239
  additionalProperties: false
3139
3240
  },
3140
3241
  async execute(input) {
@@ -3145,7 +3246,14 @@ function createAgentToolset(registry, options) {
3145
3246
  ...effective.scope ? { scope: effective.scope } : {},
3146
3247
  ...options.budget ? { budget: options.budget } : {}
3147
3248
  });
3148
- const projected = effective.empty ? { ...snapshot, components: [], procedures: [] } : snapshot;
3249
+ const projected = {
3250
+ ...snapshot,
3251
+ // A disjoint request is snapshotted unscoped, so any `truncated`
3252
+ // count belongs to a surface this payload does not contain. Keeping
3253
+ // it would claim a budget dropped what scope did.
3254
+ ...effective.empty ? { components: [], procedures: [], truncated: void 0 } : {},
3255
+ ...effective.rejected.length > 0 ? { scopeRejected: { prefixes: effective.rejected } } : {}
3256
+ };
3149
3257
  return {
3150
3258
  status: "ok",
3151
3259
  invocationId: `inv_${randomBase62(12)}`,
@@ -3161,8 +3269,14 @@ function createAgentToolset(registry, options) {
3161
3269
  inputSchema: {
3162
3270
  type: "object",
3163
3271
  properties: {
3164
- capabilityId: { type: "string" },
3165
- instanceId: { type: "string" }
3272
+ capabilityId: {
3273
+ type: "string",
3274
+ description: "Observation id, verbatim from `observations[].capabilityId` in a discover result."
3275
+ },
3276
+ instanceId: {
3277
+ type: "string",
3278
+ description: "Only when several components share a type: `components[].instanceId` picks one."
3279
+ }
3166
3280
  },
3167
3281
  required: ["capabilityId"],
3168
3282
  additionalProperties: false
@@ -3187,15 +3301,31 @@ function createAgentToolset(registry, options) {
3187
3301
  },
3188
3302
  {
3189
3303
  name: "surface_act",
3190
- description: "[meta] Invoke an action or procedure by capabilityId.",
3304
+ description: "[meta] Invoke an action or procedure by capabilityId. Echo the surfaceVersion you discovered so a surface that changed underneath a destructive plan is rejected rather than executed.",
3191
3305
  inputSchema: {
3192
3306
  type: "object",
3193
3307
  properties: {
3194
- capabilityId: { type: "string" },
3195
- instanceId: { type: "string" },
3196
- input: {},
3197
- invocationId: { type: "string" },
3198
- confirmationId: { type: "string" }
3308
+ capabilityId: {
3309
+ type: "string",
3310
+ description: "Action `capabilityId` or `procedureId`, verbatim from a discover result."
3311
+ },
3312
+ instanceId: {
3313
+ type: "string",
3314
+ description: "Only when several components share a type: `components[].instanceId` picks one."
3315
+ },
3316
+ input: { description: "Arguments matching that capability's `inputSchema`." },
3317
+ invocationId: {
3318
+ type: "string",
3319
+ description: "Reuse a previous call's id to retry without executing twice; required when resuming after CONFIRMATION_REQUIRED."
3320
+ },
3321
+ confirmationId: {
3322
+ type: "string",
3323
+ description: "The id returned with CONFIRMATION_REQUIRED, sent back after the user approves."
3324
+ },
3325
+ surfaceVersion: {
3326
+ type: "string",
3327
+ description: "The `surfaceVersion` you planned against. Send it for destructive or externally-visible calls: a surface that moved underneath the plan then fails instead of executing. Omitted, the call binds to what is live now."
3328
+ }
3199
3329
  },
3200
3330
  required: ["capabilityId"],
3201
3331
  additionalProperties: false
@@ -3209,7 +3339,7 @@ function createAgentToolset(registry, options) {
3209
3339
  capabilityId: req.capabilityId,
3210
3340
  ...registrationId !== void 0 ? { registrationId } : {},
3211
3341
  ...req.instanceId !== void 0 ? { instanceId: req.instanceId } : {},
3212
- surfaceVersion: snapshot.surfaceVersion,
3342
+ surfaceVersion: req.surfaceVersion ?? snapshot.surfaceVersion,
3213
3343
  kind: "action"
3214
3344
  },
3215
3345
  req.input,
@@ -3222,6 +3352,7 @@ function createAgentToolset(registry, options) {
3222
3352
  }
3223
3353
  }
3224
3354
  ];
3355
+ return verbs.map((verb) => ({ ...verb, state: { available: true } }));
3225
3356
  }
3226
3357
  function computeTools() {
3227
3358
  if (mode === "meta") {
@@ -3230,13 +3361,15 @@ function createAgentToolset(registry, options) {
3230
3361
  }
3231
3362
  const version = registry.getVersion();
3232
3363
  if (cachedTools && cachedVersion === version) return cachedTools;
3233
- cachedTools = buildDirectTools();
3364
+ const built = buildDirectTools();
3365
+ cachedTools = built.tools;
3366
+ cachedWireNames = built.wireNames;
3234
3367
  cachedVersion = version;
3235
3368
  return cachedTools;
3236
3369
  }
3237
3370
  function signatureOf(tools) {
3238
3371
  return JSON.stringify(
3239
- tools.map((t) => [t.name, t.description, t.inputSchema])
3372
+ tools.map((t) => [t.name, t.description, t.inputSchema, t.state])
3240
3373
  );
3241
3374
  }
3242
3375
  const unsubscribe = registry.subscribe((event) => {
@@ -3260,6 +3393,11 @@ function createAgentToolset(registry, options) {
3260
3393
  cachedSignature ??= signatureOf(tools);
3261
3394
  return tools;
3262
3395
  },
3396
+ wireNameMap() {
3397
+ if (mode === "meta") return /* @__PURE__ */ new Map();
3398
+ computeTools();
3399
+ return cachedWireNames;
3400
+ },
3263
3401
  subscribe(listener) {
3264
3402
  listeners.add(listener);
3265
3403
  return () => {
@@ -3278,17 +3416,25 @@ function createAgentToolset(registry, options) {
3278
3416
  function intersectScope(floor, requested) {
3279
3417
  const hasFloor = floor !== void 0 && floor.length > 0;
3280
3418
  if (requested === void 0 || requested.length === 0) {
3281
- return hasFloor ? { scope: floor, empty: false } : { empty: false };
3419
+ return hasFloor ? { scope: floor, empty: false, rejected: [] } : { empty: false, rejected: [] };
3282
3420
  }
3283
- if (!hasFloor) return { scope: requested, empty: false };
3421
+ if (!hasFloor) return { scope: requested, empty: false, rejected: [] };
3284
3422
  const out = /* @__PURE__ */ new Set();
3285
- for (const f of floor) {
3286
- for (const r of requested) {
3287
- if (r === f || r.startsWith(`${f}.`)) out.add(r);
3288
- else if (f.startsWith(`${r}.`)) out.add(f);
3423
+ const rejected = [];
3424
+ for (const r of requested) {
3425
+ let admitted = false;
3426
+ for (const f of floor) {
3427
+ if (r === f || r.startsWith(`${f}.`)) {
3428
+ out.add(r);
3429
+ admitted = true;
3430
+ } else if (f.startsWith(`${r}.`)) {
3431
+ out.add(f);
3432
+ admitted = true;
3433
+ }
3289
3434
  }
3435
+ if (!admitted && !rejected.includes(r)) rejected.push(r);
3290
3436
  }
3291
- return out.size > 0 ? { scope: [...out], empty: false } : { empty: true };
3437
+ return out.size > 0 ? { scope: [...out], empty: false, rejected } : { empty: true, rejected };
3292
3438
  }
3293
3439
  function findRegistrationId(snapshot, capabilityId, instanceId) {
3294
3440
  const matches = [];
@@ -3315,6 +3461,7 @@ export {
3315
3461
  MAX_ID_LENGTH,
3316
3462
  MAX_WIRE_NAME_LENGTH,
3317
3463
  action,
3464
+ assignWireNames,
3318
3465
  audit,
3319
3466
  authenticated,
3320
3467
  composeInvokeChain,
@@ -3343,6 +3490,7 @@ export {
3343
3490
  parseCapabilityId,
3344
3491
  rateLimit,
3345
3492
  requireConfirmation,
3493
+ stableDescriptionOf,
3346
3494
  tenantBoundary,
3347
3495
  validateComponentDefinition,
3348
3496
  validateJsonSchemaDocument,