@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/runtime.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { TimerGoalWakeScheduler } from "./wake-scheduler.js";
3
- const DEFAULT_BUDGET = { maxIterations: 25 };
3
+ const DEFAULT_BUDGET = {};
4
4
  const DEFAULT_RETRY_POLICY = {
5
5
  maxAttempts: 3,
6
6
  baseDelayMs: 250,
@@ -54,21 +54,40 @@ export class GoalRuntime {
54
54
  async getTerminalCandidate(scopeId) {
55
55
  return this.storage.getTerminalCandidate(scopeId);
56
56
  }
57
- async create(scopeId, objective, budget = this.budget) {
57
+ async listCheckpoints(scopeId) {
58
+ return this.storage.listCheckpoints(scopeId);
59
+ }
60
+ async create(scopeId, objective, budget = this.budget, name) {
58
61
  const trimmedObjective = objective.trim();
59
62
  if (!trimmedObjective)
60
63
  throw new Error("Goal objective cannot be empty");
61
- if (!Number.isInteger(budget.maxIterations) || budget.maxIterations <= 0) {
64
+ if (budget.maxIterations !== undefined &&
65
+ (!Number.isInteger(budget.maxIterations) || budget.maxIterations <= 0)) {
62
66
  throw new Error("Goal maxIterations must be a positive integer");
63
67
  }
68
+ if (budget.maxIterations !== undefined &&
69
+ this.budget.maxIterations !== undefined &&
70
+ budget.maxIterations > this.budget.maxIterations) {
71
+ throw new Error(`Goal maxIterations cannot exceed the configured limit of ${this.budget.maxIterations}`);
72
+ }
64
73
  if (budget.maxTokens !== undefined &&
65
74
  (!Number.isFinite(budget.maxTokens) || budget.maxTokens <= 0)) {
66
75
  throw new Error("Goal maxTokens must be a positive finite number");
67
76
  }
77
+ if (budget.maxTokens !== undefined &&
78
+ this.budget.maxTokens !== undefined &&
79
+ budget.maxTokens > this.budget.maxTokens) {
80
+ throw new Error(`Goal maxTokens cannot exceed the configured limit of ${this.budget.maxTokens}`);
81
+ }
68
82
  if (budget.maxRuntimeMs !== undefined &&
69
83
  (!Number.isFinite(budget.maxRuntimeMs) || budget.maxRuntimeMs <= 0)) {
70
84
  throw new Error("Goal maxRuntimeMs must be a positive finite number");
71
85
  }
86
+ if (budget.maxRuntimeMs !== undefined &&
87
+ this.budget.maxRuntimeMs !== undefined &&
88
+ budget.maxRuntimeMs > this.budget.maxRuntimeMs) {
89
+ throw new Error(`Goal maxRuntimeMs cannot exceed the configured limit of ${this.budget.maxRuntimeMs}`);
90
+ }
72
91
  if (await this.storage.get(scopeId)) {
73
92
  throw new Error("This session already has a goal; clear it before creating another");
74
93
  }
@@ -76,6 +95,7 @@ export class GoalRuntime {
76
95
  const goal = {
77
96
  id: randomUUID(),
78
97
  scopeId,
98
+ name: name?.trim() || trimmedObjective.slice(0, 72),
79
99
  objective: trimmedObjective,
80
100
  status: "active",
81
101
  budget: { ...budget },
@@ -92,18 +112,27 @@ export class GoalRuntime {
92
112
  const current = await this.requireGoal(scopeId);
93
113
  if (current.status === "complete")
94
114
  throw new Error("Cannot change a complete goal budget");
95
- if (update.maxIterations === undefined && update.maxTokens === undefined) {
96
- throw new Error("A goal budget update requires maxIterations or maxTokens");
115
+ if (update.maxIterations === undefined &&
116
+ update.maxRuntimeMs === undefined &&
117
+ update.maxTokens === undefined &&
118
+ !update.disabled) {
119
+ throw new Error("A goal limit update requires turns, runtime, or off");
97
120
  }
98
121
  if (update.maxIterations !== undefined &&
99
122
  (!Number.isInteger(update.maxIterations) || update.maxIterations <= 0)) {
100
123
  throw new Error("Goal maxIterations must be a positive integer");
101
124
  }
125
+ if (update.maxRuntimeMs !== undefined &&
126
+ (!Number.isFinite(update.maxRuntimeMs) || update.maxRuntimeMs <= 0)) {
127
+ throw new Error("Goal maxRuntimeMs must be a positive finite number");
128
+ }
102
129
  if (update.maxTokens !== undefined &&
103
130
  (!Number.isFinite(update.maxTokens) || update.maxTokens <= 0)) {
104
131
  throw new Error("Goal maxTokens must be a positive finite number");
105
132
  }
106
- if (update.maxIterations !== undefined && update.maxIterations > this.budget.maxIterations) {
133
+ if (update.maxIterations !== undefined &&
134
+ this.budget.maxIterations !== undefined &&
135
+ update.maxIterations > this.budget.maxIterations) {
107
136
  throw new Error(`Goal maxIterations cannot exceed the configured limit of ${this.budget.maxIterations}`);
108
137
  }
109
138
  if (update.maxTokens !== undefined &&
@@ -111,6 +140,11 @@ export class GoalRuntime {
111
140
  update.maxTokens > this.budget.maxTokens) {
112
141
  throw new Error(`Goal maxTokens cannot exceed the configured limit of ${this.budget.maxTokens}`);
113
142
  }
143
+ if (update.maxRuntimeMs !== undefined &&
144
+ this.budget.maxRuntimeMs !== undefined &&
145
+ update.maxRuntimeMs > this.budget.maxRuntimeMs) {
146
+ throw new Error(`Goal maxRuntimeMs cannot exceed the configured limit of ${this.budget.maxRuntimeMs}`);
147
+ }
114
148
  const minimumIterations = current.status === "active" ? current.usage.iterations + 1 : current.usage.iterations;
115
149
  if (update.maxIterations !== undefined && update.maxIterations < minimumIterations) {
116
150
  throw new Error(current.status === "active"
@@ -123,11 +157,14 @@ export class GoalRuntime {
123
157
  ? `Goal maxTokens must leave capacity beyond ${current.usage.tokens} accounted tokens`
124
158
  : `Goal maxTokens cannot be lower than ${current.usage.tokens} accounted tokens`);
125
159
  }
126
- const nextBudget = {
127
- ...current.budget,
128
- ...(update.maxIterations === undefined ? {} : { maxIterations: update.maxIterations }),
129
- ...(update.maxTokens === undefined ? {} : { maxTokens: update.maxTokens }),
130
- };
160
+ const nextBudget = update.disabled
161
+ ? {}
162
+ : {
163
+ ...current.budget,
164
+ ...(update.maxIterations === undefined ? {} : { maxIterations: update.maxIterations }),
165
+ ...(update.maxRuntimeMs === undefined ? {} : { maxRuntimeMs: update.maxRuntimeMs }),
166
+ ...(update.maxTokens === undefined ? {} : { maxTokens: update.maxTokens }),
167
+ };
131
168
  const candidate = {
132
169
  ...current,
133
170
  budget: nextBudget,
@@ -147,6 +184,88 @@ export class GoalRuntime {
147
184
  }
148
185
  return next;
149
186
  }
187
+ async updateDetails(scopeId, update) {
188
+ const current = await this.requireGoal(scopeId);
189
+ if (current.status === "complete")
190
+ throw new Error("Cannot edit a closed goal");
191
+ const name = update.name?.trim();
192
+ const objective = update.objective?.trim();
193
+ if (update.name !== undefined && !name)
194
+ throw new Error("Goal name cannot be empty");
195
+ if (update.objective !== undefined && !objective)
196
+ throw new Error("Goal objective cannot be empty");
197
+ if (name === undefined && objective === undefined)
198
+ throw new Error("A goal update requires name or objective");
199
+ const next = {
200
+ ...current,
201
+ ...(name === undefined ? {} : { name }),
202
+ ...(objective === undefined ? {} : { objective }),
203
+ version: current.version + 1,
204
+ updatedAt: this.now().toISOString(),
205
+ };
206
+ if (!(await this.storage.updateBudget(next, current.version))) {
207
+ throw new Error("Goal changed while it was being edited; retry the command");
208
+ }
209
+ await this.record({ type: "goal.updated", goal: next });
210
+ if (await this.storage.getPendingEvaluation(scopeId)) {
211
+ await this.processPendingEvaluation(scopeId);
212
+ }
213
+ return (await this.storage.get(scopeId)) ?? next;
214
+ }
215
+ async addCheckpoint(scopeId, input) {
216
+ const goal = await this.requireGoal(scopeId);
217
+ if (goal.status === "complete")
218
+ throw new Error("Cannot checkpoint a closed goal");
219
+ const summary = input.summary.trim();
220
+ if (!summary)
221
+ throw new Error("A checkpoint summary is required");
222
+ const checkpoint = {
223
+ id: randomUUID(),
224
+ scopeId,
225
+ goalId: goal.id,
226
+ summary,
227
+ evidence: input.evidence?.trim() || undefined,
228
+ nextStep: input.nextStep?.trim() || undefined,
229
+ blocker: input.blocker?.trim() || undefined,
230
+ createdAt: this.now().toISOString(),
231
+ };
232
+ if (!(await this.storage.addCheckpoint(checkpoint))) {
233
+ throw new Error("Goal changed while its checkpoint was being recorded; retry");
234
+ }
235
+ await this.record({ type: "goal.checkpoint_added", goal, checkpoint });
236
+ return checkpoint;
237
+ }
238
+ async snooze(scopeId, durationMs) {
239
+ if (!Number.isFinite(durationMs) || durationMs <= 0)
240
+ throw new Error("Goal snooze duration must be positive");
241
+ const current = await this.requireGoal(scopeId);
242
+ if (current.status === "complete")
243
+ throw new Error("Cannot snooze a closed goal");
244
+ const snoozedUntil = new Date(this.now().getTime() + durationMs).toISOString();
245
+ const next = {
246
+ ...current,
247
+ status: "active",
248
+ blockedReason: undefined,
249
+ snoozedUntil,
250
+ version: current.version + 1,
251
+ updatedAt: this.now().toISOString(),
252
+ };
253
+ if (!(await this.storage.updateBudget(next, current.version))) {
254
+ throw new Error("Goal changed while it was being snoozed; retry the command");
255
+ }
256
+ const claim = await this.storage.getContinuationClaim(scopeId);
257
+ if (claim)
258
+ await this.storage.deleteContinuationClaim(scopeId, claim.claimId);
259
+ this.scheduleRecovery(scopeId, snoozedUntil);
260
+ await this.record({ type: "goal.snoozed", goal: next, snoozedUntil });
261
+ return next;
262
+ }
263
+ async closeGoal(scopeId) {
264
+ const current = await this.requireGoal(scopeId);
265
+ if (current.status === "complete")
266
+ return current;
267
+ return this.setStatus(scopeId, "complete");
268
+ }
150
269
  async setStatus(scopeId, status) {
151
270
  const current = await this.requireGoal(scopeId);
152
271
  const transitionAllowed = (status === "active" && (current.status === "paused" || current.status === "blocked")) ||
@@ -158,6 +277,7 @@ export class GoalRuntime {
158
277
  ...current,
159
278
  status,
160
279
  blockedReason: undefined,
280
+ snoozedUntil: undefined,
161
281
  version: current.version + 1,
162
282
  updatedAt: this.now().toISOString(),
163
283
  };
@@ -234,9 +354,25 @@ export class GoalRuntime {
234
354
  try {
235
355
  if (await this.storage.getPendingEvaluation(scopeId))
236
356
  await this.processPendingEvaluation(scopeId);
237
- const goal = await this.storage.get(scopeId);
357
+ let goal = await this.storage.get(scopeId);
238
358
  if (!goal || goal.status !== "active")
239
359
  return;
360
+ if (goal.snoozedUntil) {
361
+ if (Date.parse(goal.snoozedUntil) > this.now().getTime()) {
362
+ this.scheduleRecovery(scopeId, goal.snoozedUntil);
363
+ return;
364
+ }
365
+ const awakened = {
366
+ ...goal,
367
+ snoozedUntil: undefined,
368
+ version: goal.version + 1,
369
+ updatedAt: this.now().toISOString(),
370
+ };
371
+ if (!(await this.storage.updateBudget(awakened, goal.version)))
372
+ return;
373
+ goal = awakened;
374
+ await this.record({ type: "goal.snooze_expired", goal });
375
+ }
240
376
  if (this.budgetExhausted(goal)) {
241
377
  await this.markBudgetLimited(goal);
242
378
  return;
@@ -489,6 +625,10 @@ export class GoalRuntime {
489
625
  }
490
626
  }
491
627
  async continueWithClaim(goal, reason) {
628
+ if (goal.snoozedUntil && Date.parse(goal.snoozedUntil) > this.now().getTime()) {
629
+ this.scheduleRecovery(goal.scopeId, goal.snoozedUntil);
630
+ return;
631
+ }
492
632
  const existingClaim = await this.storage.getContinuationClaim(goal.scopeId);
493
633
  if (existingClaim) {
494
634
  if (existingClaim.goalId === goal.id && existingClaim.goalVersion === goal.version)
@@ -647,7 +787,8 @@ export class GoalRuntime {
647
787
  await this.record({ type: "goal.status_changed", goal: next, previousStatus: current.status });
648
788
  }
649
789
  budgetExhausted(goal) {
650
- return (goal.usage.iterations >= goal.budget.maxIterations ||
790
+ return ((goal.budget.maxIterations !== undefined &&
791
+ goal.usage.iterations >= goal.budget.maxIterations) ||
651
792
  (goal.budget.maxTokens !== undefined && goal.usage.tokens >= goal.budget.maxTokens) ||
652
793
  (goal.budget.maxRuntimeMs !== undefined &&
653
794
  this.now().getTime() - Date.parse(goal.createdAt) >= goal.budget.maxRuntimeMs));
@@ -1,4 +1,4 @@
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 SqliteGoalStorage implements GoalStorage {
3
3
  private readonly db;
4
4
  constructor(path: string);
@@ -6,6 +6,8 @@ export declare class SqliteGoalStorage implements GoalStorage {
6
6
  create(goal: AgentGoal): Promise<void>;
7
7
  replace(goal: AgentGoal, expectedVersion: number): Promise<boolean>;
8
8
  updateBudget(goal: AgentGoal, expectedVersion: number): Promise<boolean>;
9
+ addCheckpoint(checkpoint: GoalCheckpoint): Promise<boolean>;
10
+ listCheckpoints(scopeId: string): Promise<GoalCheckpoint[]>;
9
11
  delete(scopeId: string, expectedVersion: number): Promise<boolean>;
10
12
  getPendingEvaluation(scopeId: string): Promise<GoalPendingEvaluation | undefined>;
11
13
  appendPendingEvaluation(pending: GoalPendingEvaluation): Promise<boolean>;
@@ -9,9 +9,11 @@ export class SqliteGoalStorage {
9
9
  const goalTableSql = `CREATE TABLE IF NOT EXISTS agent_goals (
10
10
  scope_id TEXT PRIMARY KEY NOT NULL,
11
11
  id TEXT UNIQUE NOT NULL,
12
+ name TEXT,
12
13
  objective TEXT NOT NULL,
13
14
  status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'blocked', 'budget_limited', 'complete')),
14
15
  blocked_reason TEXT,
16
+ snoozed_until TEXT,
15
17
  max_iterations INTEGER NOT NULL DEFAULT 25,
16
18
  max_tokens INTEGER,
17
19
  max_runtime_ms INTEGER,
@@ -29,6 +31,8 @@ export class SqliteGoalStorage {
29
31
  this.db.exec(`PRAGMA journal_mode = WAL; ${goalTableSql};`);
30
32
  const columns = new Set(this.db.prepare("PRAGMA table_info(agent_goals)").all().map(({ name }) => name));
31
33
  for (const [name, definition] of [
34
+ ["name", "TEXT"],
35
+ ["snoozed_until", "TEXT"],
32
36
  ["max_iterations", "INTEGER NOT NULL DEFAULT 25"],
33
37
  ["max_tokens", "INTEGER"],
34
38
  ["max_runtime_ms", "INTEGER"],
@@ -71,6 +75,19 @@ export class SqliteGoalStorage {
71
75
  }
72
76
  this.db.exec(`
73
77
  PRAGMA foreign_keys = ON;
78
+ CREATE TABLE IF NOT EXISTS agent_goal_checkpoints (
79
+ id TEXT PRIMARY KEY NOT NULL,
80
+ scope_id TEXT NOT NULL,
81
+ goal_id TEXT NOT NULL,
82
+ summary TEXT NOT NULL,
83
+ evidence TEXT,
84
+ next_step TEXT,
85
+ blocker TEXT,
86
+ created_at TEXT NOT NULL,
87
+ FOREIGN KEY (scope_id) REFERENCES agent_goals(scope_id) ON DELETE CASCADE
88
+ );
89
+ CREATE INDEX IF NOT EXISTS agent_goal_checkpoints_scope_created
90
+ ON agent_goal_checkpoints(scope_id, created_at DESC);
74
91
  CREATE TABLE IF NOT EXISTS agent_goal_terminal_candidates (
75
92
  scope_id TEXT PRIMARY KEY NOT NULL,
76
93
  goal_id TEXT NOT NULL,
@@ -132,11 +149,13 @@ export class SqliteGoalStorage {
132
149
  return {
133
150
  id: row.id,
134
151
  scopeId: row.scope_id,
152
+ name: row.name ?? undefined,
135
153
  objective: row.objective,
136
154
  status: row.status,
137
155
  blockedReason: row.blocked_reason ?? undefined,
156
+ snoozedUntil: row.snoozed_until ?? undefined,
138
157
  budget: {
139
- maxIterations: row.max_iterations,
158
+ maxIterations: row.max_iterations > 0 ? row.max_iterations : undefined,
140
159
  maxTokens: row.max_tokens ?? undefined,
141
160
  maxRuntimeMs: row.max_runtime_ms ?? undefined,
142
161
  },
@@ -161,35 +180,35 @@ export class SqliteGoalStorage {
161
180
  async create(goal) {
162
181
  this.db
163
182
  .prepare(`INSERT INTO agent_goals
164
- (scope_id, id, objective, status, blocked_reason, max_iterations, max_tokens,
165
- max_runtime_ms, iterations_used, tokens_used, last_settled_at,
166
- last_evaluation_id, last_evaluation_outcome, last_evaluation_reason, last_evaluation_at,
167
- version, created_at, updated_at)
168
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
169
- .run(goal.scopeId, goal.id, goal.objective, goal.status, goal.blockedReason ?? null, goal.budget.maxIterations, goal.budget.maxTokens ?? null, goal.budget.maxRuntimeMs ?? null, goal.usage.iterations, goal.usage.tokens, goal.lastSettledAt ?? null, goal.lastEvaluation?.id ?? null, goal.lastEvaluation?.outcome ?? null, goal.lastEvaluation?.reason ?? null, goal.lastEvaluation?.at ?? null, goal.version, goal.createdAt, goal.updatedAt);
183
+ (scope_id, id, name, objective, status, blocked_reason, snoozed_until,
184
+ max_iterations, max_tokens, max_runtime_ms, iterations_used, tokens_used,
185
+ last_settled_at, last_evaluation_id, last_evaluation_outcome,
186
+ last_evaluation_reason, last_evaluation_at, version, created_at, updated_at)
187
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
188
+ .run(goal.scopeId, goal.id, goal.name ?? null, goal.objective, goal.status, goal.blockedReason ?? null, goal.snoozedUntil ?? null, goal.budget.maxIterations ?? 0, goal.budget.maxTokens ?? null, goal.budget.maxRuntimeMs ?? null, goal.usage.iterations, goal.usage.tokens, goal.lastSettledAt ?? null, goal.lastEvaluation?.id ?? null, goal.lastEvaluation?.outcome ?? null, goal.lastEvaluation?.reason ?? null, goal.lastEvaluation?.at ?? null, goal.version, goal.createdAt, goal.updatedAt);
170
189
  }
171
190
  async replace(goal, expectedVersion) {
172
191
  const result = this.db
173
- .prepare(`UPDATE agent_goals SET objective = ?, status = ?, blocked_reason = ?,
174
- max_iterations = ?, max_tokens = ?, max_runtime_ms = ?, iterations_used = ?,
175
- tokens_used = ?, last_settled_at = ?, last_evaluation_id = ?,
192
+ .prepare(`UPDATE agent_goals SET name = ?, objective = ?, status = ?, blocked_reason = ?,
193
+ snoozed_until = ?, max_iterations = ?, max_tokens = ?, max_runtime_ms = ?,
194
+ iterations_used = ?, tokens_used = ?, last_settled_at = ?, last_evaluation_id = ?,
176
195
  last_evaluation_outcome = ?, last_evaluation_reason = ?, last_evaluation_at = ?,
177
196
  version = ?, updated_at = ?
178
197
  WHERE scope_id = ? AND id = ? AND version = ?`)
179
- .run(goal.objective, goal.status, goal.blockedReason ?? null, goal.budget.maxIterations, goal.budget.maxTokens ?? null, goal.budget.maxRuntimeMs ?? null, goal.usage.iterations, goal.usage.tokens, goal.lastSettledAt ?? null, goal.lastEvaluation?.id ?? null, goal.lastEvaluation?.outcome ?? null, goal.lastEvaluation?.reason ?? null, goal.lastEvaluation?.at ?? null, goal.version, goal.updatedAt, goal.scopeId, goal.id, expectedVersion);
198
+ .run(goal.name ?? null, goal.objective, goal.status, goal.blockedReason ?? null, goal.snoozedUntil ?? null, goal.budget.maxIterations ?? 0, goal.budget.maxTokens ?? null, goal.budget.maxRuntimeMs ?? null, goal.usage.iterations, goal.usage.tokens, goal.lastSettledAt ?? null, goal.lastEvaluation?.id ?? null, goal.lastEvaluation?.outcome ?? null, goal.lastEvaluation?.reason ?? null, goal.lastEvaluation?.at ?? null, goal.version, goal.updatedAt, goal.scopeId, goal.id, expectedVersion);
180
199
  return result.changes === 1;
181
200
  }
182
201
  async updateBudget(goal, expectedVersion) {
183
202
  this.db.exec("BEGIN IMMEDIATE");
184
203
  try {
185
204
  const result = this.db
186
- .prepare(`UPDATE agent_goals SET objective = ?, status = ?, blocked_reason = ?,
187
- max_iterations = ?, max_tokens = ?, max_runtime_ms = ?, iterations_used = ?,
188
- tokens_used = ?, last_settled_at = ?, last_evaluation_id = ?,
205
+ .prepare(`UPDATE agent_goals SET name = ?, objective = ?, status = ?, blocked_reason = ?,
206
+ snoozed_until = ?, max_iterations = ?, max_tokens = ?, max_runtime_ms = ?,
207
+ iterations_used = ?, tokens_used = ?, last_settled_at = ?, last_evaluation_id = ?,
189
208
  last_evaluation_outcome = ?, last_evaluation_reason = ?, last_evaluation_at = ?,
190
209
  version = ?, updated_at = ?
191
210
  WHERE scope_id = ? AND id = ? AND version = ?`)
192
- .run(goal.objective, goal.status, goal.blockedReason ?? null, goal.budget.maxIterations, goal.budget.maxTokens ?? null, goal.budget.maxRuntimeMs ?? null, goal.usage.iterations, goal.usage.tokens, goal.lastSettledAt ?? null, goal.lastEvaluation?.id ?? null, goal.lastEvaluation?.outcome ?? null, goal.lastEvaluation?.reason ?? null, goal.lastEvaluation?.at ?? null, goal.version, goal.updatedAt, goal.scopeId, goal.id, expectedVersion);
211
+ .run(goal.name ?? null, goal.objective, goal.status, goal.blockedReason ?? null, goal.snoozedUntil ?? null, goal.budget.maxIterations ?? 0, goal.budget.maxTokens ?? null, goal.budget.maxRuntimeMs ?? null, goal.usage.iterations, goal.usage.tokens, goal.lastSettledAt ?? null, goal.lastEvaluation?.id ?? null, goal.lastEvaluation?.outcome ?? null, goal.lastEvaluation?.reason ?? null, goal.lastEvaluation?.at ?? null, goal.version, goal.updatedAt, goal.scopeId, goal.id, expectedVersion);
193
212
  if (result.changes !== 1) {
194
213
  this.db.exec("ROLLBACK");
195
214
  return false;
@@ -214,6 +233,40 @@ export class SqliteGoalStorage {
214
233
  throw error;
215
234
  }
216
235
  }
236
+ async addCheckpoint(checkpoint) {
237
+ const result = this.db
238
+ .prepare(`INSERT INTO agent_goal_checkpoints
239
+ (id, scope_id, goal_id, summary, evidence, next_step, blocker, created_at)
240
+ SELECT ?, ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (
241
+ SELECT 1 FROM agent_goals WHERE scope_id = ? AND id = ? AND status != 'complete'
242
+ )`)
243
+ .run(checkpoint.id, checkpoint.scopeId, checkpoint.goalId, checkpoint.summary, checkpoint.evidence ?? null, checkpoint.nextStep ?? null, checkpoint.blocker ?? null, checkpoint.createdAt, checkpoint.scopeId, checkpoint.goalId);
244
+ return result.changes === 1;
245
+ }
246
+ async listCheckpoints(scopeId) {
247
+ const rows = this.db
248
+ .prepare("SELECT * FROM agent_goal_checkpoints WHERE scope_id = ? ORDER BY created_at DESC, id DESC")
249
+ .all(scopeId);
250
+ return rows.map((row) => {
251
+ if (typeof row.id !== "string" ||
252
+ typeof row.scope_id !== "string" ||
253
+ typeof row.goal_id !== "string" ||
254
+ typeof row.summary !== "string" ||
255
+ typeof row.created_at !== "string") {
256
+ throw new Error("Stored goal checkpoint is malformed");
257
+ }
258
+ return {
259
+ id: row.id,
260
+ scopeId: row.scope_id,
261
+ goalId: row.goal_id,
262
+ summary: row.summary,
263
+ evidence: typeof row.evidence === "string" ? row.evidence : undefined,
264
+ nextStep: typeof row.next_step === "string" ? row.next_step : undefined,
265
+ blocker: typeof row.blocker === "string" ? row.blocker : undefined,
266
+ createdAt: row.created_at,
267
+ };
268
+ });
269
+ }
217
270
  async delete(scopeId, expectedVersion) {
218
271
  const result = this.db
219
272
  .prepare("DELETE FROM agent_goals WHERE scope_id = ? AND version = ?")
@@ -334,8 +387,7 @@ export class SqliteGoalStorage {
334
387
  this.db.exec("BEGIN IMMEDIATE");
335
388
  try {
336
389
  const updated = this.db
337
- .prepare(`UPDATE agent_goals SET objective = ?, status = ?, blocked_reason = ?,
338
- max_iterations = ?, max_tokens = ?, max_runtime_ms = ?, iterations_used = ?,
390
+ .prepare(`UPDATE agent_goals SET status = ?, blocked_reason = ?, iterations_used = ?,
339
391
  tokens_used = ?, last_settled_at = ?, last_evaluation_id = ?,
340
392
  last_evaluation_outcome = ?, last_evaluation_reason = ?, last_evaluation_at = ?,
341
393
  version = ?, updated_at = ?
@@ -343,7 +395,7 @@ export class SqliteGoalStorage {
343
395
  SELECT 1 FROM agent_goal_pending_evaluations
344
396
  WHERE scope_id = ? AND evaluation_id = ?
345
397
  )`)
346
- .run(goal.objective, goal.status, goal.blockedReason ?? null, goal.budget.maxIterations, goal.budget.maxTokens ?? null, goal.budget.maxRuntimeMs ?? null, goal.usage.iterations, goal.usage.tokens, goal.lastSettledAt ?? null, goal.lastEvaluation?.id ?? null, goal.lastEvaluation?.outcome ?? null, goal.lastEvaluation?.reason ?? null, goal.lastEvaluation?.at ?? null, goal.version, goal.updatedAt, goal.scopeId, goal.id, expectedGoalVersion, goal.scopeId, expectedEvaluationId);
398
+ .run(goal.status, goal.blockedReason ?? null, goal.usage.iterations, goal.usage.tokens, goal.lastSettledAt ?? null, goal.lastEvaluation?.id ?? null, goal.lastEvaluation?.outcome ?? null, goal.lastEvaluation?.reason ?? null, goal.lastEvaluation?.at ?? null, goal.version, goal.updatedAt, goal.scopeId, goal.id, expectedGoalVersion, goal.scopeId, expectedEvaluationId);
347
399
  if (updated.changes !== 1) {
348
400
  this.db.exec("ROLLBACK");
349
401
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinet/agent-goal",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "type": "module",
5
5
  "description": "Standalone single-agent durable goal loop for Pi",
6
6
  "author": "Will Porcellini <5994936+gugu91@users.noreply.github.com>",