@webskill/sdk 0.4.0 → 0.5.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.
@@ -991,22 +991,22 @@ function createScriptContext(deps) {
991
991
  ...onWarning ? { onWarning } : {}
992
992
  };
993
993
  }
994
- const isRecord$2 = (v) => typeof v === "object" && v !== null;
994
+ const isRecord$3 = (v) => typeof v === "object" && v !== null;
995
995
  /**
996
996
  * $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
997
997
  * 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
998
998
  */
999
999
  function extractChartSpec(data) {
1000
- if (!isRecord$2(data)) return void 0;
1000
+ if (!isRecord$3(data)) return void 0;
1001
1001
  const raw = data["$chart"];
1002
- if (!isRecord$2(raw)) return void 0;
1002
+ if (!isRecord$3(raw)) return void 0;
1003
1003
  const { kind, labels, series } = raw;
1004
1004
  if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
1005
1005
  if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
1006
1006
  if (!Array.isArray(series)) return void 0;
1007
1007
  const validSeries = [];
1008
1008
  for (const item of series) {
1009
- if (!isRecord$2(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
1009
+ if (!isRecord$3(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
1010
1010
  const name = item["name"];
1011
1011
  validSeries.push({
1012
1012
  ...typeof name === "string" ? { name } : {},
@@ -1057,7 +1057,7 @@ const actionIntents = /* @__PURE__ */ new Set([
1057
1057
  "download",
1058
1058
  "refresh"
1059
1059
  ]);
1060
- function isRecord$1(value) {
1060
+ function isRecord$2(value) {
1061
1061
  return typeof value === "object" && value !== null && !Array.isArray(value);
1062
1062
  }
1063
1063
  function reject(message) {
@@ -1074,7 +1074,7 @@ function isJsonValue(value, depth = 0) {
1074
1074
  if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1075
1075
  if (typeof value === "number") return Number.isFinite(value);
1076
1076
  if (Array.isArray(value)) return value.every((item) => isJsonValue(item, depth + 1));
1077
- if (!isRecord$1(value)) return false;
1077
+ if (!isRecord$2(value)) return false;
1078
1078
  return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
1079
1079
  }
1080
1080
  /**
@@ -1084,10 +1084,10 @@ function isJsonValue(value, depth = 0) {
1084
1084
  function assertNode(value, path, depth, counter) {
1085
1085
  if (depth > MAX_NODE_DEPTH) reject(`A UI spec tree must not nest deeper than ${MAX_NODE_DEPTH} levels`);
1086
1086
  if (++counter.nodes > MAX_NODES) reject(`A UI spec tree must contain at most ${MAX_NODES} nodes`);
1087
- if (!isRecord$1(value)) reject(`${path} must be an object`);
1087
+ if (!isRecord$2(value)) reject(`${path} must be an object`);
1088
1088
  requireString(value["component"], `${path}.component`);
1089
1089
  if (value["id"] !== void 0) requireString(value["id"], `${path}.id`);
1090
- if (value["props"] !== void 0 && (!isRecord$1(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
1090
+ if (value["props"] !== void 0 && (!isRecord$2(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
1091
1091
  const children = value["children"];
1092
1092
  if (children === void 0) return;
1093
1093
  if (!Array.isArray(children)) reject(`${path}.children must be an array`);
@@ -1097,7 +1097,7 @@ function assertActions(value) {
1097
1097
  if (value === void 0) return;
1098
1098
  if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
1099
1099
  for (const action of value) {
1100
- if (!isRecord$1(action)) reject("A surface action must be an object");
1100
+ if (!isRecord$2(action)) reject("A surface action must be an object");
1101
1101
  requireString(action["id"], "Surface action ID");
1102
1102
  const intent = action["intent"];
1103
1103
  if (typeof intent !== "string" || !actionIntents.has(intent)) reject("Surface action intent is invalid");
@@ -1113,7 +1113,7 @@ function validateUiSpecNode(value) {
1113
1113
  return structuredClone(value);
1114
1114
  }
1115
1115
  function assertPatch(value) {
1116
- if (!isRecord$1(value)) reject("A surface patch operation must be an object");
1116
+ if (!isRecord$2(value)) reject("A surface patch operation must be an object");
1117
1117
  if (value["op"] !== "replace" && value["op"] !== "merge" && value["op"] !== "append") reject("Surface patch operation is invalid");
1118
1118
  requireString(value["path"], "Surface patch path");
1119
1119
  if (!value["path"].startsWith("/")) reject("Surface patch path must be a JSON pointer");
@@ -1121,7 +1121,7 @@ function assertPatch(value) {
1121
1121
  }
1122
1122
  /** Validates an individual event in the framework-neutral surface stream. @experimental */
1123
1123
  function validateUiSpecEvent(value) {
1124
- if (!isRecord$1(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
1124
+ if (!isRecord$2(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
1125
1125
  if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
1126
1126
  switch (value["type"]) {
1127
1127
  case "open":
@@ -1179,16 +1179,74 @@ function validateUiSpecEvent(value) {
1179
1179
  }
1180
1180
  /** Extracts validated surface stream events from structured tool output. @experimental */
1181
1181
  function extractUiSpecEvents(data) {
1182
- if (!isRecord$1(data) || data["$surface"] === void 0) return [];
1182
+ if (!isRecord$2(data) || data["$surface"] === void 0) return [];
1183
1183
  const raw = data["$surface"];
1184
1184
  return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
1185
1185
  }
1186
+ /** 跨会话表单填写值在 `user:{userId}` scope 下的 key(FR-5.7) @experimental */
1187
+ const FORM_VALUES_KEY = "formValues";
1188
+ /**
1189
+ * 跨会话稳定的字段标识(FR-5.6)。必须带技能名:
1190
+ * 只有字段名时,两个技能各自的 `email` 会互相串号。
1191
+ * @experimental
1192
+ */
1193
+ function formFieldKey(skillName, fieldName) {
1194
+ return `${skillName}#${fieldName}`;
1195
+ }
1196
+ /** memory 里的原始值形状不受控(宿主可能手改文件),逐条过滤而不是整体信任 @experimental */
1197
+ function readFormValues(raw) {
1198
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
1199
+ const out = {};
1200
+ for (const [key, entry] of Object.entries(raw)) {
1201
+ if (typeof entry !== "object" || entry === null) continue;
1202
+ const record = entry;
1203
+ if (typeof record.ts !== "number" || !("value" in record)) continue;
1204
+ out[key] = {
1205
+ value: record.value,
1206
+ ts: record.ts
1207
+ };
1208
+ }
1209
+ return out;
1210
+ }
1211
+ /** 合并本次提交并按上限裁剪最旧(FR-5.12) @experimental */
1212
+ function putFormValues(current, updates, limit) {
1213
+ const merged = {
1214
+ ...current,
1215
+ ...updates
1216
+ };
1217
+ const keys = Object.keys(merged);
1218
+ if (limit <= 0) return {};
1219
+ if (keys.length <= limit) return merged;
1220
+ const kept = keys.sort((a, b) => merged[a].ts - merged[b].ts).slice(keys.length - limit);
1221
+ return Object.fromEntries(kept.map((key) => [key, merged[key]]));
1222
+ }
1223
+ /** 清除单个字段;不传 fieldKey 即全部清除(FR-5.11) @experimental */
1224
+ function clearFormValues(current, fieldKey) {
1225
+ if (fieldKey === void 0) return {};
1226
+ const { [fieldKey]: _removed, ...rest } = current;
1227
+ return rest;
1228
+ }
1229
+ /**
1230
+ * 宿主侧的清除入口(FR-5.11):设置面板的「清除填写历史」直接调它,
1231
+ * 不必自己知道 scope 与 key 的约定。
1232
+ * @experimental
1233
+ */
1234
+ async function clearStoredFormValues(memory, userId, fieldKey) {
1235
+ const scope = `user:${userId}`;
1236
+ if (fieldKey === void 0) {
1237
+ await memory.delete(scope, FORM_VALUES_KEY);
1238
+ return;
1239
+ }
1240
+ const next = clearFormValues(readFormValues(await memory.get(scope, FORM_VALUES_KEY)), fieldKey);
1241
+ await memory.set(scope, FORM_VALUES_KEY, next);
1242
+ }
1186
1243
  /**
1187
1244
  * JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
1188
1245
  * providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
1189
1246
  * type 映射:string→text、number/integer→number、boolean→boolean、enum→select、其余→textarea。
1247
+ * 传入 skillName 时给每个字段带上跨会话稳定的 `fieldKey`(FR-5.6)。
1190
1248
  */
1191
- function schemaToForm(schema, providedArgs) {
1249
+ function schemaToForm(schema, providedArgs, options) {
1192
1250
  const required = new Set(Array.isArray(schema.required) ? schema.required : []);
1193
1251
  const fields = [];
1194
1252
  for (const [name, prop] of Object.entries(schema.properties ?? {})) {
@@ -1199,6 +1257,7 @@ function schemaToForm(schema, providedArgs) {
1199
1257
  type: mapFieldType(prop),
1200
1258
  ...required.has(name) ? { required: true } : {},
1201
1259
  ...typeof prop.description === "string" ? { description: prop.description } : {},
1260
+ ...options?.skillName !== void 0 ? { fieldKey: formFieldKey(options.skillName, name) } : {},
1202
1261
  ...provided !== void 0 ? { defaultValue: provided } : prop.default !== void 0 ? { defaultValue: prop.default } : {}
1203
1262
  };
1204
1263
  if (Array.isArray(prop.enum)) field.options = prop.enum.map((v) => ({
@@ -1328,6 +1387,36 @@ var TraceRecorder = class {
1328
1387
  return [...this.#events];
1329
1388
  }
1330
1389
  };
1390
+ const isRecord$1 = (v) => typeof v === "object" && v !== null;
1391
+ /** 工具结果可经 `$todo` 标记记入 trace 的事件类型;其余类型不接受,防止工具源伪造 run.* */
1392
+ const TODO_TRACE_TYPES = /* @__PURE__ */ new Set([
1393
+ "todo.created",
1394
+ "todo.updated",
1395
+ "todo.cleared"
1396
+ ]);
1397
+ /**
1398
+ * `$todo` 约定的形状校验:JSON content 的 data 含 `$todo` 键(单条或数组)→ trace 事件。
1399
+ *
1400
+ * 与 `$chart` / `$surface` 同一条既有通道——待办清单的状态机全部在 `@webskill/agent`,
1401
+ * runtime 只认这三个事件类型名,不含任何计划态逻辑。畸形条目忽略不炸。
1402
+ * @experimental
1403
+ */
1404
+ function extractTodoTraceEvents(data) {
1405
+ if (!isRecord$1(data) || data["$todo"] === void 0) return [];
1406
+ const raw = data["$todo"];
1407
+ const entries = Array.isArray(raw) ? raw : [raw];
1408
+ const events = [];
1409
+ for (const entry of entries) {
1410
+ if (!isRecord$1(entry)) continue;
1411
+ const { type, ...rest } = entry;
1412
+ if (typeof type !== "string" || !TODO_TRACE_TYPES.has(type)) continue;
1413
+ events.push({
1414
+ type,
1415
+ data: rest
1416
+ });
1417
+ }
1418
+ return events;
1419
+ }
1331
1420
  const RUN_SNAPSHOT_SCHEMA_VERSION = 2;
1332
1421
  /** @experimental */
1333
1422
  function isUnsupportedRunSnapshot(entry) {
@@ -1519,6 +1608,7 @@ var AgentLoop = class {
1519
1608
  toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
1520
1609
  toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
1521
1610
  paramHistoryLimit: config.paramHistoryLimit ?? 50,
1611
+ formValueLimit: config.formValueLimit ?? 100,
1522
1612
  temperature: config.temperature,
1523
1613
  renderResult: config.renderResult
1524
1614
  };
@@ -2180,6 +2270,7 @@ var AgentLoop = class {
2180
2270
  type: "chart",
2181
2271
  chart
2182
2272
  });
2273
+ for (const todo of extractTodoTraceEvents(item.data)) state.trace.record(todo.type, { data: todo.data });
2183
2274
  try {
2184
2275
  for (const event of extractUiSpecEvents(item.data)) await this.#renderSurface(state, event);
2185
2276
  } catch (e) {
@@ -2200,7 +2291,7 @@ var AgentLoop = class {
2200
2291
  durationMs
2201
2292
  }
2202
2293
  });
2203
- this.#emitTool(state, "failed", call);
2294
+ this.#emitTool(state, "failed", call, result.error?.code);
2204
2295
  }
2205
2296
  for (const artifact of result.artifacts ?? []) state.trace.record("artifact.created", { data: {
2206
2297
  artifactId: artifact.id,
@@ -2375,7 +2466,7 @@ var AgentLoop = class {
2375
2466
  * 集合挂在 LoopState 上、**不写进快照**:写进快照会让跨进程恢复的
2376
2467
  * 消费者永远收不到它本来就没见过的事件。
2377
2468
  */
2378
- #emitTool(state, status, call) {
2469
+ #emitTool(state, status, call, errorCode) {
2379
2470
  const key = `${call.id}:${status}`;
2380
2471
  if (state.emittedToolEvents.has(key)) return;
2381
2472
  state.emittedToolEvents.add(key);
@@ -2389,7 +2480,8 @@ var AgentLoop = class {
2389
2480
  status,
2390
2481
  name: call.name,
2391
2482
  callId: call.id,
2392
- args: summarizeArgs(call.arguments)
2483
+ args: summarizeArgs(call.arguments),
2484
+ ...errorCode !== void 0 ? { errorCode } : {}
2393
2485
  }
2394
2486
  });
2395
2487
  }
@@ -2661,11 +2753,13 @@ var AgentLoop = class {
2661
2753
  let args = call.arguments;
2662
2754
  const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
2663
2755
  if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
2756
+ const fields = schemaToForm(def.inputSchema, args, { skillName });
2757
+ await this.#attachFormSuggestions(fields, state);
2664
2758
  const value = await this.#interact(state, {
2665
2759
  type: "form",
2666
2760
  id: this.#nextInteractionId(state),
2667
2761
  title: `Missing parameters for ${call.name}`,
2668
- fields: schemaToForm(def.inputSchema, args)
2762
+ fields
2669
2763
  }, {
2670
2764
  tool: call.name,
2671
2765
  missing
@@ -2837,6 +2931,47 @@ var AgentLoop = class {
2837
2931
  });
2838
2932
  return history.slice(-limit);
2839
2933
  });
2934
+ await this.#rememberFormValues(state, request, value);
2935
+ }
2936
+ /**
2937
+ * 跨会话字段值的写入(FR-5.6/5.12)。与 `paramHistory` 双写而不是合并:
2938
+ * 那边是按时间的追加序列(运行观测),这边是按字段的最新值(召回),形态不同。
2939
+ */
2940
+ async #rememberFormValues(state, request, value) {
2941
+ const autofill = this.#deps.formAutofill;
2942
+ if (!autofill || request.type !== "form") return;
2943
+ if (typeof value !== "object" || value === null) return;
2944
+ const submitted = value;
2945
+ const ts = Date.parse(state.now());
2946
+ const updates = {};
2947
+ for (const field of request.fields) {
2948
+ if (field.fieldKey === void 0) continue;
2949
+ const next = submitted[field.name];
2950
+ if (next === void 0 || next === "") continue;
2951
+ updates[field.fieldKey] = {
2952
+ value: next,
2953
+ ts
2954
+ };
2955
+ }
2956
+ if (Object.keys(updates).length === 0) return;
2957
+ await this.#memoryMutate(`user:${autofill.userId}`, FORM_VALUES_KEY, state, (current) => putFormValues(readFormValues(current), updates, this.#config.formValueLimit));
2958
+ }
2959
+ /**
2960
+ * 召回(FR-5.8):命中的历史值挂在 `suggestion` 上,**绝不写进 `defaultValue`**——
2961
+ * 后者会被渲染器直接填进控件,等于静默预填(AC-5.6 禁止)。
2962
+ */
2963
+ async #attachFormSuggestions(fields, state) {
2964
+ const autofill = this.#deps.formAutofill;
2965
+ if (!autofill) return;
2966
+ const stored = readFormValues(await this.#memoryGet(`user:${autofill.userId}`, FORM_VALUES_KEY, state));
2967
+ for (const field of fields) {
2968
+ if (field.fieldKey === void 0 || field.defaultValue !== void 0) continue;
2969
+ const hit = stored[field.fieldKey];
2970
+ if (hit !== void 0) field.suggestion = {
2971
+ value: hit.value,
2972
+ ts: hit.ts
2973
+ };
2974
+ }
2840
2975
  }
2841
2976
  };
2842
2977
  /**
@@ -2977,6 +3112,7 @@ var WebSkillRuntime = class {
2977
3112
  hooks: this.#deps.hooks,
2978
3113
  eventBus: this.#events,
2979
3114
  longTerm: this.#deps.longTerm,
3115
+ formAutofill: this.#deps.formAutofill,
2980
3116
  externalTools: this.#deps.externalTools,
2981
3117
  skillProviders: this.#deps.skillProviders,
2982
3118
  catalogFilter: this.#deps.catalogFilter,
@@ -3083,6 +3219,7 @@ var WebSkillRuntime = class {
3083
3219
  hooks: this.#deps.hooks,
3084
3220
  eventBus: this.#events,
3085
3221
  longTerm: this.#deps.longTerm,
3222
+ formAutofill: this.#deps.formAutofill,
3086
3223
  externalTools: this.#deps.externalTools,
3087
3224
  skillProviders: this.#deps.skillProviders,
3088
3225
  catalogFilter: this.#deps.catalogFilter,
@@ -3839,12 +3976,15 @@ function summarizeToolCalls(run) {
3839
3976
  if (typeof name !== "string" || typeof callId !== "string") continue;
3840
3977
  const args = event.data?.["args"];
3841
3978
  const durationMs = event.data?.["durationMs"];
3979
+ const errorCode = event.data?.["code"];
3842
3980
  calls.push({
3843
3981
  callId,
3844
3982
  name,
3845
3983
  status: event.type === "tool.completed" ? "completed" : "failed",
3846
3984
  ...typeof args === "string" ? { args } : {},
3847
- ...typeof durationMs === "number" ? { durationMs } : {}
3985
+ ...typeof durationMs === "number" ? { durationMs } : {},
3986
+ ...typeof errorCode === "string" ? { errorCode } : {},
3987
+ ...event.type === "tool.failed" && typeof event.message === "string" ? { errorMessage: event.message } : {}
3848
3988
  });
3849
3989
  }
3850
3990
  return calls;
@@ -4036,4 +4176,4 @@ var FsSessionStore = class {
4036
4176
  };
4037
4177
 
4038
4178
  //#endregion
4039
- export { createScriptContext as A, networkUrlHost as B, RUN_TRACE_SCHEMA_VERSION as C, WebSkillRuntime as D, TraceRecorder as E, fromVercelStreamPart as F, resolveToolName as G, normalizeToolContent as H, isNetworkAllowed as I, toLlmToolSpec as J, schemaToForm as K, isUnsupportedRunSnapshot as L, extractChartSpec as M, extractUiSpecEvents as N, bridgeError as O, fromVercelResult as P, mergeCatalogEntries as R, RUN_SNAPSHOT_SCHEMA_VERSION as S, SerializingMemoryStore as T, normalizeToolError as U, normalizeErrorCode as V, parseBridgeRequest as W, validateUiSpecEvent as X, toVercelToolSpecs as Y, validateUiSpecNode as Z, OpenAiCompatibleClient as _, AnthropicClient as a, READ_SKILL_FILE_TOOL as b, FS_SESSION_PAGE_SIZE as c, FsRunSnapshotStore as d, FsRunTraceStore as f, HookRunner as g, GoogleGenAiClient as h, AgentLoop as i, createWebSkillApi as j, buildRenderResult as k, FsArtifactStore as l, FullDisclosureRouter as m, ASK_USER_TOOL as n, CapabilityApproval as o, FsSessionStore as p, summarizeToolCalls as q, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsMemoryStore as u, ProgressiveRouter as v, SESSION_SCHEMA_VERSION as w, READ_SKILL_FILE_TOOL_NAME as x, READ_SKILL_FILE_INPUT_SCHEMA as y, networkPolicyLibSource as z };
4179
+ export { schemaToForm as $, buildRenderResult as A, fromVercelStreamPart as B, RUN_SNAPSHOT_SCHEMA_VERSION as C, TraceRecorder as D, SerializingMemoryStore as E, extractChartSpec as F, networkUrlHost as G, isUnsupportedRunSnapshot as H, extractTodoTraceEvents as I, normalizeToolError as J, normalizeErrorCode as K, extractUiSpecEvents as L, clearStoredFormValues as M, createScriptContext as N, WebSkillRuntime as O, createWebSkillApi as P, resolveToolName as Q, formFieldKey as R, READ_SKILL_FILE_TOOL_NAME as S, SESSION_SCHEMA_VERSION as T, mergeCatalogEntries as U, isNetworkAllowed as V, networkPolicyLibSource as W, putFormValues as X, parseBridgeRequest as Y, readFormValues as Z, HookRunner as _, AnthropicClient as a, READ_SKILL_FILE_INPUT_SCHEMA as b, FORM_VALUES_KEY as c, FsMemoryStore as d, summarizeToolCalls as et, FsRunSnapshotStore as f, GoogleGenAiClient as g, FullDisclosureRouter as h, AgentLoop as i, validateUiSpecNode as it, clearFormValues as j, bridgeError as k, FS_SESSION_PAGE_SIZE as l, FsSessionStore as m, ASK_USER_TOOL as n, toVercelToolSpecs as nt, CapabilityApproval as o, FsRunTraceStore as p, normalizeToolContent as q, ASK_USER_TOOL_NAME as r, validateUiSpecEvent as rt, EventBus as s, ASK_USER_INPUT_SCHEMA as t, toLlmToolSpec as tt, FsArtifactStore as u, OpenAiCompatibleClient as v, RUN_TRACE_SCHEMA_VERSION as w, READ_SKILL_FILE_TOOL as x, ProgressiveRouter as y, fromVercelResult as z };