@zhushanwen/pi-subagent-workflow 0.4.3 → 2.0.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-subagent-workflow",
3
- "version": "0.4.3",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -46,8 +46,8 @@
46
46
  "@earendil-works/pi-coding-agent": "*",
47
47
  "@earendil-works/pi-ai": "*",
48
48
  "@earendil-works/pi-tui": "*",
49
- "@sinclair/typebox": "*",
50
- "@zhushanwen/pi-structured-output": "0.3.5"
49
+ "typebox": "*",
50
+ "@zhushanwen/pi-structured-output": "2.0.1"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "@earendil-works/pi-coding-agent": {
@@ -29,7 +29,7 @@ vi.mock("@earendil-works/pi-ai", () => ({
29
29
  vi.mock("@earendil-works/pi-ai", () => ({
30
30
  StringEnum: (values: string[]) => ({ type: "string", enum: values }),
31
31
  }));
32
- vi.mock("@sinclair/typebox", () => ({
32
+ vi.mock("typebox", () => ({
33
33
  Type: {
34
34
  Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
35
35
  Optional: (schema: unknown) => ({ ...(schema as object), optional: true }),
@@ -46,12 +46,15 @@ describe("formatSchemaInstruction", () => {
46
46
  const out = formatSchemaInstruction({ type: "object" });
47
47
  // 完整结构快照——任何指令措辞/顺序/缩进漂移都会被捕获。
48
48
  // 注意第三行末尾的 em-dash(—),防止有人把它替换成普通连字符。
49
+ // [HISTORICAL] 方案 A 后文案更新:告知 LLM schema 由系统注入,只需传 data。
49
50
  expect(out).toBe(
50
51
  [
51
52
  "MANDATORY: Structured Output Requirement",
52
53
  "You MUST call the `structured-output` tool with your final answer.",
53
54
  "Do NOT output the JSON directly in your text response — you MUST use the structured-output tool.",
54
- "The schema for the structured output is:",
55
+ "The schema is enforced by the system — call structured-output with ONLY the `data` parameter.",
56
+ "Do NOT pass a `schema` parameter; the system validates `data` against the authoritative schema automatically.",
57
+ "The schema for your `data` is:",
55
58
  "```json",
56
59
  '{',
57
60
  ' "type": "object"',
@@ -51,7 +51,7 @@ vi.mock("@earendil-works/pi-ai", () => ({
51
51
  vi.mock("@earendil-works/pi-ai", () => ({
52
52
  StringEnum: (values: string[]) => ({ type: "string", enum: values }),
53
53
  }));
54
- vi.mock("@sinclair/typebox", () => ({
54
+ vi.mock("typebox", () => ({
55
55
  Type: {
56
56
  Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
57
57
  Optional: (schema: unknown) => ({ ...(schema as object), optional: true }),
@@ -12,19 +12,19 @@
12
12
  // 不导入 index.ts(它经 getAgentDir 值导入触发 alias 解析失败——alias 指向
13
13
  // .d.ts-only stub)。改测叶子注册函数。session_start 的
14
14
  // (event, ctx) 双参数契约由 tsconfig 的精确 stub 类型在编译期强制(见
15
- // shared/types/mariozechner/index.d.ts 注释:modelRegistry/cwd/ui 不在 event 上)。
15
+ // 注释:modelRegistry/cwd/ui 不在 event 上)。
16
16
 
17
17
  import { describe, expect, it, vi } from "vitest";
18
18
 
19
19
  // registerSubagentTool 经 subagent-tool.ts 值导入 StringEnum(pi-ai)+ Type(typebox)。
20
- // shared/types stub 是 .d.ts(仅类型),pnpm optional-peer-dep 插件拦截值导入 → vi.mock 兜底。
20
+ // stub 是 .d.ts(仅类型),pnpm optional-peer-dep 插件拦截值导入 → vi.mock 兜底。
21
21
  vi.mock("@earendil-works/pi-ai", () => ({
22
22
  StringEnum: (values: string[]) => ({ type: "string", enum: values }),
23
23
  }));
24
24
  vi.mock("@earendil-works/pi-ai", () => ({
25
25
  StringEnum: (values: string[]) => ({ type: "string", enum: values }),
26
26
  }));
27
- vi.mock("@sinclair/typebox", () => ({
27
+ vi.mock("typebox", () => ({
28
28
  Type: {
29
29
  Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
30
30
  Optional: (schema: unknown) => ({ ...schema as object, optional: true }),
@@ -263,7 +263,7 @@ describe("subagent tool contract [MANDATORY]", () => {
263
263
  // ============================================================
264
264
  describe("session_start handler signature (compile-time guarantee)", () => {
265
265
  it("ExtensionHandler<SessionStartEvent> is (event, ctx) two-param — enforced by stub type", () => {
266
- // 此测试是编译期断言:shared/types/mariozechner/index.d.ts:111 声明
266
+ // 此测试是编译期断言:stub 类型声明
267
267
  // ExtensionHandler<E, R> = (event: E, ctx: ExtensionContext) => Promise<R|void> | R | void;
268
268
  // 且 SessionStartEvent 注释明确 modelRegistry/cwd/ui 不在 event 上(在 ctx)。
269
269
  // index.ts:66 `pi.on("session_start", (_event, ctx) => {...})` 通过此类型检查即证明契约。
@@ -21,7 +21,7 @@ vi.mock("@earendil-works/pi-ai", () => ({
21
21
  vi.mock("@earendil-works/pi-ai", () => ({
22
22
  StringEnum: (values: string[]) => ({ type: "string", enum: values }),
23
23
  }));
24
- vi.mock("@sinclair/typebox", () => ({
24
+ vi.mock("typebox", () => ({
25
25
  Type: {
26
26
  Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
27
27
  Optional: (schema: unknown) => ({ ...schema as object, optional: true }),
@@ -144,7 +144,7 @@ interface QueueItem {
144
144
  *
145
145
  * 单 session 假设(M-2,与 index.ts lastSessionId 同源):本队列是进程级单例(实例挂在
146
146
  * globalThis[Symbol.for("@zhushanwen/pi-subagents.dialogQueue")],见 getOrCreateDialogQueue)。
147
- * rejectAll()/clear() 清空所有 pending dialog——无 per-session 隔离。Pi 当前架构保证单进程
147
+ * rejectAll() 清空所有 pending dialog——无 per-session 隔离。Pi 当前架构保证单进程
148
148
  * 单 session 串行(同进程不会并发多个 session),故 session_shutdown 调 rejectAll() 只会清掉
149
149
  * 当前 session 的 pending。若未来 Pi 支持同进程多 session 并发,session A 退出会误清 session B
150
150
  * 的 pending dialog——届时需改为 per-session 隔离(入队项 QueueItem 带 sessionId,rejectAll
@@ -314,9 +314,10 @@ export class DialogGlobalQueue {
314
314
  this.processing = false;
315
315
  }
316
316
 
317
- /** 清空队列(dispose 用)。pending 项的 Promise settle(dispose 时调用方已不关心)。
318
- * 如需 settle,dispose 前应先 rejectChildDialogs rejectAll */
319
- clear(): void {
317
+ /** 清空队列状态(仅在 rejectAll 之后调用)。pending Promise 必须先由 rejectAll settle
318
+ * settle Promise 的纯状态重置——单独调用会导致 Promise 永挂(footgun),故设为 private
319
+ * 外部调用方应使用 rejectAll()(它 settle 所有 pending + 重置状态,是原子操作)。 */
320
+ private resetState(): void {
320
321
  this.queue = [];
321
322
  this.current = undefined;
322
323
  this.processing = false;
@@ -16,7 +16,11 @@
16
16
  // 集中到单一修改点,未来 Pi 新增 mode 值时只改本文件。
17
17
  // - 业务语义命名("gui"/"headless")比原始枚举值更清晰表达意图。
18
18
 
19
- import type { ExtensionMode } from "@earendil-works/pi-coding-agent";
19
+ // ExtensionMode Pi SDK core/extensions/types.d.ts 中定义为
20
+ // `"tui" | "rpc" | "json" | "print"`(ExtensionContext.mode 的类型),但未从包根
21
+ // 导出。这里定义与 SDK 同构的本地别名(ctx.mode 的字面量联合),由同包其他模块复用,
22
+ // 避免依赖未导出的内部类型。
23
+ export type ExtensionMode = "tui" | "rpc" | "json" | "print";
20
24
 
21
25
  /** 主进程运行模式分类。基于 ExtensionMode 聚合为业务语义。
22
26
  * - "tui":纯 Pi TUI,ctx.ui.custom 可用,用户在终端交互
@@ -10,7 +10,7 @@
10
10
 
11
11
  /**
12
12
  * ModelRegistry 的最小接口(duck-typed,测试可 mock)。
13
- * 字段结构与 Pi SDK 的 ctx.modelRegistry 对齐(见 shared/types stub)。
13
+ * 字段结构与 Pi SDK 的 ctx.modelRegistry 对齐。
14
14
  */
15
15
  export interface ModelRegistryLike {
16
16
  /** 返回所有已配置鉴权的可用模型。 */
@@ -9,7 +9,7 @@
9
9
  import { type ChildProcess,execFileSync, spawn } from "node:child_process";
10
10
  import * as fs from "node:fs";
11
11
 
12
- import type { ExtensionMode } from "@earendil-works/pi-coding-agent";
12
+ import type { ExtensionMode } from "./host-mode.ts";
13
13
 
14
14
  import { type MirrorFlags, mirrorMainProcessFlags } from "./argv-mirror.ts";
15
15
  import { writeAliveMarker } from "./alive-store.ts";
@@ -109,6 +109,17 @@ function computeWatchdogMs(maxTurns: number | undefined | null): number {
109
109
  /** stderr 累积上限(字符)。防止失控子进程打满父进程内存。保留尾部便于诊断。 */
110
110
  const STDERR_MAX_CHARS = 64 * 1024;
111
111
 
112
+ /**
113
+ * 跨包契约 env 名:workflow 子进程把权威 JSON Schema 通过此 env 传给 structured-output 扩展。
114
+ *
115
+ * [跨包契约 SSOT] 此字面量是两个独立 npm 包(@zhushanwen/pi-subagent-workflow 与
116
+ * @zhushanwen/pi-structured-output)之间的隐式 env 契约。structured-output 包内同名常量为
117
+ * `ENV_SCHEMA = "PI_WORKFLOW_SCHEMA"`(见 extensions/structured-output/src/index.ts)。
118
+ * 两包是独立 npm 包不能直接 import,故各自保留常量但显式标注此契约关系——
119
+ * 任一端改名必须同步另一端,否则权威 schema 注入会静默断桥(子进程不注册 tool/hook)。
120
+ */
121
+ const SCHEMA_ENV_VAR = "PI_WORKFLOW_SCHEMA";
122
+
112
123
  // ============================================================
113
124
  // W4: ask_user RPC 系统提示词
114
125
  // ============================================================
@@ -266,7 +277,7 @@ export interface RunOptions {
266
277
  * workflow 路径(executeAndAwait)不传此字段——其 onEvent 是开的,
267
278
  * text_delta 经 onEvent 到 workflow liveRecord,不走 streaming 通道。 */
268
279
  stream?: SubagentStream;
269
- /** D-A6 bridge: workflow schema JSON 字符串,存在时注入 childEnv.PI_WORKFLOW_SCHEMA
280
+ /** D-A6 bridge: workflow schema JSON 字符串,存在时注入 childEnv[SCHEMA_ENV_VAR](PI_WORKFLOW_SCHEMA)。
270
281
  * workflow 编排层通过 ExecuteOptions.schemaEnv 透传此处,
271
282
  * runSpawn 将其注入子进程环境变量,激活 structured-output 扩展注册 tool。
272
283
  * tool 层 execute 不传此字段 → childEnv 不注入 → BC-6 行为不变。 */
@@ -287,7 +298,7 @@ export interface RunOptions {
287
298
  * 将 schemaEnv 注入 childEnv(D-A6 bridge)。
288
299
  *
289
300
  * [模块内直调] —— 纯 env 赋值。从 runSpawn 的 childEnv 构造块调用。
290
- * 存在时设 childEnv.PI_WORKFLOW_SCHEMA → 子进程 structured-output 扩展读取并注册 tool。
301
+ * 存在时设 childEnv[SCHEMA_ENV_VAR] → 子进程 structured-output 扩展读取并注册 tool。
291
302
  * 不存在时 childEnv 不变(BC-6:tool 层不传 schemaEnv → 行为与合并前一致)。
292
303
  */
293
304
  export function applySchemaEnvToChildEnv(
@@ -295,7 +306,7 @@ export function applySchemaEnvToChildEnv(
295
306
  schemaEnv?: string,
296
307
  ): void {
297
308
  if (schemaEnv) {
298
- childEnv.PI_WORKFLOW_SCHEMA = schemaEnv;
309
+ childEnv[SCHEMA_ENV_VAR] = schemaEnv;
299
310
  }
300
311
  }
301
312
 
@@ -315,7 +326,9 @@ export function formatSchemaInstruction(schema: Record<string, unknown>): string
315
326
  "MANDATORY: Structured Output Requirement",
316
327
  "You MUST call the `structured-output` tool with your final answer.",
317
328
  "Do NOT output the JSON directly in your text response — you MUST use the structured-output tool.",
318
- "The schema for the structured output is:",
329
+ "The schema is enforced by the system — call structured-output with ONLY the `data` parameter.",
330
+ "Do NOT pass a `schema` parameter; the system validates `data` against the authoritative schema automatically.",
331
+ "The schema for your `data` is:",
319
332
  "```json",
320
333
  JSON.stringify(schema, null, SCHEMA_JSON_INDENT),
321
334
  "```",
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
 
7
- import type { ExtensionMode } from "@earendil-works/pi-coding-agent";
7
+ import type { ExtensionMode } from "./host-mode.ts";
8
8
 
9
9
  import type { AgentResult as WorkflowAgentResult } from "../orchestration/models/types.ts";
10
10
  // D-A10: workflow 侧 AgentResult 映射(executeAndAwait 出口)
@@ -3,7 +3,7 @@
3
3
  // UI 请求可观测性状态(从 subagent-service.ts 提取,降低主文件行数)。
4
4
  // 持有 sessionMode + handler 缺失告警去重集合,供 SubagentService 委托调用。
5
5
 
6
- import type { ExtensionMode } from "@earendil-works/pi-coding-agent";
6
+ import type { ExtensionMode } from "./host-mode.ts";
7
7
 
8
8
  // ── 跨模块桥接(ui-request-queue 无 ctx.service 引用时走这里) ──
9
9
  //
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ import * as fs from "node:fs";
17
17
  import * as os from "node:os";
18
18
  import * as path from "node:path";
19
19
 
20
- import type { ExtensionAPI, ExtensionContext, ModelSelectEvent, ResourcesDiscoverEvent, ResourcesDiscoverResult, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
20
+ import type { ExtensionAPI, ExtensionContext, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
21
21
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
22
22
 
23
23
  import type { AgentRegistry } from "./execution/agent-registry.ts";
@@ -90,7 +90,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
90
90
  // resources_discover:不再注入额外 skill 目录(ADR-031 废弃 discovery.json)。
91
91
  // pi 核心 auto-discovery 已覆盖 .agents/skills 等标准目录,子 session 的
92
92
  // --skill 由 agent({skill}) 调用方显式传入,无需 extension 额外补充。
93
- pi.on("resources_discover", (_event: ResourcesDiscoverEvent, _ctx: ExtensionContext): ResourcesDiscoverResult => {
93
+ pi.on("resources_discover", (_event, _ctx: ExtensionContext) => {
94
94
  return {};
95
95
  });
96
96
 
@@ -342,7 +342,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
342
342
  // ════════════════════════════════════════════════════════════
343
343
  // model_select:用户切换 model 时刷新缓存
344
344
  // ════════════════════════════════════════════════════════════
345
- pi.on("model_select", (event: ModelSelectEvent, ctx: ExtensionContext) => {
345
+ pi.on("model_select", (event, ctx: ExtensionContext) => {
346
346
  const service = getModelConfigService();
347
347
  if (service && typeof service.setCtxModel === "function") {
348
348
  service.setCtxModel(event.model);
@@ -396,10 +396,9 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
396
396
  }
397
397
 
398
398
  // M2: 清理 dialog queue 运行时状态(queue/current/processing)。
399
- // [#10] rejectAll() settle 所有 pending dialog Promise(防闭包泄漏:未 settle 的
399
+ // [#10] rejectAll() settle 所有 pending dialog Promise(防闭包泄漏:未 settle 的
400
400
  // Promise 持有 resolve/reject 闭包及 handler 上下文,session 退出后仍挂在全球队列上),
401
- // clear() 重置 queue/current/processing(防异常退出后 processing=true 卡死下次 session)。
402
- // rejectAll() 由 dialog-queue.ts 提供(Group B 新增);若其内部已 reset 状态,此处 clear() 为幂等兜底。
401
+ // 并内部重置 queue/current/processing(原子操作,无 footgun)。
403
402
  // 单 session 假设(M-2,同 lastSessionId):rejectAll() 清空进程级单例的所有 pending,
404
403
  // 依赖 Pi 单进程单 session 串行保证——不会误清其他 session。多 session 并发的迁移策略
405
404
  // 见 DialogGlobalQueue 类注释(rejectAllForSession)。
@@ -407,7 +406,6 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
407
406
  // 在 /new /resume /fork 时不丢失注册)。
408
407
  const dialogQueue = getOrCreateDialogQueue();
409
408
  dialogQueue.rejectAll();
410
- dialogQueue.clear();
411
409
  });
412
410
 
413
411
  // ════════════════════════════════════════════════════════════
@@ -12,7 +12,7 @@
12
12
  import type { Component } from "@earendil-works/pi-tui";
13
13
  import { StringEnum } from "@earendil-works/pi-ai";
14
14
  import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
15
- import { Type } from "@sinclair/typebox";
15
+ import { type Static, Type } from "typebox";
16
16
 
17
17
  import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
18
18
  import { getSubagentService } from "../execution/subagent-service.ts";
@@ -27,44 +27,15 @@ import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./t
27
27
  // ============================================================
28
28
 
29
29
  /**
30
- * execute 回调的 params 类型(手写副本——stub registerTool unknown,
31
- * 无法从 SubagentParams schema 反向推断参数类型)。
30
+ * execute 回调的 params 类型由 SubagentParams schema 经 Static 投影得出(见下方 cb 签名)。
32
31
  * action 与对应 param 不匹配时 handler 内 throw。
33
32
  */
34
- interface ListParam {
35
- includeFinished?: boolean;
36
- limit?: number;
37
- }
38
-
39
- interface CancelParam {
40
- subagentId: string;
41
- }
42
-
43
- interface SubagentExecuteParams {
44
- action: "start" | "list" | "cancel";
45
- // action:"start" 的 13 字段拍平到顶层(弱模型常省略 startParam 嵌套层导致调用失败)。
46
- // 拍平后这些字段直接在顶层(全部 optional——schema flat 无法表达「action 条件必填」,
47
- // 由 startHandler runtime 校验 task/slug 必填)。
48
- task?: string;
49
- slug?: string;
50
- agent?: string;
51
- model?: string;
52
- thinkingLevel?: string;
53
- skillPath?: string;
54
- appendSystemPrompt?: string[];
55
- schema?: Record<string, unknown>;
56
- maxTurns?: number;
57
- graceTurns?: number;
58
- fork?: boolean;
59
- worktree?: boolean;
60
- cwd?: string;
61
- listParam?: ListParam;
62
- cancelParam?: CancelParam;
63
- }
64
-
65
33
  type SubagentExecuteCb = (
66
34
  toolCallId: string,
67
- params: SubagentExecuteParams,
35
+ // Pi SDK 从 parameters schema 反向推断 params 类型。typebox v1 的 StringEnum
36
+ // 在 Static 投影下退化为 string(非字面量联合),因此 action 在 cb 入参里是 string,
37
+ // 由下方 isSubagentAction 类型守卫收窄到字面量联合后再 switch。
38
+ params: Static<typeof SubagentParams>,
68
39
  signal: AbortSignal | undefined,
69
40
  onUpdate?: (partialResult: AgentToolResult<SubagentToolResult>) => void,
70
41
  // ctx 在 SDK 契约里必填;此处保持 optional 以兼容 onUpdate? 在前(TS 参数顺序约束),
@@ -166,6 +137,16 @@ function assertNever(value: never): string {
166
137
  return String(value);
167
138
  }
168
139
 
140
+ /** Subagent action 字面量联合(与 parameters schema 的 StringEnum 取值一致)。 */
141
+ type SubagentAction = "start" | "list" | "cancel";
142
+
143
+ /** 类型守卫:把 schema 投影出的 string 形式 action 收窄回字面量联合。
144
+ * typebox v1 的 StringEnum Static 退化为 string,需运行时校验 + 类型收窄
145
+ * 才能恢复 switch 的 exhaustiveness 约束。 */
146
+ function isSubagentAction(value: string): value is SubagentAction {
147
+ return value === "start" || value === "list" || value === "cancel";
148
+ }
149
+
169
150
  /** unknown 是否为含 model/thinkingLevel 的对象(类型守卫,替代全可选结构 `as`)。 */
170
151
  function isModelOverrideObj(a: unknown): a is { model?: unknown; thinkingLevel?: unknown } {
171
152
  return typeof a === "object" && a !== null;
@@ -324,6 +305,11 @@ const executeSubagent: SubagentExecuteCb = async (
324
305
  const service = getSubagentService();
325
306
  if (!service) throw new Error("subagents runtime not initialized");
326
307
 
308
+ // typebox v1 的 StringEnum 在 Static 投影下退化为 string,此处类型守卫收窄回
309
+ // 字面量联合,恢复 switch 的 exhaustiveness 约束(default 分支 = never)。
310
+ if (!isSubagentAction(params.action)) {
311
+ throw new Error(`Unknown subagent action: ${params.action}`);
312
+ }
327
313
  switch (params.action) {
328
314
  case "start":
329
315
  // 拍平后直接传顶层 params(StartHandlerInput 是 SubagentExecuteParams 子集,
@@ -109,7 +109,7 @@ function walled(theme: ThemeLike, content: string, contentWidth: number): string
109
109
  }
110
110
 
111
111
  // ── Minimal TUI duck-types(避免直接 import TUI/KeybindingsManager 类型 ──
112
- // 共享类型 fallback shared/types/mariozechner/index.d.ts 不导出 TUI 类,
112
+ // 共享类型 fallback 不导出 TUI 类,
113
113
  // workspace 跨包 typecheck 会报 "no exported member 'TUI'"。
114
114
  // 此处用结构化接口替代——只声明 view 实际用到的成员(requestRender + terminal)。
115
115
 
@@ -102,11 +102,14 @@ export function resolveAgentOpts(
102
102
  "This task requires structured output.",
103
103
  "Your FINAL action must be calling the `structured-output` tool.",
104
104
  "",
105
- `structured-output parameters:`,
106
- ` schema = ${schemaJson}`,
107
- ` data = <your result conforming to the schema above>`,
105
+ "The schema is enforced by the system (PI_WORKFLOW_SCHEMA). You only pass `data` — do NOT pass a `schema` parameter.",
106
+ `Your \`data\` must conform to this schema:`,
107
+ "```json",
108
+ schemaJson,
109
+ "```",
108
110
  "",
109
111
  "Rules:",
112
+ "- Call structured-output with ONLY the `data` parameter. The system validates it against the schema above automatically.",
110
113
  "- Do NOT output JSON in your text response — use the structured-output tool.",
111
114
  "- Do NOT skip this step. The structured-output call IS your result.",
112
115
  "- Complete all other work FIRST, then call structured-output as the last action.",