@zhushanwen/pi-structured-output 5.1.7 → 5.1.9

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/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @zhushanwen/pi-structured-output
2
+
3
+ 结构化输出 pi extension:让模型用经过 JSON Schema 校验的工具调用代替自由文本交付结果。按 `PI_WORKFLOW_SCHEMA` 环境变量装配两种变体——workflow 模式以引擎注入的权威 schema 强制约束产出,日常模式校验模型自报 schema 的数据。
4
+
5
+ ## 两种模式
6
+
7
+ 启动时读取 `PI_WORKFLOW_SCHEMA`(该 env 由 subagent workflow 引擎注入子进程):
8
+
9
+ | | workflow 模式(env 有值) | 日常模式(env 无值) |
10
+ |---|---|---|
11
+ | 工具参数 | 单参数——parameters 即权威 schema 本身 | 双参数 `{schema, data}` 自报形态 |
12
+ | 校验权威 | pi 参数层直接按权威 schema 校验模型 arguments,execute 透传不二次校验 | Ajv 校验自报 data(防御链见下) |
13
+ | 强制手段 | turn_end 未成功调用工具(完全未调用,或调用了但全部校验失败)→ steer 注入重试(整个子进程生命周期最多 2 次);同签名失败连续 3 次 → 写日志后 abort 当前 turn + shutdown 子进程,15s 内未退出则进程硬退兜底 | 无(普通工具,失败抛错由模型自修) |
14
+
15
+ ## 提供的工具
16
+
17
+ ### `structured-output`
18
+
19
+ **workflow 模式**(`PI_WORKFLOW_SCHEMA` 有值时注册):
20
+
21
+ - object 根 schema:arguments 即数据本身,如 `structured-output({ score: 8 })`
22
+ - 非 object 根 schema(array/string/number/boolean/组合根等):包装为 `{ value: <data> }`(tool call arguments 协议上必须是 object),错误路径带 `value.` 前缀
23
+ - 根级 `additionalProperties` 未声明时注入 `false`:模型多传的字段被参数层显式拒绝(作者显式声明则尊重不动)
24
+ - 注册期 fail-fast:非法 schema(非 object/boolean 根、boolean true、无任何关键字的 object)在子进程加载期即终止并指回 workflow 脚本的 schema 定义;schema 超 256KiB 时 stderr 提示精简/拆分(硬拒绝在引擎注入侧)
25
+
26
+ **日常模式**(交互式 pi 注册):
27
+
28
+ - `schema`:JSON Schema draft-07 对象
29
+ - `data`:待校验的值(原始类型/object/array 均可)
30
+
31
+ 日常模式防御链(编译前拦截,全部抛错并带回显与纠错提示):
32
+
33
+ 1. 互换检测——schema 像数据且 data 像 schema → 判定为参数装反,拒绝
34
+ 2. 无关键字 schema 拒绝——`{}` / `{a:1}` 会被 Ajv(`strict:false`)编译成"接受一切",静默放行垃圾数据
35
+ 3. Ajv 编译失败 → 报 Invalid JSON Schema
36
+ 4. 校验失败 → 报告失败字段路径
37
+
38
+ ## 安装
39
+
40
+ ```bash
41
+ # npm 方式(正式)
42
+ pi install npm:@zhushanwen/pi-structured-output
43
+
44
+ # 本地路径加载(开发调试;-e 为 --extension 简写,可多次传入)
45
+ pi -e /path/to/extensions/universal/structured-output
46
+ pi --extension /path/to/extensions/universal/structured-output
47
+ ```
48
+
49
+ ## 示例
50
+
51
+ 日常模式(模型自报 schema + data):
52
+
53
+ ```
54
+ structured-output({
55
+ schema: { type: "object", properties: { score: { type: "number" } }, required: ["score"] },
56
+ data: { score: 8 }
57
+ })
58
+ ```
59
+
60
+ workflow 模式(schema 由引擎注入,模型只提交数据):
61
+
62
+ ```
63
+ structured-output({ score: 8 }) # object 根:arguments 即数据
64
+ structured-output({ value: [1, 2, 3] }) # array 根:包装在 value 字段
65
+ ```
66
+
67
+ ## 文件结构
68
+
69
+ ```
70
+ structured-output/
71
+ ├── index.ts # 入口 — re-export src/index.ts
72
+ └── src/
73
+ ├── index.ts # 装配分岔:读 PI_WORKFLOW_SCHEMA 选择变体 + re-export
74
+ ├── tool-definition.ts # 双变体工具定义(workflow 单参数 / 日常双参数)
75
+ ├── execute.ts # 校验编排(workflow 透传 / 日常防御链)+ 根形态判定
76
+ ├── ajv-validator.ts # Ajv 编译缓存(WeakMap)
77
+ ├── schema-guards.ts # 形态守卫纯函数(互换检测 / 关键字识别 / JSON 解析回显)
78
+ ├── workflow-hook.ts # turn_end 强制调用 hook + RetryState 状态机
79
+ ├── loop-gate.ts # 同签名失败 ×3 有界闸门(abort + shutdown + 硬退兜底)
80
+ └── text-primitives.ts # 截断/错误块有界化共享原语
81
+ ```
82
+
83
+ ## License
84
+
85
+ MIT
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-structured-output",
3
- "version": "5.1.7",
3
+ "version": "5.1.9",
4
4
  "description": "Structured output tool for Pi — workflow schema enforcement via pi's parameter layer; interactive mode validates self-reported schemas with Ajv",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
- "xyz-agent": {
7
+ "taiji": {
8
8
  "role": "universal"
9
9
  },
10
10
  "pi": {
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "ajv": "^8.17.0",
28
- "@zhushanwen/pi-ext-guards": "0.4.0"
28
+ "@zhushanwen/pi-ext-guards": "0.4.1"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@earendil-works/pi-coding-agent": "^0.84.4",
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Hook 机制(仅 workflow 模式):
11
11
  * turn_end 时检查模型是否调用了 structured-output 工具。
12
- * 如果没调 → 通过 pi.sendUserMessage() 注入 steering message 强制调用。
12
+ * 如果没调 → 通过 pi.sendMessage()(custom message)注入 steering message 强制调用。
13
13
  * 最多重试 2 次,防止无限循环。
14
14
  *
15
15
  * 失败闸门(仅 workflow 模式,D3/U2):
package/src/loop-gate.ts CHANGED
@@ -458,7 +458,7 @@ export function armForceExitTeardown(): void {
458
458
 
459
459
  /**
460
460
  * terminal 态日志(§5.2 形态 b):
461
- * - stderr:子进程 stderr 直出(xyz-agent runtime 的 pi-*.jsonl tee / 本地探针可见)
461
+ * - stderr:子进程 stderr 直出(taiji runtime 的 pi-*.jsonl tee / 本地探针可见)
462
462
  * - appendEntry:session JSONL 持久化记录(事后排查通道,不进 LLM 上下文)
463
463
  * 两者内容同源,指引文案逐字对齐设计 §5.2。
464
464
  */
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Workflow hook:turn_end 时检查模型是否成功调用 structured-output 工具。
3
- * 未成功时通过 pi.sendUserMessage({deliverAs:"steer"}) 注入 steering message 重试。
3
+ * 未成功时通过 pi.sendMessage(custom message,display:false)以 steer 方式注入
4
+ * steering message 重试——提示词不伪装用户气泡,对话流用户内容 100% 来自用户输入。
4
5
  * 最多重试 MAX_HOOK_RETRIES 次,防止无限循环。
5
6
  *
6
7
  * RetryState:从旧 4 个 mutable 闭包(soCallCount/soSucceededEver/hookRetryCount/
@@ -90,6 +91,14 @@ export class RetryState {
90
91
  /** steer 发送失败告警的 appendEntry customType(session JSONL 持久化,不进 LLM 上下文)。 */
91
92
  export const HOOK_ENTRY_TYPE = "structured-output:hook";
92
93
 
94
+ /**
95
+ * steer reminder 的 sendMessage customType(role:"custom" 消息,pi convertToLlm 对其
96
+ * 无条件转 LLM user 消息——LLM 可见性与 user message 无差别;display:false 不渲染
97
+ * 用户气泡,对话流归属语义结构性正确。锚点登记 docs/pi-semantics.json PS-43:
98
+ * pi dist/core/messages.js:89-96 case "custom" 无条件 role:"user")。
99
+ */
100
+ export const RETRY_REMINDER_CUSTOM_TYPE = "structured-output:retry-reminder";
101
+
93
102
  /**
94
103
  * steer 发送失败告警(审查项#8 失败路径):双通道落盘(同 loop-gate writeTerminatedLog
95
104
  * 惯例)——stderr 直出 + appendEntry 持久化。预算未扣减由「不调 onTurnEnd」结构保证,
@@ -194,7 +203,8 @@ function buildSteerReminder(
194
203
 
195
204
  /**
196
205
  * 注册 turn_end hook,检查模型是否成功调用 structured-output 工具。
197
- * 未成功时通过 pi.sendUserMessage({deliverAs:"steer"}) 注入 steering message 重试。
206
+ * 未成功时通过 pi.sendMessage(custom message,display:false)以 steer 方式注入
207
+ * steering message 重试。
198
208
  * 最多重试 MAX_HOOK_RETRIES 次,防止无限循环。
199
209
  *
200
210
  * @returns 共享的 RetryState(U2:index.ts 拿它接线 loop-gate 的 onTerminal 回调)。
@@ -240,14 +250,18 @@ export function setupWorkflowHook(pi: PiAPI, schemaJson: string): RetryState {
240
250
  // 故 onTurnEnd()(清空 lastSchemaError)必须在发送成功之后调用。
241
251
  const reminder = buildSteerReminder(calledButFailed, state.lastSchemaError, schemaJson, isObjectRoot);
242
252
 
243
- // 审查项#8:await 发送结果——发送失败(如 compaction 中 prompt() 抛错 / 扩展已
253
+ // 审查项#8:await 发送结果——发送失败(如 compaction 中开轮抛错 / 扩展已
244
254
  // 被 assertActive 拒绝)不扣减重试预算(不调 onTurnEnd),否则 fire-and-forget
245
255
  // 丢一份 steer + 白扣一次预算,两次即永久哑火。
246
- // pi 0.84.1 实装(loader.js):extension 侧 sendUserMessage 同步转发且吞掉异步
247
- // rejection(转 emitError)返回 void——await 对 undefined 立即解析;此处的
248
- // try/catch 兜住同步 throw(assertActive)与未来 pi 返回真 Promise 的形态。
256
+ // pi 0.84.4 实装(loader.js):extension 侧 sendMessage 同步转发且吞掉异步
257
+ // rejection(bindCore .catch(emitError) 转事件)返回 void——await 对 undefined
258
+ // 立即解析;此处的 try/catch 兜住同步 throw(assertActive)与未来 pi 返回真
259
+ // Promise 的形态。
249
260
  try {
250
- await pi.sendUserMessage(reminder, { deliverAs: "steer" });
261
+ await pi.sendMessage(
262
+ { customType: RETRY_REMINDER_CUSTOM_TYPE, content: reminder, display: false },
263
+ { deliverAs: "steer", triggerTurn: true },
264
+ );
251
265
  } catch (err) {
252
266
  writeSteerFailedLog(pi, state.hookRetryCount, err);
253
267
  return;