agent-accelerator 0.5.4 → 0.5.5

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/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export { Agent, SubAgentModelError } from "./agent/agent.ts";
37
37
  */
38
38
  export { SubAgent, agentFromEnv } from "./agent/subagent.ts";
39
39
  export type { SubAgentConfig } from "./agent/subagent.ts";
40
+ export type { EditableAgentConfig } from "./types/agent.ts";
40
41
  export {
41
42
  tool,
42
43
  toStandardToolDeclarations,
@@ -44,7 +45,6 @@ export {
44
45
  TOOL_NAME_PATTERN,
45
46
  normalizeThinkingV2,
46
47
  resolveWhenBusy,
47
- normalizeWorkerTimeout,
48
48
  normalizeMaxAttempts,
49
49
  } from "./tools/tool.ts";
50
50
  export type { CreateToolOptions } from "./tools/tool.ts";
@@ -94,7 +94,7 @@ export {
94
94
  updateScheduledAction,
95
95
  runScheduledAction,
96
96
  skipScheduledAction,
97
- killScheduledAction,
97
+ deleteScheduledAction,
98
98
  } from "./scheduler/scheduler.ts";
99
99
  export { getDatetimeTool } from "./scheduler/tool.ts";
100
100
  export { setHome, resolveHome } from "./utils/home.ts";
@@ -106,7 +106,7 @@ export type {
106
106
  ScheduledState,
107
107
  ScheduledRunStatus,
108
108
  ScheduleSpec,
109
- KillResult,
109
+ DeleteResult,
110
110
  } from "./types/scheduler.ts";
111
111
 
112
112
  // Providers & Registry — unified multi-provider layer, all native REST
@@ -14,6 +14,8 @@
14
14
  * lossy error codes, beta event-vocabulary drift.
15
15
  *
16
16
  * Notes:
17
+ * - EVERY request streams, `generate()` included (it collects its own `stream()`): OpenRouter only cancels a request that
18
+ * streams ("Stream cancellation" in its docs), so a plain non-streaming request could not be stopped by an interrupt.
17
19
  * - STATELESS per request: every turn sends the full canonical `messages`
18
20
  * array explicitly. Completions defines no `store`/`previous_response_id`/
19
21
  * `conversation`/`prompt_cache_key`/`session_id` body primitives, so none
@@ -44,10 +46,8 @@ import { buildSessionHeaders, setHeaderCaseInsensitive } from "../utils/headers.
44
46
  import { normalizeMediaInput } from "../utils/media.ts";
45
47
  import { safeStringify } from "../utils/serialization.ts";
46
48
  import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
47
- import { withRetries } from "../utils/retry.ts";
49
+ import { isAbortError } from "../types/errors.ts";
48
50
  import {
49
- parseArguments,
50
- normalizeThinkingParts,
51
51
  newToolCallId,
52
52
  redactedHeaders,
53
53
  readErrorPayload,
@@ -122,61 +122,14 @@ interface ChatRequestBody {
122
122
  [k: string]: unknown;
123
123
  }
124
124
 
125
- interface ChatChoice {
126
- index?: number;
127
- message?: {
128
- role?: string;
129
- content?: string | null;
130
- tool_calls?: Array<{
131
- id?: string;
132
- type?: string;
133
- index?: number;
134
- function?: { name?: string; arguments?: string };
135
- }>;
136
- reasoning?: string | null;
137
- reasoning_details?: Array<{ type?: string; text?: string }>;
138
- refusal?: string | null;
139
- };
140
- delta?: {
141
- role?: string;
142
- content?: string | null;
143
- tool_calls?: Array<{
144
- index?: number;
145
- id?: string;
146
- type?: string;
147
- function?: { name?: string; arguments?: string };
148
- }>;
149
- reasoning_content?: string;
150
- reasoning?: string;
151
- reasoning_details?: Array<{ type?: string; text?: string }>;
152
- refusal?: string | null;
153
- };
154
- finish_reason?: string | null;
155
- native_finish_reason?: string | null;
156
- error?: { message?: string; code?: string | number };
157
- }
158
-
159
- interface ChatResponse {
160
- id?: string;
161
- object?: string;
162
- created?: number;
163
- model?: string;
164
- provider?: string;
165
- choices?: ChatChoice[];
166
- usage?: {
167
- prompt_tokens?: number;
168
- completion_tokens?: number;
169
- total_tokens?: number;
170
- prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
171
- completion_tokens_details?: { reasoning_tokens?: number };
172
- cost?: number;
173
- [k: string]: unknown;
174
- };
175
- error?: {
176
- message?: string;
177
- code?: string | number;
178
- metadata?: { error_type?: string; provider_code?: string | number; [k: string]: unknown };
179
- };
125
+ /** The usage block of a Chat Completions chunk (the last chunk of a stream carries it, with OpenRouter's `cost`). */
126
+ interface ChatUsage {
127
+ prompt_tokens?: number;
128
+ completion_tokens?: number;
129
+ total_tokens?: number;
130
+ prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
131
+ completion_tokens_details?: { reasoning_tokens?: number };
132
+ cost?: number;
180
133
  [k: string]: unknown;
181
134
  }
182
135
 
@@ -442,7 +395,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<{
442
395
  // Response mapping (Chat Completions -> canonical)
443
396
  // ---------------------------------------------------------------------------
444
397
 
445
- function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
398
+ function mapUsage(raw?: ChatUsage): TokenUsage {
446
399
  const input = raw?.prompt_tokens ?? 0;
447
400
  const output = raw?.completion_tokens ?? 0;
448
401
  // Canonical invariant: cache hits are a SUBSET of input (no turn may
@@ -463,81 +416,6 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
463
416
  return usage;
464
417
  }
465
418
 
466
- function throwResponseError(response: ChatResponse, modelId: string): void {
467
- const message =
468
- response.error && typeof response.error.message === "string" && response.error.message
469
- ? response.error.message
470
- : "OpenRouter response failed";
471
- const failure: Record<string, unknown> & Error = new Error(message) as Record<string, unknown> & Error;
472
- if (response.error?.code !== undefined) failure["code"] = response.error.code;
473
- // Chat skin keeps the stable typed code INSIDE metadata (unlike Responses'
474
- // top-level `error_type`).
475
- const errorType = (response.error?.metadata as { error_type?: string } | undefined)?.error_type;
476
- if (typeof errorType === "string") failure["errorType"] = errorType;
477
- throw toConciseProviderError(failure, "openrouter", modelId);
478
- }
479
-
480
- function parseResponse(
481
- response: ChatResponse,
482
- modelId: string,
483
- durationMs: number,
484
- raw: ProviderRawData
485
- ): ProviderGenerateResult {
486
- // Provider-interrupted generations arrive as HTTP 200 carrying ONLY `error`
487
- // (no `choices`) — must check the body, not just the status.
488
- if (response.error) throwResponseError(response, modelId);
489
-
490
- const choice = response.choices?.[0];
491
- const message = choice?.message;
492
- // Q-23: refusal content surfaces as text with a `refusal` finish reason.
493
- const refusal = typeof message?.refusal === "string" && message.refusal ? message.refusal : "";
494
- const text = typeof message?.content === "string" && message.content ? message.content : refusal;
495
- // Thinking: `reasoning_details[].text` duplicates `message.reasoning` when
496
- // both ride along (observed live) — prefer details, else the plain field.
497
- const thinkingParts: string[] = [];
498
- const detailTexts: string[] = [];
499
- for (const block of message?.reasoning_details ?? []) {
500
- if (block?.type === "reasoning.text" && block.text) detailTexts.push(block.text);
501
- }
502
- if (detailTexts.length > 0) {
503
- thinkingParts.push(...detailTexts);
504
- } else if (typeof message?.reasoning === "string" && message.reasoning) {
505
- thinkingParts.push(message.reasoning);
506
- }
507
- const toolCalls: ToolCallRecord[] = [];
508
- for (const tc of message?.tool_calls ?? []) {
509
- const args = parseArguments(tc.function?.arguments);
510
- toolCalls.push({
511
- id: tc.id && !tc.id.includes("|") && tc.id.length <= 40 ? tc.id : clampToolCallId(tc.id || newToolCallId()),
512
- name: tc.function?.name || "unknown",
513
- arguments: args,
514
- rawArguments:
515
- typeof tc.function?.arguments === "string"
516
- ? tc.function.arguments
517
- : JSON.stringify(tc.function?.arguments ?? {}),
518
- });
519
- }
520
-
521
- // Normalized finish reasons: tool_calls|stop|length|content_filter|error.
522
- // Q-23: refusal content forces `refusal` (even when the wire says stop).
523
- const wireReason = choice?.finish_reason ?? undefined;
524
- const finishReason =
525
- toolCalls.length > 0 ? "tool_calls" : refusal ? "refusal" : wireReason || "stop";
526
- return {
527
- text,
528
- thinking: normalizeThinkingParts(thinkingParts),
529
- thoughtSignature: undefined,
530
- toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
531
- usage: mapUsage(response.usage),
532
- finishReason,
533
- responseId: response.id,
534
- model: modelId,
535
- provider: "openrouter",
536
- raw,
537
- durationMs,
538
- };
539
- }
540
-
541
419
  // ---------------------------------------------------------------------------
542
420
  // Provider
543
421
  // ---------------------------------------------------------------------------
@@ -665,28 +543,6 @@ export class OpenRouterChatCompletionsProvider implements Provider {
665
543
  return { method: "POST", headers, body: JSON.stringify(body), signal };
666
544
  }
667
545
 
668
- private async doFetch(
669
- url: string,
670
- body: ChatRequestBody,
671
- options: ProviderRequestOptions | undefined,
672
- apiKey: string,
673
- sessionId: string | undefined,
674
- signal?: AbortSignal
675
- ): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
676
- const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
677
- try {
678
- const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
679
- const text = await res.text();
680
- const headers: Record<string, string> = {};
681
- res.headers.forEach((v, k) => {
682
- headers[k] = v;
683
- });
684
- return { status: res.status, statusText: res.statusText, headers, text };
685
- } finally {
686
- cleanup();
687
- }
688
- }
689
-
690
546
  private throwIfError(
691
547
  status: number,
692
548
  url: string,
@@ -715,81 +571,40 @@ export class OpenRouterChatCompletionsProvider implements Provider {
715
571
  throw toConciseProviderError(err, "openrouter", modelId);
716
572
  }
717
573
 
574
+ // OpenRouter can only cancel a request that streams ("Stream cancellation": for a non-streaming request the model keeps processing and you are billed in full), so even a plain generate is sent as a stream and collected here: an interrupt then closes the connection and stops processing and billing at the providers OpenRouter can cancel; in: the loop's non-streaming turn, out: the same ProviderGenerateResult a JSON reply gave.
718
575
  async generate(
719
576
  model: string | ModelSpec,
720
577
  context: ProviderContext,
721
578
  options?: ProviderRequestOptions
722
579
  ): Promise<ProviderGenerateResult> {
723
- const startTime = Date.now();
724
- const clean = cleanModelId(model);
725
- const apiKey = resolveApiKey(options);
726
- if (!apiKey) {
727
- throw new Error(
728
- "[Agent Accelerator] Missing OpenRouter API key. Set OPENROUTER_API_KEY or pass apiKey."
729
- );
730
- }
731
- // Fail fast before any network call when the catalog knows the model
732
- // lacks the requested modality (clear one-liner instead of a confusing
733
- // router 404/400). Unknown models skip the guard; the router verdict
734
- // surfaces concisely.
735
- assertModalitiesSupported(context, "openrouter", clean);
736
- const baseUrl = resolveBaseUrl(options);
737
- const url = `${baseUrl}/chat/completions`;
738
- const sessionId = options?.sessionId;
739
- const tools = options?.tools as StandardToolDeclaration[] | undefined;
740
- const body = await this.buildBody(clean, context, options, sessionId, tools, false);
741
-
742
- // Audit trail: record the actual wire headers (attribution + session
743
- // affinity included), redacted.
744
- const auditRaw: Record<string, string> = {
745
- ...buildSessionHeaders("openrouter", options?.headers, sessionId),
746
- "Content-Type": "application/json",
747
- Authorization: "[REDACTED]",
748
- };
749
- const rawRequest = {
750
- url,
751
- method: "POST",
752
- headers: redactedHeaders(auditRaw),
753
- body,
754
- };
755
-
756
- const doCall = async (): Promise<ProviderGenerateResult> => {
757
- const res = await this.doFetch(url, body, options, apiKey, sessionId, options?.signal);
758
- this.throwIfError(res.status, url, res.text, clean, res.headers);
759
- let response: ChatResponse;
760
- try {
761
- response = JSON.parse(res.text) as ChatResponse;
762
- } catch {
763
- throw toConciseProviderError(
764
- Object.assign(new Error("Invalid JSON response from OpenRouter Chat Completions API"), {
765
- statusCode: res.status,
766
- responseBody: res.text.slice(0, 500),
767
- url,
768
- }),
769
- "openrouter",
770
- clean
771
- );
772
- }
773
- // Stateless turns never establish chains, but the session store still
774
- // records the turn so other providers' switch detection keeps working.
775
- noteProviderTurn(sessionId, "openrouter");
776
- return parseResponse(
777
- response,
778
- clean,
779
- Date.now() - startTime,
780
- { request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
781
- );
782
- };
783
-
580
+ // The whole call stays bounded by `timeoutMs`; the stream's own timer only covers the wait for its first byte.
581
+ const { signal, cleanup } = combinedSignal(options?.signal, timeoutFor(options as { timeoutMs?: number } | undefined));
784
582
  try {
785
- return await withRetries(doCall, {
786
- ...retryOptionsFrom(options),
787
- signal: options?.signal,
788
- label: { providerId: "openrouter", modelId: clean },
789
- });
583
+ // Nobody reads the deltas here, so the stream keeps none (a burst of frames would overflow its buffer for nothing).
584
+ const events = this.streamInto(new AssistantMessageEventStream({ buffer: false }), model, context, { ...options, signal });
585
+ const res = await events.result();
586
+ return {
587
+ text: res.text,
588
+ thinking: res.thinking,
589
+ thoughtSignature: undefined,
590
+ toolCalls: res.toolCalls.length > 0 ? res.toolCalls : undefined,
591
+ usage: res.usage,
592
+ finishReason: res.finishReason,
593
+ responseId: res.responseId,
594
+ model: res.model,
595
+ provider: "openrouter",
596
+ raw: res.raw,
597
+ durationMs: res.durationMs,
598
+ };
790
599
  } catch (err) {
791
- if (err instanceof Error && (err as { name?: string }).name === "AbortError") throw err;
600
+ // A stream reports its own generic abort; the caller's reason (`interrupt(new Error("..."))`) is what the run should carry.
601
+ const reason = options?.signal?.reason;
602
+ if (options?.signal?.aborted && reason instanceof Error && isAbortError(err)) {
603
+ throw Object.assign(new Error(reason.message), { name: "AbortError" });
604
+ }
792
605
  throw err;
606
+ } finally {
607
+ cleanup();
793
608
  }
794
609
  }
795
610
 
@@ -798,7 +613,16 @@ export class OpenRouterChatCompletionsProvider implements Provider {
798
613
  context: ProviderContext,
799
614
  options?: ProviderRequestOptions
800
615
  ): AssistantMessageEventStream {
801
- const eventStream = new AssistantMessageEventStream();
616
+ return this.streamInto(new AssistantMessageEventStream(), model, context, options);
617
+ }
618
+
619
+ // Runs one streamed request, pushing its events into `eventStream`; in: stream() (a normal stream) and generate() (one that keeps no events), out: that same stream, finished with the AgentResponse.
620
+ private streamInto(
621
+ eventStream: AssistantMessageEventStream,
622
+ model: string | ModelSpec,
623
+ context: ProviderContext,
624
+ options?: ProviderRequestOptions
625
+ ): AssistantMessageEventStream {
802
626
  const startTime = Date.now();
803
627
  const clean = cleanModelId(model);
804
628
 
@@ -1022,7 +846,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
1022
846
  finishReason = fr;
1023
847
  sawFinishReason = true;
1024
848
  }
1025
- const chunkUsage = msg["usage"] as ChatResponse["usage"] | undefined;
849
+ const chunkUsage = msg["usage"] as ChatUsage | undefined;
1026
850
  if (chunkUsage) {
1027
851
  usage = mapUsage(chunkUsage);
1028
852
  sawUsage = true;
@@ -135,11 +135,11 @@ export function combinedSignal(signal?: AbortSignal, timeoutMs?: number): { sign
135
135
  if (signal) {
136
136
  if (signal.aborted) {
137
137
  cleanup();
138
- ctrl.abort();
138
+ ctrl.abort(signal.reason); // the caller's reason survives (`interrupt(new Error("..."))`)
139
139
  } else {
140
140
  signal.addEventListener("abort", () => {
141
141
  cleanup();
142
- try { ctrl.abort(); } catch {}
142
+ try { ctrl.abort(signal.reason); } catch {}
143
143
  }, { once: true });
144
144
  }
145
145
  }
@@ -3,11 +3,11 @@ Schedules a sub-agent (a "scheduled action") to run LATER, once or on repeat, on
3
3
  Call get_datetime first whenever you create or reschedule, then pick DD from today's date.
4
4
 
5
5
  COMMANDS (one tool, the `command` field)
6
- - create: schedule a new action. Needs instructions, task and scheduled_action.{name, id, timeToDeliver, meridiem, startCookingInAdvance}. Optional: role, tools, timeoutMs, scheduled_action.repeatEvery, scheduled_action.retries.
6
+ - create: schedule a new action. Needs instructions, task and scheduled_action.{name, id, timeToDeliver, meridiem, startCookingInAdvance}. Optional: role, tools, timeoutMs, scheduled_action.repeatEvery, scheduled_action.retries, scheduled_action.onMissed.
7
7
  - list: every action with its metadata and state (waiting, cooking, missed, done).
8
8
  - get: one action by `id`, with its latest run result.
9
9
  - update: change instructions, task, tools, timeoutMs, role or any scheduled_action field of the action `id`.
10
- - kill: delete the action `id` for good. If the answer says it is running, ask the user before retrying with force: true. purge: true also deletes its past run files.
10
+ - delete: delete the action `id` for good. If the answer says it is running, ask the user before retrying with force: true. purge: true also deletes its past run files.
11
11
  - run_now: run the action `id` immediately, outside its schedule. Also fixes a missed run.
12
12
  - skip: ignore a missed run of `id` and wait for the next scheduled time.
13
13
 
@@ -18,7 +18,9 @@ SCHEDULED_ACTION FIELDS
18
18
  - meridiem: "AM" or "PM" for timeToDeliver.
19
19
  - startCookingInAdvance: "DD:HH:MM" as a duration (HH 00-23): how long before delivery the run starts. "00:00:10" = 10 minutes, "00:01:00" = 1 hour.
20
20
  - repeatEvery (optional): "DD:HH:MM" gap between deliveries. "01:00:00" = every day, "07:00:00" = every week. Omit to run once.
21
+ - timeoutMs (optional, a top-level field of create and update, not inside scheduled_action): stop a run that takes longer than this many milliseconds. Omit it for no limit of your own; the host program may set a maximum that this cannot exceed.
21
22
  - retries (optional): extra attempts if a run fails, used only while there is still time before delivery. Default 0.
23
+ - onMissed (optional): what to do when the host program was not running at delivery time. "skip" = move on to the next time. "run" = run it when the program is back. Omit it to be told (state missed) and decide with run_now or skip.
22
24
 
23
25
  EXAMPLES
24
26
  Daily brief, first delivery tomorrow at 7:00 AM, starting 30 minutes ahead: