@zhushanwen/pi-structured-output 2.0.0 → 5.0.0-dev.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +88 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-structured-output",
3
- "version": "2.0.0",
3
+ "version": "5.0.0-dev.0",
4
4
  "description": "Structured output tool for Pi — enforces JSON Schema via tool call mechanism with Ajv validation",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/src/index.ts CHANGED
@@ -110,6 +110,18 @@ function tryParseJson(raw: unknown): unknown {
110
110
  }
111
111
  }
112
112
 
113
+ /**
114
+ * 把 authoritative schema 的 unknown 收窄为合法 JSON Schema 根类型(object | boolean)。
115
+ * draft-07 允许 boolean 根 schema(true=接受一切,false=拒绝一切)。
116
+ * 独立守卫使后续 getOrCompileValidator(authoritative) 在类型层面也成立,
117
+ * 避免在守卫块外直接用 unknown。非合法形态抛清晰错误(与日常模式第 235 行同构)。
118
+ */
119
+ function assertJsonSchemaRoot(value: unknown): asserts value is Record<string, unknown> | boolean {
120
+ if (!(isPlainObject(value) || typeof value === "boolean")) {
121
+ throw new Error(`authoritative schema must be a JSON Schema object or boolean, got ${typeof value}`);
122
+ }
123
+ }
124
+
113
125
  /** turn_end event 是否可安全访问 message.stopReason(用于判断模型是否还在调工具链)。 */
114
126
  function isTurnEndEvent(e: unknown): e is { message?: { stopReason?: string } } {
115
127
  return typeof e === "object" && e !== null;
@@ -129,7 +141,15 @@ const CORRECT_USAGE_HINT =
129
141
  /**
130
142
  * 执行 schema 校验。从 createToolDefinition.execute 抽出以便单元测试直接调用。
131
143
  *
132
- * 防御顺序(编译前拦截,治静默腐败的根):
144
+ * 两种模式:
145
+ * - 权威模式(workflow):`authoritativeSchema` 存在时,只用它校验 data,LLM 传入的
146
+ * `schema` 不参与校验(仅用于错误回显)。这从根上杜绝 LLM 自报 schema 自洽绕过
147
+ * ([HISTORICAL] 2026-08-01 事故:ds-flash 重写 add_channels.items 的 schema 后
148
+ * 自洽通过,4 条 channel 修复静默丢失)。权威分支提前 return,跳过日常防御链
149
+ * (权威 schema 由 workflow 脚本写死,不存在互换/keyword-less 风险)。
150
+ * - 日常模式(交互式):无 `authoritativeSchema`,走下方防御链。
151
+ *
152
+ * 日常模式防御顺序(编译前拦截,治静默腐败的根):
133
153
  * 1. 互换检测 — schema 像 data(无 keyword)且 data 像 schema(有 keyword)→ 抛纠错
134
154
  * 2. keyword-less schema 拒绝 — schema 是对象但无任何识别 keyword({} / {a:1})
135
155
  * → 抛 "no recognized keyword",否则 ajv strict:false 会编译成"接受一切"
@@ -139,6 +159,8 @@ const CORRECT_USAGE_HINT =
139
159
  export async function executeStructuredOutput(params: {
140
160
  schema: unknown;
141
161
  data: unknown;
162
+ /** 权威 schema(workflow 模式由 PI_WORKFLOW_SCHEMA env 注入)。存在时成为唯一校验权威。 */
163
+ authoritativeSchema?: unknown;
142
164
  }): Promise<{
143
165
  content: Array<{ type: "text"; text: string }>;
144
166
  // data 可能是 primitive/array/object(根 schema 决定),故 details 为 unknown。
@@ -149,6 +171,46 @@ export async function executeStructuredOutput(params: {
149
171
  const schema = tryParseJson(params.schema);
150
172
  const data = tryParseJson(params.data);
151
173
 
174
+ // ── 权威模式(workflow):用 PI_WORKFLOW_SCHEMA 声明的期望 schema 校验 data。 ──
175
+ // LLM 传入的 schema 仅用于错误回显(告知期望形态),不参与校验——否则 LLM
176
+ // 可同时控制 schema 与 data 自洽绕过任何约束。日常模式无权威 schema 走下方防御链。
177
+ const authoritative =
178
+ params.authoritativeSchema !== undefined ? tryParseJson(params.authoritativeSchema) : undefined;
179
+ if (authoritative !== undefined) {
180
+ let validate: ValidateFunction;
181
+ try {
182
+ // 先用 assert 函数把 unknown 收窄为 Record<string,unknown> | boolean,
183
+ // 使后续 getOrCompileValidator(authoritative) 在类型层面也成立(type-safety)。
184
+ // 运行时行为不变:非 object/boolean 抛清晰错误,由外层 catch 包成含 echo 的错误。
185
+ assertJsonSchemaRoot(authoritative);
186
+ validate = getOrCompileValidator(authoritative);
187
+ } catch (e) {
188
+ throw new Error(
189
+ `Invalid authoritative JSON Schema (from PI_WORKFLOW_SCHEMA): ${(e as Error).message}. `
190
+ + `Received schema=${echo(schema)}, data=${echo(data)}`,
191
+ );
192
+ }
193
+ const valid = validate(data);
194
+ if (!valid) {
195
+ const errors = validate.errors
196
+ ?.map((err) => `${err.instancePath} ${err.message}`)
197
+ .join("; ");
198
+ throw new Error(
199
+ `Schema validation failed (authoritative): ${errors}. `
200
+ + `The authoritative schema (PI_WORKFLOW_SCHEMA) is: ${echo(authoritative)}. `
201
+ + `Received schema=${echo(schema)}, data=${echo(data)}`,
202
+ );
203
+ }
204
+ return {
205
+ content: [
206
+ { type: "text" as const, text: "Structured output recorded successfully." },
207
+ ],
208
+ details: data,
209
+ };
210
+ }
211
+
212
+ // ── 日常模式防御链 ──────────────────────────────────────────────
213
+
152
214
  // 1. 互换检测:schema 像数据(对象无 keyword)且 data 像 schema(对象有 keyword)。
153
215
  // 这是最严重的静默腐败路径——若放行,ajv 会把"数据形态的 schema"编译成接受一切,
154
216
  // 真正的 schema(此时在 data 里)被丢弃,校验通过并存入垃圾。
@@ -211,14 +273,16 @@ export async function executeStructuredOutput(params: {
211
273
 
212
274
  // ── Tool definition (shared between modes) ─────────────────────
213
275
 
214
- function createToolDefinition() {
276
+ export function createToolDefinition() {
215
277
  return {
216
278
  name: TOOL_NAME,
217
279
  label: "Structured Output",
218
280
  description:
219
281
  "Return structured output validated against a JSON Schema. "
220
282
  + "Call this tool to produce validated JSON data. "
221
- + "Pass `schema` (a JSON Schema draft-07 object) and `data` (the value to validate).\n\n"
283
+ + "Pass `schema` (a JSON Schema draft-07 object) and `data` (the value to validate). "
284
+ + "When the schema is system-enforced (workflow mode), pass ONLY `data` — "
285
+ + "the `schema` parameter is ignored (the system validates `data` against the authoritative schema).\n\n"
222
286
  + "schema describes the shape; data fills the values; they must match.\n\n"
223
287
  + "✅ Correct (full call): structured_output({schema:{type:'object',properties:{name:{type:'string'},age:{type:'number'}},required:['name']}, data:{name:'Alice',age:30}})\n"
224
288
  + "✅ Correct: schema={type:'array',items:{type:'string'}}, data=['a','b','c']\n"
@@ -253,7 +317,22 @@ function createToolDefinition() {
253
317
  _toolCallId: string,
254
318
  params: { schema: unknown; data: unknown },
255
319
  ) {
256
- return executeStructuredOutput(params);
320
+ // workflow 模式(PI_WORKFLOW_SCHEMA 存在):权威 schema 成为唯一校验权威,
321
+ // LLM 传入的 params.schema 被降级为错误回显,无法影响校验结果。
322
+ //
323
+ // 运行假设:workflow 子进程是单 session 进程(由 applySchemaEnvToChildEnv 在
324
+ // session-runner 注入 PI_WORKFLOW_SCHEMA)。Pi extension 状态在 session_start 重建,
325
+ // 但 process.env 在进程级共享——这里依赖「workflow 子进程不会复用未注入 env 的 session」
326
+ // 的单 session 约定,故直接读 process.env 而非维护 per-session 缓存。
327
+ //
328
+ // 判空用 `|| undefined` 归一空串为 undefined(truthy 语义),与 entry 的 `if (schemaEnv)`
329
+ // 和 applySchemaEnvToChildEnv 的 `if (schemaEnv)` 统一:空串 env 视为未设置。
330
+ const authoritativeSchema = process.env[ENV_SCHEMA] || undefined;
331
+ return executeStructuredOutput(
332
+ authoritativeSchema !== undefined
333
+ ? { ...params, authoritativeSchema }
334
+ : params,
335
+ );
257
336
  },
258
337
  };
259
338
  }
@@ -345,14 +424,16 @@ function setupWorkflowHook(pi: PiAPI, schemaJson: string): void {
345
424
  "[MANDATORY] Your structured-output call FAILED validation:",
346
425
  lastSchemaError,
347
426
  "",
348
- `The correct schema is: ${schemaJson}`,
349
- "Call the structured-output tool AGAIN with data conforming to this schema.",
427
+ "The schema is enforced by the system (PI_WORKFLOW_SCHEMA) — do NOT pass your own `schema` parameter.",
428
+ `The required schema for your \`data\` is: ${schemaJson}`,
429
+ "Call the structured-output tool AGAIN with ONLY the `data` parameter conforming to this schema.",
350
430
  "Do NOT output the result as text — call the tool.",
351
431
  ].join("\n")
352
432
  : [
353
433
  "[MANDATORY] You MUST call the structured-output tool now.",
354
434
  "Your task requires a structured output. Do NOT respond with plain text.",
355
- `Call the structured-output tool with: schema = ${schemaJson}, data = <your result>`,
435
+ `The schema is enforced by the system. Call structured-output with ONLY \`data\` matching this shape: ${schemaJson}`,
436
+ "Do NOT pass a `schema` parameter — the system validates `data` against the authoritative schema automatically.",
356
437
  "This is enforced by the workflow system. Just call the tool.",
357
438
  ].join("\n");
358
439