@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.
Files changed (3) hide show
  1. package/index.ts +1 -0
  2. package/package.json +38 -0
  3. 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
+ }