@signalridge/pi-goal 0.49.7

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/src/errors.ts ADDED
@@ -0,0 +1,170 @@
1
+ import {
2
+ isContextOverflow,
3
+ isRetryableAssistantError,
4
+ type AssistantMessage as PiAssistantMessage,
5
+ type Usage,
6
+ } from "@earendil-works/pi-ai";
7
+ import { stripTerminalSequences } from "@earendil-works/pi-tui";
8
+ import { assistantUsageTokens, nonNegativeFiniteNumber } from "./accounting.js";
9
+
10
+ export type AgentStopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
11
+
12
+ export interface AssistantMessageLike {
13
+ role: "assistant";
14
+ stopReason?: AgentStopReason;
15
+ errorMessage?: string;
16
+ content?: PiAssistantMessage["content"];
17
+ api?: PiAssistantMessage["api"];
18
+ provider?: PiAssistantMessage["provider"];
19
+ model?: string;
20
+ usage?: Usage;
21
+ timestamp?: number;
22
+ }
23
+
24
+ const USAGE_LIMIT_GOAL_ERROR_PATTERNS = [
25
+ /usage[_\s-]*(?:limit|cap)|chatgpt.{0,32}usage/i,
26
+ /quota.{0,32}(?:reached|exceeded|exhausted|depleted)|(?:reached|exceeded|exhausted|depleted).{0,32}quota/i,
27
+ /insufficient[_\s-]*(?:quota|credits?)|out of credits|out of budget|available balance|payment required/i,
28
+ /(?:credit|balance).{0,32}(?:low|exhausted|depleted)|billing/i,
29
+ ] as const;
30
+ const NON_RETRYABLE_GOAL_ERROR_RE = /multi-auth rotation failed|credentials tried|unauthori[sz]ed|invalid api key/i;
31
+ const RETRYABLE_GOAL_ERROR_PATTERNS = [
32
+ /overloaded|rate.?limit|too many requests|\b(?:429|500|502|503|504)\b|service.?unavailable|server.?error|internal.?error/i,
33
+ /provider.?returned.?error|you can retry your request|try your request again|please retry your request/i,
34
+ /network.?error|connection.?(?:error|refused|lost)|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up/i,
35
+ /timed? out|timeout|terminated|websocket.?(?:closed|error)|ended without|stream ended before message_stop|http2 request did not get a response|retry delay/i,
36
+ /context[_\s-]*length[_\s-]*exceeded|input exceeds the context window/i,
37
+ ] as const;
38
+
39
+ export function safeTerminalText(value: string) {
40
+ return [...stripTerminalSequences(value)]
41
+ .map((character) => {
42
+ const codePoint = character.codePointAt(0) ?? 0;
43
+ if (character === "\n") return character;
44
+ return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f) ? " " : character;
45
+ })
46
+ .join("")
47
+ .trim();
48
+ }
49
+
50
+ export function safeGoalMenuText(value: string, maxCharacters = 120) {
51
+ const sanitized = safeTerminalText(value).replace(/\s+/gu, " ").trim();
52
+ const characters = [...sanitized];
53
+ return characters.length <= maxCharacters ? sanitized : `${characters.slice(0, maxCharacters).join("")}…`;
54
+ }
55
+
56
+ export function notifyTerminal(
57
+ ui: { notify: (message: string, level?: "info" | "warning" | "error") => void },
58
+ message: string,
59
+ level?: "info" | "warning" | "error",
60
+ ) {
61
+ ui.notify(safeTerminalText(message), level);
62
+ }
63
+
64
+ export function formatError(error: unknown) {
65
+ return truncateNotification(error instanceof Error ? error.message : String(error));
66
+ }
67
+
68
+ export function truncateNotification(value: string) {
69
+ const safe = [...safeTerminalText(value)];
70
+ return safe.length > 160 ? `${safe.slice(0, 157).join("")}...` : safe.join("");
71
+ }
72
+
73
+ export function isUsageLimitedGoalInterruption(assistant: AssistantMessageLike) {
74
+ const errorMessage = assistant.errorMessage;
75
+ return (
76
+ assistant.stopReason === "error" &&
77
+ typeof errorMessage === "string" &&
78
+ USAGE_LIMIT_GOAL_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage))
79
+ );
80
+ }
81
+
82
+ export function isRetryableGoalInterruption(assistant: AssistantMessageLike) {
83
+ if (assistant.stopReason !== "error" || !assistant.errorMessage) return false;
84
+ if (isUsageLimitedGoalInterruption(assistant) || NON_RETRYABLE_GOAL_ERROR_RE.test(assistant.errorMessage)) {
85
+ return false;
86
+ }
87
+ return (
88
+ isGoalContextOverflow(assistant) ||
89
+ isRetryableAssistantError(toPiAssistantMessage(assistant)) ||
90
+ RETRYABLE_GOAL_ERROR_PATTERNS.some((pattern) => pattern.test(assistant.errorMessage ?? ""))
91
+ );
92
+ }
93
+
94
+ export function isGoalContextOverflow(assistant: AssistantMessageLike) {
95
+ return isContextOverflow(toPiAssistantMessage(assistant));
96
+ }
97
+
98
+ export function findFinalAssistantMessage(messages: unknown[]): AssistantMessageLike | undefined {
99
+ for (let index = messages.length - 1; index >= 0; index--) {
100
+ const message = messages[index];
101
+ if (!message || typeof message !== "object") continue;
102
+ const candidate = message as Record<string, unknown>;
103
+ if (candidate.role !== "assistant") continue;
104
+ const assistant: AssistantMessageLike = {
105
+ role: "assistant",
106
+ stopReason: isAgentStopReason(candidate.stopReason) ? candidate.stopReason : undefined,
107
+ errorMessage: typeof candidate.errorMessage === "string" ? candidate.errorMessage : undefined,
108
+ };
109
+ if (Array.isArray(candidate.content)) {
110
+ assistant.content = candidate.content as PiAssistantMessage["content"];
111
+ }
112
+ if (typeof candidate.api === "string") assistant.api = candidate.api;
113
+ if (typeof candidate.provider === "string") assistant.provider = candidate.provider;
114
+ if (typeof candidate.model === "string") assistant.model = candidate.model;
115
+ if (typeof candidate.timestamp === "number") assistant.timestamp = candidate.timestamp;
116
+ const usage = normalizeUsage(candidate.usage);
117
+ if (usage) assistant.usage = usage;
118
+ return assistant;
119
+ }
120
+ return undefined;
121
+ }
122
+
123
+ function toPiAssistantMessage(assistant: AssistantMessageLike): PiAssistantMessage {
124
+ return {
125
+ role: "assistant",
126
+ content: assistant.content ?? [],
127
+ api: assistant.api ?? "openai-responses",
128
+ provider: assistant.provider ?? "unknown",
129
+ model: assistant.model ?? "unknown",
130
+ usage: assistant.usage ?? zeroUsage(),
131
+ stopReason: assistant.stopReason ?? "error",
132
+ errorMessage: assistant.errorMessage,
133
+ timestamp: assistant.timestamp ?? Date.now(),
134
+ };
135
+ }
136
+
137
+ function zeroUsage(): Usage {
138
+ return {
139
+ input: 0,
140
+ output: 0,
141
+ cacheRead: 0,
142
+ cacheWrite: 0,
143
+ totalTokens: 0,
144
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
145
+ };
146
+ }
147
+
148
+ function isAgentStopReason(value: unknown): value is AgentStopReason {
149
+ return ["stop", "length", "toolUse", "error", "aborted"].includes(String(value));
150
+ }
151
+
152
+ function normalizeUsage(value: unknown): Usage | undefined {
153
+ if (!value || typeof value !== "object") return undefined;
154
+ const usage = value as Partial<Usage>;
155
+ if (typeof usage.input !== "number" || typeof usage.output !== "number") return undefined;
156
+ return {
157
+ input: nonNegativeFiniteNumber(usage.input),
158
+ output: nonNegativeFiniteNumber(usage.output),
159
+ cacheRead: nonNegativeFiniteNumber(usage.cacheRead),
160
+ cacheWrite: nonNegativeFiniteNumber(usage.cacheWrite),
161
+ totalTokens: assistantUsageTokens(usage),
162
+ cost: {
163
+ input: usage.cost?.input ?? 0,
164
+ output: usage.cost?.output ?? 0,
165
+ cacheRead: usage.cost?.cacheRead ?? 0,
166
+ cacheWrite: usage.cost?.cacheWrite ?? 0,
167
+ total: usage.cost?.total ?? 0,
168
+ },
169
+ };
170
+ }
package/src/goal.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerGoalCommand } from "./command-registration.js";
3
+ import { GoalCommandController } from "./commands.js";
4
+ import { registerGoalLifecycle } from "./lifecycle.js";
5
+ import { GoalRunController } from "./run-protocol.js";
6
+ import { GoalRuntime } from "./runtime.js";
7
+ import { registerGoalTools } from "./tools.js";
8
+
9
+ interface GoalOptions {
10
+ settingsPath?: string;
11
+ }
12
+
13
+ function registerGoalRuntime(pi: ExtensionAPI, options: GoalOptions = {}) {
14
+ const runtime = new GoalRuntime(pi);
15
+ const commands = new GoalCommandController(runtime);
16
+ const runController = new GoalRunController(runtime, commands);
17
+
18
+ // Keep registration order explicit: managed-run bus listeners exist before tools,
19
+ // command routing, and session lifecycle bind the per-factory runtime.
20
+ runController.register(pi);
21
+ registerGoalTools(pi, runtime);
22
+ registerGoalCommand(pi, runtime, commands, options);
23
+ registerGoalLifecycle(pi, runtime, commands, runController, options);
24
+ }
25
+
26
+ export default function goal(pi: ExtensionAPI, options: GoalOptions = {}) {
27
+ registerGoalRuntime(pi, options);
28
+ }
29
+
30
+ export {
31
+ assistantUsageTokens,
32
+ cumulativeAssistantTokens,
33
+ formatDuration,
34
+ formatTokenCount,
35
+ } from "./accounting.js";
36
+
37
+ export {
38
+ completeGoalArguments,
39
+ parseCommand,
40
+ parseTokenBudget,
41
+ validateObjective,
42
+ } from "./command.js";
43
+
44
+ export { buildGoalSystemPrompt } from "./prompts.js";
45
+
46
+ export {
47
+ findFinalAssistantMessage,
48
+ formatStatus,
49
+ isContradictoryCompletionSummary,
50
+ isRetryableGoalInterruption,
51
+ isUsageLimitedGoalInterruption,
52
+ } from "./runtime.js";
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./goal.js";