@zhushanwen/pi-goal 0.16.0 → 0.16.2
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
CHANGED
|
@@ -26,15 +26,9 @@ ln -s /path/to/taiji/extensions/universal/goal \
|
|
|
26
26
|
pi install npm:@zhushanwen/pi-goal
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## 工具激活
|
|
30
30
|
|
|
31
|
-
`goal_control` 工具为 `exposure
|
|
32
|
-
|
|
33
|
-
```json
|
|
34
|
-
{ "defaultTools": ["+tool_search"] }
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
`+` 前缀是增量修饰(保留 read/bash/edit/write 默认集)。未开启 `tool_search` 时该工具对模型不可见。
|
|
31
|
+
`goal_control` 工具为 direct 常驻——注册不声明 `exposure` 字段(pi SDK 缺省 `direct`),工具声明随会话常驻、对模型默认可见,无需经 `tool_search` 检索激活,独立 pi 开箱即用。
|
|
38
32
|
|
|
39
33
|
## 使用
|
|
40
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token budget, agent-reported blocking, and steering templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
30
30
|
"@earendil-works/pi-tui": "^1.0.0",
|
|
31
31
|
"typebox": "1.3.27",
|
|
32
|
-
"@zhushanwen/pi-pending-notifications": "0.
|
|
32
|
+
"@zhushanwen/pi-pending-notifications": "0.11.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zhushanwen/extension-protocol": "0.
|
|
35
|
+
"@zhushanwen/extension-protocol": "0.24.0",
|
|
36
36
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
37
37
|
"@zhushanwen/pi-session-path": "0.1.0",
|
|
38
38
|
"@zhushanwen/state-machine": "0.2.0"
|
|
@@ -36,6 +36,8 @@ type ExecuteResult = {
|
|
|
36
36
|
|
|
37
37
|
interface CapturedTool {
|
|
38
38
|
name: string;
|
|
39
|
+
/** pi SDK 的 exposure 缺省值 "direct"(常驻)——不写字段时 SDK 侧默认(pi-semantics PS-91),本视图可缺省。 */
|
|
40
|
+
exposure?: string;
|
|
39
41
|
execute: (
|
|
40
42
|
toolCallId: string,
|
|
41
43
|
params: Record<string, unknown>,
|
|
@@ -123,6 +125,18 @@ function createViaHandler(
|
|
|
123
125
|
|
|
124
126
|
// ── 测试场景 ─────────────────────────────────────────
|
|
125
127
|
|
|
128
|
+
// exposure 重划终态锚(C-ext-05,2026-10-23 ADR-0136):goal_control 保持 direct 常驻 =
|
|
129
|
+
// 注册时不写 exposure 字段(SDK 缺省 direct,pi-semantics PS-91)——误写 "deferred" 会把
|
|
130
|
+
// 目标管理入口从模型默认可见集收走(tool_search 才可激活),此处红。
|
|
131
|
+
describe("goal_control SDK 契约 — exposure 注册终态(C-ext-05)", () => {
|
|
132
|
+
it("direct 常驻:不写 exposure 字段(SDK 缺省 direct)", () => {
|
|
133
|
+
const { pi } = makeFixture("rpc");
|
|
134
|
+
const tool = captureTool(pi);
|
|
135
|
+
expect(tool.name).toBe("goal_control");
|
|
136
|
+
expect(tool.exposure).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
126
140
|
describe("goal_control execute — RPC 模式 tool result 无 __gui__", () => {
|
|
127
141
|
it("RPC + 有 budget 的 create → details 无 __gui__ 字段(content 仍正常)", async () => {
|
|
128
142
|
const { pi, ctx } = makeFixture("rpc");
|
|
@@ -53,6 +53,32 @@ describe("buildGoalControlSummary", () => {
|
|
|
53
53
|
);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it("report_blocked reason 含换行 → summary 归一单行(S-1)", () => {
|
|
57
|
+
const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
|
|
58
|
+
const summary = buildGoalControlSummary(
|
|
59
|
+
{ action: "report_blocked", reason: "第一行\n第二段\n\n尾行" } as never,
|
|
60
|
+
details,
|
|
61
|
+
);
|
|
62
|
+
expect(summary).toBe("Blocked: 第一行 第二段 尾行");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("report_blocked reason 含 emoji 超长 → 截断不劈代理对(S-3)", () => {
|
|
66
|
+
const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
|
|
67
|
+
// "x🎉" = 3 个 UTF-16 码元;UTF-16 slice(0,40) 会在第 40 码元处劈开 🎉 的代理对。
|
|
68
|
+
// 码点口径:50 码点 > 40 → 保留前 40 码点 = 20 组完整 "x🎉"
|
|
69
|
+
const summary = buildGoalControlSummary(
|
|
70
|
+
{ action: "report_blocked", reason: "x🎉".repeat(25) } as never,
|
|
71
|
+
details,
|
|
72
|
+
);
|
|
73
|
+
expect(summary).not.toContain("\n");
|
|
74
|
+
// Array.from 按码点切分后,孤立代理项表现为单码元的代理字符
|
|
75
|
+
const loneSurrogates = Array.from(summary).filter(
|
|
76
|
+
(ch) => ch.length === 1 && /[\uD800-\uDFFF]/.test(ch),
|
|
77
|
+
);
|
|
78
|
+
expect(loneSurrogates).toHaveLength(0);
|
|
79
|
+
expect(summary).toBe(`Blocked: ${"x🎉".repeat(20)}…`);
|
|
80
|
+
});
|
|
81
|
+
|
|
56
82
|
it("report_blocked 无 reason → 省略后缀", () => {
|
|
57
83
|
const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
|
|
58
84
|
expect(buildGoalControlSummary({ action: "report_blocked" } as never, details)).toBe("Blocked");
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
35
35
|
import { Text } from "@earendil-works/pi-tui";
|
|
36
|
-
import {
|
|
36
|
+
import { resultContentText } from "@zhushanwen/extension-protocol";
|
|
37
37
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
38
38
|
import { type Static, Type } from "typebox";
|
|
39
39
|
|
|
@@ -149,8 +149,21 @@ export interface GoalControlDetails {
|
|
|
149
149
|
/** 收起态摘要中 reason 的保留长度(超长截断加省略号)。 */
|
|
150
150
|
const BLOCKED_REASON_SUMMARY_CHARS = 40;
|
|
151
151
|
|
|
152
|
+
/** 多行文本 → 单行摘要文本(换行与连续空白折叠为单空格;plan tool.ts 同模式)。
|
|
153
|
+
* details.summary 单行契约见 extension-conventions.md「Tool result details.summary 约定」
|
|
154
|
+
* (自由文本 reason 可含 \n,截断前先归一)。 */
|
|
155
|
+
function toSingleLine(text: string): string {
|
|
156
|
+
return text.replace(/\s+/g, " ").trim();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 码点安全截断到上限(超长补省略号;Array.from 按码点切分,emoji 代理对不劈半)。 */
|
|
160
|
+
function elide(text: string, max: number): string {
|
|
161
|
+
const chars = Array.from(text);
|
|
162
|
+
return chars.length > max ? chars.slice(0, max).join("") + "…" : text;
|
|
163
|
+
}
|
|
164
|
+
|
|
152
165
|
/** 收起态一行摘要:create → slug;complete → goalId(complete 的 details 无 slug);
|
|
153
|
-
* report_blocked → reason
|
|
166
|
+
* report_blocked → reason 归一单行后码点安全截断(超长加省略号)。 */
|
|
154
167
|
export function buildGoalControlSummary(
|
|
155
168
|
params: GoalControlParamsT,
|
|
156
169
|
details: GoalControlDetails,
|
|
@@ -161,11 +174,9 @@ export function buildGoalControlSummary(
|
|
|
161
174
|
case "complete":
|
|
162
175
|
return `Goal completed: ${details.goalId}`;
|
|
163
176
|
case "report_blocked": {
|
|
164
|
-
const reason = params.reason
|
|
177
|
+
const reason = toSingleLine(params.reason ?? "");
|
|
165
178
|
if (reason === "") return "Blocked";
|
|
166
|
-
return reason
|
|
167
|
-
? `Blocked: ${reason.slice(0, BLOCKED_REASON_SUMMARY_CHARS)}…`
|
|
168
|
-
: `Blocked: ${reason}`;
|
|
179
|
+
return `Blocked: ${elide(reason, BLOCKED_REASON_SUMMARY_CHARS)}`;
|
|
169
180
|
}
|
|
170
181
|
default:
|
|
171
182
|
return "Done";
|
|
@@ -367,9 +378,6 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
|
|
|
367
378
|
pi.registerTool({
|
|
368
379
|
name: "goal_control",
|
|
369
380
|
label: "Goal Control",
|
|
370
|
-
// deferred(tool-declaration-slimming D4):不常驻声明,tool_search 命中即激活;
|
|
371
|
-
// 迁移写入的 +tool_search 条目(runtime pi-codemode-settings)承载检索入口
|
|
372
|
-
exposure: "deferred",
|
|
373
381
|
description: `管理当前会话的目标(goal)。目标用于追踪需要完成验证的复杂工作。动作:create(创建)| complete(完成)| report_blocked(阻塞报告)——各参数必填性与用法见 schema 字段说明,触发纪律见 guidelines。
|
|
374
382
|
|
|
375
383
|
控制权归属:pause/resume 和 budget 变更由用户经 /goal 命令控制,你不能修改。`,
|
|
@@ -459,7 +467,7 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
|
|
|
459
467
|
const d = hasGoalDetails(result) ? result.details : undefined;
|
|
460
468
|
// 展开态 = content 纯文本(多行详情);content 缺失时回退折叠形态
|
|
461
469
|
if (options.expanded) {
|
|
462
|
-
const content =
|
|
470
|
+
const content = resultContentText(result);
|
|
463
471
|
if (content !== "") return new Text(content, 0, 0);
|
|
464
472
|
}
|
|
465
473
|
// 折叠态 = details.summary 一行(dim);旧 entry 无 summary 时保留原 ◆ 状态行
|
|
@@ -471,19 +479,6 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
|
|
|
471
479
|
});
|
|
472
480
|
}
|
|
473
481
|
|
|
474
|
-
/** unknown 值按字段名安全取值(index-signature 收窄,替代全可选结构断言)。 */
|
|
475
|
-
function fieldValue(v: unknown, key: string): unknown {
|
|
476
|
-
if (typeof v !== "object" || v === null) return undefined;
|
|
477
|
-
return (v as Record<string, unknown>)[key];
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/** content[0] 的 text 纯文本(content 缺失或非 text 类型时兜底空串)。 */
|
|
481
|
-
function getContentText(result: unknown): string {
|
|
482
|
-
const content = fieldValue(result, "content");
|
|
483
|
-
if (!Array.isArray(content)) return "";
|
|
484
|
-
return firstContentText({ content });
|
|
485
|
-
}
|
|
486
|
-
|
|
487
482
|
/** 旧形态回退(details 无 summary 的历史 entry):原 ◆ 状态行,视觉零变化。 */
|
|
488
483
|
function renderLegacyGoalResult(d: GoalControlDetails | undefined, theme: Theme): Text {
|
|
489
484
|
if (!d) return new Text(theme.fg("dim", "goal_control"), 0, 0);
|