agent-accelerator 0.5.8 → 0.6.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 (67) hide show
  1. package/README.md +26 -5
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +374 -167
  7. package/src/agent/architecture.md +14 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +8 -3
  10. package/src/agent/delegation.ts +82 -29
  11. package/src/agent/loop.ts +449 -373
  12. package/src/agent/subagent.ts +1 -3
  13. package/src/agent/subagents-tool.ts +45 -7
  14. package/src/agent/subagents.md +3 -0
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/worker.ts +58 -28
  17. package/src/agent/workers.ts +113 -20
  18. package/src/architecture.md +16 -15
  19. package/src/index.ts +34 -10
  20. package/src/models/architecture.md +4 -3
  21. package/src/models/catalog-cache.ts +6 -5
  22. package/src/models/catalog.ts +46 -49
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +150 -62
  25. package/src/providers/openai-compat.ts +60 -58
  26. package/src/providers/openai.ts +57 -39
  27. package/src/providers/openrouter.ts +87 -32
  28. package/src/providers/registry.ts +12 -26
  29. package/src/providers/shared.ts +89 -39
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +9 -8
  32. package/src/scheduler/scheduled-actions.md +4 -0
  33. package/src/scheduler/scheduler.ts +186 -60
  34. package/src/scheduler/store.ts +23 -10
  35. package/src/scheduler/time.ts +17 -3
  36. package/src/scheduler/tool.ts +33 -7
  37. package/src/session/architecture.md +7 -6
  38. package/src/session/journal.ts +38 -12
  39. package/src/session/store.ts +122 -61
  40. package/src/session/usage.ts +58 -9
  41. package/src/streaming/architecture.md +4 -3
  42. package/src/streaming/event-stream.ts +26 -12
  43. package/src/telemetry.ts +6 -2
  44. package/src/tools/architecture.md +8 -7
  45. package/src/tools/executor.ts +50 -48
  46. package/src/tools/schema.ts +10 -6
  47. package/src/tools/subagent-result.ts +6 -1
  48. package/src/tools/tool.ts +9 -11
  49. package/src/types/agent.ts +2 -21
  50. package/src/types/architecture.md +11 -10
  51. package/src/types/core.ts +1 -9
  52. package/src/types/errors.ts +26 -4
  53. package/src/types/model.ts +0 -4
  54. package/src/types/tool.ts +12 -2
  55. package/src/update-models.ts +5 -5
  56. package/src/utils/architecture.md +16 -15
  57. package/src/utils/cache.ts +1 -3
  58. package/src/utils/durable.ts +17 -5
  59. package/src/utils/env.ts +9 -7
  60. package/src/utils/errors.ts +50 -44
  61. package/src/utils/headers.ts +3 -10
  62. package/src/utils/media.ts +23 -8
  63. package/src/utils/raw-capture.ts +9 -2
  64. package/src/utils/retry.ts +17 -31
  65. package/src/utils/safe-io.ts +29 -7
  66. package/src/utils/serialization.ts +1 -4
  67. package/src/utils/session.ts +1 -2
package/src/providers.ts CHANGED
@@ -18,12 +18,7 @@ import { TRUNCATED_ARGS_KEY } from "./types/tool.ts";
18
18
  import { telemetryBus } from "./telemetry.ts";
19
19
 
20
20
  /** Classification of a canonical capability on a given provider. */
21
- export type ProviderCapabilityStatus =
22
- | "NATIVE"
23
- | "TRANSLATED"
24
- | "AUTOMATIC"
25
- | "EMULATED"
26
- | "UNSUPPORTED";
21
+ export type ProviderCapabilityStatus = "NATIVE" | "TRANSLATED" | "AUTOMATIC" | "EMULATED" | "UNSUPPORTED";
27
22
 
28
23
  /**
29
24
  * Single warning mechanism for provider capability differences.
@@ -65,8 +60,12 @@ export function emitProviderWarning(options: {
65
60
  reason: options.reason,
66
61
  fallback: options.fallback,
67
62
  });
68
- try { (globalThis as any).__agentAccelWarnSink?.(msg, options); } catch {}
69
- try { console.warn(msg); } catch {}
63
+ try {
64
+ (globalThis as any).__agentAccelWarnSink?.(msg, options);
65
+ } catch {}
66
+ try {
67
+ console.warn(msg);
68
+ } catch {}
70
69
  }
71
70
 
72
71
  /** Clears deduped-warning state (mainly for tests). */
@@ -124,9 +123,7 @@ export function clearSessionRouting(sessionId?: string): void {
124
123
  * Maps a canonical ThinkingLevel onto Google Interactions `thinking_level`.
125
124
  * Google values: minimal|low|medium|high (no off switch, no xhigh/max).
126
125
  */
127
- export function mapThinkingLevelToGoogle(
128
- level: ThinkingLevel | string | undefined
129
- ): { thinkingLevel?: string } {
126
+ export function mapThinkingLevelToGoogle(level: ThinkingLevel | string | undefined): { thinkingLevel?: string } {
130
127
  if (!level) return {};
131
128
  const norm = String(level).toLowerCase().trim();
132
129
  if (norm === "dynamic") return {}; // server dynamic default (NATIVE)
@@ -171,9 +168,7 @@ export function mapServiceTierToGoogle(tier: ServiceTier | undefined): "flex" |
171
168
  * the discontinued Responses skin, which clamped `xhigh` — everything passes
172
169
  * through. `max` rides the same verbatim path. `dynamic` omits (server default).
173
170
  */
174
- export function mapThinkingLevelToOpenRouterChat(
175
- level: ThinkingLevel | string | undefined
176
- ): { effort?: string } {
171
+ export function mapThinkingLevelToOpenRouterChat(level: ThinkingLevel | string | undefined): { effort?: string } {
177
172
  if (!level) return {};
178
173
  const norm = String(level).toLowerCase().trim();
179
174
  if (norm === "dynamic") return {}; // server default
@@ -216,9 +211,16 @@ export function resolveCache(
216
211
  }
217
212
 
218
213
  /** Warns once per provider/model that per-media `resolution` is ignored (no such control on that wire for the given kinds of media); in: an adapter's request messages, out: telemetry bus only. */
219
- export function warnIgnoredMediaResolution(provider: string, messages: Message[], modelRef: string, kinds: Array<"image" | "video"> = ["image", "video"]): void {
214
+ export function warnIgnoredMediaResolution(
215
+ provider: string,
216
+ messages: Message[],
217
+ modelRef: string,
218
+ kinds: Array<"image" | "video"> = ["image", "video"]
219
+ ): void {
220
220
  const used = messages.some(
221
- (m) => Array.isArray(m.content) && m.content.some((p) => (p.type === "image" || p.type === "video") && kinds.includes(p.type) && p.resolution)
221
+ (m) =>
222
+ Array.isArray(m.content) &&
223
+ m.content.some((p) => (p.type === "image" || p.type === "video") && kinds.includes(p.type) && p.resolution)
222
224
  );
223
225
  if (!used) return;
224
226
  emitProviderWarning({
@@ -231,11 +233,7 @@ export function warnIgnoredMediaResolution(provider: string, messages: Message[]
231
233
  }
232
234
 
233
235
  /** OpenRouter Chat Completions `tool_choice` wire values. */
234
- export type OpenRouterChatToolChoice =
235
- | "auto"
236
- | "none"
237
- | "required"
238
- | { type: "function"; function: { name: string } };
236
+ export type OpenRouterChatToolChoice = "auto" | "none" | "required" | { type: "function"; function: { name: string } };
239
237
 
240
238
  /**
241
239
  * Maps canonical toolChoice onto Chat Completions `tool_choice`.
@@ -260,9 +258,7 @@ export function mapToolChoiceToOpenRouterChat(
260
258
  * medium). Unlike OpenRouter (which clamps xhigh), OpenAI documents xhigh
261
259
  * and max natively so they pass through verbatim. `dynamic` omits (server default).
262
260
  */
263
- export function mapThinkingLevelToOpenAI(
264
- level: ThinkingLevel | string | undefined
265
- ): { effort?: string } {
261
+ export function mapThinkingLevelToOpenAI(level: ThinkingLevel | string | undefined): { effort?: string } {
266
262
  if (!level) return {};
267
263
  const norm = String(level).toLowerCase().trim();
268
264
  if (norm === "dynamic") return {}; // server default
@@ -386,98 +382,36 @@ export function selectRawArguments(startText: string, deltaText: string): string
386
382
  }
387
383
 
388
384
  // ---------------------------------------------------------------------------
389
- // Generation controls (maxOutputTokens / stop / seed)
385
+ // Generation controls (maxOutputTokens)
390
386
  // ---------------------------------------------------------------------------
391
387
 
392
388
  /** Canonical generation controls as carried on `ProviderRequestOptions`. */
393
389
  export interface GenerationParams {
394
390
  maxOutputTokens?: number;
395
- stop?: string[];
396
- seed?: number;
397
391
  }
398
392
 
399
- /** Most stop sequences a Chat Completions request may carry (OpenRouter chat-completion api-ref `maxItems`, OpenAI the same). */
400
- const MAX_CHAT_STOP = 4;
401
- // True for a finite number; in: the generation-param mappers, out: a type guard.
402
- const finiteNumber = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
403
-
404
- /** Normalizes the public `stop` option (`string | string[]`) to a clean array, or undefined. */
405
- export function normalizeStop(stop: string | string[] | undefined): string[] | undefined {
406
- const list = (Array.isArray(stop) ? stop : stop === undefined ? [] : [stop]).filter(
407
- (s): s is string => typeof s === "string" && s.length > 0
408
- );
409
- return list.length > 0 ? list : undefined;
393
+ // The output-token cap as a whole number above zero, else undefined; in: the three generation-param mappers, out: their wire field.
394
+ function outputTokenCap(params: GenerationParams | undefined): number | undefined {
395
+ const n = params?.maxOutputTokens;
396
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
410
397
  }
411
398
 
412
- /**
413
- * Standard Chat Completions fields (OpenRouter + generic compat endpoints):
414
- * `max_tokens`, `stop`, `seed`. Only set values are
415
- * emitted, so strict endpoints never see anything the caller did not ask for.
416
- */
417
- export function mapGenerationParamsToChat(params: GenerationParams | undefined, provider = "chat", modelRef = ""): Record<string, unknown> {
418
- const out: Record<string, unknown> = {};
419
- if (!params) return out;
420
- if (finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0) out["max_tokens"] = Math.floor(params.maxOutputTokens);
421
- if (params.stop && params.stop.length > 0) {
422
- out["stop"] = params.stop.slice(0, MAX_CHAT_STOP);
423
- if (params.stop.length > MAX_CHAT_STOP) {
424
- emitProviderWarning({
425
- provider,
426
- capability: "stop sequences",
427
- requested: `${params.stop.length} sequences (${modelRef})`,
428
- reason: `Chat Completions allows at most ${MAX_CHAT_STOP} stop sequences.`,
429
- fallback: `the first ${MAX_CHAT_STOP}`,
430
- });
431
- }
432
- }
433
- if (finiteNumber(params.seed)) out["seed"] = Math.floor(params.seed);
434
- return out;
399
+ /** Chat Completions (OpenRouter + generic compat endpoints): `max_tokens`, only when the caller set it, so strict endpoints never see anything unasked. */
400
+ export function mapGenerationParamsToChat(params: GenerationParams | undefined): { max_tokens?: number } {
401
+ const cap = outputTokenCap(params);
402
+ return cap === undefined ? {} : { max_tokens: cap };
435
403
  }
436
404
 
437
- // Warns that `stop`/`seed` have no parameter on this wire and are dropped; in: the OpenAI and Google mappers, out: provider_warning events.
438
- function warnUnsupportedStopSeed(provider: string, modelRef: string, params: GenerationParams, reasonSuffix: string): void {
439
- if (params.stop && params.stop.length > 0) {
440
- emitProviderWarning({
441
- provider,
442
- capability: "stop sequences",
443
- requested: `${params.stop.join("|")} (${modelRef})`,
444
- reason: `the ${reasonSuffix} defines no stop-sequence parameter.`,
445
- fallback: "no stop sequences (the model decides when to stop)",
446
- });
447
- }
448
- if (finiteNumber(params.seed)) {
449
- emitProviderWarning({
450
- provider,
451
- capability: "seed",
452
- requested: `${params.seed} (${modelRef})`,
453
- reason: `the ${reasonSuffix} defines no seed parameter.`,
454
- fallback: "non-deterministic sampling",
455
- });
456
- }
405
+ /** OpenAI Responses: `max_output_tokens`. */
406
+ export function mapGenerationParamsToOpenAI(params: GenerationParams | undefined): { max_output_tokens?: number } {
407
+ const cap = outputTokenCap(params);
408
+ return cap === undefined ? {} : { max_output_tokens: cap };
457
409
  }
458
410
 
459
- /** OpenAI Responses: `max_output_tokens` is native; `stop` and `seed` do not exist on this wire (warn + drop). */
460
- export function mapGenerationParamsToOpenAI(
461
- params: GenerationParams | undefined,
462
- modelRef: string
463
- ): { max_output_tokens?: number } {
464
- if (!params) return {};
465
- warnUnsupportedStopSeed("openai", modelRef, params, "OpenAI Responses API");
466
- return finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0
467
- ? { max_output_tokens: Math.floor(params.maxOutputTokens) }
468
- : {};
469
- }
470
-
471
- /** Google Interactions `generation_config`: `max_output_tokens` is documented; `stop` and `seed` are not (warn + drop). */
472
- export function mapGenerationParamsToGoogle(
473
- params: GenerationParams | undefined,
474
- modelRef: string
475
- ): { max_output_tokens?: number } {
476
- if (!params) return {};
477
- warnUnsupportedStopSeed("google", modelRef, params, "Gemini Interactions API");
478
- return finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0
479
- ? { max_output_tokens: Math.floor(params.maxOutputTokens) }
480
- : {};
411
+ /** Google Interactions `generation_config`: `max_output_tokens`. */
412
+ export function mapGenerationParamsToGoogle(params: GenerationParams | undefined): { max_output_tokens?: number } {
413
+ const cap = outputTokenCap(params);
414
+ return cap === undefined ? {} : { max_output_tokens: cap };
481
415
  }
482
416
 
483
417
  // ---------------------------------------------------------------------------
@@ -488,12 +422,7 @@ export function mapGenerationParamsToGoogle(
488
422
  function isZodLike(value: unknown): boolean {
489
423
  if (!value || typeof value !== "object") return false;
490
424
  const v = value as Record<string, unknown>;
491
- return (
492
- typeof v["safeParse"] === "function" ||
493
- typeof v["parse"] === "function" ||
494
- "_def" in v ||
495
- "~standard" in v
496
- );
425
+ return typeof v["safeParse"] === "function" || typeof v["parse"] === "function" || "_def" in v || "~standard" in v;
497
426
  }
498
427
 
499
428
  // True when a plain object has JSON Schema keywords; in: structured-output normalization, out: a boolean.
@@ -575,7 +504,9 @@ export function normalizeStructuredOutput(output: unknown): StructuredOutputSpec
575
504
  * present (bare Zod schema or `{ schema: Zod }`). Used by the loop for
576
505
  * post-parse validation; `undefined` means JSON-parse only.
577
506
  */
578
- export function extractOutputValidator(output: unknown): { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } } | undefined {
507
+ export function extractOutputValidator(
508
+ output: unknown
509
+ ): { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } } | undefined {
579
510
  if (!output) return undefined;
580
511
  if (isZodLike(output) && typeof (output as { safeParse?: unknown }).safeParse === "function") {
581
512
  return output as { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } };
@@ -637,9 +568,17 @@ export function parseStructuredOutput(text: string, output: unknown): unknown {
637
568
  * Maps a normalized spec onto OpenAI Responses `text.format`.
638
569
  * `{ type: json_schema, name, schema, strict }` (docs: responses/create).
639
570
  */
640
- export function mapOutputToOpenAI(
641
- spec: StructuredOutputSpec | undefined
642
- ): { format: { type: "json_schema"; name: string; schema: Record<string, unknown>; strict: boolean; description?: string } } | undefined {
571
+ export function mapOutputToOpenAI(spec: StructuredOutputSpec | undefined):
572
+ | {
573
+ format: {
574
+ type: "json_schema";
575
+ name: string;
576
+ schema: Record<string, unknown>;
577
+ strict: boolean;
578
+ description?: string;
579
+ };
580
+ }
581
+ | undefined {
643
582
  if (!spec) return undefined;
644
583
  return {
645
584
  format: {
@@ -670,8 +609,10 @@ export function mapOutputToGoogle(
670
609
  }
671
610
 
672
611
  /** Chat Completions `response_format` wire shape (OpenRouter + compat). */
673
- export type ChatCompletionsResponseFormat =
674
- | { type: "json_schema"; json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string } };
612
+ export type ChatCompletionsResponseFormat = {
613
+ type: "json_schema";
614
+ json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string };
615
+ };
675
616
 
676
617
  /**
677
618
  * Maps a normalized spec onto Chat Completions `response_format`.
@@ -2,20 +2,21 @@
2
2
 
3
3
  Scheduled actions: a worker that runs later (once or repeating) and saves its answer to a file. Off unless `dynamicSubagents.allowScheduledActions` is set.
4
4
 
5
- | File | What it is |
6
- | :-- | :-- |
7
- | `scheduler.ts` | One `Scheduler` per home folder: a 1 s unref'd clock that runs only while something is pending, startup sweep, cook/deliver state machine, and the public functions (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`). |
8
- | `store.ts` | Disk layer under `<home>/scheduled/`: `<id>/task.json`, `<id>/delivery.json`, run files, one short mutex per action; validation of records. |
9
- | `time.ts` | Pure `DD:HH:MM` and local-clock maths. No timezone, DST or `Intl`; `now` is always passed in. |
10
- | `tool.ts` + `scheduled-actions.md` | The model-facing `scheduled_actions` tool and `get_datetime`; the `.md` is the tool description. |
5
+ | File | What it is |
6
+ | :--------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `scheduler.ts` | One `Scheduler` per home folder: a 1 s unref'd clock that runs only while something is pending, startup sweep, dead-owner recovery, heartbeats, cook/deliver state machine, and the public functions (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`). |
8
+ | `store.ts` | Disk layer under `<home>/scheduled/`: `<id>/task.json`, `<id>/delivery.json`, run files, one short mutex per action; validation of records. |
9
+ | `time.ts` | Pure `DD:HH:MM` and local-clock maths. No timezone, DST or `Intl`; `now` is always passed in. |
10
+ | `tool.ts` + `scheduled-actions.md` | The model-facing `scheduled_actions` tool and `get_datetime`; the `.md` is the tool description. |
11
11
 
12
12
  ## Invariants
13
+
13
14
  - The files are the truth: every write re-reads first and goes through the durable writer. The file format is a stable public contract: see `docs/scheduled-action-files.md` and add a changelog row for any change.
14
- - Write order when a run ends: run file, then `<id>/delivery.json`, then `<id>/task.json`. The startup sweep repairs a crash in between.
15
+ - Write order when a run ends: run file, then `<id>/delivery.json`, then `<id>/task.json`. A crash in between is repaired by the dead-owner recovery (`recoverCooking`, at start and every 15 s): the delivery is rebuilt from the run file, or the run is marked `missed` when no run file exists.
15
16
  - `startCook` advances the next times BEFORE the run starts; a run still cooking when the next is due is skipped, never stacked.
16
17
  - Runs are fresh stateless workers built by `buildDynamicWorkerConfig`; their spending guardrail is `workerBudget` only.
17
18
  - The Agent registry holds `WeakRef`s by name: keep every Agent that owns actions referenced, or its runs become `missed`.
18
- - Several processes may share a home (the file store is safe for it; the home lock is lifted in a later change of this release). Ownership: a process only starts, delivers or recovers actions whose Agent it registered. An action nobody here owns is left alone until its delivery time passed with no run started, then marked `missed` (`no_agent`).
19
+ - Several processes may share a home: scheduled actions take no home lock, only the short per-action mutex (`<id>/.mutex`); the one-process lock is on session folders only. Ownership: a process only starts, delivers or recovers actions whose Agent it registered. An action nobody here owns is left alone until its delivery time passed with no run started, then marked `missed` (`no_agent`).
19
20
  - Exactly once: a scheduled start re-checks under the action's mutex that it is still `waiting` and due; `markMissed` only changes the state the caller saw; a delivery is emitted only by the process that removed its entry from `delivery.json`.
20
21
  - A run in flight keeps `<id>/.heartbeat` (pid, host, token; mtime refreshed every 5 s, stale after 60 s), written before the record shows `cooking`. A cooking action whose owner is dead (pid gone, stale, or none) is recovered by a live process that owns its Agent, at start and every 15 s. `delete` of a run another live process is cooking is refused, even with `force`.
21
22
  - Times come from `Date`, local clock only. Tests inject `scheduler.now` / `tickMs` and call `tick()`.
@@ -3,6 +3,7 @@ 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
+
6
7
  - 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
8
  - list: every action with its metadata and state (waiting, cooking, missed, done).
8
9
  - get: one action by `id`, with its latest run result.
@@ -12,6 +13,7 @@ COMMANDS (one tool, the `command` field)
12
13
  - skip: ignore a missed run of `id` and wait for the next scheduled time.
13
14
 
14
15
  SCHEDULED_ACTION FIELDS
16
+
15
17
  - name: human label.
16
18
  - id: lowercase kebab-case, at most 48 characters, e.g. "daily-ai-brief". The SDK appends 6 characters ("daily-ai-brief-d64aj2"); use that full id in every later command.
17
19
  - timeToDeliver: "DD:HH:MM". DD = days from today (00 today, 01 tomorrow, 55 = 55 days ahead). HH:MM = clock time on a 12-hour clock, HH 01-12.
@@ -30,12 +32,14 @@ Weekly report, every Monday at 9:00 AM (use get_datetime to count the days to th
30
32
  {"command":"create","instructions":"You review a codebase and report its health.","task":"Report test health, open risks and notable changes this week.","scheduled_action":{"name":"Weekly Codebase Health Report","id":"weekly-code-health","timeToDeliver":"03:09:00","meridiem":"AM","startCookingInAdvance":"00:02:00","repeatEvery":"07:00:00"}}
31
33
 
32
34
  TIMING ADVICE (do not guess; talk to the user)
35
+
33
36
  - A run must finish before the next one is due, so startCookingInAdvance must be shorter than repeatEvery.
34
37
  - Pick the lead time from the job: a quick lookup fits the 10-minute lead ("00:00:10"); research with many tools needs much more.
35
38
  - If the job looks too heavy for the gap the user asked for, do not schedule it blindly. Say so and suggest a wider gap, an earlier start, or a lighter task.
36
39
  - A run still cooking when the next one is due is skipped, never stacked, so a gap that is too small means lost runs.
37
40
 
38
41
  RULES
42
+
39
43
  - Each run starts blank and remembers nothing from earlier runs: put everything the worker needs in instructions and task.
40
44
  - Results go to files, not into this chat. Tell the user the action is scheduled and when it will be delivered.
41
45
  - Never put secrets in instructions or task: they are stored as plain text.