@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.
- package/package.json +1 -1
- package/src/index.ts +88 -7
package/package.json
CHANGED
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)
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
`
|
|
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
|
|