@gajae-code/coding-agent 0.2.1 → 0.2.3
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/CHANGELOG.md +59 -1
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/commands/contribution-prep.d.ts +18 -0
- package/dist/types/commands/deep-interview.d.ts +41 -0
- package/dist/types/commands/session.d.ts +24 -0
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +2 -2
- package/dist/types/config/models-config-schema.d.ts +17 -9
- package/dist/types/config/settings-schema.d.ts +37 -24
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +33 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
- package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
- package/dist/types/goals/runtime.d.ts +3 -9
- package/dist/types/goals/state.d.ts +3 -6
- package/dist/types/goals/tools/goal-tool.d.ts +1 -69
- package/dist/types/hooks/skill-state.d.ts +5 -0
- package/dist/types/memories/index.d.ts +1 -1
- package/dist/types/memory-backend/local-backend.d.ts +3 -3
- package/dist/types/modes/components/hook-selector.d.ts +7 -0
- package/dist/types/modes/components/settings-selector.d.ts +0 -2
- package/dist/types/modes/components/status-line/types.d.ts +0 -3
- package/dist/types/modes/components/status-line.d.ts +0 -3
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -12
- package/dist/types/modes/theme/defaults/index.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +1 -2
- package/dist/types/modes/types.d.ts +1 -7
- package/dist/types/modes/utils/context-usage.d.ts +6 -2
- package/dist/types/sdk.d.ts +6 -2
- package/dist/types/session/agent-session.d.ts +47 -1
- package/dist/types/session/contribution-prep.d.ts +47 -0
- package/dist/types/session/session-manager.d.ts +3 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
- package/dist/types/setup/provider-onboarding.d.ts +29 -5
- package/dist/types/skill-state/active-state.d.ts +30 -1
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
- package/dist/types/skill-state/initial-phase.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +9 -4
- package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/types.d.ts +11 -0
- package/dist/types/tools/index.d.ts +20 -1
- package/dist/types/tools/skill.d.ts +47 -0
- package/dist/types/utils/changelog.d.ts +18 -2
- package/package.json +7 -7
- package/src/cli/args.ts +3 -2
- package/src/cli/setup-cli.ts +26 -12
- package/src/cli.ts +7 -1
- package/src/commands/contribution-prep.ts +41 -0
- package/src/commands/deep-interview.ts +30 -23
- package/src/commands/launch.ts +10 -1
- package/src/commands/ralplan.ts +10 -22
- package/src/commands/session.ts +150 -0
- package/src/commands/setup.ts +2 -0
- package/src/commands/state.ts +15 -4
- package/src/commands/team.ts +23 -3
- package/src/config/model-registry.ts +10 -2
- package/src/config/models-config-schema.ts +120 -102
- package/src/config/settings-schema.ts +42 -25
- package/src/config.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +32 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +22 -2
- package/src/defaults/gjc/skills/team/SKILL.md +39 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -25
- package/src/discovery/helpers.ts +24 -1
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +546 -0
- package/src/gjc-runtime/goal-mode-request.ts +2 -19
- package/src/gjc-runtime/launch-tmux.ts +83 -43
- package/src/gjc-runtime/ralplan-runtime.ts +460 -0
- package/src/gjc-runtime/state-runtime.ts +731 -0
- package/src/gjc-runtime/team-runtime.ts +708 -52
- package/src/gjc-runtime/tmux-common.ts +119 -0
- package/src/gjc-runtime/tmux-sessions.ts +165 -0
- package/src/gjc-runtime/ultragoal-guard.ts +6 -3
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
- package/src/goals/runtime.ts +38 -144
- package/src/goals/state.ts +36 -7
- package/src/goals/tools/goal-tool.ts +15 -172
- package/src/hooks/skill-state.ts +39 -18
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/internal-urls/memory-protocol.ts +3 -2
- package/src/main.ts +2 -3
- package/src/memories/index.ts +2 -1
- package/src/memory-backend/local-backend.ts +14 -6
- package/src/modes/components/hook-selector.ts +156 -1
- package/src/modes/components/settings-selector.ts +5 -12
- package/src/modes/components/skill-hud/render.ts +4 -0
- package/src/modes/components/status-line/segments.ts +5 -16
- package/src/modes/components/status-line/types.ts +0 -3
- package/src/modes/components/status-line.ts +0 -6
- package/src/modes/controllers/command-controller.ts +27 -4
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/controllers/selector-controller.ts +4 -11
- package/src/modes/interactive-mode.ts +18 -219
- package/src/modes/theme/defaults/dark-poimandres.json +0 -1
- package/src/modes/theme/defaults/light-poimandres.json +0 -1
- package/src/modes/theme/theme.ts +0 -6
- package/src/modes/types.ts +1 -7
- package/src/modes/utils/context-usage.ts +66 -17
- package/src/prompts/agents/architect.md +3 -0
- package/src/prompts/agents/executor.md +2 -0
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/goals/goal-continuation.md +1 -4
- package/src/prompts/goals/goal-mode-active.md +3 -5
- package/src/prompts/system/subagent-system-prompt.md +6 -0
- package/src/prompts/system/system-prompt.md +5 -7
- package/src/prompts/tools/goal.md +4 -4
- package/src/prompts/tools/skill.md +28 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/sdk.ts +51 -11
- package/src/session/agent-session.ts +222 -21
- package/src/session/contribution-prep.ts +320 -0
- package/src/session/session-manager.ts +9 -1
- package/src/setup/model-onboarding-guidance.ts +6 -3
- package/src/setup/provider-onboarding.ts +177 -16
- package/src/skill-state/active-state.ts +188 -25
- package/src/skill-state/deep-interview-mutation-guard.ts +72 -21
- package/src/skill-state/initial-phase.ts +17 -0
- package/src/skill-state/workflow-hud.ts +23 -5
- package/src/skill-state/workflow-state-contract.ts +121 -0
- package/src/slash-commands/builtin-registry.ts +75 -25
- package/src/slash-commands/helpers/context-report.ts +123 -13
- package/src/task/agents.ts +1 -0
- package/src/task/commands.ts +1 -5
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +91 -4
- package/src/task/types.ts +6 -0
- package/src/tools/ask.ts +2 -0
- package/src/tools/gh.ts +212 -2
- package/src/tools/index.ts +25 -6
- package/src/tools/skill.ts +153 -0
- package/src/utils/changelog.ts +67 -44
- package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
- package/dist/types/commands/question.d.ts +0 -7
- package/dist/types/modes/loop-limit.d.ts +0 -22
- package/src/commands/gjc-runtime-bridge.ts +0 -227
- package/src/commands/question.ts +0 -12
- package/src/modes/loop-limit.ts +0 -140
- package/src/prompts/commands/orchestrate.md +0 -49
- package/src/prompts/goals/goal-budget-limit.md +0 -16
- package/src/prompts/tools/create-goal.md +0 -3
- package/src/prompts/tools/get-goal.md +0 -3
- package/src/prompts/tools/update-goal.md +0 -3
package/src/goals/runtime.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { prompt, Snowflake } from "@gajae-code/utils";
|
|
2
|
-
import goalBudgetLimitPrompt from "../prompts/goals/goal-budget-limit.md" with { type: "text" };
|
|
3
2
|
import goalContinuationPrompt from "../prompts/goals/goal-continuation.md" with { type: "text" };
|
|
4
3
|
import goalModeActivePrompt from "../prompts/goals/goal-mode-active.md" with { type: "text" };
|
|
5
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
type Goal,
|
|
6
|
+
type GoalModeState,
|
|
7
|
+
type GoalRuntimeEvent,
|
|
8
|
+
type GoalTokenUsage,
|
|
9
|
+
normalizeGoalModeState,
|
|
10
|
+
} from "./state";
|
|
6
11
|
|
|
7
12
|
export interface GoalRuntimeHost {
|
|
8
13
|
getState(): GoalModeState | undefined;
|
|
@@ -32,10 +37,9 @@ export interface GoalWallClockSnapshot {
|
|
|
32
37
|
export interface GoalRuntimeSnapshot {
|
|
33
38
|
turnSnapshot?: GoalTurnSnapshot;
|
|
34
39
|
wallClock: GoalWallClockSnapshot;
|
|
35
|
-
budgetReportedFor?: string;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
export type GoalPromptKind = "active" | "continuation"
|
|
42
|
+
export type GoalPromptKind = "active" | "continuation";
|
|
39
43
|
|
|
40
44
|
function cloneGoal(goal: Goal): Goal {
|
|
41
45
|
return { ...goal };
|
|
@@ -45,19 +49,6 @@ function cloneState(state: GoalModeState): GoalModeState {
|
|
|
45
49
|
return { ...state, goal: cloneGoal(state.goal) };
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
function budgetValue(goal: Goal): string {
|
|
49
|
-
return goal.tokenBudget === undefined ? "none" : String(goal.tokenBudget);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function remainingValue(goal: Goal): string {
|
|
53
|
-
return goal.tokenBudget === undefined ? "unbounded" : String(Math.max(0, goal.tokenBudget - goal.tokensUsed));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function remainingTokens(goal: Goal | null | undefined): number | null {
|
|
57
|
-
if (!goal || goal.tokenBudget === undefined) return null;
|
|
58
|
-
return Math.max(0, goal.tokenBudget - goal.tokensUsed);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
52
|
export function escapeXmlText(input: string): string {
|
|
62
53
|
let firstEscapable = -1;
|
|
63
54
|
for (let index = 0; index < input.length; index++) {
|
|
@@ -97,9 +88,8 @@ export function goalTokenDelta(current: GoalTokenUsage, baseline: GoalTokenUsage
|
|
|
97
88
|
// Diverges from OpenAI code backend-rs: OpenAI code backend omits cache creation because its target providers
|
|
98
89
|
// do not bill cache writes distinctly through the token-usage stream. Pi receives
|
|
99
90
|
// cacheWrite separately on Anthropic/Bedrock; rotating a 1h ephemeral cache or
|
|
100
|
-
// re-anchoring a changed system prompt can write 100K+ tokens, which
|
|
101
|
-
//
|
|
102
|
-
// not new work consumed by the goal.
|
|
91
|
+
// re-anchoring a changed system prompt can write 100K+ tokens, which usage accounting must track.
|
|
92
|
+
// cacheRead is excluded because it is reused prefix, not new work consumed by the goal.
|
|
103
93
|
return (
|
|
104
94
|
Math.max(0, current.input - baseline.input) +
|
|
105
95
|
Math.max(0, current.cacheWrite - baseline.cacheWrite) +
|
|
@@ -108,48 +98,22 @@ export function goalTokenDelta(current: GoalTokenUsage, baseline: GoalTokenUsage
|
|
|
108
98
|
}
|
|
109
99
|
|
|
110
100
|
export function renderGoalPrompt(kind: GoalPromptKind, goal: Goal): string {
|
|
111
|
-
const template =
|
|
112
|
-
kind === "active"
|
|
113
|
-
? goalModeActivePrompt
|
|
114
|
-
: kind === "continuation"
|
|
115
|
-
? goalContinuationPrompt
|
|
116
|
-
: goalBudgetLimitPrompt;
|
|
101
|
+
const template = kind === "active" ? goalModeActivePrompt : goalContinuationPrompt;
|
|
117
102
|
return prompt.render(template, {
|
|
118
103
|
objective: escapeXmlText(goal.objective),
|
|
119
104
|
tokensUsed: String(goal.tokensUsed),
|
|
120
|
-
tokenBudget: budgetValue(goal),
|
|
121
|
-
remainingTokens: remainingValue(goal),
|
|
122
105
|
timeUsedSeconds: String(goal.timeUsedSeconds),
|
|
123
106
|
});
|
|
124
107
|
}
|
|
125
108
|
|
|
126
|
-
export function completionBudgetReport(goal: Goal): string | null {
|
|
127
|
-
const parts: string[] = [];
|
|
128
|
-
if (goal.tokenBudget !== undefined) {
|
|
129
|
-
parts.push(`tokens used: ${goal.tokensUsed} of ${goal.tokenBudget}`);
|
|
130
|
-
}
|
|
131
|
-
if (goal.timeUsedSeconds > 0) {
|
|
132
|
-
parts.push(`time used: ${goal.timeUsedSeconds} seconds`);
|
|
133
|
-
}
|
|
134
|
-
if (parts.length === 0) return null;
|
|
135
|
-
return `Goal achieved. Report final budget usage to the user: ${parts.join("; ")}.`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function validateTokenBudget(tokenBudget: number | undefined): void {
|
|
139
|
-
if (tokenBudget !== undefined && (!Number.isInteger(tokenBudget) || tokenBudget <= 0)) {
|
|
140
|
-
throw new Error("goal token_budget must be a positive integer when provided");
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
109
|
function isAccountingStatus(goal: Goal): boolean {
|
|
145
|
-
return goal.status === "active"
|
|
110
|
+
return goal.status === "active";
|
|
146
111
|
}
|
|
147
112
|
|
|
148
113
|
export class GoalRuntime {
|
|
149
114
|
readonly #host: GoalRuntimeHost;
|
|
150
115
|
#turnSnapshot: GoalTurnSnapshot | undefined;
|
|
151
116
|
#wallClock: GoalWallClockSnapshot;
|
|
152
|
-
#budgetReportedFor: string | undefined;
|
|
153
117
|
#accountingTail: Promise<void> = Promise.resolve();
|
|
154
118
|
|
|
155
119
|
constructor(host: GoalRuntimeHost) {
|
|
@@ -163,7 +127,6 @@ export class GoalRuntime {
|
|
|
163
127
|
? { ...this.#turnSnapshot, baselineUsage: { ...this.#turnSnapshot.baselineUsage } }
|
|
164
128
|
: undefined,
|
|
165
129
|
wallClock: { ...this.#wallClock },
|
|
166
|
-
budgetReportedFor: this.#budgetReportedFor,
|
|
167
130
|
};
|
|
168
131
|
}
|
|
169
132
|
|
|
@@ -172,7 +135,7 @@ export class GoalRuntime {
|
|
|
172
135
|
}
|
|
173
136
|
|
|
174
137
|
#hasAccountingState(): boolean {
|
|
175
|
-
const state = this.#host.getState();
|
|
138
|
+
const state = normalizeGoalModeState(this.#host.getState());
|
|
176
139
|
return Boolean(state?.enabled && isAccountingStatus(state.goal));
|
|
177
140
|
}
|
|
178
141
|
|
|
@@ -192,7 +155,7 @@ export class GoalRuntime {
|
|
|
192
155
|
}
|
|
193
156
|
|
|
194
157
|
#getStateClone(): GoalModeState | undefined {
|
|
195
|
-
const state = this.#host.getState();
|
|
158
|
+
const state = normalizeGoalModeState(this.#host.getState());
|
|
196
159
|
return state ? cloneState(state) : undefined;
|
|
197
160
|
}
|
|
198
161
|
|
|
@@ -228,7 +191,7 @@ export class GoalRuntime {
|
|
|
228
191
|
|
|
229
192
|
onTurnStart(turnId: string, baselineUsage: GoalTokenUsage): void {
|
|
230
193
|
this.#turnSnapshot = { turnId, baselineUsage: { ...baselineUsage } };
|
|
231
|
-
const state = this.#
|
|
194
|
+
const state = this.#getStateClone();
|
|
232
195
|
if (state?.enabled && isAccountingStatus(state.goal)) {
|
|
233
196
|
this.#turnSnapshot.activeGoalId = state.goal.id;
|
|
234
197
|
if (this.#wallClock.activeGoalId !== state.goal.id) {
|
|
@@ -240,12 +203,12 @@ export class GoalRuntime {
|
|
|
240
203
|
async onToolCompleted(toolName: string): Promise<void> {
|
|
241
204
|
if (toolName === "goal") return;
|
|
242
205
|
if (!this.#hasAccountingState()) return;
|
|
243
|
-
await this.flushUsage(
|
|
206
|
+
await this.flushUsage();
|
|
244
207
|
}
|
|
245
208
|
|
|
246
209
|
async onGoalToolCompleted(): Promise<void> {
|
|
247
210
|
if (!this.#hasAccountingState()) return;
|
|
248
|
-
await this.flushUsage(
|
|
211
|
+
await this.flushUsage();
|
|
249
212
|
}
|
|
250
213
|
|
|
251
214
|
async onAgentEnd(options?: { turnCompleted?: boolean; currentUsage?: GoalTokenUsage }): Promise<void> {
|
|
@@ -253,19 +216,19 @@ export class GoalRuntime {
|
|
|
253
216
|
this.#turnSnapshot = undefined;
|
|
254
217
|
return;
|
|
255
218
|
}
|
|
256
|
-
await this.flushUsage(
|
|
219
|
+
await this.flushUsage(options?.currentUsage);
|
|
257
220
|
this.#turnSnapshot = undefined;
|
|
258
221
|
}
|
|
259
222
|
|
|
260
223
|
async onTaskAborted(_options?: { reason?: "interrupted" | "internal" }): Promise<void> {
|
|
261
|
-
const state = this.#
|
|
224
|
+
const state = this.#getStateClone();
|
|
262
225
|
const needsAccounting = state?.enabled && isAccountingStatus(state.goal);
|
|
263
226
|
if (!needsAccounting) {
|
|
264
227
|
this.#turnSnapshot = undefined;
|
|
265
228
|
return;
|
|
266
229
|
}
|
|
267
230
|
await this.#withAccounting(async () => {
|
|
268
|
-
await this.#flushUsageLocked(
|
|
231
|
+
await this.#flushUsageLocked();
|
|
269
232
|
this.#turnSnapshot = undefined;
|
|
270
233
|
const cloned = this.#getStateClone();
|
|
271
234
|
if (!cloned?.enabled || !isAccountingStatus(cloned.goal)) return;
|
|
@@ -290,38 +253,7 @@ export class GoalRuntime {
|
|
|
290
253
|
return state;
|
|
291
254
|
}
|
|
292
255
|
|
|
293
|
-
async
|
|
294
|
-
validateTokenBudget(newBudget);
|
|
295
|
-
return await this.#withAccounting(async () => {
|
|
296
|
-
this.#budgetReportedFor = undefined;
|
|
297
|
-
await this.#flushUsageLocked("suppressed");
|
|
298
|
-
const state = this.#getStateClone();
|
|
299
|
-
if (!state?.goal) return undefined;
|
|
300
|
-
state.goal.tokenBudget = newBudget;
|
|
301
|
-
state.goal.updatedAt = this.#now();
|
|
302
|
-
let shouldSteer = false;
|
|
303
|
-
if (newBudget !== undefined && state.goal.tokensUsed >= newBudget) {
|
|
304
|
-
if (state.goal.status === "active") {
|
|
305
|
-
state.goal.status = "budget-limited";
|
|
306
|
-
shouldSteer = true;
|
|
307
|
-
}
|
|
308
|
-
} else if (state.goal.status === "budget-limited") {
|
|
309
|
-
state.goal.status = "active";
|
|
310
|
-
state.enabled = true;
|
|
311
|
-
this.#markActiveAccounting(state.goal);
|
|
312
|
-
}
|
|
313
|
-
await this.#commitState(state, { persist: state.enabled ? "goal" : "goal_paused" });
|
|
314
|
-
if (shouldSteer) {
|
|
315
|
-
await this.#sendBudgetLimitSteer(state.goal);
|
|
316
|
-
}
|
|
317
|
-
return state;
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async #flushUsageLocked(
|
|
322
|
-
steering: GoalBudgetSteering,
|
|
323
|
-
currentUsage: GoalTokenUsage = this.#host.getCurrentUsage(),
|
|
324
|
-
): Promise<void> {
|
|
256
|
+
async #flushUsageLocked(currentUsage: GoalTokenUsage = this.#host.getCurrentUsage()): Promise<void> {
|
|
325
257
|
const state = this.#getStateClone();
|
|
326
258
|
if (!state?.enabled || !isAccountingStatus(state.goal)) return;
|
|
327
259
|
if (this.#turnSnapshot?.activeGoalId !== state.goal.id && this.#wallClock.activeGoalId !== state.goal.id) return;
|
|
@@ -339,13 +271,6 @@ export class GoalRuntime {
|
|
|
339
271
|
state.goal.tokensUsed += tokenDelta;
|
|
340
272
|
state.goal.timeUsedSeconds += wallSeconds;
|
|
341
273
|
state.goal.updatedAt = this.#now();
|
|
342
|
-
const flippedToBudgetLimited =
|
|
343
|
-
state.goal.tokenBudget !== undefined &&
|
|
344
|
-
state.goal.tokensUsed >= state.goal.tokenBudget &&
|
|
345
|
-
state.goal.status === "active";
|
|
346
|
-
if (flippedToBudgetLimited) {
|
|
347
|
-
state.goal.status = "budget-limited";
|
|
348
|
-
}
|
|
349
274
|
|
|
350
275
|
if (this.#turnSnapshot?.activeGoalId === state.goal.id) {
|
|
351
276
|
this.#turnSnapshot.baselineUsage = { ...currentUsage };
|
|
@@ -355,29 +280,18 @@ export class GoalRuntime {
|
|
|
355
280
|
}
|
|
356
281
|
|
|
357
282
|
await this.#commitState(state, { persist: "goal" });
|
|
358
|
-
|
|
359
|
-
if (state.goal.status !== "budget-limited") {
|
|
360
|
-
this.#budgetReportedFor = undefined;
|
|
361
|
-
}
|
|
362
|
-
if (steering === "allowed" && flippedToBudgetLimited && this.#budgetReportedFor !== state.goal.id) {
|
|
363
|
-
await this.#sendBudgetLimitSteer(state.goal);
|
|
364
|
-
}
|
|
365
283
|
}
|
|
366
284
|
|
|
367
|
-
async flushUsage(
|
|
368
|
-
|
|
369
|
-
currentUsage: GoalTokenUsage = this.#host.getCurrentUsage(),
|
|
370
|
-
): Promise<void> {
|
|
371
|
-
await this.#withAccounting(() => this.#flushUsageLocked(steering, currentUsage));
|
|
285
|
+
async flushUsage(currentUsage: GoalTokenUsage = this.#host.getCurrentUsage()): Promise<void> {
|
|
286
|
+
await this.#withAccounting(() => this.#flushUsageLocked(currentUsage));
|
|
372
287
|
}
|
|
373
288
|
|
|
374
|
-
#createGoalState(objective: string
|
|
289
|
+
#createGoalState(objective: string): GoalModeState {
|
|
375
290
|
const now = this.#now();
|
|
376
291
|
const goal: Goal = {
|
|
377
292
|
id: String(Snowflake.next()),
|
|
378
293
|
objective,
|
|
379
294
|
status: "active",
|
|
380
|
-
tokenBudget,
|
|
381
295
|
tokensUsed: 0,
|
|
382
296
|
timeUsedSeconds: 0,
|
|
383
297
|
createdAt: now,
|
|
@@ -386,33 +300,29 @@ export class GoalRuntime {
|
|
|
386
300
|
return { enabled: true, mode: "active", goal };
|
|
387
301
|
}
|
|
388
302
|
|
|
389
|
-
async createGoal(input: { objective: string
|
|
303
|
+
async createGoal(input: { objective: string }): Promise<GoalModeState> {
|
|
390
304
|
const objective = validateGoalObjective(input.objective, "create");
|
|
391
|
-
validateTokenBudget(input.tokenBudget);
|
|
392
305
|
return await this.#withAccounting(async () => {
|
|
393
|
-
const existing = this.#
|
|
306
|
+
const existing = this.#getStateClone();
|
|
394
307
|
if (existing?.goal && existing.goal.status !== "dropped" && existing.goal.status !== "complete") {
|
|
395
308
|
throw new Error("cannot create a new goal because this session already has a goal");
|
|
396
309
|
}
|
|
397
|
-
const state = this.#createGoalState(objective
|
|
398
|
-
this.#budgetReportedFor = undefined;
|
|
310
|
+
const state = this.#createGoalState(objective);
|
|
399
311
|
this.#markActiveAccounting(state.goal);
|
|
400
312
|
await this.#commitState(state, { persist: "goal" });
|
|
401
313
|
return state;
|
|
402
314
|
});
|
|
403
315
|
}
|
|
404
316
|
|
|
405
|
-
async replaceGoal(input: { objective: string
|
|
317
|
+
async replaceGoal(input: { objective: string }): Promise<GoalModeState> {
|
|
406
318
|
const objective = validateGoalObjective(input.objective, "replace");
|
|
407
|
-
validateTokenBudget(input.tokenBudget);
|
|
408
319
|
return await this.#withAccounting(async () => {
|
|
409
|
-
const existing = this.#
|
|
320
|
+
const existing = this.#getStateClone();
|
|
410
321
|
if (!existing?.enabled || !isAccountingStatus(existing.goal)) {
|
|
411
322
|
throw new Error("cannot replace goal because no goal is active");
|
|
412
323
|
}
|
|
413
|
-
await this.#flushUsageLocked(
|
|
414
|
-
const state = this.#createGoalState(objective
|
|
415
|
-
this.#budgetReportedFor = undefined;
|
|
324
|
+
await this.#flushUsageLocked();
|
|
325
|
+
const state = this.#createGoalState(objective);
|
|
416
326
|
this.#markActiveAccounting(state.goal);
|
|
417
327
|
await this.#commitState(state, { persist: "goal" });
|
|
418
328
|
return state;
|
|
@@ -429,7 +339,6 @@ export class GoalRuntime {
|
|
|
429
339
|
state.reason = undefined;
|
|
430
340
|
state.goal.status = "active";
|
|
431
341
|
state.goal.updatedAt = this.#now();
|
|
432
|
-
this.#budgetReportedFor = undefined;
|
|
433
342
|
this.#markActiveAccounting(state.goal);
|
|
434
343
|
await this.#commitState(state, { persist: "goal" });
|
|
435
344
|
return state;
|
|
@@ -438,18 +347,17 @@ export class GoalRuntime {
|
|
|
438
347
|
|
|
439
348
|
async pauseGoal(): Promise<GoalModeState | undefined> {
|
|
440
349
|
return await this.#withAccounting(async () => {
|
|
441
|
-
await this.#flushUsageLocked(
|
|
350
|
+
await this.#flushUsageLocked();
|
|
442
351
|
const state = this.#getStateClone();
|
|
443
352
|
if (!state?.goal) return undefined;
|
|
444
353
|
state.enabled = false;
|
|
445
354
|
state.mode = "active";
|
|
446
355
|
state.reason = undefined;
|
|
447
|
-
if (state.goal.status === "active"
|
|
356
|
+
if (state.goal.status === "active") {
|
|
448
357
|
state.goal.status = "paused";
|
|
449
358
|
}
|
|
450
359
|
state.goal.updatedAt = this.#now();
|
|
451
360
|
this.#clearActiveAccounting();
|
|
452
|
-
this.#budgetReportedFor = undefined;
|
|
453
361
|
await this.#commitState(state, { persist: "goal_paused" });
|
|
454
362
|
return state;
|
|
455
363
|
});
|
|
@@ -457,12 +365,11 @@ export class GoalRuntime {
|
|
|
457
365
|
|
|
458
366
|
async dropGoal(): Promise<Goal | undefined> {
|
|
459
367
|
return await this.#withAccounting(async () => {
|
|
460
|
-
await this.#flushUsageLocked(
|
|
368
|
+
await this.#flushUsageLocked();
|
|
461
369
|
const state = this.#getStateClone();
|
|
462
370
|
if (!state?.goal) return undefined;
|
|
463
371
|
const dropped = { ...state.goal, status: "dropped" as const, updatedAt: this.#now() };
|
|
464
372
|
this.#clearActiveAccounting();
|
|
465
|
-
this.#budgetReportedFor = undefined;
|
|
466
373
|
await this.#host.emit({
|
|
467
374
|
type: "goal_updated",
|
|
468
375
|
goal: dropped,
|
|
@@ -475,7 +382,7 @@ export class GoalRuntime {
|
|
|
475
382
|
|
|
476
383
|
async completeGoalFromTool(): Promise<Goal> {
|
|
477
384
|
return await this.#withAccounting(async () => {
|
|
478
|
-
await this.#flushUsageLocked(
|
|
385
|
+
await this.#flushUsageLocked();
|
|
479
386
|
const state = this.#getStateClone();
|
|
480
387
|
if (!state?.goal) {
|
|
481
388
|
throw new Error("cannot complete goal because no goal is active");
|
|
@@ -492,33 +399,20 @@ export class GoalRuntime {
|
|
|
492
399
|
state.mode = "exiting";
|
|
493
400
|
state.reason = "completed";
|
|
494
401
|
this.#clearActiveAccounting();
|
|
495
|
-
this.#budgetReportedFor = undefined;
|
|
496
402
|
await this.#commitState(state, { persist: "goal" });
|
|
497
403
|
return state.goal;
|
|
498
404
|
});
|
|
499
405
|
}
|
|
500
406
|
|
|
501
407
|
buildActivePrompt(): string | undefined {
|
|
502
|
-
const state = this.#
|
|
503
|
-
return state?.enabled && state.goal
|
|
504
|
-
? renderGoalPrompt("active", state.goal)
|
|
505
|
-
: undefined;
|
|
408
|
+
const state = this.#getStateClone();
|
|
409
|
+
return state?.enabled && state.goal.status === "active" ? renderGoalPrompt("active", state.goal) : undefined;
|
|
506
410
|
}
|
|
507
411
|
|
|
508
412
|
buildContinuationPrompt(): string | undefined {
|
|
509
|
-
const state = this.#
|
|
413
|
+
const state = this.#getStateClone();
|
|
510
414
|
return state?.enabled && state.goal.status === "active"
|
|
511
415
|
? renderGoalPrompt("continuation", state.goal)
|
|
512
416
|
: undefined;
|
|
513
417
|
}
|
|
514
|
-
|
|
515
|
-
async #sendBudgetLimitSteer(goal: Goal): Promise<void> {
|
|
516
|
-
if (this.#budgetReportedFor === goal.id) return;
|
|
517
|
-
this.#budgetReportedFor = goal.id;
|
|
518
|
-
await this.#host.sendHiddenMessage({
|
|
519
|
-
customType: "goal-budget-limit",
|
|
520
|
-
content: renderGoalPrompt("budget-limit", goal),
|
|
521
|
-
deliverAs: "steer",
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
418
|
}
|
package/src/goals/state.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { UsageStatistics } from "../session/session-manager";
|
|
2
2
|
|
|
3
|
-
export type GoalStatus = "active" | "paused" | "
|
|
3
|
+
export type GoalStatus = "active" | "paused" | "complete" | "dropped";
|
|
4
4
|
|
|
5
5
|
export interface Goal {
|
|
6
6
|
id: string;
|
|
7
7
|
objective: string;
|
|
8
8
|
status: GoalStatus;
|
|
9
|
-
tokenBudget?: number;
|
|
10
9
|
tokensUsed: number;
|
|
11
10
|
timeUsedSeconds: number;
|
|
12
11
|
createdAt: number;
|
|
@@ -19,12 +18,9 @@ export interface GoalModeState {
|
|
|
19
18
|
reason?: "completed";
|
|
20
19
|
goal: Goal;
|
|
21
20
|
}
|
|
22
|
-
|
|
23
21
|
export interface GoalToolDetails {
|
|
24
22
|
op: "create" | "get" | "complete" | "resume" | "drop";
|
|
25
23
|
goal?: Goal | null;
|
|
26
|
-
remainingTokens?: number | null;
|
|
27
|
-
completionBudgetReport?: string | null;
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
export type GoalRuntimeEvent =
|
|
@@ -33,5 +29,38 @@ export type GoalRuntimeEvent =
|
|
|
33
29
|
|
|
34
30
|
export type GoalTokenUsage = Pick<UsageStatistics, "input" | "output" | "cacheRead" | "cacheWrite">;
|
|
35
31
|
|
|
36
|
-
export
|
|
37
|
-
|
|
32
|
+
export function normalizeGoal(candidate: unknown): Goal | null {
|
|
33
|
+
if (typeof candidate !== "object" || candidate === null) return null;
|
|
34
|
+
const value = candidate as Record<string, unknown>;
|
|
35
|
+
if (
|
|
36
|
+
typeof value.id !== "string" ||
|
|
37
|
+
typeof value.objective !== "string" ||
|
|
38
|
+
typeof value.status !== "string" ||
|
|
39
|
+
typeof value.tokensUsed !== "number" ||
|
|
40
|
+
typeof value.timeUsedSeconds !== "number" ||
|
|
41
|
+
typeof value.createdAt !== "number" ||
|
|
42
|
+
typeof value.updatedAt !== "number"
|
|
43
|
+
) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const status = value.status === "budget-limited" ? "active" : value.status;
|
|
47
|
+
if (status !== "active" && status !== "paused" && status !== "complete" && status !== "dropped") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
id: value.id,
|
|
52
|
+
objective: value.objective,
|
|
53
|
+
status,
|
|
54
|
+
tokensUsed: value.tokensUsed,
|
|
55
|
+
timeUsedSeconds: value.timeUsedSeconds,
|
|
56
|
+
createdAt: value.createdAt,
|
|
57
|
+
updatedAt: value.updatedAt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeGoalModeState(candidate: GoalModeState | undefined): GoalModeState | undefined {
|
|
62
|
+
if (!candidate) return undefined;
|
|
63
|
+
const goal = normalizeGoal(candidate.goal);
|
|
64
|
+
if (!goal) return undefined;
|
|
65
|
+
return { ...candidate, goal };
|
|
66
|
+
}
|