@zhushanwen/pi-structured-output 5.0.2 → 5.1.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-structured-output",
3
- "version": "5.0.2",
3
+ "version": "5.1.1",
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",
@@ -27,7 +27,7 @@
27
27
  "ajv": "^8.17.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "@earendil-works/pi-coding-agent": "^0.84.1",
30
+ "@earendil-works/pi-coding-agent": "^0.84.4",
31
31
  "typebox": "*"
32
32
  },
33
33
  "devDependencies": {
package/src/execute.ts CHANGED
@@ -1,17 +1,15 @@
1
1
  /**
2
- * executeStructuredOutput 编排 + 校验双函数(IF-6 拆分)。
2
+ * executeStructuredOutput 编排 + 日常校验函数(IF-6 拆分;U1 权威分支透传化)。
3
3
  *
4
4
  * 两种模式:
5
- * - 权威模式(workflow):`authoritativeSchema` 存在时,只用它校验 data,LLM 传入的
6
- * `schema` 不参与校验(仅用于错误回显)。这从根上杜绝 LLM 自报 schema 自洽绕过
7
- * ([HISTORICAL] 2026-08-01 事故:ds-flash 重写 add_channels.items 的 schema 后
8
- * 自洽通过,4 channel 修复静默丢失)。
5
+ * - workflow 模式(`authoritativeSchema` 存在):透传(D2)。pi-ai 参数层
6
+ * (validateToolArguments)已按注册进工具的 parameters(= 权威 schema,见
7
+ * createWorkflowToolDefinition)校验 + 类型矫正过 arguments——execute ajv
8
+ * 不是双保险而是第二校验权威(方案 A [HISTORICAL] 明令禁止的形态),已删除。
9
+ * 注册期 fail-fast 防御(keyword-less 拒绝 / boolean true 拦截)上移至
10
+ * createWorkflowToolDefinition。非 object 根在注册期被包装为 {value},此处解包。
9
11
  * - 日常模式(交互式):无 `authoritativeSchema`,走 validateAgainstSelfReported 防御链。
10
12
  *
11
- * 权威模式设防(SO-1 修复):权威分支不再跳过防御链——keyword-less 权威 schema
12
- * ({} / {a:1})被显式拒绝(ERR-3),boolean true(accept-all,无形状约束)被拦截
13
- * (ERR-7),否则 workflow 声明的约束会在 ajv strict:false 下静默失效。
14
- *
15
13
  * 日常模式防御顺序(编译前拦截,治静默腐败的根):
16
14
  * 1. 互换检测 — schema 像数据(无 keyword)且 data 像 schema(有 keyword)→ 抛纠错
17
15
  * 2. keyword-less schema 拒绝 — schema 是对象但无任何识别 keyword({} / {a:1})
@@ -24,7 +22,6 @@ import type { ValidateFunction } from "ajv";
24
22
 
25
23
  import { getOrCompileValidator } from "./ajv-validator.js";
26
24
  import {
27
- assertJsonSchemaRoot,
28
25
  CORRECT_USAGE_HINT,
29
26
  echo,
30
27
  hasSchemaKeyword,
@@ -33,60 +30,51 @@ import {
33
30
  } from "./schema-guards.js";
34
31
 
35
32
  /**
36
- * 权威模式校验(IF-6)。authSchema workflow 脚本(PI_WORKFLOW_SCHEMA env)注入,
37
- * 是唯一校验权威——LLM 传入的 schema 不参与校验,仅用于编排层错误回显。
33
+ * 判定权威 schema 的根数据形态是否为 object(U1/P6)。
34
+ *
35
+ * tool call arguments 协议上必为 object:object 根 schema 可直接作工具 parameters
36
+ * (arguments 即 data);否则(array/string/number/boolean/enum/组合根等)需在注册期
37
+ * 包一层 {value},execute 侧对称解包。判定口径与注册期包装严格同源(同一函数),
38
+ * 避免「注册期包装了但 execute 不解包」或反向的漂移。
38
39
  *
39
- * 权威模式不再跳过日常防御链(SO-1 修复):keyword-less 权威 schema 必须先过
40
- * schema-guards 检查,否则 ajv strict:false 会静默编译成 accept-all,workflow 的
41
- * 形状约束失效且零报错(与 08-01 事故同类的静默腐败路径)。
40
+ * draft-07 语义:无 type 时类型关键字按值形态适用——properties/required
41
+ * object 特有关键字的存在意味着作者在描述 object 输出,arguments(必为 object)
42
+ * 直接被这些约束校验,故算 object 根。组合根(anyOf/oneOf/allOf/$ref/enum)
43
+ * 可能接受非 object 值,保真起见一律包装({value} 内可容纳任意成员类型)。
42
44
  *
43
- * @returns 校验通过恒为 true;任何失败形态抛错(带恢复指引 + 回显)。
45
+ * [同源锚定] @zhushanwen/pi-subagent-workflow agent-opts-resolver.ts 持有
46
+ * 本函数的本地副本(两包独立 npm 不能直接 import,optional peer 不保证存在),
47
+ * 其 ASP 文案按本判定同源条件化——改动本函数判定逻辑必须同步该副本。
44
48
  */
45
- export function validateWithAuthoritative(data: unknown, authSchema: object): boolean {
46
- // 类型收窄:签名声明 object(C2 契约),hasSchemaKeyword 需要索引签名。
47
- // 编排层已 assertJsonSchemaRoot 保证 plain object;直接调用方传 plain object。
48
- if (!isPlainObject(authSchema)) {
49
- throw new Error(
50
- "Authoritative schema (PI_WORKFLOW_SCHEMA) must be a plain object, got "
51
- + typeof authSchema,
52
- );
53
- }
54
-
55
- // 1. keyword-less 拒绝(ERR-3):权威 schema 必须用 JSON Schema 关键字描述形状。
56
- // 无 keyword 的对象会被 ajv 编译成"接受一切",必须显式拦截并给恢复指引。
57
- if (!hasSchemaKeyword(authSchema)) {
58
- throw new Error(
59
- "Authoritative schema (PI_WORKFLOW_SCHEMA) has no recognized keyword. "
60
- + "A workflow schema must describe shape via type/properties/items/... "
61
- + "👉 检查 workflow 脚本的 outputSchema 定义,补全 JSON Schema 关键字。 "
62
- + `Received schema=${echo(authSchema)}, data=${echo(data)}`,
63
- );
64
- }
65
-
66
- // 2. 编译 + 校验。编译失败抛清晰错误(含权威 schema 回显供 workflow 作者修正)。
67
- let validate: ValidateFunction;
68
- try {
69
- validate = getOrCompileValidator(authSchema);
70
- } catch (e) {
71
- throw new Error(
72
- `Invalid authoritative JSON Schema (from PI_WORKFLOW_SCHEMA): ${(e as Error).message}. `
73
- + `The authoritative schema (PI_WORKFLOW_SCHEMA) is: ${echo(authSchema)}. `
74
- + `Received data=${echo(data)}`,
75
- );
76
- }
49
+ export function isObjectRootSchema(schema: unknown): schema is Record<string, unknown> {
50
+ if (!isPlainObject(schema)) return false;
51
+ if (schema.type === "object") return true;
52
+ if (Array.isArray(schema.type) && schema.type.includes("object")) return true;
53
+ const OBJECT_ONLY_KEYS = [
54
+ "properties",
55
+ "required",
56
+ "patternProperties",
57
+ "additionalProperties",
58
+ "minProperties",
59
+ "maxProperties",
60
+ "dependencies",
61
+ "dependentRequired",
62
+ "propertyNames",
63
+ ];
64
+ return OBJECT_ONLY_KEYS.some((k) => k in schema);
65
+ }
77
66
 
78
- const valid = validate(data);
79
- if (!valid) {
80
- const errors = validate.errors
81
- ?.map((err) => `${err.instancePath} ${err.message}`)
82
- .join("; ");
83
- throw new Error(
84
- `Schema validation failed (authoritative): ${errors}. `
85
- + `The authoritative schema (PI_WORKFLOW_SCHEMA) is: ${echo(authSchema)}. `
86
- + `Received data=${echo(data)}`,
87
- );
67
+ /**
68
+ * 非 object 根包装的解包(P6 的对称操作)。
69
+ * 注册期 {value} 包装 + 参数层 required ["value"] 保证到达这里的 arguments
70
+ * 形如 {value: <data>};防御性 guard:意外形态(缺 value 字段)原样透传,
71
+ * 不在 execute 层制造第二道校验。
72
+ */
73
+ function unwrapValueField(data: unknown): unknown {
74
+ if (isPlainObject(data) && "value" in data) {
75
+ return data.value;
88
76
  }
89
- return true;
77
+ return data;
90
78
  }
91
79
 
92
80
  /**
@@ -152,15 +140,17 @@ export function validateAgainstSelfReported(schema: unknown, data: unknown): boo
152
140
  }
153
141
 
154
142
  /**
155
- * 执行 schema 校验。从 createToolDefinition.execute 抽出以便单元测试直接调用。
143
+ * 执行 schema 编排。从工具 execute 抽出以便单元测试直接调用。
156
144
  *
157
- * 编排:tryParseJson 归一 权威分支(assertJsonSchemaRoot 收窄 + boolean 拦截 +
158
- * validateWithAuthoritative)→ 日常分支 validateAgainstSelfReported。
145
+ * 编排:workflow 透传分支(authoritativeSchema 存在;非 object 根解包 value)
146
+ * 日常分支 validateAgainstSelfReported。
159
147
  */
160
148
  export async function executeStructuredOutput(params: {
161
- schema: unknown;
162
- data: unknown;
163
- /** 权威 schemaworkflow 模式由 PI_WORKFLOW_SCHEMA env 注入)。存在时成为唯一校验权威。 */
149
+ /** LLM 自报 schema(仅日常分支消费;workflow 分支忽略)。 */
150
+ schema?: unknown;
151
+ /** workflow 分支 = 模型 argumentsobject 根即 data 本身 / 非 object 根为 {value} 包装);日常分支 = 自报 data。 */
152
+ data?: unknown;
153
+ /** 权威 schema(workflow 模式由 PI_WORKFLOW_SCHEMA env 派生)。存在时走透传分支(D2)。 */
164
154
  authoritativeSchema?: unknown;
165
155
  }): Promise<{
166
156
  content: Array<{ type: "text"; text: string }>;
@@ -168,58 +158,14 @@ export async function executeStructuredOutput(params: {
168
158
  // 测试断言 toEqual(42)/toEqual(true)/toEqual(["a","b","c"]),不可窄化为 Record。
169
159
  details: unknown;
170
160
  }> {
171
- // Normalize: some models pass schema/data as JSON strings instead of objects
172
- const schema = tryParseJson(params.schema);
173
- const data = tryParseJson(params.data);
174
-
175
- // ── 权威模式(workflow):用 PI_WORKFLOW_SCHEMA 声明的期望 schema 校验 data。 ──
176
- // LLM 传入的 schema 仅用于错误回显(告知期望形态),不参与校验——否则 LLM
177
- // 可同时控制 schema 与 data 自洽绕过任何约束。日常模式无权威 schema 走下方防御链。
161
+ // ── workflow 模式:透传(D2)──
162
+ // tryParseJson 兼容 string(env 原值)与 object(注册期已解析)两种传入形态。
163
+ // object 根(注册期包装 {value})→ 解包还原;object 根 → arguments 即 data
164
+ // 解包判定与注册期包装判定同源(isObjectRootSchema)。
178
165
  const authoritative =
179
166
  params.authoritativeSchema !== undefined ? tryParseJson(params.authoritativeSchema) : undefined;
180
167
  if (authoritative !== undefined) {
181
- try {
182
- // 先用 assert 函数把 unknown 收窄为 Record<string,unknown> | boolean,
183
- // 使后续分支在类型层面成立(type-safety)。非 object/boolean 抛清晰错误,
184
- // 由外层 catch 包成含 echo 的错误。
185
- assertJsonSchemaRoot(authoritative);
186
- } catch (e) {
187
- throw new Error(
188
- `Invalid authoritative JSON Schema (from PI_WORKFLOW_SCHEMA): ${(e as Error).message}. `
189
- + `Received schema=${echo(schema)}, data=${echo(data)}`,
190
- );
191
- }
192
-
193
- if (authoritative === true) {
194
- // ERR-7:boolean true(accept-all)不提供任何形状约束,workflow 用它等于没校验。
195
- // 必须改为 object schema 才构成真正的约束(keyword-less 拒绝见 validateWithAuthoritative)。
196
- throw new Error(
197
- "Authoritative schema (PI_WORKFLOW_SCHEMA) is boolean true (accept-all), "
198
- + "provides no shape constraint. 👉 改为带 type/properties/items 的 object schema。"
199
- + `Received schema=${echo(schema)}, data=${echo(data)}`,
200
- );
201
- }
202
-
203
- if (authoritative === false) {
204
- // boolean false = reject-all(draft-07 合法根,有形状约束语义:拒绝一切)。
205
- // 保留原行为:编译 + 校验失败抛 'Schema validation failed (authoritative)'。
206
- const validate = getOrCompileValidator(false);
207
- const valid = validate(data);
208
- if (!valid) {
209
- const errors = validate.errors
210
- ?.map((err) => `${err.instancePath} ${err.message}`)
211
- .join("; ");
212
- throw new Error(
213
- `Schema validation failed (authoritative): ${errors}. `
214
- + `The authoritative schema (PI_WORKFLOW_SCHEMA) is: ${echo(authoritative)}. `
215
- + `Received schema=${echo(schema)}, data=${echo(data)}`,
216
- );
217
- }
218
- } else {
219
- // object 权威 schema:过 keyword-less 检查(ERR-3)+ 编译 + 校验。
220
- validateWithAuthoritative(data, authoritative);
221
- }
222
-
168
+ const data = isObjectRootSchema(authoritative) ? params.data : unwrapValueField(params.data);
223
169
  return {
224
170
  content: [
225
171
  { type: "text" as const, text: "Structured output recorded successfully." },
@@ -229,6 +175,9 @@ export async function executeStructuredOutput(params: {
229
175
  }
230
176
 
231
177
  // ── 日常模式防御链(validateAgainstSelfReported:互换/keyword-less/编译/校验)──
178
+ // Normalize: some models pass schema/data as JSON strings instead of objects
179
+ const schema = tryParseJson(params.schema);
180
+ const data = tryParseJson(params.data);
232
181
  validateAgainstSelfReported(schema, data);
233
182
 
234
183
  return {
package/src/index.ts CHANGED
@@ -1,42 +1,74 @@
1
1
  /**
2
2
  * Structured Output Extension — 条件激活的 schema 校验工具 + hook
3
3
  *
4
- * 激活模式:
5
- * - 日常 pi(interactive / 普通 print):不设置 PI_WORKFLOW_SCHEMA,扩展不注册工具
6
- * - workflow 子进程:agent-pool 设置 PI_WORKFLOW_SCHEMA=<json>,扩展注册工具 + hook
4
+ * 装配分岔(D1,U1):读 PI_WORKFLOW_SCHEMA——
5
+ * - 有值(workflow 子进程):注册 workflow 变体(parameters = 权威 schema 本身,
6
+ * D4 根级 additionalProperties 注入 / P6 非 object 根 {value} 包装 / 注册期
7
+ * fail-fast 防御都在 createWorkflowToolDefinition 内)+ turn_end 强制 hook
8
+ * - 无值(日常 pi):注册日常变体(双参数自报形态,行为不变,G4)
7
9
  *
8
10
  * Hook 机制(仅 workflow 模式):
9
11
  * turn_end 时检查模型是否调用了 structured-output 工具。
10
12
  * 如果没调 → 通过 pi.sendUserMessage() 注入 steering message 强制调用。
11
13
  * 最多重试 2 次,防止无限循环。
12
14
  *
15
+ * 失败闸门(仅 workflow 模式,D3/U2):
16
+ * tool_execution_end 上计数同签名校验失败,连续 3 次 → terminal:写日志
17
+ * (stderr + session JSONL 双通道,含恢复指引)后 ctx.abort()(停当前 turn)+
18
+ * ctx.shutdown() 优雅终止子进程(RPC mode 于 agent_settled 后 exit),并武装
19
+ * 15s 兑底硬退 timer(R3 F-2 bounded teardown:pi 0.84.1 ExtensionAPI 无子进程
20
+ * 信号能力,扩展与子进程同进程,process.exit 是唯一硬杀手段;覆盖 shutdown
21
+ * 请求后 pi 挂死不 settle 的异常态);同时标记 RetryState.terminal,turn_end
22
+ * hook 不再 steer。
23
+ *
13
24
  * 模块拆分(M4):实现体分布于 ajv-validator.ts(编译缓存)/
14
25
  * schema-guards.ts(形态守卫)/ execute.ts(校验编排)/ tool-definition.ts(工具定义)/
15
- * workflow-hook.ts(hook + RetryState)。本文件仅剩 entry 装配与 re-export。
26
+ * workflow-hook.ts(hook + RetryState)/ loop-gate.ts(D3 失败闸门)。
27
+ * 本文件仅剩 entry 装配与 re-export。
16
28
  */
17
29
 
18
30
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
31
 
32
+ import { setupLoopGate, LoopGate } from "./loop-gate.js";
20
33
  import { executeStructuredOutput } from "./execute.js";
21
- import { createToolDefinition, ENV_SCHEMA } from "./tool-definition.js";
34
+ import {
35
+ createDailyToolDefinition,
36
+ createWorkflowToolDefinition,
37
+ ENV_SCHEMA,
38
+ SO_SCHEMA_SIZE_WARN_BYTES,
39
+ } from "./tool-definition.js";
22
40
  import { RetryState, setupWorkflowHook } from "./workflow-hook.js";
23
41
 
24
42
  /** Pi Extension API — properly typed via ExtensionAPI from pi-coding-agent SDK */
25
43
  type PiAPI = ExtensionAPI;
26
44
 
27
45
  // re-export 供测试与外部直接调用(import 路径 ../src/index.js 保持稳定)
28
- export { executeStructuredOutput, createToolDefinition, RetryState };
46
+ export {
47
+ executeStructuredOutput,
48
+ createDailyToolDefinition,
49
+ createWorkflowToolDefinition,
50
+ ENV_SCHEMA,
51
+ SO_SCHEMA_SIZE_WARN_BYTES,
52
+ RetryState,
53
+ setupLoopGate,
54
+ LoopGate,
55
+ };
29
56
 
30
57
  // ── Extension entry ────────────────────────────────────────────
31
58
 
32
59
  export default function structuredOutputExtension(pi: PiAPI): void {
33
60
  const schemaEnv = process.env[ENV_SCHEMA];
34
61
 
35
- // Always register the tool so it's available in all sessions (interactive, workflow, etc.)
36
- pi.registerTool(createToolDefinition());
37
-
38
62
  if (schemaEnv) {
39
- // ── Workflow 模式:额外注册 hook 强制调用 ──
40
- setupWorkflowHook(pi, schemaEnv);
63
+ // ── Workflow 模式:单参数工具(注册期 fail-fast 防御)+ 强制调用 hook + 失败闸门 ──
64
+ pi.registerTool(createWorkflowToolDefinition(schemaEnv));
65
+ const hookState = setupWorkflowHook(pi, schemaEnv);
66
+ // D3/U2:闸门 terminal(同签名失败 ×3)→ 标记 hook 状态停 steer,写日志后
67
+ // ctx.shutdown() 优雅终止(RPC mode 于 agent_settled 后 exit)。硬闸门在软
68
+ // steer 之上兜底:G2 与模型配合度无关。
69
+ setupLoopGate(pi, { onTerminal: () => hookState.markTerminal() });
70
+ } else {
71
+ // ── 日常模式:双参数自报形态(行为不变,G4)──
72
+ pi.registerTool(createDailyToolDefinition());
41
73
  }
42
74
  }