agent-accelerator 0.5.1 → 0.5.3

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/README.md CHANGED
@@ -131,7 +131,9 @@ An `Agent` holds:
131
131
  | `apiKey` | `string` | Overrides environment-based API-key lookup for this agent. |
132
132
  | `baseUrl` | `string` | Overrides the default endpoint for this agent. |
133
133
  | `stateless` | `boolean` | When `true`, history is cleared before and after each `run`. Useful for one-shot evaluators. Defaults to `false`. |
134
- | `persist` | `{ dir?: string; file?: string }` | Durable sessions: rewrites the session file after every step (user/assistant/tool/sub-agent). `{ dir }` uses `sessions/<sessionId>/session.json`, `{ file }` a single `.session.json`, `{}` uses `<home>/sessions`. Writes are atomic and never fail a turn. |
134
+ | `runRetries` | `number` | A run that FAILED for a transient reason (provider down or overloaded, timeouts, a cut stream, after `retry` gave up) is tried again from where it stopped, waiting 5 s, 15 s, 45 s, then 60 s. Default `3`, `0` = off. Finished tool calls are not repeated; the response reports the failed attempts' tool calls and usage. Never for aborts, 400/401/403/404, bad config, budgets or limits. `run()` and queued runs only, not `stream()`. |
135
+ | `resume` | `"automatic" \| "explicit"` | Crash recovery (needs `persist: { dir }` + the same `sessionId`): `automatic` (default) continues a cut-off run when the Agent is constructed, `explicit` waits for `agent.resume()`. Finished tool calls are never run again. `agent.interrupted()` lists cut-off runs. |
136
+ | `persist` | `{ dir?: string; file?: string }` | Durable sessions, saved after every step (user/assistant/tool/sub-agent). `{ dir }` keeps `sessions/<sessionId>/session.json` (snapshot) plus an append-only `journal.ndjson` (one line per step, so a long chat costs the same per step as a short one), `{ file }` a single `.session.json`, `{}` uses `<home>/sessions`. Writes are durable (fsync, `.bak` of the last good file, atomic rename) and never fail a turn. |
135
137
  | `home` | `string` | Folder for everything the SDK writes to disk (`sessions/`, `scheduled_actions/`, `telemetry/`). Defaults to `setHome(dir)`, then the `AGENT_ACCEL_HOME` env var, then `.agent-accel/` in the working directory. An explicit `persist.dir` / `persist.file` still wins. |
136
138
 
137
139
  When `dynamicSubagents.enabled` is set without a resolvable worker model, construction throws `SubAgentModelError`.
@@ -364,7 +366,7 @@ telemetryBus.onEvent((e) => {
364
366
 
365
367
  **How it behaves**
366
368
 
367
- * **One local clock.** A single timer per home folder ticks once a second against the machine's own time and only asks what is overdue, so drift or sleep cannot skip a run. There is no timezone or daylight-saving logic. The timer is `unref`'d and runs only while something is pending, so actions run only while your process is alive.
369
+ * **One local clock.** A single timer per home folder (guarded by a one-process lock, `<home>/.lock`) ticks once a second against the machine's own time and only asks what is overdue, so drift or sleep cannot skip a run. There is no timezone or daylight-saving logic. The timer is `unref`'d and runs only while something is pending, so actions run only while your process is alive.
368
370
  * **The creating `Agent` runs it.** The task stores the Agent's `name`; each run goes to the live Agent with that name (latest wins), using its worker model and tool pool. The scheduler holds only a weak reference, so keep your `Agent` referenced; if none is alive, the run counts as missed.
369
371
  * **Each run is fresh.** A new stateless dynamic sub-agent built from the saved fields, on your fixed worker model and tool pool, never touching the Main Agent's session. It inherits the parent's limits, retry policy, `fileAccess` and `captureRaw`.
370
372
  * **Files are the truth.** Everything lives in `<home>/scheduled_actions/`: `tasks.json` (every action), `deliveries.json` (saved results waiting for their delivery time) and one folder per action with one `.md` file per run (a small header, then the result). The file is re-read and validated before each run, so editing `instructions` or `task` there takes effect on the next run. API keys, the model and tool code are never stored. Don't put secrets in `instructions`.
@@ -406,6 +408,34 @@ const agent = new Agent({
406
408
 
407
409
  The agent loop answers the 3rd consecutive identical call (same name and arguments) with a `repeat_blocked` error, so the model can reuse the prior result or change its arguments. Tools marked `repeatable: true`, or `idempotent: true` unless `repeatable` is set, are exempt.
408
410
 
411
+
412
+ ### Asking permission: `ctx.stop`
413
+
414
+ The SDK has no approval modes; you write the check inside `execute` (your own function that asks your user) and call `ctx.stop(reason)` when the answer is no:
415
+
416
+ ```ts
417
+ const pay = tool({
418
+ description: "Pay an invoice.",
419
+ input: z.object({ invoiceId: z.string(), amount: z.number() }),
420
+ execute: async ({ invoiceId, amount }, ctx) => {
421
+ if (!(await getPermission(`Pay $${amount} for ${invoiceId}?`))) ctx.stop("user declined the payment");
422
+ return await payInvoice(invoiceId, amount); // never reached after a stop
423
+ },
424
+ });
425
+ ```
426
+
427
+ `ctx.stop` throws, so the code after it never runs (even if you forget `return`). The reason is saved as that call's result, calls that were running in parallel in the same batch still run to completion (none is skipped or aborted), and then the run ends with `finishReason: "stopped"` and **no further model turn**. The agent waits for the next user message; the model then sees the reason as the tool's result. A steer typed while the batch ran becomes the next run. The stop is saved with the call, so a crash cannot turn a "no" into a "yes": after a restart the run is closed as stopped without calling the model. A tool's `timeoutMs` counts the time spent waiting for your check, so leave it off for tools that ask a person.
428
+
429
+ To guard a built-in tool such as `spawn_subagents` or `scheduled_actions`, wrap it on the agent after construction:
430
+
431
+ ```ts
432
+ const inner = agent.tools["spawn_subagents"]!;
433
+ agent.tools["spawn_subagents"] = {
434
+ ...inner,
435
+ execute: async (input, ctx) => ((await getPermission("Spawn workers?")) ? inner.execute(input, ctx) : ctx.stop("spawning was not approved")),
436
+ };
437
+ ```
438
+
409
439
  ### Tool Failures and Tracing
410
440
 
411
441
  When a tool throws, the model gets the error line plus a JSON block: error `name`/`code`/`status`, `attempts`, `timedOut`, the last progress the tool reported, and a hint not to repeat finished work. Report progress from inside a tool with `ctx.emit({ type: "progress", ... })`, or throw `new ToolError(msg, { context: { progress } })`.
@@ -429,6 +459,20 @@ Every call also emits `tool.*` events on `telemetryBus` (nothing is written unle
429
459
  telemetryBus.onEvent((e) => e.code.startsWith("tool.") && appendFileSync("tools.ndjson", JSON.stringify(e) + "\n"));
430
460
  ```
431
461
 
462
+ **Tool logs.** With `persist: { dir }` every tool call also gets its own log file in the session folder, `tool-logs/<n>-<tool>.ndjson` (numbered in the order calls start, continuing after restarts). The SDK writes the input, each attempt, retry, the full result or error (stack included) and the duration; your tool adds its own lines with `ctx.log(message, data?)`. Every line is flushed to disk as it is written, so after a crash the file shows how far the call got:
463
+
464
+ ```ts
465
+ execute: async ({ files }, ctx) => {
466
+ for (const [i, f] of files.entries()) {
467
+ await upload(f);
468
+ ctx.log(`uploaded ${i + 1} of ${files.length}`, { file: f });
469
+ }
470
+ }
471
+ ```
472
+
473
+ The call's result carries `logFile` (relative to the session folder), and the saved session keeps only that reference. The log is for you and your harness: it is never sent to the model, nothing in the SDK reads it back (a tool that lets the agent read its own logs is yours to build), and it is not size-capped. Without `persist: { dir }` (or for `stateless` agents) nothing is written and `ctx.log` is a no-op.
474
+
475
+
432
476
  ### Tool Timing and Deadlines
433
477
 
434
478
  Every `ToolResultRecord` includes `durationMs`, measured in whole milliseconds with a minimum displayed value of `1ms`. The value covers the tool execution attempt (including retry/backoff time), but excludes queue wait and input-schema validation.
@@ -520,6 +564,24 @@ Model resolution never silently selects a default model: an empty model throws.
520
564
 
521
565
  An unknown prefix with no catalog match still routes to a custom provider, allowing private model IDs and endpoints.
522
566
 
567
+ ### Google long decode (Gemini 4)
568
+
569
+ Google caps one request at 65,536 output tokens. Set `maxOutputTokens` above
570
+ that on a `gemini-4-*` model and the SDK runs the request as a background
571
+ interaction automatically (no extra option): up to 1,048,576 tokens minus the
572
+ input, for up to 24 hours. `run()` polls until it finishes (2 s, then every
573
+ 5 s); `stream()` reconnects after a dropped or silent connection
574
+ (`last_event_id`, 15 min of silence counts as dropped). Aborting cancels the
575
+ interaction on Google's side, so it stops billing. Each HTTP request is still
576
+ timed by `limits.requestTimeoutMs`, but the whole run is not. Any other Google
577
+ model with `maxOutputTokens` above 65,536 fails before the network with a
578
+ `ConfigError`. Use it for hard reasoning, not chat: you pay for every token of
579
+ every 64K hop.
580
+
581
+ A custom `Provider` can opt into the same treatment with the optional
582
+ `isLongRunning(model, options)` method: when it returns `true`, the loop skips
583
+ its whole-call, first-byte and idle timers and leaves timing to the adapter.
584
+
523
585
  ### Registry
524
586
 
525
587
  * `resolveModel("google/x" | ModelSpec)` → `{ provider, modelId, modelSpec? }`. Use to inspect routing before execution.
@@ -909,7 +971,7 @@ SubAgentExecutionMetadata {
909
971
  }
910
972
  ```
911
973
 
912
- `finishReason` is `"max_turns"` when a finite `maxTurns` limit stopped the run with tool calls still pending (with an `agent.max_turns_truncated` warning on `telemetryBus` naming the dropped tools).
974
+ `finishReason` is `"stopped"` when a tool ended the run with `ctx.stop(reason)` (see *Asking permission*). It is `"max_turns"` when a finite `maxTurns` limit stopped the run with tool calls still pending (with an `agent.max_turns_truncated` warning on `telemetryBus` naming the dropped tools).
913
975
 
914
976
  ### SubAgentStep
915
977
 
@@ -1118,10 +1180,19 @@ Message {
1118
1180
  role: "system" | "user" | "assistant" | "tool",
1119
1181
  content: string | ContentPart[],
1120
1182
  name?,
1121
- thoughtSignature?
1183
+ thoughtSignature?,
1184
+ providerSteps? // { provider, steps }: raw output steps of an assistant turn
1122
1185
  }
1123
1186
  ```
1124
1187
 
1188
+ `providerSteps` keeps a provider's raw output for one assistant turn (today:
1189
+ Google's `steps`, including thought signatures). When Google needs the full
1190
+ history again (after a restart, an expired chain or a provider switch), those
1191
+ turns are resent exactly as received, which keeps reasoning continuity and
1192
+ the implicit-cache prefix intact. Other providers ignore it. Adapters return
1193
+ it on `ProviderGenerateResult` / per-turn stream `AgentResponse`; it is never
1194
+ part of `toJSON()`.
1195
+
1125
1196
  ### ProviderContext
1126
1197
 
1127
1198
  ```ts
@@ -1140,9 +1211,9 @@ Supported content parts include:
1140
1211
  * `ThinkingPart { type:"thinking", thinking, thoughtSignature? }` — preserved reasoning.
1141
1212
  * `ToolCallPart { type:"tool_call", id, name, arguments, rawArguments?, thoughtSignature? }`
1142
1213
  * `ToolResultPart { type:"tool_result", id, name, result, isError? }`
1143
- * `ImagePart { type:"image", image, mimeType? }`
1214
+ * `ImagePart { type:"image", image, mimeType?, resolution? }`
1144
1215
  * `AudioPart { type:"audio", audio, mimeType? }`
1145
- * `VideoPart { type:"video", video, mimeType? }`
1216
+ * `VideoPart { type:"video", video, mimeType?, resolution? }`
1146
1217
  * `FilePart { type:"file", file, mimeType?, filename? }` — PDFs/documents.
1147
1218
 
1148
1219
  `image`, `audio`, `video`, and `file` inputs accept:
@@ -1171,6 +1242,20 @@ Media normalization is handled automatically by providers (mapped to provider-na
1171
1242
  Provider matrix: text + image + wav/mp3 audio + PDF work on all supported
1172
1243
  providers. Video works on Gemini only.
1173
1244
 
1245
+ On Google, links Google fetches itself go as a `uri` without downloading:
1246
+ YouTube links, Files API links, `gs://` objects and public `https://` URLs of
1247
+ URL-supported types (PDF, CSV, text, JSON, PNG/JPEG/WebP/BMP, common video;
1248
+ max 15 MB). Audio URLs and `http://` links are downloaded and inlined. Inline
1249
+ types are checked before the network; Google reads only PDF and CSV
1250
+ documents. The supported lists are the `GOOGLE_*_MIME` constants at the top of
1251
+ `src/providers/google.ts`.
1252
+
1253
+ `resolution` (`MediaResolution`: `"low" | "medium" | "high" | "ultra_high"`)
1254
+ sets how much detail Google spends on one image or video, and so how many
1255
+ input tokens it costs (image: 280 / 560 / 1120 / 2240; video per frame:
1256
+ `low` and `medium` 70, `high` 280). Omit it for Google's default. Other
1257
+ providers ignore it with one `provider_warning`.
1258
+
1174
1259
  Before any network call, the executor checks `image` / `audio` / `video` /
1175
1260
  `file`-as-PDF parts against the catalog's `modalities.input` for that
1176
1261
  provider/model and fails fast with a one-line error naming the gap
@@ -1189,6 +1274,7 @@ never in the gitignored snapshot.
1189
1274
  * `getEnv(key, fallback?)` — resolves an environment variable.
1190
1275
  * `getApiKey(provider, explicit?, env?)` — resolves API keys using the configured environment lookup order.
1191
1276
  * `buildSessionHeaders(provider, cache?, custom?, sessionId?)` — builds provider-specific affinity headers. Normally handled automatically.
1277
+ * Two retry layers: `retry` repeats one failed provider CALL (seconds); `runRetries` repeats a whole failed RUN from where it stopped (minutes, `agent.run_retry` on the bus) while the agent stays busy, so queued prompts wait.
1192
1278
  * `withRetries(fn, { maxRetries?, maxRetryDelayMs?, signal?, label? })` / `isTransientError(err)` — bounded retries for transient provider failures (defaults: 2 retries, 5s cap, aborts never retried).
1193
1279
 
1194
1280
  ---
@@ -1234,6 +1320,10 @@ bun run examples/13-structured-output.ts
1234
1320
  bun run examples/14-scheduled-actions.ts [--quick]
1235
1321
  # scheduled actions walkthrough on your computer's clock: starts working ~1 minute from now,
1236
1322
  # delivers ~2 minutes from now (code-scheduled and model-scheduled); --quick skips the waiting
1323
+
1324
+ bun run examples/15-durable-agent.ts
1325
+ # a durable agent: killed mid-tool (SIGKILL), picked up by a new process (interrupted()/resume()),
1326
+ # finished tools not repeated, run retry, ctx.stop approval, tool logs (about 5 s of waiting)
1237
1327
  ```
1238
1328
 
1239
1329
  Every example ends its `run()` with `.catch(fail)` (`examples/_shared.ts`),
@@ -1285,10 +1375,10 @@ saveSessionFile(".session.json", agent, telemetry);
1285
1375
 
1286
1376
  * `agent.exportSession(telemetry)` / `agent.importSession(saved)` round-trip messages, system prompt, model, thinking level, instructions, cache, worker model, and sub-agent traces without touching `AgentContext` internals.
1287
1377
  * `SessionTelemetry` accumulates `input / output / cacheRead / cacheWrite / reasoning / cost`, with clamped `turnHitRate()` / `totalHitRate()`, a dual `formatBar(res)`, and `formatSessionBanner(saved, telemetry, path)` for startup. Cost follows the [TokenUsage](#response-usage-and-raw-data) rules.
1288
- * Files are JSON with one line per top-level field and one line per turn (2-space indent), written atomically (temp + rename).
1378
+ * Files are JSON with one line per top-level field and one line per turn (2-space indent), written durably (temp file, fsync, `.bak` of the last good version, atomic rename; a damaged file falls back to its `.bak` on load). Google turns also store their raw `providerSteps`, so a resumed session replays them exactly.
1289
1379
  * Binary media (bytes, data URLs, base64) is extracted to `media/` on save via `saveSessionDir("sessions", agent, telemetry)` → `sessions/<sessionId>/{session.json, media/*}`; remote URLs and local paths stay references. `loadSessionDir(dir)` resolves `media/…` refs back to absolute paths, and `findLatestSessionDir("sessions")` resumes the most recent session.
1290
1380
  * Sub-agent traces persist in a dedicated `subagents` section keyed by TrackingID (`{ trackingId, name, sessionId, parentSessionId, status, task, turns, usage, steps, text }`). Files stay lean: redundant `rawArguments` and repeated step text are dropped on save (rebuilt/kept live in memory).
1291
- * With `persist: { dir }` or `{ file }`, the session file is rewritten after every step, so a crash loses at most the in-flight step.
1381
+ * With `persist: { dir }` each step appends one line to `journal.ndjson` (fsynced); the journal is folded into a fresh `session.json` after 500 lines or 4 MB, or when history changes another way (rollback, `reset()`, `importSession`). `loadSessionDir` replays the journal over the snapshot; a torn last line is ignored, damage earlier stops the replay with a `session.journal_corrupt` warning. A run that a crash cut off is picked up again by a new Agent with the same `sessionId` (see `resume`): the conversation is reloaded, tool calls with a saved result are not repeated, the rest run, then waiting prompts follow. The journal also records where each run began (`runs`: id, index of its user message, time) and the prompts still waiting in the steer inbox and follow-up queue (`pending`, read back by `loadSessionDir` as `session.pending`; `importSession` restores `runs`). Embedded media is written once as `media/<kind>-h<hash>.<ext>`. With `persist: { file }` the single file is rewritten after every step. Either way a crash loses at most the in-flight step. Do not call `saveSessionDir` on a folder an Agent persists to.
1292
1382
 
1293
1383
  ---
1294
1384
  ## Scripts and Structure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-accelerator",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
5
5
  "license": "MIT",
6
6
  "author": "Sashvat Bharat",