@agent-native/core 0.168.12 → 0.169.0

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.
Files changed (39) hide show
  1. package/corpus/templates/clips/app/lib/capture-install-options.ts +20 -2
  2. package/corpus/templates/dispatch/app/root.tsx +9 -1
  3. package/dist/agent/engine/first-event-timeout.d.ts +8 -0
  4. package/dist/agent/engine/first-event-timeout.js +8 -0
  5. package/dist/agent/production-agent.d.ts +0 -30
  6. package/dist/agent/production-agent.js +17 -38
  7. package/dist/agent/run-loop-with-resume.d.ts +38 -25
  8. package/dist/agent/run-loop-with-resume.js +140 -55
  9. package/dist/agent/run-manager.d.ts +83 -68
  10. package/dist/agent/run-manager.js +280 -94
  11. package/dist/agent/run-store.d.ts +31 -0
  12. package/dist/agent/run-store.js +42 -12
  13. package/dist/app-config/agent.d.ts +2 -0
  14. package/dist/app-config/agent.js +33 -0
  15. package/dist/app-config/run-lifecycle-invariants.d.ts +248 -0
  16. package/dist/app-config/run-lifecycle-invariants.js +342 -0
  17. package/dist/app-config/schema.d.ts +2 -0
  18. package/dist/app-config/store.js +9 -1
  19. package/dist/client/EnvironmentBadge.d.ts +5 -4
  20. package/dist/client/EnvironmentBadge.js +19 -8
  21. package/dist/client/agent-chat-adapter.d.ts +0 -2
  22. package/dist/client/agent-chat-adapter.js +7 -23
  23. package/dist/client/app-providers.d.ts +3 -2
  24. package/dist/client/app-providers.js +3 -2
  25. package/dist/collab/awareness.d.ts +2 -2
  26. package/dist/collab/routes.d.ts +1 -1
  27. package/dist/jobs/background-automation-runner.d.ts +25 -0
  28. package/dist/jobs/background-automation-runner.js +104 -21
  29. package/dist/jobs/run-history.d.ts +7 -1
  30. package/dist/jobs/run-history.js +57 -14
  31. package/dist/notifications/routes.d.ts +3 -3
  32. package/dist/observability/traces.d.ts +13 -0
  33. package/dist/observability/traces.js +369 -317
  34. package/dist/resources/handlers.d.ts +1 -1
  35. package/dist/server/agent-chat-plugin.js +2 -4
  36. package/dist/server/beta-opt-out-html.js +3 -2
  37. package/dist/server/onboarding-html.js +3 -2
  38. package/dist/server/realtime-token.d.ts +1 -1
  39. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { z } from "zod";
3
+ import { resolveBackgroundRunHardTimeoutMs } from "../agent/run-manager.js";
3
4
  import { getDbExec, intType, isPostgres } from "../db/client.js";
4
5
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
5
6
  import { runMigrations } from "../db/migrations.js";
@@ -17,20 +18,38 @@ registerEvent({
17
18
  threadId: z.string().nullable(),
18
19
  status: z.enum(["success", "error", "interrupted"]),
19
20
  error: z.string().nullable(),
21
+ errorCode: z.string().nullable(),
22
+ /** Wall-clock from `started_at` to now. Null when the row predates the
23
+ * start timestamp being readable, so "not measured" stays distinct from
24
+ * "took no time". */
25
+ durationMs: z.number().nullable(),
20
26
  }),
21
27
  });
22
28
  const TABLE = "automation_runs";
23
29
  const MAX_ERROR_LENGTH = 500;
30
+ const MAX_ERROR_CODE_LENGTH = 100;
24
31
  /**
25
- * Generous multiple of the runner's own 10 minute hard abort
26
- * (BACKGROUND_RUN_HARD_TIMEOUT_MS). Past this, no run is still alive.
32
+ * Past this, no run of this automation is still alive.
33
+ *
34
+ * DERIVED from the runner's own hard abort rather than pinned, because that
35
+ * abort became configurable: a fixed 15 minutes against a longer configured
36
+ * timeout would report a still-executing run as `interrupted` and expire its
37
+ * claim lease, redispatching it on top of itself. Half again the abort leaves
38
+ * room for wind-down and the terminal write without ever preceding them.
27
39
  */
28
- const RUN_LIVENESS_CEILING_MS = 15 * 60_000;
40
+ function resolveRunLivenessCeilingMs() {
41
+ return Math.ceil(resolveBackgroundRunHardTimeoutMs() * 1.5);
42
+ }
29
43
  const INTERRUPTED_RUN_MESSAGE = "Worker stopped before a terminal result was recorded. The serverless worker may have timed out or been recycled. No delivery was confirmed.";
30
- // The background worker has a shorter hard timeout than this lease. A worker
31
- // that dies after claiming can therefore be redelivered without overlapping a
32
- // still-live execution under normal runtime limits.
33
- const CLAIM_LEASE_MS = RUN_LIVENESS_CEILING_MS;
44
+ /**
45
+ * Derived at read time alongside `INTERRUPTED_RUN_MESSAGE`: a process killed
46
+ * mid-run cannot write its own code any more than it can write its own message.
47
+ */
48
+ const INTERRUPTED_RUN_ERROR_CODE = "background_automation_interrupted";
49
+ // The background worker's hard timeout is always shorter than this lease, by
50
+ // construction above. A worker that dies after claiming can therefore be
51
+ // redelivered without overlapping a still-live execution.
52
+ const claimLeaseMs = () => resolveRunLivenessCeilingMs();
34
53
  /** Rows kept per automation, so a per-minute schedule cannot grow forever. */
35
54
  const RUNS_RETAINED_PER_AUTOMATION = 50;
36
55
  /** Authoritative release-time schema for durable automation history. */
@@ -76,6 +95,11 @@ export const AUTOMATION_RUN_MIGRATIONS = [
76
95
  name: "automation-runs-app-id",
77
96
  sql: `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS app_id TEXT`,
78
97
  },
98
+ {
99
+ version: 5,
100
+ name: "automation-runs-error-code",
101
+ sql: `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS error_code TEXT`,
102
+ },
79
103
  ];
80
104
  export async function runAutomationRunMigrations(nitroApp) {
81
105
  await runMigrations(AUTOMATION_RUN_MIGRATIONS, {
@@ -102,6 +126,7 @@ export async function ensureTable() {
102
126
  started_at ${intType()} NOT NULL,
103
127
  finished_at ${intType()},
104
128
  error TEXT,
129
+ error_code TEXT,
105
130
  claimed_at ${intType()},
106
131
  dispatch_pending ${intType()} NOT NULL DEFAULT 0
107
132
  )
@@ -111,6 +136,7 @@ export async function ensureTable() {
111
136
  await ensureTableExists(TABLE, createSql);
112
137
  await ensureColumnExists(TABLE, "claimed_at", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS claimed_at ${intType()}`);
113
138
  await ensureColumnExists(TABLE, "dispatch_pending", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS dispatch_pending ${intType()} NOT NULL DEFAULT 0`);
139
+ await ensureColumnExists(TABLE, "error_code", `ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS error_code TEXT`);
114
140
  await ensureIndexExists(`idx_${TABLE}_owner_automation`, indexSql);
115
141
  return;
116
142
  }
@@ -121,6 +147,7 @@ export async function ensureTable() {
121
147
  ["claimed_at", `${intType()}`],
122
148
  ["dispatch_pending", `${intType()} NOT NULL DEFAULT 0`],
123
149
  ["app_id", "TEXT"],
150
+ ["error_code", "TEXT"],
124
151
  ]) {
125
152
  if (columns.has(name))
126
153
  continue;
@@ -146,7 +173,7 @@ export async function ensureTable() {
146
173
  function toRun(row, now) {
147
174
  const stored = String(row.status);
148
175
  const startedAt = Number(row.started_at);
149
- const status = stored === "running" && now - startedAt > RUN_LIVENESS_CEILING_MS
176
+ const status = stored === "running" && now - startedAt > resolveRunLivenessCeilingMs()
150
177
  ? "interrupted"
151
178
  : stored;
152
179
  return {
@@ -167,6 +194,11 @@ function toRun(row, now) {
167
194
  : row.error == null
168
195
  ? null
169
196
  : String(row.error),
197
+ errorCode: row.error_code == null && status === "interrupted"
198
+ ? INTERRUPTED_RUN_ERROR_CODE
199
+ : row.error_code == null
200
+ ? null
201
+ : String(row.error_code),
170
202
  };
171
203
  }
172
204
  /**
@@ -212,7 +244,7 @@ export async function claimAutomationRun(id) {
212
244
  const now = Date.now();
213
245
  const result = await getDbExec().execute({
214
246
  sql: `UPDATE ${TABLE} SET claimed_at = ? WHERE id = ? AND dispatch_pending = 1 AND (claimed_at IS NULL OR claimed_at <= ?) AND status = 'running'`,
215
- args: [now, id, now - CLAIM_LEASE_MS],
247
+ args: [now, id, now - claimLeaseMs()],
216
248
  });
217
249
  return Number(result.rowsAffected ?? 0) > 0;
218
250
  }
@@ -234,7 +266,7 @@ export async function listUnclaimedAutomationRuns(options) {
234
266
  AND started_at <= ?
235
267
  ORDER BY started_at ASC LIMIT ${limit}`,
236
268
  args: [
237
- Date.now() - CLAIM_LEASE_MS,
269
+ Date.now() - claimLeaseMs(),
238
270
  ...(appId ? [appId] : []),
239
271
  Date.now() - olderThanMs,
240
272
  ],
@@ -263,19 +295,28 @@ async function pruneAutomationRuns(owner, automation) {
263
295
  args: [owner, automation, owner, automation],
264
296
  });
265
297
  }
266
- export async function finishAutomationRun(id, status, error) {
298
+ export async function finishAutomationRun(id, status, error, errorCode) {
267
299
  await ensureTable();
268
300
  const existing = await getDbExec().execute({
269
- sql: `SELECT owner, automation, path, org_id, run_id, thread_id FROM ${TABLE} WHERE id = ? LIMIT 1`,
301
+ sql: `SELECT owner, automation, path, org_id, run_id, thread_id, started_at FROM ${TABLE} WHERE id = ? LIMIT 1`,
270
302
  args: [id],
271
303
  });
272
304
  const row = existing.rows?.[0];
305
+ const finishedAt = Date.now();
273
306
  await getDbExec().execute({
274
- sql: `UPDATE ${TABLE} SET status = ?, finished_at = ?, error = ? WHERE id = ?`,
275
- args: [status, Date.now(), error?.slice(0, MAX_ERROR_LENGTH) ?? null, id],
307
+ sql: `UPDATE ${TABLE} SET status = ?, finished_at = ?, error = ?, error_code = ? WHERE id = ?`,
308
+ args: [
309
+ status,
310
+ finishedAt,
311
+ error?.slice(0, MAX_ERROR_LENGTH) ?? null,
312
+ errorCode?.slice(0, MAX_ERROR_CODE_LENGTH) ?? null,
313
+ id,
314
+ ],
276
315
  });
277
316
  if (!row)
278
317
  return;
318
+ const rawStartedAt = Number(row.started_at);
319
+ const startedAt = Number.isFinite(rawStartedAt) ? rawStartedAt : null;
279
320
  try {
280
321
  emitBusEvent("automation.run.finished", {
281
322
  automationRunId: id,
@@ -287,6 +328,8 @@ export async function finishAutomationRun(id, status, error) {
287
328
  threadId: row.thread_id == null ? null : String(row.thread_id),
288
329
  status,
289
330
  error: error?.slice(0, MAX_ERROR_LENGTH) ?? null,
331
+ errorCode: errorCode?.slice(0, MAX_ERROR_CODE_LENGTH) ?? null,
332
+ durationMs: startedAt === null ? null : Math.max(0, finishedAt - startedAt),
290
333
  }, { owner: String(row.owner) });
291
334
  }
292
335
  catch (eventError) {
@@ -11,14 +11,14 @@
11
11
  * DELETE /_agent-native/notifications/:id — delete
12
12
  */
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
- error?: undefined;
15
14
  count: number;
16
15
  updated?: undefined;
16
+ error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
- error?: undefined;
20
19
  count?: undefined;
21
20
  updated: number;
21
+ error?: undefined;
22
22
  ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
@@ -26,8 +26,8 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
26
26
  error: string;
27
27
  ok?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  count?: undefined;
31
30
  updated?: undefined;
31
+ error?: undefined;
32
32
  ok: boolean;
33
33
  }>>;
@@ -44,6 +44,19 @@ export declare function instrumentAgentLoop(opts: {
44
44
  * reads. */
45
45
  userId: string | null;
46
46
  config: ObservabilityConfig;
47
+ /**
48
+ * Name for this run's root span, in the local trace store and in PostHog LLM
49
+ * analytics. Defaults to `"agent_run"`. Without it every path emits the same
50
+ * name and a scheduled automation is indistinguishable from a chat turn in
51
+ * the one view where telling them apart is the whole question.
52
+ */
53
+ spanName?: string;
54
+ /**
55
+ * Free-form run context. Persisted onto the local store's parent span AND
56
+ * forwarded to PostHog as trace properties — a channel that reached only the
57
+ * SQL store was a channel that could not answer "which automation was this?"
58
+ * in LLM analytics.
59
+ */
47
60
  metadata?: Record<string, unknown> | null;
48
61
  experimentAssignments?: Array<{
49
62
  experimentId: string;