@zhushanwen/pi-structured-output 0.1.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/index.ts +1 -0
- package/package.json +38 -0
- package/src/index.ts +112 -0
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./src/index.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhushanwen/pi-structured-output",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Structured output tool for Pi — enforces JSON Schema via tool call mechanism with Ajv validation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"pi": {
|
|
8
|
+
"extensions": [
|
|
9
|
+
"./src/index.ts"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi-package",
|
|
14
|
+
"extension",
|
|
15
|
+
"structured-output",
|
|
16
|
+
"json-schema"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"files": [
|
|
20
|
+
"src/",
|
|
21
|
+
"index.ts"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"ajv": "^8.17.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
28
|
+
"@sinclair/typebox": "*"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^24.0.0",
|
|
32
|
+
"vitest": "^4.1.8"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"typecheck": "npx tsc --noEmit",
|
|
36
|
+
"test": "vitest run"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Output Extension
|
|
3
|
+
*
|
|
4
|
+
* Detects STRUCTURED_OUTPUT_SCHEMA env var on session start, registers a tool
|
|
5
|
+
* with Ajv-compiled validation, injects system prompt, and enforces tool usage
|
|
6
|
+
* via turn_end + sendUserMessage.
|
|
7
|
+
*
|
|
8
|
+
* Design: FR-1 to FR-5 from spec, FR-4 dual-layer enforcement.
|
|
9
|
+
* Reference: Claude Code's SyntheticOutputTool (Ajv + Stop hook).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import Ajv, { type ValidateFunction } from "ajv";
|
|
13
|
+
import { Type } from "@sinclair/typebox";
|
|
14
|
+
|
|
15
|
+
/** Pi Extension API — typed as any because shared stub has no real signatures */
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
type PiAPI = any;
|
|
18
|
+
|
|
19
|
+
const ENV_KEY = "STRUCTURED_OUTPUT_SCHEMA";
|
|
20
|
+
const TOOL_NAME = "structured-output";
|
|
21
|
+
|
|
22
|
+
const SYSTEM_PROMPT =
|
|
23
|
+
"你必须在完成分析后调用 structured-output tool 来返回结构化结果。" +
|
|
24
|
+
"不要在文本回复中输出 JSON,直接调用 structured-output tool。" +
|
|
25
|
+
"这是你返回最终结果的唯一方式。";
|
|
26
|
+
|
|
27
|
+
const ENFORCEMENT_MESSAGE = "你必须调用 structured-output tool 来返回结果。";
|
|
28
|
+
|
|
29
|
+
export default function structuredOutputExtension(pi: PiAPI): void {
|
|
30
|
+
const schemaStr = process.env[ENV_KEY];
|
|
31
|
+
if (!schemaStr) return;
|
|
32
|
+
|
|
33
|
+
// Parse schema
|
|
34
|
+
let schema: Record<string, unknown>;
|
|
35
|
+
try {
|
|
36
|
+
schema = JSON.parse(schemaStr);
|
|
37
|
+
} catch {
|
|
38
|
+
console.error(`[${TOOL_NAME}] Failed to parse ${ENV_KEY}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Compile with Ajv
|
|
43
|
+
const ajv = new Ajv({ strict: false });
|
|
44
|
+
let validate: ValidateFunction;
|
|
45
|
+
try {
|
|
46
|
+
validate = ajv.compile(schema);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error(`[${TOOL_NAME}] Invalid JSON Schema:`, (e as Error).message);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Register tool — passthrough parameters (schema is dynamic per session)
|
|
53
|
+
pi.registerTool({
|
|
54
|
+
name: TOOL_NAME,
|
|
55
|
+
label: "Structured Output",
|
|
56
|
+
description:
|
|
57
|
+
"Return structured output conforming to the JSON Schema. You MUST call this tool to return your final result.",
|
|
58
|
+
promptSnippet: "Call structured-output with your final structured answer",
|
|
59
|
+
promptGuidelines: [
|
|
60
|
+
"You MUST call structured-output as your final action.",
|
|
61
|
+
"Do not output JSON in your text response — use this tool instead.",
|
|
62
|
+
],
|
|
63
|
+
parameters: Type.Record(Type.String(), Type.Any()),
|
|
64
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
65
|
+
const valid = validate(params);
|
|
66
|
+
if (!valid) {
|
|
67
|
+
const errors = validate.errors
|
|
68
|
+
?.map((err) => `${err.instancePath} ${err.message}`)
|
|
69
|
+
.join("; ");
|
|
70
|
+
throw new Error(`Schema validation failed: ${errors}`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{ type: "text" as const, text: "Structured output recorded successfully." },
|
|
75
|
+
],
|
|
76
|
+
details: params,
|
|
77
|
+
terminate: true,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// System prompt injection
|
|
83
|
+
pi.on("before_agent_start", async (_event: unknown, ctx: { addSystemInstruction: (s: string) => void }) => {
|
|
84
|
+
ctx.addSystemInstruction(SYSTEM_PROMPT);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Enforcement: track tool calls via tool_execution_start flag
|
|
88
|
+
let hasStructuredOutputCall = false;
|
|
89
|
+
|
|
90
|
+
pi.on("tool_execution_start", async (event: { toolName: string }) => {
|
|
91
|
+
if (event.toolName === TOOL_NAME) {
|
|
92
|
+
hasStructuredOutputCall = true;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
pi.on("turn_end", async () => {
|
|
97
|
+
if (!hasStructuredOutputCall) {
|
|
98
|
+
pi.sendUserMessage(ENFORCEMENT_MESSAGE);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Block non-workflow usage
|
|
103
|
+
pi.on("tool_call", async (event: { toolName: string }) => {
|
|
104
|
+
if (event.toolName === TOOL_NAME && !process.env[ENV_KEY]) {
|
|
105
|
+
return {
|
|
106
|
+
block: true as const,
|
|
107
|
+
reason: "This tool is only available in workflow structured-output mode",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
});
|
|
112
|
+
}
|