@springbrand/agent-runtime 0.2.0-alpha.22 → 0.2.0-alpha.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.2.0-alpha.22",
3
+ "version": "0.2.0-alpha.24",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -5,6 +5,9 @@ export interface StoredRuntimeEvent {
5
5
  body: string;
6
6
  createdAt: number;
7
7
  deliveredAt: number | null;
8
+ attemptCount: number;
9
+ quarantinedAt: number | null;
10
+ lastError: string | null;
8
11
  }
9
12
 
10
13
  export interface NewRuntimeEvent {
@@ -18,6 +21,9 @@ type RuntimeEventRow = {
18
21
  body: string;
19
22
  created_at: number;
20
23
  delivered_at: number | null;
24
+ attempt_count: number;
25
+ quarantined_at: number | null;
26
+ last_error: string | null;
21
27
  };
22
28
 
23
29
  function mapRow(row: RuntimeEventRow): StoredRuntimeEvent {
@@ -26,6 +32,9 @@ function mapRow(row: RuntimeEventRow): StoredRuntimeEvent {
26
32
  body: row.body,
27
33
  createdAt: row.created_at,
28
34
  deliveredAt: row.delivered_at,
35
+ attemptCount: row.attempt_count,
36
+ quarantinedAt: row.quarantined_at,
37
+ lastError: row.last_error,
29
38
  };
30
39
  }
31
40
 
@@ -42,9 +51,11 @@ export class RuntimeEventOutboxRepository {
42
51
  }
43
52
  this.sql`
44
53
  INSERT INTO pi_turn_event_outbox (
45
- event_id, body, created_at, delivered_at
54
+ event_id, body, created_at, delivered_at,
55
+ attempt_count, quarantined_at, last_error
46
56
  ) VALUES (
47
- ${event.eventId}, ${event.body}, ${event.createdAt}, NULL
57
+ ${event.eventId}, ${event.body}, ${event.createdAt}, NULL,
58
+ 0, NULL, NULL
48
59
  )
49
60
  `;
50
61
  return true;
@@ -52,9 +63,10 @@ export class RuntimeEventOutboxRepository {
52
63
 
53
64
  listPending(): StoredRuntimeEvent[] {
54
65
  return this.sql<RuntimeEventRow>`
55
- SELECT event_id, body, created_at, delivered_at
66
+ SELECT event_id, body, created_at, delivered_at,
67
+ attempt_count, quarantined_at, last_error
56
68
  FROM pi_turn_event_outbox
57
- WHERE delivered_at IS NULL
69
+ WHERE delivered_at IS NULL AND quarantined_at IS NULL
58
70
  ORDER BY created_at, rowid
59
71
  `.map(mapRow);
60
72
  }
@@ -64,7 +76,7 @@ export class RuntimeEventOutboxRepository {
64
76
  this.sql<{ pending: number }>`
65
77
  SELECT 1 AS pending
66
78
  FROM pi_turn_event_outbox
67
- WHERE delivered_at IS NULL
79
+ WHERE delivered_at IS NULL AND quarantined_at IS NULL
68
80
  LIMIT 1
69
81
  `[0],
70
82
  );
@@ -106,14 +118,33 @@ export class RuntimeEventOutboxRepository {
106
118
  markDelivered(eventId: string, deliveredAt: number): void {
107
119
  this.sql`
108
120
  UPDATE pi_turn_event_outbox
109
- SET delivered_at = ${deliveredAt}
121
+ SET delivered_at = ${deliveredAt}, last_error = NULL
110
122
  WHERE event_id = ${eventId} AND delivered_at IS NULL
111
123
  `;
112
124
  }
113
125
 
114
- private find(eventId: string): StoredRuntimeEvent | null {
126
+ markFailed(eventId: string, error: string): number {
127
+ this.sql`
128
+ UPDATE pi_turn_event_outbox
129
+ SET attempt_count = attempt_count + 1, last_error = ${error}
130
+ WHERE event_id = ${eventId}
131
+ AND delivered_at IS NULL AND quarantined_at IS NULL
132
+ `;
133
+ return this.find(eventId)?.attemptCount ?? 0;
134
+ }
135
+
136
+ quarantine(eventId: string, quarantinedAt: number, error: string): void {
137
+ this.sql`
138
+ UPDATE pi_turn_event_outbox
139
+ SET quarantined_at = ${quarantinedAt}, last_error = ${error}
140
+ WHERE event_id = ${eventId} AND delivered_at IS NULL
141
+ `;
142
+ }
143
+
144
+ find(eventId: string): StoredRuntimeEvent | null {
115
145
  const row = this.sql<RuntimeEventRow>`
116
- SELECT event_id, body, created_at, delivered_at
146
+ SELECT event_id, body, created_at, delivered_at,
147
+ attempt_count, quarantined_at, last_error
117
148
  FROM pi_turn_event_outbox
118
149
  WHERE event_id = ${eventId}
119
150
  `[0];
package/src/db/schema.ts CHANGED
@@ -102,8 +102,25 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
102
102
  event_id TEXT PRIMARY KEY,
103
103
  body TEXT NOT NULL,
104
104
  created_at INTEGER NOT NULL,
105
- delivered_at INTEGER
105
+ delivered_at INTEGER,
106
+ attempt_count INTEGER NOT NULL DEFAULT 0,
107
+ quarantined_at INTEGER,
108
+ last_error TEXT
106
109
  )`;
110
+ const runtimeEventColumns = new Set(
111
+ sql<{ name: string }>`PRAGMA table_info(pi_turn_event_outbox)`
112
+ .map((column) => column.name),
113
+ );
114
+ if (!runtimeEventColumns.has("attempt_count")) {
115
+ sql`ALTER TABLE pi_turn_event_outbox
116
+ ADD COLUMN attempt_count INTEGER NOT NULL DEFAULT 0`;
117
+ }
118
+ if (!runtimeEventColumns.has("quarantined_at")) {
119
+ sql`ALTER TABLE pi_turn_event_outbox ADD COLUMN quarantined_at INTEGER`;
120
+ }
121
+ if (!runtimeEventColumns.has("last_error")) {
122
+ sql`ALTER TABLE pi_turn_event_outbox ADD COLUMN last_error TEXT`;
123
+ }
107
124
  sql`CREATE TABLE IF NOT EXISTS pi_telemetry_outbox (
108
125
  event_id TEXT PRIMARY KEY,
109
126
  body TEXT NOT NULL,
package/src/runtime.ts CHANGED
@@ -129,6 +129,7 @@ import {
129
129
 
130
130
  const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
131
131
  const TURN_EVENT_RETRY_SECONDS = 10;
132
+ const RUNTIME_EVENT_MAX_ATTEMPTS = 5;
132
133
 
133
134
  // 计划续跑丢失多久后由看门狗接手。必须显著长于一个正常执行片的时长,
134
135
  // 否则会在健康的长执行片上误判并派发出重复的一片。
@@ -1103,25 +1104,41 @@ export abstract class AgentRuntimeKernel<
1103
1104
  if (!turnEvents.onSubagentUsage) continue;
1104
1105
  const confirmation = await turnEvents.onSubagentUsage(payload.event);
1105
1106
  if (confirmation?.confirmed !== true) {
1106
- failed = true;
1107
- break;
1107
+ throw new Error("Runtime event was not confirmed");
1108
1108
  }
1109
1109
  } else {
1110
1110
  if (!turnEvents.onLifecycleFact) continue;
1111
1111
  const confirmation = await turnEvents.onLifecycleFact(payload.event);
1112
1112
  if (confirmation?.confirmed !== true) {
1113
- failed = true;
1114
- break;
1113
+ throw new Error("Runtime event was not confirmed");
1115
1114
  }
1116
1115
  }
1117
1116
  this.db.transaction(() =>
1118
1117
  this.db.runtimeEvents.markDelivered(row.eventId, Date.now())
1119
1118
  );
1120
1119
  } catch (error) {
1120
+ const failure = errorText(error);
1121
+ const attemptCount = this.db.transaction(() => {
1122
+ const nextAttemptCount = this.db.runtimeEvents.markFailed(
1123
+ row.eventId,
1124
+ failure,
1125
+ );
1126
+ if (nextAttemptCount >= RUNTIME_EVENT_MAX_ATTEMPTS) {
1127
+ this.db.runtimeEvents.quarantine(row.eventId, Date.now(), failure);
1128
+ }
1129
+ return nextAttemptCount;
1130
+ });
1131
+ if (attemptCount >= RUNTIME_EVENT_MAX_ATTEMPTS) {
1132
+ console.warn(
1133
+ "[runtime-turn-event:quarantined]",
1134
+ json({ eventId: row.eventId, attemptCount, error: failure }),
1135
+ );
1136
+ continue;
1137
+ }
1121
1138
  failed = true;
1122
1139
  console.warn(
1123
1140
  "[runtime-turn-event:degraded]",
1124
- json({ eventId: row.eventId, error: errorText(error) }),
1141
+ json({ eventId: row.eventId, attemptCount, error: failure }),
1125
1142
  );
1126
1143
  break;
1127
1144
  }