@pinet/agent-goal 0.2.6

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.
@@ -0,0 +1,23 @@
1
+ import type { AgentGoal, GoalContinuationClaim, GoalPendingEvaluation, GoalStorage, GoalTerminalCandidateRecord } from "./domain.js";
2
+ export declare class SqliteGoalStorage implements GoalStorage {
3
+ private readonly db;
4
+ constructor(path: string);
5
+ get(scopeId: string): Promise<AgentGoal | undefined>;
6
+ create(goal: AgentGoal): Promise<void>;
7
+ replace(goal: AgentGoal, expectedVersion: number): Promise<boolean>;
8
+ delete(scopeId: string, expectedVersion: number): Promise<boolean>;
9
+ getPendingEvaluation(scopeId: string): Promise<GoalPendingEvaluation | undefined>;
10
+ appendPendingEvaluation(pending: GoalPendingEvaluation): Promise<boolean>;
11
+ putPendingEvaluation(pending: GoalPendingEvaluation): Promise<boolean>;
12
+ replacePendingEvaluation(pending: GoalPendingEvaluation, expectedEvaluationId: string): Promise<boolean>;
13
+ deletePendingEvaluation(scopeId: string, expectedEvaluationId: string): Promise<boolean>;
14
+ commitEvaluation(goal: AgentGoal, expectedGoalVersion: number, expectedEvaluationId: string): Promise<boolean>;
15
+ getTerminalCandidate(scopeId: string): Promise<GoalTerminalCandidateRecord | undefined>;
16
+ putTerminalCandidate(candidate: GoalTerminalCandidateRecord): Promise<boolean>;
17
+ deleteTerminalCandidate(scopeId: string, expectedCandidateId: string): Promise<boolean>;
18
+ getContinuationClaim(scopeId: string): Promise<GoalContinuationClaim | undefined>;
19
+ createContinuationClaim(claim: GoalContinuationClaim): Promise<boolean>;
20
+ replaceContinuationClaim(claim: GoalContinuationClaim, expectedClaimId: string): Promise<boolean>;
21
+ deleteContinuationClaim(scopeId: string, expectedClaimId: string): Promise<boolean>;
22
+ close(): void;
23
+ }
@@ -0,0 +1,420 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ export class SqliteGoalStorage {
5
+ db;
6
+ constructor(path) {
7
+ mkdirSync(dirname(path), { recursive: true });
8
+ this.db = new DatabaseSync(path, { timeout: 5000 });
9
+ const goalTableSql = `CREATE TABLE IF NOT EXISTS agent_goals (
10
+ scope_id TEXT PRIMARY KEY NOT NULL,
11
+ id TEXT UNIQUE NOT NULL,
12
+ objective TEXT NOT NULL,
13
+ status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'blocked', 'budget_limited', 'complete')),
14
+ blocked_reason TEXT,
15
+ max_iterations INTEGER NOT NULL DEFAULT 25,
16
+ max_tokens INTEGER,
17
+ max_runtime_ms INTEGER,
18
+ iterations_used INTEGER NOT NULL DEFAULT 0,
19
+ tokens_used INTEGER NOT NULL DEFAULT 0,
20
+ last_settled_at TEXT,
21
+ last_evaluation_id TEXT,
22
+ last_evaluation_outcome TEXT,
23
+ last_evaluation_reason TEXT,
24
+ last_evaluation_at TEXT,
25
+ version INTEGER NOT NULL,
26
+ created_at TEXT NOT NULL,
27
+ updated_at TEXT NOT NULL
28
+ )`;
29
+ this.db.exec(`PRAGMA journal_mode = WAL; ${goalTableSql};`);
30
+ const columns = new Set(this.db.prepare("PRAGMA table_info(agent_goals)").all().map(({ name }) => name));
31
+ for (const [name, definition] of [
32
+ ["max_iterations", "INTEGER NOT NULL DEFAULT 25"],
33
+ ["max_tokens", "INTEGER"],
34
+ ["max_runtime_ms", "INTEGER"],
35
+ ["iterations_used", "INTEGER NOT NULL DEFAULT 0"],
36
+ ["tokens_used", "INTEGER NOT NULL DEFAULT 0"],
37
+ ["last_settled_at", "TEXT"],
38
+ ["last_evaluation_id", "TEXT"],
39
+ ["last_evaluation_outcome", "TEXT"],
40
+ ["last_evaluation_reason", "TEXT"],
41
+ ["last_evaluation_at", "TEXT"],
42
+ ]) {
43
+ if (!columns.has(name))
44
+ this.db.exec(`ALTER TABLE agent_goals ADD COLUMN ${name} ${definition}`);
45
+ }
46
+ const tableDefinition = this.db
47
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'agent_goals'")
48
+ .get();
49
+ if (!tableDefinition.sql.includes("budget_limited")) {
50
+ this.db.exec(`
51
+ PRAGMA foreign_keys = OFF;
52
+ BEGIN IMMEDIATE;
53
+ DROP TABLE IF EXISTS agent_goal_pending_evaluations;
54
+ DROP TABLE IF EXISTS agent_goal_continuations;
55
+ ALTER TABLE agent_goals RENAME TO agent_goals_legacy;
56
+ ${goalTableSql};
57
+ INSERT INTO agent_goals
58
+ (scope_id, id, objective, status, blocked_reason, max_iterations, max_tokens,
59
+ max_runtime_ms, iterations_used, tokens_used, last_settled_at,
60
+ last_evaluation_id, last_evaluation_outcome, last_evaluation_reason, last_evaluation_at,
61
+ version, created_at, updated_at)
62
+ SELECT scope_id, id, objective, status, blocked_reason, max_iterations, max_tokens,
63
+ max_runtime_ms, iterations_used, tokens_used, last_settled_at,
64
+ last_evaluation_id, last_evaluation_outcome, last_evaluation_reason, last_evaluation_at,
65
+ version, created_at, updated_at
66
+ FROM agent_goals_legacy;
67
+ DROP TABLE agent_goals_legacy;
68
+ COMMIT;
69
+ PRAGMA foreign_keys = ON;
70
+ `);
71
+ }
72
+ this.db.exec(`
73
+ PRAGMA foreign_keys = ON;
74
+ CREATE TABLE IF NOT EXISTS agent_goal_terminal_candidates (
75
+ scope_id TEXT PRIMARY KEY NOT NULL,
76
+ goal_id TEXT NOT NULL,
77
+ goal_version INTEGER NOT NULL,
78
+ candidate_id TEXT UNIQUE NOT NULL,
79
+ outcome TEXT NOT NULL CHECK (outcome IN ('complete', 'blocked')),
80
+ reason TEXT NOT NULL,
81
+ created_at TEXT NOT NULL,
82
+ FOREIGN KEY (scope_id) REFERENCES agent_goals(scope_id) ON DELETE CASCADE
83
+ );
84
+ CREATE TABLE IF NOT EXISTS agent_goal_pending_evaluations (
85
+ scope_id TEXT PRIMARY KEY NOT NULL,
86
+ goal_id TEXT NOT NULL,
87
+ goal_version INTEGER NOT NULL,
88
+ evaluation_id TEXT UNIQUE NOT NULL,
89
+ iterations_delta INTEGER NOT NULL DEFAULT 1,
90
+ latest_output TEXT NOT NULL,
91
+ token_delta INTEGER NOT NULL,
92
+ candidate_outcome TEXT CHECK (candidate_outcome IN ('complete', 'blocked')),
93
+ candidate_reason TEXT,
94
+ attempt INTEGER NOT NULL,
95
+ available_at TEXT NOT NULL,
96
+ last_error TEXT,
97
+ created_at TEXT NOT NULL,
98
+ updated_at TEXT NOT NULL,
99
+ FOREIGN KEY (scope_id) REFERENCES agent_goals(scope_id) ON DELETE CASCADE
100
+ );
101
+ CREATE TABLE IF NOT EXISTS agent_goal_continuations (
102
+ scope_id TEXT PRIMARY KEY NOT NULL,
103
+ goal_id TEXT NOT NULL,
104
+ goal_version INTEGER NOT NULL,
105
+ claim_id TEXT UNIQUE NOT NULL,
106
+ state TEXT NOT NULL CHECK (state IN ('claimed', 'deferred', 'started')),
107
+ reason TEXT NOT NULL,
108
+ attempt INTEGER NOT NULL,
109
+ available_at TEXT NOT NULL,
110
+ expires_at TEXT NOT NULL,
111
+ last_error TEXT,
112
+ created_at TEXT NOT NULL,
113
+ updated_at TEXT NOT NULL,
114
+ FOREIGN KEY (scope_id) REFERENCES agent_goals(scope_id) ON DELETE CASCADE
115
+ );
116
+ `);
117
+ const pendingColumns = new Set(this.db.prepare("PRAGMA table_info(agent_goal_pending_evaluations)").all().map(({ name }) => name));
118
+ if (!pendingColumns.has("iterations_delta")) {
119
+ this.db.exec("ALTER TABLE agent_goal_pending_evaluations ADD COLUMN iterations_delta INTEGER NOT NULL DEFAULT 1");
120
+ }
121
+ if (!pendingColumns.has("candidate_outcome")) {
122
+ this.db.exec("ALTER TABLE agent_goal_pending_evaluations ADD COLUMN candidate_outcome TEXT CHECK (candidate_outcome IN ('complete', 'blocked'))");
123
+ }
124
+ if (!pendingColumns.has("candidate_reason")) {
125
+ this.db.exec("ALTER TABLE agent_goal_pending_evaluations ADD COLUMN candidate_reason TEXT");
126
+ }
127
+ }
128
+ async get(scopeId) {
129
+ const row = this.db.prepare("SELECT * FROM agent_goals WHERE scope_id = ?").get(scopeId);
130
+ if (!row)
131
+ return undefined;
132
+ return {
133
+ id: row.id,
134
+ scopeId: row.scope_id,
135
+ objective: row.objective,
136
+ status: row.status,
137
+ blockedReason: row.blocked_reason ?? undefined,
138
+ budget: {
139
+ maxIterations: row.max_iterations,
140
+ maxTokens: row.max_tokens ?? undefined,
141
+ maxRuntimeMs: row.max_runtime_ms ?? undefined,
142
+ },
143
+ usage: { iterations: row.iterations_used, tokens: row.tokens_used },
144
+ lastSettledAt: row.last_settled_at ?? undefined,
145
+ lastEvaluation: row.last_evaluation_id &&
146
+ row.last_evaluation_outcome &&
147
+ row.last_evaluation_reason &&
148
+ row.last_evaluation_at
149
+ ? {
150
+ id: row.last_evaluation_id,
151
+ outcome: row.last_evaluation_outcome,
152
+ reason: row.last_evaluation_reason,
153
+ at: row.last_evaluation_at,
154
+ }
155
+ : undefined,
156
+ version: row.version,
157
+ createdAt: row.created_at,
158
+ updatedAt: row.updated_at,
159
+ };
160
+ }
161
+ async create(goal) {
162
+ this.db
163
+ .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);
170
+ }
171
+ async replace(goal, expectedVersion) {
172
+ 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 = ?,
176
+ last_evaluation_outcome = ?, last_evaluation_reason = ?, last_evaluation_at = ?,
177
+ version = ?, updated_at = ?
178
+ 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);
180
+ return result.changes === 1;
181
+ }
182
+ async delete(scopeId, expectedVersion) {
183
+ const result = this.db
184
+ .prepare("DELETE FROM agent_goals WHERE scope_id = ? AND version = ?")
185
+ .run(scopeId, expectedVersion);
186
+ return result.changes === 1;
187
+ }
188
+ async getPendingEvaluation(scopeId) {
189
+ const row = this.db
190
+ .prepare("SELECT * FROM agent_goal_pending_evaluations WHERE scope_id = ?")
191
+ .get(scopeId);
192
+ return row
193
+ ? {
194
+ scopeId: row.scope_id,
195
+ goalId: row.goal_id,
196
+ goalVersion: row.goal_version,
197
+ evaluationId: row.evaluation_id,
198
+ iterationsDelta: row.iterations_delta,
199
+ progress: {
200
+ latestOutput: row.latest_output,
201
+ tokenDelta: row.token_delta,
202
+ terminalCandidate: row.candidate_outcome && row.candidate_reason
203
+ ? { outcome: row.candidate_outcome, reason: row.candidate_reason }
204
+ : undefined,
205
+ },
206
+ attempt: row.attempt,
207
+ availableAt: row.available_at,
208
+ lastError: row.last_error ?? undefined,
209
+ createdAt: row.created_at,
210
+ updatedAt: row.updated_at,
211
+ }
212
+ : undefined;
213
+ }
214
+ async appendPendingEvaluation(pending) {
215
+ const result = this.db
216
+ .prepare(`INSERT INTO agent_goal_pending_evaluations
217
+ (scope_id, goal_id, goal_version, evaluation_id, iterations_delta, latest_output,
218
+ token_delta, candidate_outcome, candidate_reason, attempt, available_at, last_error,
219
+ created_at, updated_at)
220
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
221
+ WHERE EXISTS (
222
+ SELECT 1 FROM agent_goals WHERE scope_id = ? AND id = ? AND version = ?
223
+ )
224
+ ON CONFLICT(scope_id) DO UPDATE SET goal_id = excluded.goal_id,
225
+ goal_version = excluded.goal_version, evaluation_id = excluded.evaluation_id,
226
+ iterations_delta = CASE
227
+ WHEN agent_goal_pending_evaluations.goal_id = excluded.goal_id
228
+ AND agent_goal_pending_evaluations.goal_version = excluded.goal_version
229
+ THEN agent_goal_pending_evaluations.iterations_delta + excluded.iterations_delta
230
+ ELSE excluded.iterations_delta END,
231
+ latest_output = excluded.latest_output,
232
+ token_delta = CASE
233
+ WHEN agent_goal_pending_evaluations.goal_id = excluded.goal_id
234
+ AND agent_goal_pending_evaluations.goal_version = excluded.goal_version
235
+ THEN agent_goal_pending_evaluations.token_delta + excluded.token_delta
236
+ ELSE excluded.token_delta END,
237
+ candidate_outcome = CASE
238
+ WHEN agent_goal_pending_evaluations.goal_id = excluded.goal_id
239
+ AND agent_goal_pending_evaluations.goal_version = excluded.goal_version
240
+ THEN COALESCE(excluded.candidate_outcome,
241
+ agent_goal_pending_evaluations.candidate_outcome)
242
+ ELSE excluded.candidate_outcome END,
243
+ candidate_reason = CASE
244
+ WHEN agent_goal_pending_evaluations.goal_id = excluded.goal_id
245
+ AND agent_goal_pending_evaluations.goal_version = excluded.goal_version
246
+ THEN COALESCE(excluded.candidate_reason,
247
+ agent_goal_pending_evaluations.candidate_reason)
248
+ ELSE excluded.candidate_reason END,
249
+ attempt = excluded.attempt, available_at = excluded.available_at,
250
+ last_error = excluded.last_error,
251
+ created_at = CASE
252
+ WHEN agent_goal_pending_evaluations.goal_id = excluded.goal_id
253
+ AND agent_goal_pending_evaluations.goal_version = excluded.goal_version
254
+ THEN agent_goal_pending_evaluations.created_at ELSE excluded.created_at END,
255
+ updated_at = excluded.updated_at`)
256
+ .run(pending.scopeId, pending.goalId, pending.goalVersion, pending.evaluationId, pending.iterationsDelta, pending.progress.latestOutput, pending.progress.tokenDelta ?? 0, pending.progress.terminalCandidate?.outcome ?? null, pending.progress.terminalCandidate?.reason ?? null, pending.attempt, pending.availableAt, pending.lastError ?? null, pending.createdAt, pending.updatedAt, pending.scopeId, pending.goalId, pending.goalVersion);
257
+ return result.changes === 1;
258
+ }
259
+ async putPendingEvaluation(pending) {
260
+ const result = this.db
261
+ .prepare(`INSERT INTO agent_goal_pending_evaluations
262
+ (scope_id, goal_id, goal_version, evaluation_id, iterations_delta, latest_output,
263
+ token_delta, candidate_outcome, candidate_reason, attempt, available_at, last_error,
264
+ created_at, updated_at)
265
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
266
+ WHERE EXISTS (
267
+ SELECT 1 FROM agent_goals WHERE scope_id = ? AND id = ? AND version = ?
268
+ )
269
+ ON CONFLICT(scope_id) DO UPDATE SET goal_id = excluded.goal_id,
270
+ goal_version = excluded.goal_version, evaluation_id = excluded.evaluation_id,
271
+ iterations_delta = excluded.iterations_delta,
272
+ latest_output = excluded.latest_output, token_delta = excluded.token_delta,
273
+ candidate_outcome = excluded.candidate_outcome,
274
+ candidate_reason = excluded.candidate_reason, attempt = excluded.attempt,
275
+ available_at = excluded.available_at,
276
+ last_error = excluded.last_error, created_at = excluded.created_at,
277
+ updated_at = excluded.updated_at`)
278
+ .run(pending.scopeId, pending.goalId, pending.goalVersion, pending.evaluationId, pending.iterationsDelta, pending.progress.latestOutput, pending.progress.tokenDelta ?? 0, pending.progress.terminalCandidate?.outcome ?? null, pending.progress.terminalCandidate?.reason ?? null, pending.attempt, pending.availableAt, pending.lastError ?? null, pending.createdAt, pending.updatedAt, pending.scopeId, pending.goalId, pending.goalVersion);
279
+ return result.changes === 1;
280
+ }
281
+ async replacePendingEvaluation(pending, expectedEvaluationId) {
282
+ const result = this.db
283
+ .prepare(`UPDATE agent_goal_pending_evaluations SET goal_id = ?, goal_version = ?,
284
+ evaluation_id = ?, iterations_delta = ?, latest_output = ?, token_delta = ?,
285
+ candidate_outcome = ?, candidate_reason = ?, attempt = ?, available_at = ?,
286
+ last_error = ?, created_at = ?, updated_at = ?
287
+ WHERE scope_id = ? AND evaluation_id = ? AND EXISTS (
288
+ SELECT 1 FROM agent_goals WHERE scope_id = ? AND id = ? AND version = ?
289
+ )`)
290
+ .run(pending.goalId, pending.goalVersion, pending.evaluationId, pending.iterationsDelta, pending.progress.latestOutput, pending.progress.tokenDelta ?? 0, pending.progress.terminalCandidate?.outcome ?? null, pending.progress.terminalCandidate?.reason ?? null, pending.attempt, pending.availableAt, pending.lastError ?? null, pending.createdAt, pending.updatedAt, pending.scopeId, expectedEvaluationId, pending.scopeId, pending.goalId, pending.goalVersion);
291
+ return result.changes === 1;
292
+ }
293
+ async deletePendingEvaluation(scopeId, expectedEvaluationId) {
294
+ const result = this.db
295
+ .prepare("DELETE FROM agent_goal_pending_evaluations WHERE scope_id = ? AND evaluation_id = ?")
296
+ .run(scopeId, expectedEvaluationId);
297
+ return result.changes === 1;
298
+ }
299
+ async commitEvaluation(goal, expectedGoalVersion, expectedEvaluationId) {
300
+ this.db.exec("BEGIN IMMEDIATE");
301
+ try {
302
+ const updated = this.db
303
+ .prepare(`UPDATE agent_goals SET objective = ?, status = ?, blocked_reason = ?,
304
+ max_iterations = ?, max_tokens = ?, max_runtime_ms = ?, iterations_used = ?,
305
+ tokens_used = ?, last_settled_at = ?, last_evaluation_id = ?,
306
+ last_evaluation_outcome = ?, last_evaluation_reason = ?, last_evaluation_at = ?,
307
+ version = ?, updated_at = ?
308
+ WHERE scope_id = ? AND id = ? AND version = ? AND EXISTS (
309
+ SELECT 1 FROM agent_goal_pending_evaluations
310
+ WHERE scope_id = ? AND evaluation_id = ?
311
+ )`)
312
+ .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);
313
+ if (updated.changes !== 1) {
314
+ this.db.exec("ROLLBACK");
315
+ return false;
316
+ }
317
+ const deleted = this.db
318
+ .prepare("DELETE FROM agent_goal_pending_evaluations WHERE scope_id = ? AND evaluation_id = ?")
319
+ .run(goal.scopeId, expectedEvaluationId);
320
+ if (deleted.changes !== 1) {
321
+ this.db.exec("ROLLBACK");
322
+ return false;
323
+ }
324
+ this.db.exec("COMMIT");
325
+ return true;
326
+ }
327
+ catch (error) {
328
+ this.db.exec("ROLLBACK");
329
+ throw error;
330
+ }
331
+ }
332
+ async getTerminalCandidate(scopeId) {
333
+ const row = this.db
334
+ .prepare("SELECT * FROM agent_goal_terminal_candidates WHERE scope_id = ?")
335
+ .get(scopeId);
336
+ return row
337
+ ? {
338
+ scopeId: row.scope_id,
339
+ goalId: row.goal_id,
340
+ goalVersion: row.goal_version,
341
+ candidateId: row.candidate_id,
342
+ outcome: row.outcome,
343
+ reason: row.reason,
344
+ createdAt: row.created_at,
345
+ }
346
+ : undefined;
347
+ }
348
+ async putTerminalCandidate(candidate) {
349
+ const result = this.db
350
+ .prepare(`INSERT INTO agent_goal_terminal_candidates
351
+ (scope_id, goal_id, goal_version, candidate_id, outcome, reason, created_at)
352
+ SELECT ?, ?, ?, ?, ?, ?, ?
353
+ WHERE EXISTS (
354
+ SELECT 1 FROM agent_goals
355
+ WHERE scope_id = ? AND id = ? AND version = ? AND status = 'active'
356
+ )
357
+ ON CONFLICT(scope_id) DO UPDATE SET goal_id = excluded.goal_id,
358
+ goal_version = excluded.goal_version, candidate_id = excluded.candidate_id,
359
+ outcome = excluded.outcome, reason = excluded.reason, created_at = excluded.created_at`)
360
+ .run(candidate.scopeId, candidate.goalId, candidate.goalVersion, candidate.candidateId, candidate.outcome, candidate.reason, candidate.createdAt, candidate.scopeId, candidate.goalId, candidate.goalVersion);
361
+ return result.changes === 1;
362
+ }
363
+ async deleteTerminalCandidate(scopeId, expectedCandidateId) {
364
+ const result = this.db
365
+ .prepare("DELETE FROM agent_goal_terminal_candidates WHERE scope_id = ? AND candidate_id = ?")
366
+ .run(scopeId, expectedCandidateId);
367
+ return result.changes === 1;
368
+ }
369
+ async getContinuationClaim(scopeId) {
370
+ const row = this.db
371
+ .prepare("SELECT * FROM agent_goal_continuations WHERE scope_id = ?")
372
+ .get(scopeId);
373
+ return row
374
+ ? {
375
+ scopeId: row.scope_id,
376
+ goalId: row.goal_id,
377
+ goalVersion: row.goal_version,
378
+ claimId: row.claim_id,
379
+ state: row.state,
380
+ reason: row.reason,
381
+ attempt: row.attempt,
382
+ availableAt: row.available_at,
383
+ expiresAt: row.expires_at,
384
+ lastError: row.last_error ?? undefined,
385
+ createdAt: row.created_at,
386
+ updatedAt: row.updated_at,
387
+ }
388
+ : undefined;
389
+ }
390
+ async createContinuationClaim(claim) {
391
+ const result = this.db
392
+ .prepare(`INSERT OR IGNORE INTO agent_goal_continuations
393
+ (scope_id, goal_id, goal_version, claim_id, state, reason, attempt,
394
+ available_at, expires_at, last_error, created_at, updated_at)
395
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
396
+ WHERE EXISTS (
397
+ SELECT 1 FROM agent_goals
398
+ WHERE scope_id = ? AND id = ? AND version = ? AND status = 'active'
399
+ )`)
400
+ .run(claim.scopeId, claim.goalId, claim.goalVersion, claim.claimId, claim.state, claim.reason, claim.attempt, claim.availableAt, claim.expiresAt, claim.lastError ?? null, claim.createdAt, claim.updatedAt, claim.scopeId, claim.goalId, claim.goalVersion);
401
+ return result.changes === 1;
402
+ }
403
+ async replaceContinuationClaim(claim, expectedClaimId) {
404
+ const result = this.db
405
+ .prepare(`UPDATE agent_goal_continuations SET goal_id = ?, goal_version = ?, claim_id = ?,
406
+ state = ?, reason = ?, attempt = ?, available_at = ?, expires_at = ?,
407
+ last_error = ?, updated_at = ? WHERE scope_id = ? AND claim_id = ?`)
408
+ .run(claim.goalId, claim.goalVersion, claim.claimId, claim.state, claim.reason, claim.attempt, claim.availableAt, claim.expiresAt, claim.lastError ?? null, claim.updatedAt, claim.scopeId, expectedClaimId);
409
+ return result.changes === 1;
410
+ }
411
+ async deleteContinuationClaim(scopeId, expectedClaimId) {
412
+ const result = this.db
413
+ .prepare("DELETE FROM agent_goal_continuations WHERE scope_id = ? AND claim_id = ?")
414
+ .run(scopeId, expectedClaimId);
415
+ return result.changes === 1;
416
+ }
417
+ close() {
418
+ this.db.close();
419
+ }
420
+ }
@@ -0,0 +1,10 @@
1
+ import type { GoalWakeScheduler } from "./domain.js";
2
+ export declare class TimerGoalWakeScheduler implements GoalWakeScheduler {
3
+ private readonly now;
4
+ private readonly timers;
5
+ private closed;
6
+ constructor(now?: () => number);
7
+ schedule(scopeId: string, wakeAt: string, wake: () => void): void;
8
+ cancel(scopeId: string): void;
9
+ close(): void;
10
+ }
@@ -0,0 +1,34 @@
1
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
2
+ export class TimerGoalWakeScheduler {
3
+ now;
4
+ timers = new Map();
5
+ closed = false;
6
+ constructor(now = Date.now) {
7
+ this.now = now;
8
+ }
9
+ schedule(scopeId, wakeAt, wake) {
10
+ if (this.closed)
11
+ return;
12
+ this.cancel(scopeId);
13
+ const delay = Math.min(MAX_TIMER_DELAY_MS, Math.max(0, Date.parse(wakeAt) - this.now()));
14
+ const timer = setTimeout(() => {
15
+ this.timers.delete(scopeId);
16
+ wake();
17
+ }, delay);
18
+ timer.unref?.();
19
+ this.timers.set(scopeId, timer);
20
+ }
21
+ cancel(scopeId) {
22
+ const timer = this.timers.get(scopeId);
23
+ if (!timer)
24
+ return;
25
+ clearTimeout(timer);
26
+ this.timers.delete(scopeId);
27
+ }
28
+ close() {
29
+ this.closed = true;
30
+ for (const timer of this.timers.values())
31
+ clearTimeout(timer);
32
+ this.timers.clear();
33
+ }
34
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@pinet/agent-goal",
3
+ "version": "0.2.6",
4
+ "type": "module",
5
+ "description": "Standalone single-agent durable goal loop for Pi",
6
+ "author": "Will Porcellini <5994936+gugu91@users.noreply.github.com>",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/gugu91/pinet.git",
11
+ "directory": "agent-goal"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "README.md",
27
+ "LICENSE",
28
+ "dist/"
29
+ ],
30
+ "keywords": [
31
+ "pi-package",
32
+ "goal",
33
+ "agent",
34
+ "autonomous"
35
+ ],
36
+ "pi": {
37
+ "extensions": [
38
+ "./dist/index.js"
39
+ ]
40
+ },
41
+ "scripts": {
42
+ "build": "node ../scripts/build-package.mjs",
43
+ "prepack": "pnpm run build",
44
+ "lint": "oxlint .",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "vitest run --config ../vitest.config.ts"
47
+ },
48
+ "dependencies": {},
49
+ "peerDependencies": {
50
+ "@earendil-works/pi-ai": ">=0.74.0",
51
+ "@earendil-works/pi-coding-agent": ">=0.74.0"
52
+ }
53
+ }