@agent-surface/core 0.5.0 → 0.7.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/dist/index.d.ts CHANGED
@@ -171,6 +171,14 @@ declare function assignWireNames(entries: readonly WireNameEntry[]): WireNameAss
171
171
  * names among them (`AS-WIRE-007`: consult `toolset.wireNameMap()` instead).
172
172
  * Returning a plausible-but-wrong canonical id would take the audit identity
173
173
  * with it, so this refuses anything it cannot re-encode byte-identically.
174
+ *
175
+ * Refusal is decided by what a name *is*, never by a substring it happens to
176
+ * contain. `view:at.a.a` encodes to `view_at__a__a`, where `_at_` is the plane
177
+ * separator meeting a segment named "at" — screening for the marker text cost
178
+ * every id with an `at` or `0` segment its own faithful encoding (`AS-ID-004`).
179
+ * Marker-bearing names are still refused, by the two checks that can tell:
180
+ * every underscore run must be exactly two (one "."), and the id must re-encode
181
+ * byte-identically.
174
182
  */
175
183
  declare function decodeWireName(name: string): string | undefined;
176
184
 
@@ -838,6 +846,11 @@ interface AgentToolsetOptions {
838
846
  * "direct": one tool per capability — provider-native input typing, catalog
839
847
  * size linear in the surface. "meta": three fixed tools with lazy discovery —
840
848
  * constant tool-block size, one extra round trip before the first act.
849
+ *
850
+ * [Experimental] applies to "meta" only (D29): the three verbs' envelope may
851
+ * change in any release — 0.6 typed `surface_act.input` and started enforcing
852
+ * the verb schemas (D32). "direct" is Draft, like the rest of the API.
853
+ *
841
854
  * Default "direct"; see the selection guide in docs/09 §choosing-a-mode.
842
855
  */
843
856
  mode?: "direct" | "meta";
package/dist/index.js CHANGED
@@ -82,7 +82,7 @@ function formatDomainCapabilityId(path) {
82
82
  return `domain:${path}`;
83
83
  }
84
84
  function parseCapabilityId(id) {
85
- if (id.length > MAX_ID_LENGTH) return void 0;
85
+ if (typeof id !== "string" || id.length > MAX_ID_LENGTH) return void 0;
86
86
  if (id.startsWith("view:")) {
87
87
  const rest = id.slice("view:".length);
88
88
  const lastDot = rest.lastIndexOf(".");
@@ -125,7 +125,7 @@ function encodeWireName(id) {
125
125
  }
126
126
  function encodeWireNameForInstance(id, instanceId, level = 0) {
127
127
  const raw = rawWireName(id, instanceId);
128
- if (level === 0 && raw.length <= MAX_WIRE_NAME_LENGTH) return raw;
128
+ if (level === 0 && raw.length <= MAX_WIRE_NAME_LENGTH && !id.includes("_")) return raw;
129
129
  const hashLength = 7 + level * 2;
130
130
  const keep = MAX_WIRE_NAME_LENGTH - SHORTENED_MARKER.length - hashLength;
131
131
  const hash = hash36(`${id}#${instanceId ?? ""}#${level}`, hashLength);
@@ -169,12 +169,15 @@ function assignWireNames(entries) {
169
169
  return { names, byName };
170
170
  }
171
171
  function decodeWireName(name) {
172
- if (name.includes(SHORTENED_MARKER) || name.includes(INSTANCE_MARKER)) return void 0;
173
172
  const planeEnd = name.indexOf("_");
174
173
  if (planeEnd <= 0) return void 0;
175
174
  const plane = name.slice(0, planeEnd);
176
175
  if (plane !== "view" && plane !== "domain") return void 0;
177
- const id = `${plane}:${name.slice(planeEnd + 1).replaceAll("__", ".")}`;
176
+ const rest = name.slice(planeEnd + 1);
177
+ if (/_{3,}/.test(rest)) return void 0;
178
+ const path = rest.replaceAll("__", ".");
179
+ if (path.split(".").some((segment) => segment === "")) return void 0;
180
+ const id = `${plane}:${path}`;
178
181
  if (id.includes("_") || !parseCapabilityId(id) || encodeWireName(id) !== name) return void 0;
179
182
  return id;
180
183
  }
@@ -1361,6 +1364,7 @@ var ConfirmationStore = class {
1361
1364
  };
1362
1365
 
1363
1366
  // src/internal.ts
1367
+ var DEV_WARN = /* @__PURE__ */ Symbol("agent-surface.dev-warn");
1364
1368
  var DevDefectError = class extends Error {
1365
1369
  constructor(message) {
1366
1370
  super(message);
@@ -3034,6 +3038,10 @@ function createAgentSurfaceRegistry(options) {
3034
3038
  dispatcher.clear();
3035
3039
  }
3036
3040
  };
3041
+ Object.defineProperty(registry, DEV_WARN, {
3042
+ value: (...args) => internals.devWarn(...args),
3043
+ enumerable: false
3044
+ });
3037
3045
  return registry;
3038
3046
  }
3039
3047
  function combineSinks(...sinks) {
@@ -3050,6 +3058,75 @@ var EMPTY_INPUT_SCHEMA = {
3050
3058
  properties: {},
3051
3059
  additionalProperties: false
3052
3060
  };
3061
+ var META_DISCOVER_SCHEMA = {
3062
+ type: "object",
3063
+ properties: {
3064
+ scope: {
3065
+ type: "array",
3066
+ items: { type: "string" },
3067
+ // No enum: valid tokens are live component types, and inlining them
3068
+ // would make this tool block churn on every mount — the churn
3069
+ // AS-META-005 and D28 exist to prevent.
3070
+ 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`.'
3071
+ }
3072
+ },
3073
+ additionalProperties: false
3074
+ };
3075
+ var META_READ_SCHEMA = {
3076
+ type: "object",
3077
+ properties: {
3078
+ capabilityId: {
3079
+ type: "string",
3080
+ description: "Observation id, verbatim from `observations[].capabilityId` in a discover result."
3081
+ },
3082
+ instanceId: {
3083
+ type: "string",
3084
+ description: "Only when several components share a type: `components[].instanceId` picks one."
3085
+ }
3086
+ },
3087
+ required: ["capabilityId"],
3088
+ additionalProperties: false
3089
+ };
3090
+ var META_ACT_SCHEMA = {
3091
+ type: "object",
3092
+ properties: {
3093
+ capabilityId: {
3094
+ type: "string",
3095
+ description: "Action `capabilityId` or `procedureId`, verbatim from a discover result."
3096
+ },
3097
+ instanceId: {
3098
+ type: "string",
3099
+ description: "Only when several components share a type: `components[].instanceId` picks one."
3100
+ },
3101
+ // Typed, and not merely described: an untyped property is the one position
3102
+ // a provider's constrained decoder cannot constrain, so the model falls
3103
+ // back to its prior — a JSON-encoded string, the shape
3104
+ // `function_call.arguments` carries — and sorts the rest of the capability's
3105
+ // arguments into the sibling modifiers below. `type: "object"` costs
3106
+ // nothing in practice: direct mode already passes `act.inputSchema`
3107
+ // straight through as the tool schema, and providers require that to be an
3108
+ // object schema at the top level. No `additionalProperties` here — the
3109
+ // capability's own schema governs what goes inside.
3110
+ input: {
3111
+ type: "object",
3112
+ description: "Arguments matching that capability's `inputSchema`, as a JSON object \u2014 not a JSON-encoded string. Everything the capability declares goes in here, never beside it."
3113
+ },
3114
+ invocationId: {
3115
+ type: "string",
3116
+ description: "Reuse a previous call's id to retry without executing twice; required when resuming after CONFIRMATION_REQUIRED."
3117
+ },
3118
+ confirmationId: {
3119
+ type: "string",
3120
+ description: "The id returned with CONFIRMATION_REQUIRED, sent back after the user approves."
3121
+ },
3122
+ surfaceVersion: {
3123
+ type: "string",
3124
+ 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."
3125
+ }
3126
+ },
3127
+ required: ["capabilityId"],
3128
+ additionalProperties: false
3129
+ };
3053
3130
  function describePrefix(plane, effect, confirmation) {
3054
3131
  const parts = [plane, effect];
3055
3132
  if (confirmation === "required") parts.push("requires confirmation");
@@ -3075,6 +3152,8 @@ function createAgentToolset(registry, options) {
3075
3152
  );
3076
3153
  }
3077
3154
  const confirmationsMode = options.confirmations ?? (options.topology === "remote" ? "two-phase" : "wait");
3155
+ const devWarn = registry[DEV_WARN] ?? (() => {
3156
+ });
3078
3157
  const listeners = /* @__PURE__ */ new Set();
3079
3158
  const pendingWaits = /* @__PURE__ */ new Set();
3080
3159
  let disposed = false;
@@ -3204,6 +3283,15 @@ function createAgentToolset(registry, options) {
3204
3283
  }));
3205
3284
  return { tools, wireNames: assignment.byName };
3206
3285
  }
3286
+ function envelopeFailure(metaCapabilityId, capabilityId, error, toolCallId) {
3287
+ return {
3288
+ status: "error",
3289
+ invocationId: toolCallId ?? `inv_${randomBase62(12)}`,
3290
+ capabilityId: typeof capabilityId === "string" && capabilityId.length > 0 ? capabilityId : metaCapabilityId,
3291
+ error,
3292
+ surfaceVersion: registry.getVersion()
3293
+ };
3294
+ }
3207
3295
  function buildMetaTools() {
3208
3296
  const snapshotFor = () => registry.snapshot({
3209
3297
  consumer: options.consumer,
@@ -3213,21 +3301,12 @@ function createAgentToolset(registry, options) {
3213
3301
  {
3214
3302
  name: "surface_discover",
3215
3303
  description: "[meta] Discover the current agent surface: components, capabilities, procedures, availability, schemas.",
3216
- inputSchema: {
3217
- type: "object",
3218
- properties: {
3219
- scope: {
3220
- type: "array",
3221
- items: { type: "string" },
3222
- // No enum: valid tokens are live component types, and inlining
3223
- // them would make this tool block churn on every mount —
3224
- // the churn AS-META-005 and D28 exist to prevent.
3225
- 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`.'
3226
- }
3227
- },
3228
- additionalProperties: false
3229
- },
3230
- async execute(input) {
3304
+ inputSchema: META_DISCOVER_SCHEMA,
3305
+ async execute(input, call) {
3306
+ const invalid = validateEnvelope("surface_discover", META_DISCOVER_SCHEMA, input);
3307
+ if (invalid) {
3308
+ return envelopeFailure("meta:surface.discover", void 0, invalid, call.toolCallId);
3309
+ }
3231
3310
  const requested = input?.scope;
3232
3311
  const effective = intersectScope(options.scope, requested);
3233
3312
  const snapshot = registry.snapshot({
@@ -3255,25 +3334,15 @@ function createAgentToolset(registry, options) {
3255
3334
  {
3256
3335
  name: "surface_read",
3257
3336
  description: "[meta] Invoke an observation by capabilityId and return its output.",
3258
- inputSchema: {
3259
- type: "object",
3260
- properties: {
3261
- capabilityId: {
3262
- type: "string",
3263
- description: "Observation id, verbatim from `observations[].capabilityId` in a discover result."
3264
- },
3265
- instanceId: {
3266
- type: "string",
3267
- description: "Only when several components share a type: `components[].instanceId` picks one."
3268
- }
3269
- },
3270
- required: ["capabilityId"],
3271
- additionalProperties: false
3272
- },
3337
+ inputSchema: META_READ_SCHEMA,
3273
3338
  async execute(input, call) {
3274
- const req = input;
3339
+ const req = input ?? {};
3340
+ const invalid = validateEnvelope("surface_read", META_READ_SCHEMA, input);
3341
+ if (invalid) {
3342
+ return envelopeFailure("meta:surface.read", req.capabilityId, invalid, call.toolCallId);
3343
+ }
3275
3344
  const snapshot = snapshotFor();
3276
- const registrationId = findRegistrationId(snapshot, req.capabilityId, req.instanceId);
3345
+ const { registrationId } = findTarget(snapshot, req.capabilityId, req.instanceId);
3277
3346
  return invokeThroughSurface(
3278
3347
  {
3279
3348
  capabilityId: req.capabilityId,
@@ -3291,38 +3360,27 @@ function createAgentToolset(registry, options) {
3291
3360
  {
3292
3361
  name: "surface_act",
3293
3362
  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.",
3294
- inputSchema: {
3295
- type: "object",
3296
- properties: {
3297
- capabilityId: {
3298
- type: "string",
3299
- description: "Action `capabilityId` or `procedureId`, verbatim from a discover result."
3300
- },
3301
- instanceId: {
3302
- type: "string",
3303
- description: "Only when several components share a type: `components[].instanceId` picks one."
3304
- },
3305
- input: { description: "Arguments matching that capability's `inputSchema`." },
3306
- invocationId: {
3307
- type: "string",
3308
- description: "Reuse a previous call's id to retry without executing twice; required when resuming after CONFIRMATION_REQUIRED."
3309
- },
3310
- confirmationId: {
3311
- type: "string",
3312
- description: "The id returned with CONFIRMATION_REQUIRED, sent back after the user approves."
3313
- },
3314
- surfaceVersion: {
3315
- type: "string",
3316
- 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."
3317
- }
3318
- },
3319
- required: ["capabilityId"],
3320
- additionalProperties: false
3321
- },
3363
+ inputSchema: META_ACT_SCHEMA,
3322
3364
  async execute(input, call) {
3323
- const req = input;
3365
+ const req = input ?? {};
3366
+ const invalid = validateEnvelope("surface_act", META_ACT_SCHEMA, input, ["input"]);
3367
+ if (invalid) {
3368
+ return envelopeFailure("meta:surface.act", req.capabilityId, invalid, call.toolCallId);
3369
+ }
3324
3370
  const snapshot = snapshotFor();
3325
- const registrationId = findRegistrationId(snapshot, req.capabilityId, req.instanceId);
3371
+ const { registrationId, inputSchema } = findTarget(
3372
+ snapshot,
3373
+ req.capabilityId,
3374
+ req.instanceId
3375
+ );
3376
+ let actInput = req.input;
3377
+ const parsed = parseStringifiedObject(actInput, inputSchema);
3378
+ if (parsed !== void 0) {
3379
+ actInput = parsed;
3380
+ devWarn(
3381
+ `[agent-surface] surface_act got \`input\` as a JSON-encoded string for "${req.capabilityId}" and parsed it; the provider is not honoring the tool schema.`
3382
+ );
3383
+ }
3326
3384
  return invokeThroughSurface(
3327
3385
  {
3328
3386
  capabilityId: req.capabilityId,
@@ -3331,7 +3389,7 @@ function createAgentToolset(registry, options) {
3331
3389
  surfaceVersion: req.surfaceVersion ?? snapshot.surfaceVersion,
3332
3390
  kind: "action"
3333
3391
  },
3334
- req.input,
3392
+ actInput,
3335
3393
  call.toolCallId,
3336
3394
  {
3337
3395
  ...req.invocationId !== void 0 ? { invocationId: req.invocationId } : {},
@@ -3425,7 +3483,7 @@ function intersectScope(floor, requested) {
3425
3483
  }
3426
3484
  return out.size > 0 ? { scope: [...out], empty: false, rejected } : { empty: true, rejected };
3427
3485
  }
3428
- function findRegistrationId(snapshot, capabilityId, instanceId) {
3486
+ function findTarget(snapshot, capabilityId, instanceId) {
3429
3487
  const matches = [];
3430
3488
  for (const component of snapshot.components) {
3431
3489
  if (instanceId !== void 0 && component.instanceId !== instanceId) continue;
@@ -3433,12 +3491,89 @@ function findRegistrationId(snapshot, capabilityId, instanceId) {
3433
3491
  ...component.observations,
3434
3492
  ...component.actions
3435
3493
  ];
3436
- if (all.some((c) => c.capabilityId === capabilityId)) matches.push(component.registrationId);
3494
+ const hit = all.find((c) => c.capabilityId === capabilityId);
3495
+ if (hit) {
3496
+ matches.push({
3497
+ registrationId: component.registrationId,
3498
+ ..."inputSchema" in hit ? { inputSchema: hit.inputSchema } : {}
3499
+ });
3500
+ }
3437
3501
  }
3438
3502
  for (const proc of snapshot.procedures) {
3439
- if (proc.procedureId === capabilityId) matches.push(proc.registrationId);
3503
+ if (proc.procedureId === capabilityId) {
3504
+ matches.push({ registrationId: proc.registrationId, inputSchema: proc.inputSchema });
3505
+ }
3506
+ }
3507
+ return matches.length === 1 ? matches[0] : {};
3508
+ }
3509
+ function validateEnvelope(verb, schema, raw, exempt = []) {
3510
+ if (raw !== void 0 && raw !== null && (typeof raw !== "object" || Array.isArray(raw))) {
3511
+ return envelopeError(verb, [
3512
+ { path: "", message: `\`${verb}\` takes a JSON object of arguments.` }
3513
+ ]);
3514
+ }
3515
+ const properties = schema.properties ?? {};
3516
+ const known = Object.keys(properties);
3517
+ const required = schema.required ?? [];
3518
+ const req = raw ?? {};
3519
+ const issues = [];
3520
+ for (const key of required) {
3521
+ if (req[key] === void 0) issues.push({ path: key, message: `\`${key}\` is required.` });
3522
+ }
3523
+ for (const [key, value] of Object.entries(req)) {
3524
+ if (value === void 0) continue;
3525
+ if (!known.includes(key)) {
3526
+ if (schema.additionalProperties === false) {
3527
+ issues.push({
3528
+ path: key,
3529
+ // The high-value half is the pointer back at `input`: it turns the
3530
+ // dead end of a hoisted capability argument into a one-retry
3531
+ // recovery. A verb without an `input` has nowhere to point.
3532
+ message: `Unknown top-level property. \`${verb}\` accepts only ${known.join(", ")}.${known.includes("input") ? " An argument the capability declares belongs inside `input`." : ""}`
3533
+ });
3534
+ }
3535
+ continue;
3536
+ }
3537
+ if (exempt.includes(key)) continue;
3538
+ const issue = checkDeclaredType(key, properties[key], value);
3539
+ if (issue) issues.push(issue);
3540
+ }
3541
+ return issues.length > 0 ? envelopeError(verb, issues) : void 0;
3542
+ }
3543
+ function checkDeclaredType(key, property, value) {
3544
+ if (property.type === "string" && (typeof value !== "string" || value.length === 0)) {
3545
+ return { path: key, message: `\`${key}\` must be a non-empty string.` };
3546
+ }
3547
+ if (property.type === "array") {
3548
+ if (!Array.isArray(value)) return { path: key, message: `\`${key}\` must be an array.` };
3549
+ const items = property.items;
3550
+ if (items?.type === "string" && !value.every((item) => typeof item === "string")) {
3551
+ return { path: key, message: `\`${key}\` must be an array of strings.` };
3552
+ }
3553
+ }
3554
+ return void 0;
3555
+ }
3556
+ function envelopeError(verb, issues) {
3557
+ return {
3558
+ code: "INVALID_INPUT",
3559
+ // Names the envelope, not the capability: pointing the model at the
3560
+ // capability's schema when the wrapper is what is wrong sends it to fix
3561
+ // something that is already correct.
3562
+ message: `The \`${verb}\` call is malformed \u2014 the fault is in the tool's own arguments, not the capability's input. Fix the listed issues and retry.`,
3563
+ retry: "with-changes",
3564
+ details: { issues }
3565
+ };
3566
+ }
3567
+ function parseStringifiedObject(value, targetSchema) {
3568
+ if (typeof value !== "string" || targetSchema?.type !== "object") return void 0;
3569
+ let parsed;
3570
+ try {
3571
+ parsed = JSON.parse(value);
3572
+ } catch {
3573
+ return void 0;
3440
3574
  }
3441
- return matches.length === 1 ? matches[0] : void 0;
3575
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
3576
+ return parsed;
3442
3577
  }
3443
3578
  export {
3444
3579
  AGENT_CAPABILITY_ERROR_CODES,