@pinet/agent-goal 0.2.13 → 0.2.14

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/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { formatGoalDashboard, formatGoalStatus } from "./dashboard.js";
4
- import { GoalWindow, } from "./goal-window.js";
4
+ import { GoalWindow, parseDuration } from "./goal-window.js";
5
5
  import { PiGoalEvaluator } from "./pi-evaluator.js";
6
6
  import { countGoalProgressTokens, formatGoalProgress, } from "./progress.js";
7
7
  import { GoalRuntime } from "./runtime.js";
8
8
  import { SqliteGoalStorage } from "./sqlite-storage.js";
9
9
  export { displayGoalText, formatGoalDashboard, formatGoalStatus } from "./dashboard.js";
10
- export { GoalWindow, } from "./goal-window.js";
10
+ export { GoalWindow, parseDuration } from "./goal-window.js";
11
11
  export { MemoryGoalStorage } from "./memory-storage.js";
12
12
  export { parseGoalEvaluation, PiGoalEvaluator } from "./pi-evaluator.js";
13
13
  export { countGoalProgressTokens, formatGoalProgress, } from "./progress.js";
@@ -24,9 +24,8 @@ export function registerAgentGoal(pi, options = {}) {
24
24
  const hiddenScopes = new Set();
25
25
  const agentCreatedGoalScopes = new Set();
26
26
  const defaultBudget = options.defaultBudget ?? {
27
- maxIterations: Number(process.env.PI_AGENT_GOAL_MAX_ITERATIONS ?? 25),
28
- maxTokens: process.env.PI_AGENT_GOAL_MAX_TOKENS
29
- ? Number(process.env.PI_AGENT_GOAL_MAX_TOKENS)
27
+ maxIterations: process.env.PI_AGENT_GOAL_MAX_ITERATIONS
28
+ ? Number(process.env.PI_AGENT_GOAL_MAX_ITERATIONS)
30
29
  : undefined,
31
30
  maxRuntimeMs: process.env.PI_AGENT_GOAL_MAX_RUNTIME_MS
32
31
  ? Number(process.env.PI_AGENT_GOAL_MAX_RUNTIME_MS)
@@ -78,27 +77,33 @@ export function registerAgentGoal(pi, options = {}) {
78
77
  ctx.ui.setWidget(WIDGET_KEY, undefined);
79
78
  };
80
79
  const applyGoalAction = async (scopeId, action) => {
81
- if (typeof action === "object") {
82
- await runtime.updateBudget(scopeId, {
83
- maxIterations: action.maxIterations,
84
- maxTokens: action.maxTokens,
85
- });
80
+ if (action === "closeGoal") {
81
+ await runtime.closeGoal(scopeId);
86
82
  return;
87
83
  }
88
- switch (action) {
89
- case "pause":
90
- await runtime.setStatus(scopeId, "paused");
91
- break;
92
- case "resume":
93
- await runtime.setStatus(scopeId, "active");
84
+ if (action === "pause" || action === "resume") {
85
+ await runtime.setStatus(scopeId, action === "pause" ? "paused" : "active");
86
+ if (action === "resume")
94
87
  await runtime.start(scopeId, "Resume the goal from current state.");
88
+ return;
89
+ }
90
+ switch (action.type) {
91
+ case "create":
92
+ await runtime.create(scopeId, action.objective, {
93
+ ...defaultBudget,
94
+ ...(action.maxIterations === undefined ? {} : { maxIterations: action.maxIterations }),
95
+ ...(action.maxRuntimeMs === undefined ? {} : { maxRuntimeMs: action.maxRuntimeMs }),
96
+ }, action.name);
97
+ await runtime.start(scopeId);
98
+ break;
99
+ case "edit":
100
+ await runtime.updateDetails(scopeId, action);
95
101
  break;
96
- case "complete":
97
- await runtime.setStatus(scopeId, "complete");
102
+ case "budget":
103
+ await runtime.updateBudget(scopeId, action);
98
104
  break;
99
- case "clear":
100
- if (!(await runtime.clear(scopeId)))
101
- throw new Error("This session has no goal");
105
+ case "snooze":
106
+ await runtime.snooze(scopeId, action.durationMs);
102
107
  break;
103
108
  }
104
109
  };
@@ -167,14 +172,8 @@ export function registerAgentGoal(pi, options = {}) {
167
172
  parameters: {
168
173
  type: "object",
169
174
  properties: {
175
+ name: { type: "string", description: "Short terminal-friendly goal name." },
170
176
  objective: { type: "string", description: "The complete user-aligned outcome to achieve." },
171
- maxIterations: {
172
- type: "integer",
173
- minimum: 1,
174
- maximum: defaultBudget.maxIterations,
175
- },
176
- maxTokens: { type: "number", exclusiveMinimum: 0 },
177
- maxRuntimeMs: { type: "number", exclusiveMinimum: 0 },
178
177
  },
179
178
  required: ["objective"],
180
179
  additionalProperties: false,
@@ -183,25 +182,7 @@ export function registerAgentGoal(pi, options = {}) {
183
182
  const params = rawParams;
184
183
  const ctx = rawCtx;
185
184
  const scopeId = ctx.sessionManager.getSessionId();
186
- if (params.maxIterations !== undefined &&
187
- params.maxIterations > defaultBudget.maxIterations) {
188
- throw new Error(`Goal maxIterations cannot exceed the configured limit of ${defaultBudget.maxIterations}`);
189
- }
190
- if (params.maxTokens !== undefined &&
191
- defaultBudget.maxTokens !== undefined &&
192
- params.maxTokens > defaultBudget.maxTokens) {
193
- throw new Error(`Goal maxTokens cannot exceed the configured limit of ${defaultBudget.maxTokens}`);
194
- }
195
- if (params.maxRuntimeMs !== undefined &&
196
- defaultBudget.maxRuntimeMs !== undefined &&
197
- params.maxRuntimeMs > defaultBudget.maxRuntimeMs) {
198
- throw new Error(`Goal maxRuntimeMs cannot exceed the configured limit of ${defaultBudget.maxRuntimeMs}`);
199
- }
200
- const goal = await runtime.create(scopeId, params.objective, {
201
- maxIterations: params.maxIterations ?? defaultBudget.maxIterations,
202
- maxTokens: params.maxTokens ?? defaultBudget.maxTokens,
203
- maxRuntimeMs: params.maxRuntimeMs ?? defaultBudget.maxRuntimeMs,
204
- });
185
+ const goal = await runtime.create(scopeId, params.objective, defaultBudget, params.name);
205
186
  agentCreatedGoalScopes.add(scopeId);
206
187
  await refreshUi(ctx);
207
188
  return {
@@ -217,9 +198,9 @@ export function registerAgentGoal(pi, options = {}) {
217
198
  });
218
199
  pi.registerTool({
219
200
  name: "update_goal_budget",
220
- label: "Update goal budget",
221
- description: "Adjust this session goal's bounded turn or token ceiling without replacing the goal. Changes remain constrained by configured limits and cannot discard accounted usage.",
222
- promptSnippet: "Adjust the active session goal's bounded turn or token budget.",
201
+ label: "Update goal limits",
202
+ description: "Set optional turns/runtime continuation limits, or turn limits off. Limits never interrupt in-flight work and changes preserve accounted usage.",
203
+ promptSnippet: "Adjust the active session goal's optional continuation limits.",
223
204
  promptGuidelines: [
224
205
  "Only change a goal budget when more or less capacity is genuinely needed for the existing user-aligned objective.",
225
206
  "Never use budget changes to broaden the objective or evade configured hard limits.",
@@ -231,15 +212,17 @@ export function registerAgentGoal(pi, options = {}) {
231
212
  maxTurns: {
232
213
  type: "integer",
233
214
  minimum: 1,
234
- maximum: defaultBudget.maxIterations,
215
+ ...(defaultBudget.maxIterations === undefined
216
+ ? {}
217
+ : { maximum: defaultBudget.maxIterations }),
235
218
  description: "New total settled-turn ceiling, including turns already accounted.",
236
219
  },
237
- maxTokens: {
220
+ maxRuntimeMs: {
238
221
  type: "number",
239
222
  exclusiveMinimum: 0,
240
- ...(defaultBudget.maxTokens === undefined ? {} : { maximum: defaultBudget.maxTokens }),
241
- description: "New total token ceiling, including tokens already accounted.",
223
+ description: "New total runtime ceiling in milliseconds.",
242
224
  },
225
+ off: { type: "boolean", description: "Disable all continuation limits." },
243
226
  },
244
227
  additionalProperties: false,
245
228
  },
@@ -248,14 +231,15 @@ export function registerAgentGoal(pi, options = {}) {
248
231
  const ctx = rawCtx;
249
232
  const goal = await runtime.updateBudget(ctx.sessionManager.getSessionId(), {
250
233
  maxIterations: params.maxTurns,
251
- maxTokens: params.maxTokens,
234
+ maxRuntimeMs: params.maxRuntimeMs,
235
+ disabled: params.off,
252
236
  });
253
237
  await refreshUi(ctx);
254
238
  return {
255
239
  content: [
256
240
  {
257
241
  type: "text",
258
- text: `Updated goal budget to ${goal.budget.maxIterations} turns${goal.budget.maxTokens === undefined ? "" : ` and ${goal.budget.maxTokens} tokens`}.`,
242
+ text: `Updated goal limits: ${goal.budget.maxIterations ?? "unlimited"} turns, ${goal.budget.maxRuntimeMs ?? "unlimited"}ms runtime.`,
259
243
  },
260
244
  ],
261
245
  details: { goal },
@@ -336,8 +320,32 @@ export function registerAgentGoal(pi, options = {}) {
336
320
  };
337
321
  },
338
322
  });
323
+ pi.registerTool({
324
+ name: "checkpoint_goal",
325
+ label: "Checkpoint goal",
326
+ description: "Record durable agent-reported progress, evidence, and the next step or blocker.",
327
+ promptSnippet: "Record a concise durable progress checkpoint for the active goal.",
328
+ parameters: {
329
+ type: "object",
330
+ properties: {
331
+ summary: { type: "string" },
332
+ evidence: { type: "string" },
333
+ nextStep: { type: "string" },
334
+ blocker: { type: "string" },
335
+ },
336
+ required: ["summary"],
337
+ additionalProperties: false,
338
+ },
339
+ async execute(_toolCallId, rawParams, _signal, _onUpdate, rawCtx) {
340
+ const checkpoint = await runtime.addCheckpoint(rawCtx.sessionManager.getSessionId(), rawParams);
341
+ return {
342
+ content: [{ type: "text", text: `Checkpoint recorded: ${checkpoint.summary}` }],
343
+ details: { checkpoint },
344
+ };
345
+ },
346
+ });
339
347
  pi.registerCommand("goal", {
340
- description: "Create, inspect, adjust budget, pause, resume, complete, clear, show, or hide this session's goal",
348
+ description: "Create or inspect a goal; update its name, objective, or limits; snooze, close, show, or hide it",
341
349
  handler: async (args, rawCtx) => {
342
350
  const ctx = rawCtx;
343
351
  activeContext = ctx;
@@ -350,9 +358,10 @@ export function registerAgentGoal(pi, options = {}) {
350
358
  while (true) {
351
359
  const goal = await runtime.get(scopeId);
352
360
  const claim = await runtime.getContinuationClaim(scopeId);
361
+ const checkpoints = await runtime.listCheckpoints(scopeId);
353
362
  const action = await ctx.ui.custom((tui, theme, _keybindings, done) => {
354
363
  openedWindow = true;
355
- return new GoalWindow(goal, claim, theme, done, () => tui.requestRender(), Date.now, actionError);
364
+ return new GoalWindow(goal, claim, theme, done, () => tui.requestRender(), Date.now, actionError, checkpoints);
356
365
  }, {
357
366
  overlay: true,
358
367
  overlayOptions: {
@@ -367,7 +376,7 @@ export function registerAgentGoal(pi, options = {}) {
367
376
  api.sendMessage({
368
377
  customType: "agent-goal.status",
369
378
  content: goal
370
- ? formatGoalDashboard(goal, claim).join("\n")
379
+ ? formatGoalDashboard(goal, claim, checkpoints).join("\n")
371
380
  : "This session has no goal.",
372
381
  display: true,
373
382
  }, { triggerTurn: false });
@@ -386,48 +395,46 @@ export function registerAgentGoal(pi, options = {}) {
386
395
  }
387
396
  }
388
397
  const command = input.toLowerCase();
389
- if (command === "budget" || command.startsWith("budget ")) {
390
- const update = {};
391
- for (const token of input.slice("budget".length).trim().split(/\s+/).filter(Boolean)) {
392
- const separator = token.indexOf("=");
393
- if (separator < 1)
394
- throw new Error(`Invalid goal budget argument: ${token}`);
395
- const key = token.slice(0, separator).toLowerCase();
396
- const value = Number(token.slice(separator + 1));
397
- if (key === "turns" || key === "maxturns" || key === "iterations") {
398
- update.maxIterations = value;
399
- }
400
- else if (key === "tokens" || key === "maxtokens") {
401
- update.maxTokens = value;
402
- }
403
- else {
404
- throw new Error(`Unknown goal budget field: ${key}`);
405
- }
406
- }
407
- const goal = await runtime.updateBudget(scopeId, update);
408
- await refreshUi(ctx);
409
- if (ctx.hasUI) {
410
- ctx.ui.notify(`Goal budget: ${goal.budget.maxIterations} turns${goal.budget.maxTokens === undefined ? "" : ` · ${goal.budget.maxTokens} tokens`}`, "info");
411
- }
412
- return;
398
+ if (command.startsWith("update name ")) {
399
+ await runtime.updateDetails(scopeId, { name: input.slice("update name ".length) });
400
+ }
401
+ else if (command.startsWith("update objective ")) {
402
+ await runtime.updateDetails(scopeId, {
403
+ objective: input.slice("update objective ".length),
404
+ });
405
+ }
406
+ else if (command === "update budget off") {
407
+ await runtime.updateBudget(scopeId, { disabled: true });
408
+ }
409
+ else if (command.startsWith("update budget turns ")) {
410
+ await runtime.updateBudget(scopeId, {
411
+ maxIterations: Number(input.slice("update budget turns ".length)),
412
+ });
413
+ }
414
+ else if (command.startsWith("update budget runtime ")) {
415
+ const duration = parseDuration(input.slice("update budget runtime ".length));
416
+ if (duration === undefined)
417
+ throw new Error("Runtime must use m, h, or d");
418
+ await runtime.updateBudget(scopeId, { maxRuntimeMs: duration });
419
+ }
420
+ else if (command.startsWith("snooze ")) {
421
+ const duration = parseDuration(input.slice("snooze ".length));
422
+ if (duration === undefined)
423
+ throw new Error("Snooze must use m, h, or d");
424
+ await runtime.snooze(scopeId, duration);
425
+ }
426
+ else if (command === "close") {
427
+ await runtime.closeGoal(scopeId);
428
+ }
429
+ else if (command === "hide") {
430
+ hiddenScopes.add(scopeId);
431
+ }
432
+ else if (command === "show") {
433
+ hiddenScopes.delete(scopeId);
413
434
  }
414
- switch (command) {
415
- case "pause":
416
- case "resume":
417
- case "complete":
418
- case "clear":
419
- await applyGoalAction(scopeId, command);
420
- break;
421
- case "hide":
422
- hiddenScopes.add(scopeId);
423
- break;
424
- case "show":
425
- hiddenScopes.delete(scopeId);
426
- break;
427
- default:
428
- await runtime.create(scopeId, input);
429
- await runtime.start(scopeId);
430
- break;
435
+ else {
436
+ await runtime.create(scopeId, input);
437
+ await runtime.start(scopeId);
431
438
  }
432
439
  await refreshUi(ctx);
433
440
  if (ctx.hasUI)
@@ -1,13 +1,16 @@
1
- import type { AgentGoal, GoalContinuationClaim, GoalPendingEvaluation, GoalStorage, GoalTerminalCandidateRecord } from "./domain.js";
1
+ import type { AgentGoal, GoalCheckpoint, GoalContinuationClaim, GoalPendingEvaluation, GoalStorage, GoalTerminalCandidateRecord } from "./domain.js";
2
2
  export declare class MemoryGoalStorage implements GoalStorage {
3
3
  private readonly goals;
4
4
  private readonly pendingEvaluations;
5
+ private readonly checkpoints;
5
6
  private readonly terminalCandidates;
6
7
  private readonly claims;
7
8
  get(scopeId: string): Promise<AgentGoal | undefined>;
8
9
  create(goal: AgentGoal): Promise<void>;
9
10
  replace(goal: AgentGoal, expectedVersion: number): Promise<boolean>;
10
11
  updateBudget(goal: AgentGoal, expectedVersion: number): Promise<boolean>;
12
+ addCheckpoint(checkpoint: GoalCheckpoint): Promise<boolean>;
13
+ listCheckpoints(scopeId: string): Promise<GoalCheckpoint[]>;
11
14
  delete(scopeId: string, expectedVersion: number): Promise<boolean>;
12
15
  getPendingEvaluation(scopeId: string): Promise<GoalPendingEvaluation | undefined>;
13
16
  appendPendingEvaluation(pending: GoalPendingEvaluation): Promise<boolean>;
@@ -9,6 +9,7 @@ function cloneGoal(goal) {
9
9
  export class MemoryGoalStorage {
10
10
  goals = new Map();
11
11
  pendingEvaluations = new Map();
12
+ checkpoints = new Map();
12
13
  terminalCandidates = new Map();
13
14
  claims = new Map();
14
15
  async get(scopeId) {
@@ -46,6 +47,18 @@ export class MemoryGoalStorage {
46
47
  }
47
48
  return true;
48
49
  }
50
+ async addCheckpoint(checkpoint) {
51
+ const goal = this.goals.get(checkpoint.scopeId);
52
+ if (!goal || goal.id !== checkpoint.goalId || goal.status === "complete")
53
+ return false;
54
+ const checkpoints = this.checkpoints.get(checkpoint.scopeId) ?? [];
55
+ checkpoints.push({ ...checkpoint });
56
+ this.checkpoints.set(checkpoint.scopeId, checkpoints);
57
+ return true;
58
+ }
59
+ async listCheckpoints(scopeId) {
60
+ return (this.checkpoints.get(scopeId) ?? []).map((checkpoint) => ({ ...checkpoint })).reverse();
61
+ }
49
62
  async delete(scopeId, expectedVersion) {
50
63
  const current = this.goals.get(scopeId);
51
64
  if (!current || current.version !== expectedVersion)
@@ -53,6 +66,7 @@ export class MemoryGoalStorage {
53
66
  this.pendingEvaluations.delete(scopeId);
54
67
  this.terminalCandidates.delete(scopeId);
55
68
  this.claims.delete(scopeId);
69
+ this.checkpoints.delete(scopeId);
56
70
  return this.goals.delete(scopeId);
57
71
  }
58
72
  async getPendingEvaluation(scopeId) {
@@ -124,7 +138,13 @@ export class MemoryGoalStorage {
124
138
  pending?.evaluationId !== expectedEvaluationId) {
125
139
  return false;
126
140
  }
127
- this.goals.set(goal.scopeId, cloneGoal(goal));
141
+ this.goals.set(goal.scopeId, {
142
+ ...cloneGoal(goal),
143
+ name: current.name,
144
+ objective: current.objective,
145
+ budget: { ...current.budget },
146
+ snoozedUntil: current.snoozedUntil,
147
+ });
128
148
  this.pendingEvaluations.delete(goal.scopeId);
129
149
  return true;
130
150
  }
package/dist/runtime.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentGoal, GoalBudget, GoalBudgetUpdate, GoalContinuation, GoalContinuationClaim, GoalEvaluator, GoalEventSink, GoalProgress, GoalRetryPolicy, GoalStatus, GoalStorage, GoalTerminalCandidate, GoalTerminalCandidateRecord, GoalWakeScheduler } from "./domain.js";
1
+ import type { AgentGoal, GoalBudget, GoalCheckpoint, GoalBudgetUpdate, GoalContinuation, GoalContinuationClaim, GoalEvaluator, GoalEventSink, GoalProgress, GoalRetryPolicy, GoalStatus, GoalStorage, GoalTerminalCandidate, GoalTerminalCandidateRecord, GoalWakeScheduler } from "./domain.js";
2
2
  export interface GoalRuntimeOptions {
3
3
  defaultBudget?: GoalBudget;
4
4
  retryPolicy?: GoalRetryPolicy;
@@ -28,8 +28,21 @@ export declare class GoalRuntime {
28
28
  get(scopeId: string): Promise<AgentGoal | undefined>;
29
29
  getContinuationClaim(scopeId: string): Promise<GoalContinuationClaim | undefined>;
30
30
  getTerminalCandidate(scopeId: string): Promise<GoalTerminalCandidateRecord | undefined>;
31
- create(scopeId: string, objective: string, budget?: GoalBudget): Promise<AgentGoal>;
31
+ listCheckpoints(scopeId: string): Promise<GoalCheckpoint[]>;
32
+ create(scopeId: string, objective: string, budget?: GoalBudget, name?: string): Promise<AgentGoal>;
32
33
  updateBudget(scopeId: string, update: GoalBudgetUpdate): Promise<AgentGoal>;
34
+ updateDetails(scopeId: string, update: {
35
+ name?: string;
36
+ objective?: string;
37
+ }): Promise<AgentGoal>;
38
+ addCheckpoint(scopeId: string, input: {
39
+ summary: string;
40
+ evidence?: string;
41
+ nextStep?: string;
42
+ blocker?: string;
43
+ }): Promise<GoalCheckpoint>;
44
+ snooze(scopeId: string, durationMs: number): Promise<AgentGoal>;
45
+ closeGoal(scopeId: string): Promise<AgentGoal>;
33
46
  setStatus(scopeId: string, status: Extract<GoalStatus, "active" | "paused" | "complete">): Promise<AgentGoal>;
34
47
  clear(scopeId: string): Promise<boolean>;
35
48
  requestTerminalCandidate(scopeId: string, candidate: GoalTerminalCandidate): Promise<GoalTerminalCandidateRecord>;