@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 +2 -2
- package/src/execute.ts +63 -114
- package/src/index.ts +43 -11
- package/src/loop-gate.ts +567 -0
- package/src/text-primitives.ts +66 -0
- package/src/tool-definition.ts +182 -19
- package/src/workflow-hook.ts +157 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-structured-output",
|
|
3
|
-
"version": "5.
|
|
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.
|
|
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 编排 +
|
|
2
|
+
* executeStructuredOutput 编排 + 日常校验函数(IF-6 拆分;U1 权威分支透传化)。
|
|
3
3
|
*
|
|
4
4
|
* 两种模式:
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
37
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* draft-07 语义:无 type 时类型关键字按值形态适用——properties/required 等
|
|
41
|
+
* object 特有关键字的存在意味着作者在描述 object 输出,arguments(必为 object)
|
|
42
|
+
* 直接被这些约束校验,故算 object 根。组合根(anyOf/oneOf/allOf/$ref/enum)
|
|
43
|
+
* 可能接受非 object 值,保真起见一律包装({value} 内可容纳任意成员类型)。
|
|
42
44
|
*
|
|
43
|
-
* @
|
|
45
|
+
* [同源锚定] @zhushanwen/pi-subagent-workflow 的 agent-opts-resolver.ts 持有
|
|
46
|
+
* 本函数的本地副本(两包独立 npm 不能直接 import,optional peer 不保证存在),
|
|
47
|
+
* 其 ASP 文案按本判定同源条件化——改动本函数判定逻辑必须同步该副本。
|
|
44
48
|
*/
|
|
45
|
-
export function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
|
143
|
+
* 执行 schema 编排。从工具 execute 抽出以便单元测试直接调用。
|
|
156
144
|
*
|
|
157
|
-
* 编排:
|
|
158
|
-
*
|
|
145
|
+
* 编排:workflow 透传分支(authoritativeSchema 存在;非 object 根解包 value)
|
|
146
|
+
* → 日常分支 validateAgainstSelfReported。
|
|
159
147
|
*/
|
|
160
148
|
export async function executeStructuredOutput(params: {
|
|
161
|
-
schema
|
|
162
|
-
|
|
163
|
-
/**
|
|
149
|
+
/** LLM 自报 schema(仅日常分支消费;workflow 分支忽略)。 */
|
|
150
|
+
schema?: unknown;
|
|
151
|
+
/** workflow 分支 = 模型 arguments(object 根即 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
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
6
|
-
*
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
40
|
-
|
|
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
|
}
|