@zhushanwen/pi-structured-output 6.1.3 → 6.2.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/README.md +2 -2
- package/package.json +8 -3
- package/src/index.ts +5 -5
- package/src/loop-gate.ts +11 -11
- package/src/render-result.ts +64 -0
- package/src/schema-guards.ts +5 -5
- package/src/text-primitives.ts +1 -1
- package/src/tool-definition.ts +27 -1
- package/src/workflow-hook.ts +10 -10
package/README.md
CHANGED
|
@@ -74,9 +74,9 @@ structured-output/
|
|
|
74
74
|
├── tool-definition.ts # 双变体工具定义(workflow 单参数 / 日常双参数)
|
|
75
75
|
├── execute.ts # 校验编排(workflow 透传 / 日常防御链)+ 根形态判定
|
|
76
76
|
├── ajv-validator.ts # Ajv 编译缓存(WeakMap)
|
|
77
|
-
├── schema-guards.ts #
|
|
77
|
+
├── schema-guards.ts # 形态检查纯函数(互换检测 / 关键字识别 / JSON 解析回显)
|
|
78
78
|
├── workflow-hook.ts # turn_end 强制调用 hook + RetryState 状态机
|
|
79
|
-
├── loop-gate.ts # 同签名失败 ×3
|
|
79
|
+
├── loop-gate.ts # 同签名失败 ×3 有界门禁(abort + shutdown + 硬退兜底)
|
|
80
80
|
└── text-primitives.ts # 截断/错误块有界化共享原语
|
|
81
81
|
```
|
|
82
82
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-structured-output",
|
|
3
|
-
"version": "6.1
|
|
3
|
+
"version": "6.2.1",
|
|
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",
|
|
@@ -26,16 +26,21 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"ajv": "^8.17.0",
|
|
29
|
+
"@zhushanwen/extension-protocol": "0.22.0",
|
|
29
30
|
"@zhushanwen/pi-ext-guards": "0.5.0"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
33
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
32
34
|
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
36
|
+
"typebox": "1.3.27"
|
|
35
37
|
},
|
|
36
38
|
"peerDependenciesMeta": {
|
|
37
39
|
"@earendil-works/pi-ai": {
|
|
38
40
|
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"@earendil-works/pi-tui": {
|
|
43
|
+
"optional": true
|
|
39
44
|
}
|
|
40
45
|
},
|
|
41
46
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
* 装配分岔(D1,U1):读 PI_WORKFLOW_SCHEMA——
|
|
5
5
|
* - 有值(workflow 子进程):注册 workflow 变体(parameters = 权威 schema 本身,
|
|
6
6
|
* D4 根级 additionalProperties 注入 / P6 非 object 根 {value} 包装 / 注册期
|
|
7
|
-
* fail-fast 防御都在 createWorkflowToolDefinition 内)+
|
|
7
|
+
* fail-fast 防御都在 createWorkflowToolDefinition 内)+ 双门禁合一 hook
|
|
8
8
|
* - 无值(日常 pi):注册日常变体(双参数自报形态,行为不变,G4)
|
|
9
9
|
*
|
|
10
10
|
* Hook 机制(仅 workflow 模式,D2 单状态机单 listener):
|
|
11
11
|
* turn_end 时检查模型是否调用了 structured-output 工具,没调/失败则 steer 注入
|
|
12
|
-
*
|
|
12
|
+
* 重试提醒(软门禁,最多重试 2 次)。同一状态机(WorkflowGate)上的硬杀门禁在
|
|
13
13
|
* 同签名校验失败达 3 次时 terminal:写日志(stderr + session JSONL 双通道,含恢复
|
|
14
14
|
* 指引)后 ctx.abort() + ctx.shutdown() 优雅终止子进程(无兜底硬退 timer,ADR-0122)——
|
|
15
|
-
* terminal
|
|
15
|
+
* terminal 后软门禁不再 steer(同一份 terminal 事实,无跨对象接线;G2 与模型
|
|
16
16
|
* 配合度无关)。装配与 listener 明细见 workflow-hook.setupWorkflowHook。
|
|
17
17
|
*
|
|
18
18
|
* 模块拆分(M4 + D2 合一):实现体分布于 ajv-validator.ts(编译缓存)/
|
|
19
|
-
* schema-guards.ts
|
|
19
|
+
* schema-guards.ts(形态检查)/ execute.ts(校验编排)/ tool-definition.ts(工具定义)/
|
|
20
20
|
* workflow-hook.ts(唯一装配入口 + steer 文案)/ loop-gate.ts(合一状态机 +
|
|
21
21
|
* 错误签名归一化 + terminal 副作用链)。本文件仅剩 entry 装配与 re-export。
|
|
22
22
|
*/
|
|
@@ -52,7 +52,7 @@ export default function structuredOutputExtension(pi: PiAPI): void {
|
|
|
52
52
|
const schemaEnv = process.env[ENV_SCHEMA];
|
|
53
53
|
|
|
54
54
|
if (schemaEnv) {
|
|
55
|
-
// ── Workflow 模式:单参数工具(注册期 fail-fast 防御)+
|
|
55
|
+
// ── Workflow 模式:单参数工具(注册期 fail-fast 防御)+ 双门禁合一 hook ──
|
|
56
56
|
// D2:软 steer 与硬杀由同一 WorkflowGate 状态机承载(单状态机单 listener),
|
|
57
57
|
// terminal 单字段单写,硬杀不需要向 steer 侧做任何回调同步。
|
|
58
58
|
pi.registerTool(createWorkflowToolDefinition(schemaEnv));
|
package/src/loop-gate.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 有界失败门禁 + steer 记账——双门禁合一状态机(D2 单状态机)+ G2「失败必有界」执行体。
|
|
3
3
|
*
|
|
4
4
|
* 状态归属(D2 单状态机):软 steer(workflow-hook 的重试提醒)与硬杀(本文件的
|
|
5
5
|
* 同签名失败 terminal)共用 WorkflowGate 一份状态——terminal 单字段单写,turn_end
|
|
6
|
-
*
|
|
6
|
+
* 检查链与硬杀幂等短路读同一份事实;若拆成两个状态机,terminal 双写就需要跨对象
|
|
7
7
|
* 回调同步,漏接线即 steer 与硬杀失步(单状态机使失步在结构上不可能)。
|
|
8
8
|
*
|
|
9
9
|
* 通道选型(设计 §6.3;pi 0.84.1 实装,登记 PS-20):
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
* 一个 listener,同一 tool_execution_end 内先 steer 记账后硬杀计数——与合一前两
|
|
52
52
|
* listener 的注册顺序一致)。
|
|
53
53
|
*
|
|
54
|
-
* 诚实边界(设计 §6.3
|
|
55
|
-
*
|
|
54
|
+
* 诚实边界(设计 §6.3):门禁只终止「同签名无进展」这一种循环形态;签名不断
|
|
55
|
+
* 变化的长尾低效不触发门禁,仍由 workflow 层 maxTurns 兜底。
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
58
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -119,7 +119,7 @@ function requiredBulletPrefix(bulletPath: string, firstField: string): string {
|
|
|
119
119
|
* 2. 日常模式 ajv(execute.ts instancePath 拼接形态,ajv locale 模板大写 "must
|
|
120
120
|
* NOT have additional properties")——模板词组含空格与固定语序,字段名不可能
|
|
121
121
|
* 携带。该形态无 "Received arguments:" 回显段,keys 分桶天然不激活
|
|
122
|
-
* (hasApError
|
|
122
|
+
* (hasApError 仅作标记;门禁仅 workflow 模式装配,此形态实为防御性保留)。
|
|
123
123
|
*/
|
|
124
124
|
const AP_LINE_SUFFIX_RE = /^(.*):\s*must not have additional properties\s*$/;
|
|
125
125
|
const AP_AJV_MESSAGE_RE = /must NOT have additional properties/;
|
|
@@ -143,7 +143,7 @@ const AP_BULLET_MESSAGE = "must not have additional properties";
|
|
|
143
143
|
*
|
|
144
144
|
* 有意决策(披露):token 集合只含字段/路径、不含错误类型——同一字段换约束试错
|
|
145
145
|
* ("must be string" → "must be number")折叠为同签名。权衡:换取「集合不变 = 无进展」
|
|
146
|
-
*
|
|
146
|
+
* 的强判定;代价是弱模型对单字段多约束的试错容忍度下降(同字段反复错达阈值即门禁)。
|
|
147
147
|
*
|
|
148
148
|
* 同时报告错误行块是否含 AP 错误行(hasApError)——normalizeErrorSignature 据此
|
|
149
149
|
* 门控 echo keys 并入(分桶动机见该函数注释);并收集 AP 错误行的路径位集合
|
|
@@ -344,19 +344,19 @@ export function normalizeErrorSignature(errorText: string): string {
|
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
/**
|
|
347
|
-
* structured-output
|
|
347
|
+
* structured-output 双门禁合一状态机(D2 单状态机;纯逻辑,可单测)。
|
|
348
348
|
*
|
|
349
349
|
* 状态分区(一个对象、一份 terminal 事实):
|
|
350
350
|
* - steer 侧(workflow-hook turn_end 消费):soCallCount / soSucceededEver /
|
|
351
351
|
* hookRetryCount / lastSchemaError。
|
|
352
352
|
* - 硬杀侧:lastSignature / consecutiveFailures / lastErrorText。
|
|
353
353
|
* - 共享终态:terminal——硬杀触发后置位(不可逆,仅 reset() 可清),turn_end
|
|
354
|
-
*
|
|
354
|
+
* 检查链据此停 steer,硬杀计数据此幂等短路。
|
|
355
355
|
*
|
|
356
356
|
* 转移表(loop-gate.test.ts / retry-state.test.ts 锁定):
|
|
357
357
|
* onToolExecEnd(hasError, errorText?)(每事件一次,先记账后计数——与合一前两
|
|
358
358
|
* listener 的注册顺序一致):
|
|
359
|
-
* - steer 记账段(不受 terminal 短路影响:terminal
|
|
359
|
+
* - steer 记账段(不受 terminal 短路影响:terminal 后迟到的失败仍记录,门禁自身
|
|
360
360
|
* 幂等,重复事件无害):
|
|
361
361
|
* - (!hasError) → soCallCount++ / soSucceededEver=true
|
|
362
362
|
* - (hasError, errorText) → soCallCount++ / lastSchemaError=errorText ?? 通用提示
|
|
@@ -380,7 +380,7 @@ export class WorkflowGate {
|
|
|
380
380
|
consecutiveFailures = 0;
|
|
381
381
|
/** 最近一次失败的原始终态文本(截断后),供 terminal 日志引用。 */
|
|
382
382
|
lastErrorText: string | null = null;
|
|
383
|
-
// ──
|
|
383
|
+
// ── 共享终态(单字段单写:硬杀计数段置位,两门禁读同一份)──────
|
|
384
384
|
terminal = false;
|
|
385
385
|
|
|
386
386
|
/**
|
|
@@ -510,7 +510,7 @@ export function runTerminalTeardown(pi: PiAPI, gate: WorkflowGate, ctx: Extensio
|
|
|
510
510
|
// (PS-30,runner.js createContext)——session 替换窗口触发 terminal 时无人接的
|
|
511
511
|
// 同步 throw 会经 async handler 变 rejected Promise 杀 pi 进程。stale 静默跳过
|
|
512
512
|
// 优雅退出(此时进程的存在意义已随 session 替换消失)。非 stale 错误原样上抛
|
|
513
|
-
|
|
513
|
+
//(检查不吞真实 bug)。
|
|
514
514
|
guardStaleCtx(() => {
|
|
515
515
|
ctx.abort();
|
|
516
516
|
ctx.shutdown();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* structured-output 工具的 TUI renderResult(折叠态一行摘要 / 展开态 content 纯文本)。
|
|
3
|
+
*
|
|
4
|
+
* 摘要不进 details:本工具的 result.details 是模型的结构化产出本体(workflow 引擎
|
|
5
|
+
* output-collector 的 extractParsedOutput 原样取走作 agent parsedOutput),混入 summary
|
|
6
|
+
* 键会污染产出契约(object 形态多出合同外字段、primitive 形态无处挂载)。因此成功路径
|
|
7
|
+
* 折叠态摘要 = 固定文案,不依赖数据。
|
|
8
|
+
*
|
|
9
|
+
* 错误路径(context.isError)不能用成功文案——pi 对失败轮同样调用 renderResult
|
|
10
|
+
*(tool-execution.js 传 {content, details} 给 renderResult,isError 只在第 4 参 context),
|
|
11
|
+
* 而 runResult 的 content 是校验失败文案。此时折叠态回退 content 首行,避免把失败显示成
|
|
12
|
+
*「schema validated」的假成功。判定靠 context.isError 事实,不做 content 文案匹配。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
17
|
+
import { firstContentText } from "@zhushanwen/extension-protocol";
|
|
18
|
+
|
|
19
|
+
/** 成功轮折叠态一行摘要(两种注册变体统一;错误轮走 firstLine(contentText))。 */
|
|
20
|
+
export const STRUCTURED_OUTPUT_SUMMARY = "Final result reported (schema validated)";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* renderResult 的 context(SDK ToolRenderContext 的有意子集——结构兼容只声明本组件
|
|
24
|
+
* 需要的字段)。SDK 实际传入更完整的 { args, toolCallId, cwd, ..., isError }。
|
|
25
|
+
*/
|
|
26
|
+
export interface StructuredOutputRenderContext {
|
|
27
|
+
isError?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** content[0] 的 text 纯文本(content 缺失或非 text 类型时兜底空串)。 */
|
|
31
|
+
/** unknown 值按字段名安全取值(index-signature 收窄,替代全可选结构断言)。 */
|
|
32
|
+
function fieldValue(v: unknown, key: string): unknown {
|
|
33
|
+
if (typeof v !== "object" || v === null) return undefined;
|
|
34
|
+
return (v as Record<string, unknown>)[key];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function contentText(result: unknown): string {
|
|
38
|
+
const content = fieldValue(result, "content");
|
|
39
|
+
if (!Array.isArray(content)) return "";
|
|
40
|
+
return firstContentText({ content });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 折叠态回退:content 文本取首行(保持一行摘要形态)。 */
|
|
44
|
+
function firstLine(text: string): string {
|
|
45
|
+
const idx = text.indexOf("\n");
|
|
46
|
+
return idx === -1 ? text : text.slice(0, idx);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 折叠态(expanded=false):成功 = 固定一行摘要;错误 = content 首行(dim)。
|
|
51
|
+
* 展开态 = content 纯文本。context 可选——旧的三参调用(测试 / 历史 caller)按成功处理。
|
|
52
|
+
*/
|
|
53
|
+
export function renderStructuredOutputResult(
|
|
54
|
+
result: unknown,
|
|
55
|
+
options: { expanded: boolean },
|
|
56
|
+
theme: Theme,
|
|
57
|
+
context?: StructuredOutputRenderContext,
|
|
58
|
+
): Text {
|
|
59
|
+
if (options.expanded) return new Text(contentText(result), 0, 0);
|
|
60
|
+
const summary = context?.isError === true
|
|
61
|
+
? firstLine(contentText(result))
|
|
62
|
+
: STRUCTURED_OUTPUT_SUMMARY;
|
|
63
|
+
return new Text(theme.fg("dim", summary), 0, 0);
|
|
64
|
+
}
|
package/src/schema-guards.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Schema
|
|
2
|
+
* Schema 形态检查 — 纯逻辑叶节点(零业务依赖,全导出)。
|
|
3
3
|
*
|
|
4
4
|
* 从 index.ts 拆出:swap 检测 + silent-corruption prevention 的一组纯函数。
|
|
5
5
|
*
|
|
6
6
|
* 核心问题:schema 和 data 参数都用 Type.Unknown(),结构无差别。弱模型常把答案
|
|
7
7
|
* 塞进 schema、把形状塞进 data。因 ajv strict:false 把无 keyword 的对象编译成
|
|
8
8
|
* "接受一切" 的 validator,互换后会校验通过、存垃圾、无报错(静默腐败)。
|
|
9
|
-
*
|
|
9
|
+
* 这组检查在编译前拦截两类形态:互换(schema 像数据 + data 像 schema)和
|
|
10
10
|
* keyword-less schema({} / {a:1} 这种会被 ajv 静默放行)。
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -80,8 +80,8 @@ export function tryParseJson(raw: unknown): unknown {
|
|
|
80
80
|
/**
|
|
81
81
|
* 把 authoritative schema 的 unknown 收窄为合法 JSON Schema 根类型(object | boolean)。
|
|
82
82
|
* draft-07 允许 boolean 根 schema(true=接受一切,false=拒绝一切)。
|
|
83
|
-
*
|
|
84
|
-
*
|
|
83
|
+
* 独立检查使后续 getOrCompileValidator(authoritative) 在类型层面也成立,
|
|
84
|
+
* 避免在检查块外直接用 unknown。非合法形态抛清晰错误。
|
|
85
85
|
*/
|
|
86
86
|
export function assertJsonSchemaRoot(value: unknown): asserts value is Record<string, unknown> | boolean {
|
|
87
87
|
if (!(isRecord(value) || typeof value === "boolean")) {
|
|
@@ -94,7 +94,7 @@ export function isTurnEndEvent(e: unknown): e is { message?: { stopReason?: stri
|
|
|
94
94
|
return typeof e === "object" && e !== null;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
/** tool_execution_end event
|
|
97
|
+
/** tool_execution_end event 结构检查(替代直接 cast,配合 taste/no-unsafe-cast)。 */
|
|
98
98
|
export function isToolExecutionEndEvent(
|
|
99
99
|
e: unknown,
|
|
100
100
|
): e is { toolName: unknown; isError: unknown; result?: unknown } {
|
package/src/text-primitives.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* 门禁签名侧截断上限:① normalizeErrorSignature fallback 分支(提取不到字段 token 的
|
|
14
14
|
* 非校验类错误文本降级为裸前缀做等值比较,消费方 loop-gate);② WorkflowGate.lastErrorText
|
|
15
15
|
* (terminal 日志引用,不进模型上下文,消费方 loop-gate)。与 steer 回灌错误块上限
|
|
16
16
|
* (STEER_ERROR_MAX_CHARS)语义独立(F5 拆分:签名只做比较原料、可随实现演化;steer
|
package/src/tool-definition.ts
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
* 两变体的描述文本均被 prompt-quality.test.ts 文本断言锁定。
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
21
22
|
import { isRecord } from "@zhushanwen/pi-ext-guards";
|
|
22
23
|
import { Type } from "typebox";
|
|
23
24
|
|
|
24
25
|
import { executeStructuredOutput, isObjectRootSchema } from "./execute.js";
|
|
26
|
+
import { renderStructuredOutputResult } from "./render-result.js";
|
|
27
|
+
import type { StructuredOutputRenderContext } from "./render-result.js";
|
|
25
28
|
import {
|
|
26
29
|
assertJsonSchemaRoot,
|
|
27
30
|
echo,
|
|
@@ -43,7 +46,7 @@ export const ENV_SCHEMA = "PI_WORKFLOW_SCHEMA";
|
|
|
43
46
|
* 各自保留、跨包契约测试锁字节相等(tests/cross-package-contract.test.ts)。
|
|
44
47
|
* 本侧职责仅可见性:注册时超限 logger.warn(无 logger API,stderr 直出惯例)提示
|
|
45
48
|
* env 通道有上限,建议拆分 schema 或精简——不拒绝注册(子进程能收到 env 说明 SW
|
|
46
|
-
*
|
|
49
|
+
* 侧门禁已放行,此处拒绝只会把可诊断的降级变成无法启动)。
|
|
47
50
|
*
|
|
48
51
|
* [跨包契约] 任一端改值必须同步另一端,否则 SW 侧硬拒绝线与 SO 侧提示线漂移。
|
|
49
52
|
*/
|
|
@@ -163,6 +166,18 @@ export function createWorkflowToolDefinition(envSchema: string) {
|
|
|
163
166
|
// 「根是否 object」对称完成(与注册期包装判定同一函数 isObjectRootSchema)。
|
|
164
167
|
return executeStructuredOutput({ data: params, authoritativeSchema: schema });
|
|
165
168
|
},
|
|
169
|
+
|
|
170
|
+
// 摘要不进 details:details 是模型的结构化产出本体(workflow 引擎 extractParsedOutput
|
|
171
|
+
// 原样消费),成功轮折叠态摘要 = render-result 的固定文案;失败轮(context.isError)
|
|
172
|
+
// 回退 content 首行,避免把校验失败显示成「schema validated」。
|
|
173
|
+
renderResult(
|
|
174
|
+
result: unknown,
|
|
175
|
+
options: { expanded: boolean },
|
|
176
|
+
theme: Theme,
|
|
177
|
+
context?: StructuredOutputRenderContext,
|
|
178
|
+
) {
|
|
179
|
+
return renderStructuredOutputResult(result, options, theme, context);
|
|
180
|
+
},
|
|
166
181
|
};
|
|
167
182
|
}
|
|
168
183
|
|
|
@@ -240,5 +255,16 @@ export function createDailyToolDefinition() {
|
|
|
240
255
|
: params,
|
|
241
256
|
);
|
|
242
257
|
},
|
|
258
|
+
|
|
259
|
+
// 摘要不进 details:同 workflow 变体(details = 数据本体,parsedOutput 契约);
|
|
260
|
+
// 失败轮折叠态回退 content 首行(context.isError)。
|
|
261
|
+
renderResult(
|
|
262
|
+
result: unknown,
|
|
263
|
+
options: { expanded: boolean },
|
|
264
|
+
theme: Theme,
|
|
265
|
+
context?: StructuredOutputRenderContext,
|
|
266
|
+
) {
|
|
267
|
+
return renderStructuredOutputResult(result, options, theme, context);
|
|
268
|
+
},
|
|
243
269
|
};
|
|
244
270
|
}
|
package/src/workflow-hook.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* steer 记账段 + 硬杀计数段,先记账后计数),newlyTerminal 时触发硬杀副作用链
|
|
7
7
|
* (runTerminalTeardown:日志 → abort+shutdown 优雅终止)。
|
|
8
8
|
* - turn_end:唯一 listener——模型未成功产出 structured-output 时以 steer 方式注入
|
|
9
|
-
*
|
|
9
|
+
* 重试提醒(软门禁),提示词不伪装用户气泡,对话流用户内容 100% 来自用户输入;
|
|
10
10
|
* 最多重试 MAX_HOOK_RETRIES 次,防止无限循环。
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* turn_end
|
|
12
|
+
* 软硬门禁合一语义:terminal 是 WorkflowGate 上的单一事实——硬杀计数段置位后,
|
|
13
|
+
* turn_end 检查链第 0 条据此停 steer;两门禁间无任何回调接线(terminal 若双写于
|
|
14
14
|
* 两个状态机,就必须跨对象回调同步,漏接线即 steer 与硬杀失步——单状态机单写使
|
|
15
15
|
* 失步在结构上不可能)。
|
|
16
16
|
*
|
|
@@ -52,10 +52,10 @@ export const HOOK_ENTRY_TYPE = "structured-output:hook";
|
|
|
52
52
|
export const RETRY_REMINDER_CUSTOM_TYPE = "structured-output:retry-reminder";
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* steer
|
|
56
|
-
*
|
|
55
|
+
* steer 检查链(原 turn_end handler 内联检查提取,判定顺序与旧实现逐条一致)。
|
|
56
|
+
* 命中任一检查即跳过 steer,且调用方不调 onTurnEnd(toolUse/error/aborted 保留
|
|
57
57
|
* soCallCount、超上限保留 lastSchemaError,与合一前逐点一致):
|
|
58
|
-
* 0.
|
|
58
|
+
* 0. 门禁 terminal(D3/U2):同签名失败已满 3 次、shutdown 已发——不再 steer,
|
|
59
59
|
* 让进程终止(防御性保留:shutdown 正常生效时进程已死,到不了这里)
|
|
60
60
|
* 1. 已经成功调用过 structured-output,不再干预
|
|
61
61
|
* 2. 不是合法 turn_end 事件
|
|
@@ -154,13 +154,13 @@ function writeSteerFailedLog(pi: PiAPI, hookRetryCount: number, err: unknown): v
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* 装配 workflow
|
|
157
|
+
* 装配 workflow 模式的双门禁(D2 唯一装配入口,仅 workflow 模式调用,见 src/index.ts)。
|
|
158
158
|
*
|
|
159
159
|
* 每个事件至多一个 listener(单 listener 契约,D2):
|
|
160
|
-
* - tool_execution_end
|
|
160
|
+
* - tool_execution_end:结构检查 → toolName 过滤 → WorkflowGate.onToolExecEnd
|
|
161
161
|
* (合一状态机:先 steer 记账后硬杀计数)→ newlyTerminal 时 runTerminalTeardown
|
|
162
162
|
* (terminal 时序与副作用明细见该函数注释)。
|
|
163
|
-
* - turn_end
|
|
163
|
+
* - turn_end:检查链 shouldSkipSteer → 两种失败形态都 steer:
|
|
164
164
|
* 1. 完全没调用(soCallCount === 0)→ 注入"必须调用"提示 + 正确 schema
|
|
165
165
|
* 2. 调了但全是 isError(soCallCount > 0 && !soSucceededEver)→ 注入具体校验错误
|
|
166
166
|
* + 正确 schema。旧实现在此处撒手交给 Pi 自然修正,但模型遇到 "Invalid JSON Schema"
|
|
@@ -193,7 +193,7 @@ export function setupWorkflowHook(pi: PiAPI, schemaJson: string): void {
|
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
pi.on("turn_end", async (event: unknown) => {
|
|
196
|
-
//
|
|
196
|
+
// 检查链(判定明细见 shouldSkipSteer 注释):命中即直接 return,不调
|
|
197
197
|
// onTurnEnd——预算不扣减,状态保留到下一个正常收尾的轮再判定 steer。
|
|
198
198
|
if (shouldSkipSteer(state, event)) return;
|
|
199
199
|
|