agent-accelerator 0.4.0 → 0.5.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 (46) hide show
  1. package/README.md +149 -57
  2. package/bunfig.toml +1 -0
  3. package/package.json +2 -3
  4. package/src/agent/agent.ts +414 -118
  5. package/src/agent/context.ts +11 -16
  6. package/src/agent/delegation.ts +111 -274
  7. package/src/agent/loop.ts +327 -99
  8. package/src/agent/subagent.ts +4 -12
  9. package/src/agent/worker.ts +435 -0
  10. package/src/index.ts +58 -5
  11. package/src/models/catalog-cache.ts +65 -25
  12. package/src/models/catalog.ts +47 -2
  13. package/src/providers/google.ts +338 -81
  14. package/src/providers/openai-compat.ts +171 -61
  15. package/src/providers/openai.ts +174 -44
  16. package/src/providers/openrouter.ts +129 -44
  17. package/src/providers/shared.ts +169 -17
  18. package/src/providers.ts +375 -11
  19. package/src/scheduler/scheduled-actions.md +41 -0
  20. package/src/scheduler/scheduler.ts +814 -0
  21. package/src/scheduler/store.ts +263 -0
  22. package/src/scheduler/time.ts +148 -0
  23. package/src/scheduler/tool.ts +151 -0
  24. package/src/session/store.ts +229 -113
  25. package/src/streaming/event-stream.ts +6 -4
  26. package/src/telemetry-file.ts +31 -0
  27. package/src/telemetry.ts +65 -0
  28. package/src/tools/executor.ts +163 -38
  29. package/src/tools/subagent-result.ts +60 -0
  30. package/src/types/agent.ts +74 -22
  31. package/src/types/model.ts +39 -0
  32. package/src/types/response.ts +9 -0
  33. package/src/types/scheduler.ts +81 -0
  34. package/src/types/tool.ts +15 -0
  35. package/src/utils/cache.ts +14 -5
  36. package/src/utils/documents.ts +112 -52
  37. package/src/utils/env.ts +21 -7
  38. package/src/utils/errors.ts +26 -1
  39. package/src/utils/headers.ts +15 -15
  40. package/src/utils/home.ts +30 -0
  41. package/src/utils/media.ts +57 -12
  42. package/src/utils/raw-capture.ts +76 -0
  43. package/src/utils/retry.ts +11 -3
  44. package/src/utils/safe-io.ts +252 -0
  45. package/src/utils/session.ts +7 -8
  46. package/src/data/README.md +0 -84
package/src/providers.ts CHANGED
@@ -10,6 +10,11 @@
10
10
  * redesigning this abstraction — only new per-provider mappers/adapters.
11
11
  */
12
12
  import type { CacheConfig, ServiceTier, ThinkingLevel } from "./types/core.ts";
13
+ import type { StructuredOutputSpec } from "./types/model.ts";
14
+ import { zodToJsonSchema } from "./tools/schema.ts";
15
+ import { ValidationError } from "./types/errors.ts";
16
+ import { TRUNCATED_ARGS_KEY } from "./types/tool.ts";
17
+ import { telemetryBus } from "./telemetry.ts";
13
18
 
14
19
  /** Classification of a canonical capability on a given provider. */
15
20
  export type ProviderCapabilityStatus =
@@ -47,11 +52,19 @@ export function emitProviderWarning(options: {
47
52
  if (emittedWarnings.has(key)) return;
48
53
  emittedWarnings.add(key);
49
54
  const requested = options.requested ? ` (requested: "${options.requested}")` : "";
50
- // Leading newline: warnings fire at the start of a turn's request build,
51
- // when the previous turn's streamed text may have left the terminal cursor
52
- // mid-line. Without it the warning glues onto streamed output.
53
- // Q-41 dual: telemetry bus (new) + console.warn (compat for existing captureWarnings tests; /cli logger replaces in 0.5).
54
- { const msg = `[Agent Accelerator] WARNING [${options.provider}] ${options.capability}${requested}: ${options.reason} Using ${options.fallback} instead.`; try { (globalThis as any).__agentAccelTelemetry?.emitWarning?.({ code: "provider_warning", provider: options.provider, capability: options.capability, message: msg }); } catch {} try { (globalThis as any).__agentAccelWarnSink?.(msg, options); } catch {} try { console.warn(msg); } catch {} }
55
+ // Primary channel: the telemetry bus (code `provider_warning`). The legacy
56
+ // global sink and `console.warn` remain for existing capture-based tests and
57
+ // CLIs that predate the bus.
58
+ const msg = `[Agent Accelerator] WARNING [${options.provider}] ${options.capability}${requested}: ${options.reason} Using ${options.fallback} instead.`;
59
+ telemetryBus.emitWarning("provider_warning", msg, {
60
+ provider: options.provider,
61
+ capability: options.capability,
62
+ requested: options.requested,
63
+ reason: options.reason,
64
+ fallback: options.fallback,
65
+ });
66
+ try { (globalThis as any).__agentAccelWarnSink?.(msg, options); } catch {}
67
+ try { console.warn(msg); } catch {}
55
68
  }
56
69
 
57
70
  /** Clears deduped-warning state (mainly for tests). */
@@ -357,17 +370,17 @@ export function mapServiceTierToOpenAI(tier: ServiceTier | undefined): "flex" |
357
370
 
358
371
  /**
359
372
  * Applies canonical cache config for OpenAI. Session affinity flows via
360
- * `prompt_cache_key` (handled by the adapter); explicit retention control
361
- * lives in `prompt_cache_options` (gpt-5.6+ explicit breakpoints) which is
362
- * out of scope for the most-important subset, so retention/cachedContentId
363
- * are UNSUPPORTED: warn and drop. `prompt_cache_retention` is deprecated.
373
+ * `prompt_cache_key` (handled by the adapter); `long` retention maps to
374
+ * `prompt_cache_retention: "24h"` when the catalog supports it (handled by
375
+ * the adapter, pi `api/openai-responses.ts:97` rule). Other explicit
376
+ * retention modes and `cachedContentId` are UNSUPPORTED: warn and drop.
364
377
  */
365
378
  export function applyCacheForOpenAI(
366
379
  cache: CacheConfig | undefined,
367
380
  modelRef: string
368
381
  ): void {
369
382
  if (!cache) return;
370
- if (cache.retention && cache.retention !== "implicit") {
383
+ if (cache.retention && cache.retention !== "implicit" && cache.retention !== "long") {
371
384
  emitProviderWarning({
372
385
  provider: "openai",
373
386
  capability: "cache retention",
@@ -487,5 +500,356 @@ export function parseStreamedToolArguments(startText: string, deltaText: string)
487
500
  // try next candidate
488
501
  }
489
502
  }
490
- return { raw: startText + deltaText };
503
+ return { [TRUNCATED_ARGS_KEY]: startText + deltaText };
504
+ }
505
+
506
+ /**
507
+ * Returns the wire string that actually parsed in
508
+ * {@link parseStreamedToolArguments} (identical candidate trial order), so
509
+ * `rawArguments` audit trails never store concatenation artifacts like
510
+ * `"{}{...}"` (empty start payload + complete delta). Falls back to the raw
511
+ * concatenation when nothing parses, preserving the truncated-call marker
512
+ * path (`{ __truncated_raw }`, never executed).
513
+ *
514
+ * @example `selectRawArguments("{}", '{"location":"Paris"}') // '{"location":"Paris"}'`
515
+ */
516
+ export function selectRawArguments(startText: string, deltaText: string): string {
517
+ const candidates = [startText + deltaText, deltaText, startText].filter((c) => c && c.trim());
518
+ for (const candidate of candidates) {
519
+ try {
520
+ const parsed: unknown = JSON.parse(candidate);
521
+ if (isPlainObject(parsed)) return candidate;
522
+ } catch {
523
+ // try next candidate
524
+ }
525
+ }
526
+ return startText + deltaText;
527
+ }
528
+
529
+ // ---------------------------------------------------------------------------
530
+ // Generation controls (maxOutputTokens / stop / seed)
531
+ // ---------------------------------------------------------------------------
532
+
533
+ /** Canonical generation controls as carried on `ProviderRequestOptions`. */
534
+ export interface GenerationParams {
535
+ maxOutputTokens?: number;
536
+ stop?: string[];
537
+ seed?: number;
538
+ }
539
+
540
+ const finiteNumber = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
541
+
542
+ /** Normalizes the public `stop` option (`string | string[]`) to a clean array, or undefined. */
543
+ export function normalizeStop(stop: string | string[] | undefined): string[] | undefined {
544
+ const list = (Array.isArray(stop) ? stop : stop === undefined ? [] : [stop]).filter(
545
+ (s): s is string => typeof s === "string" && s.length > 0
546
+ );
547
+ return list.length > 0 ? list : undefined;
548
+ }
549
+
550
+ /**
551
+ * Standard Chat Completions fields (OpenRouter + generic compat endpoints):
552
+ * `max_tokens`, `stop`, `seed`. Only set values are
553
+ * emitted, so strict endpoints never see anything the caller did not ask for.
554
+ */
555
+ export function mapGenerationParamsToChat(params: GenerationParams | undefined): Record<string, unknown> {
556
+ const out: Record<string, unknown> = {};
557
+ if (!params) return out;
558
+ if (finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0) out["max_tokens"] = Math.floor(params.maxOutputTokens);
559
+ if (params.stop && params.stop.length > 0) out["stop"] = params.stop;
560
+ if (finiteNumber(params.seed)) out["seed"] = Math.floor(params.seed);
561
+ return out;
562
+ }
563
+
564
+ function warnUnsupportedStopSeed(provider: string, modelRef: string, params: GenerationParams, reasonSuffix: string): void {
565
+ if (params.stop && params.stop.length > 0) {
566
+ emitProviderWarning({
567
+ provider,
568
+ capability: "stop sequences",
569
+ requested: `${params.stop.join("|")} (${modelRef})`,
570
+ reason: `the ${reasonSuffix} defines no stop-sequence parameter.`,
571
+ fallback: "no stop sequences (the model decides when to stop)",
572
+ });
573
+ }
574
+ if (finiteNumber(params.seed)) {
575
+ emitProviderWarning({
576
+ provider,
577
+ capability: "seed",
578
+ requested: `${params.seed} (${modelRef})`,
579
+ reason: `the ${reasonSuffix} defines no seed parameter.`,
580
+ fallback: "non-deterministic sampling",
581
+ });
582
+ }
583
+ }
584
+
585
+ /** OpenAI Responses: `max_output_tokens` is native; `stop` and `seed` do not exist on this wire (warn + drop). */
586
+ export function mapGenerationParamsToOpenAI(
587
+ params: GenerationParams | undefined,
588
+ modelRef: string
589
+ ): { max_output_tokens?: number } {
590
+ if (!params) return {};
591
+ warnUnsupportedStopSeed("openai", modelRef, params, "OpenAI Responses API");
592
+ return finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0
593
+ ? { max_output_tokens: Math.floor(params.maxOutputTokens) }
594
+ : {};
595
+ }
596
+
597
+ /** Google Interactions `generation_config`: `max_output_tokens` is documented; `stop` and `seed` are not (warn + drop). */
598
+ export function mapGenerationParamsToGoogle(
599
+ params: GenerationParams | undefined,
600
+ modelRef: string
601
+ ): { max_output_tokens?: number } {
602
+ if (!params) return {};
603
+ warnUnsupportedStopSeed("google", modelRef, params, "Gemini Interactions API");
604
+ return finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0
605
+ ? { max_output_tokens: Math.floor(params.maxOutputTokens) }
606
+ : {};
607
+ }
608
+
609
+ // ---------------------------------------------------------------------------
610
+ // Structured outputs (canonical, pure, no I/O)
611
+ // ---------------------------------------------------------------------------
612
+
613
+ function isZodLike(value: unknown): boolean {
614
+ if (!value || typeof value !== "object") return false;
615
+ const v = value as Record<string, unknown>;
616
+ return (
617
+ typeof v["safeParse"] === "function" ||
618
+ typeof v["parse"] === "function" ||
619
+ "_def" in v ||
620
+ "~standard" in v
621
+ );
622
+ }
623
+
624
+ function looksLikeJsonSchema(value: unknown): boolean {
625
+ if (!isPlainObject(value)) return false;
626
+ const v = value as Record<string, unknown>;
627
+ return (
628
+ typeof v["type"] === "string" ||
629
+ isPlainObject(v["properties"]) ||
630
+ Array.isArray(v["anyOf"]) ||
631
+ Array.isArray(v["oneOf"]) ||
632
+ Array.isArray(v["allOf"]) ||
633
+ typeof v["$schema"] === "string" ||
634
+ isPlainObject(v["$defs"]) ||
635
+ isPlainObject(v["definitions"])
636
+ );
637
+ }
638
+
639
+ function sanitizeOutputName(raw: unknown): string {
640
+ const base = typeof raw === "string" && raw.trim() ? raw.trim() : "structured_output";
641
+ const cleaned = base.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "structured_output";
642
+ return cleaned;
643
+ }
644
+
645
+ function schemaToJsonSchema(schema: unknown, what: string): Record<string, unknown> {
646
+ if (isZodLike(schema)) {
647
+ try {
648
+ const converted = zodToJsonSchema(schema);
649
+ if (isPlainObject(converted)) return converted;
650
+ } catch {
651
+ // fall through to error below
652
+ }
653
+ }
654
+ if (isPlainObject(schema)) return schema as Record<string, unknown>;
655
+ throw new ValidationError(
656
+ `[Agent Accelerator] Invalid structured output ${what}: expected a Zod schema, a JSON Schema object, or { name, schema, strict }.`
657
+ );
658
+ }
659
+
660
+ /**
661
+ * Normalizes `AgentConfig.output` into a provider-agnostic spec.
662
+ *
663
+ * Accepts a Zod schema, a JSON Schema object, or an explicit
664
+ * `{ name?, description?, schema, strict? }` wrapper. Returns `undefined`
665
+ * when no output was requested. Throws `ValidationError` for invalid shapes.
666
+ *
667
+ * @example `normalizeStructuredOutput(z.object({ city: z.string() }))`
668
+ */
669
+ export function normalizeStructuredOutput(output: unknown): StructuredOutputSpec | undefined {
670
+ if (output === undefined || output === null) return undefined;
671
+ if (isPlainObject(output) && "schema" in (output as Record<string, unknown>)) {
672
+ const rec = output as Record<string, unknown>;
673
+ const schema = schemaToJsonSchema(rec["schema"], "`output.schema`");
674
+ return {
675
+ name: sanitizeOutputName(rec["name"]),
676
+ schema,
677
+ strict: typeof rec["strict"] === "boolean" ? (rec["strict"] as boolean) : true,
678
+ ...(typeof rec["description"] === "string" && (rec["description"] as string).trim()
679
+ ? { description: (rec["description"] as string).trim() }
680
+ : {}),
681
+ };
682
+ }
683
+ if (isZodLike(output) || looksLikeJsonSchema(output)) {
684
+ return {
685
+ name: "structured_output",
686
+ schema: schemaToJsonSchema(output, "`output`"),
687
+ strict: true,
688
+ };
689
+ }
690
+ throw new ValidationError(
691
+ "[Agent Accelerator] Invalid structured output: expected a Zod schema, a JSON Schema object, or { name, schema, strict }."
692
+ );
693
+ }
694
+
695
+ /**
696
+ * Extracts the original Zod validator from a canonical output option, when
697
+ * present (bare Zod schema or `{ schema: Zod }`). Used by the loop for
698
+ * post-parse validation; `undefined` means JSON-parse only.
699
+ */
700
+ export function extractOutputValidator(output: unknown): { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } } | undefined {
701
+ if (!output) return undefined;
702
+ if (isZodLike(output) && typeof (output as { safeParse?: unknown }).safeParse === "function") {
703
+ return output as { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } };
704
+ }
705
+ if (isPlainObject(output) && isZodLike((output as Record<string, unknown>)["schema"])) {
706
+ const inner = (output as Record<string, unknown>)["schema"] as { safeParse?: unknown };
707
+ if (typeof inner.safeParse === "function") {
708
+ return inner as { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } };
709
+ }
710
+ }
711
+ return undefined;
712
+ }
713
+
714
+ function stripCodeFences(text: string): string {
715
+ const trimmed = text.trim();
716
+ const fenced = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/i);
717
+ if (fenced && typeof fenced[1] === "string") return fenced[1].trim();
718
+ return trimmed;
719
+ }
720
+
721
+ /**
722
+ * Parses and validates a final answer against a canonical output option.
723
+ * Strips markdown fences, requires valid JSON, then applies the Zod validator
724
+ * when the original option carried one. Throws `ValidationError` on mismatch.
725
+ *
726
+ * @example `parseStructuredOutput('{"city":"Paris"}', z.object({ city: z.string() }))`
727
+ */
728
+ export function parseStructuredOutput(text: string, output: unknown): unknown {
729
+ const cleaned = stripCodeFences(typeof text === "string" ? text : String(text ?? ""));
730
+ if (!cleaned) {
731
+ throw new ValidationError(
732
+ "[Agent Accelerator] Structured output validation failed: model returned empty text, expected JSON matching the output schema."
733
+ );
734
+ }
735
+ let parsed: unknown;
736
+ try {
737
+ parsed = JSON.parse(cleaned);
738
+ } catch {
739
+ throw new ValidationError(
740
+ `[Agent Accelerator] Structured output validation failed: model text is not valid JSON (${cleaned.slice(0, 120)}).`
741
+ );
742
+ }
743
+ const validator = extractOutputValidator(output);
744
+ if (validator) {
745
+ const res = validator.safeParse(parsed);
746
+ if (!res.success) {
747
+ const detail = res.error?.message ? ` ${res.error.message}`.slice(0, 500) : "";
748
+ throw new ValidationError(
749
+ `[Agent Accelerator] Structured output validation failed: JSON does not match the output schema.${detail}`
750
+ );
751
+ }
752
+ return res.data ?? parsed;
753
+ }
754
+ return parsed;
755
+ }
756
+
757
+ /**
758
+ * Maps a normalized spec onto OpenAI Responses `text.format`.
759
+ * `{ type: json_schema, name, schema, strict }` (docs: responses/create).
760
+ */
761
+ export function mapOutputToOpenAI(
762
+ spec: StructuredOutputSpec | undefined
763
+ ): { format: { type: "json_schema"; name: string; schema: Record<string, unknown>; strict: boolean; description?: string } } | undefined {
764
+ if (!spec) return undefined;
765
+ return {
766
+ format: {
767
+ type: "json_schema",
768
+ name: spec.name,
769
+ schema: spec.schema,
770
+ strict: spec.strict ?? true,
771
+ ...(spec.description ? { description: spec.description } : {}),
772
+ },
773
+ };
774
+ }
775
+
776
+ /**
777
+ * Maps a normalized spec onto Google Interactions `response_format`.
778
+ * `{ type: text, mime_type: application/json, schema }` (docs: structured-output).
779
+ */
780
+ export function mapOutputToGoogle(
781
+ spec: StructuredOutputSpec | undefined
782
+ ): { responseFormat: { type: "text"; mime_type: "application/json"; schema: Record<string, unknown> } } | undefined {
783
+ if (!spec) return undefined;
784
+ return {
785
+ responseFormat: {
786
+ type: "text",
787
+ mime_type: "application/json",
788
+ schema: spec.schema,
789
+ },
790
+ };
791
+ }
792
+
793
+ /** Chat Completions `response_format` wire shape (OpenRouter + compat). */
794
+ export type ChatCompletionsResponseFormat =
795
+ | { type: "json_schema"; json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string } };
796
+
797
+ /**
798
+ * Maps a normalized spec onto Chat Completions `response_format`.
799
+ * `{ type: json_schema, json_schema: { name, schema, strict } }`
800
+ * (docs: openai completions/create, openrouter parameters).
801
+ */
802
+ export function mapOutputToOpenRouterChat(
803
+ spec: StructuredOutputSpec | undefined
804
+ ): { response_format: ChatCompletionsResponseFormat } | undefined {
805
+ if (!spec) return undefined;
806
+ return {
807
+ response_format: {
808
+ type: "json_schema",
809
+ json_schema: {
810
+ name: spec.name,
811
+ schema: spec.schema,
812
+ strict: spec.strict ?? true,
813
+ ...(spec.description ? { description: spec.description } : {}),
814
+ },
815
+ },
816
+ };
817
+ }
818
+
819
+ /**
820
+ * Maps a normalized spec onto generic compat Chat Completions
821
+ * `response_format` (same standard shape as OpenRouter Chat).
822
+ */
823
+ export function mapOutputToCompatChat(
824
+ spec: StructuredOutputSpec | undefined
825
+ ): { response_format: ChatCompletionsResponseFormat } | undefined {
826
+ return mapOutputToOpenRouterChat(spec);
827
+ }
828
+
829
+ /**
830
+ * Builds the emulated structured-output instruction for models without
831
+ * native `response_format` support (catalog `structured_output: false`).
832
+ * Appended to the system prompt so the model still targets JSON; the loop
833
+ * still validates into `AgentResponse.parsed`.
834
+ *
835
+ * @example `augmentSystemPromptWithStructuredOutput(sys, spec)`
836
+ */
837
+ export function buildStructuredOutputInstruction(spec: StructuredOutputSpec): string {
838
+ return `Return ONLY valid JSON matching this JSON Schema, with no markdown fences, no commentary, no extra text.\nSchema: ${JSON.stringify(spec.schema)}`;
839
+ }
840
+
841
+ /**
842
+ * Returns a system prompt with the emulated structured-output instruction
843
+ * appended (stable per Agent since `output` is agent-level, so prefix-cache
844
+ * affinity is preserved). No-op when spec is undefined.
845
+ */
846
+ export function augmentSystemPromptWithStructuredOutput(
847
+ systemPrompt: string | undefined,
848
+ spec: StructuredOutputSpec | undefined
849
+ ): string | undefined {
850
+ if (!spec) return systemPrompt;
851
+ const instruction = `[Structured Output]\n${buildStructuredOutputInstruction(spec)}`;
852
+ if (!systemPrompt) return instruction;
853
+ if (systemPrompt.includes("[Structured Output]")) return systemPrompt;
854
+ return `${systemPrompt}\n\n${instruction}`;
491
855
  }
@@ -0,0 +1,41 @@
1
+ Schedules a sub-agent (a "scheduled action") to run LATER, once or on repeat, on its own. Every run starts blank, does its task with the worker tools, and saves its answer to a file. It does NOT reply in this chat. Use it for deliveries such as "a research brief every day at 7:00 AM" or "a codebase health report every Monday at 9:00 AM".
2
+
3
+ Call get_datetime first whenever you create or reschedule, then pick DD from today's date.
4
+
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.
7
+ - list: every action with its metadata and state (waiting, cooking, missed, done).
8
+ - get: one action by `id`, with its latest run result.
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.
11
+ - run_now: run the action `id` immediately, outside its schedule. Also fixes a missed run.
12
+ - skip: ignore a missed run of `id` and wait for the next scheduled time.
13
+
14
+ SCHEDULED_ACTION FIELDS
15
+ - name: human label.
16
+ - 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
+ - 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.
18
+ - meridiem: "AM" or "PM" for timeToDeliver.
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
+ - repeatEvery (optional): "DD:HH:MM" gap between deliveries. "01:00:00" = every day, "07:00:00" = every week. Omit to run once.
21
+ - retries (optional): extra attempts if a run fails, used only while there is still time before delivery. Default 0.
22
+
23
+ EXAMPLES
24
+ Daily brief, first delivery tomorrow at 7:00 AM, starting 30 minutes ahead:
25
+ {"command":"create","instructions":"You write a short, sourced AI research brief.","task":"Summarize the most important AI news of the last day in 5 bullets.","scheduled_action":{"name":"Daily AI Research Brief","id":"daily-ai-brief","timeToDeliver":"01:07:00","meridiem":"AM","startCookingInAdvance":"00:00:30","repeatEvery":"01:00:00"}}
26
+
27
+ Weekly report, every Monday at 9:00 AM (use get_datetime to count the days to the next Monday), starting 2 hours ahead:
28
+ {"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"}}
29
+
30
+ TIMING ADVICE (do not guess; talk to the user)
31
+ - A run must finish before the next one is due, so startCookingInAdvance must be shorter than repeatEvery.
32
+ - 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.
33
+ - 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.
34
+ - A run still cooking when the next one is due is skipped, never stacked, so a gap that is too small means lost runs.
35
+
36
+ RULES
37
+ - Each run starts blank and remembers nothing from earlier runs: put everything the worker needs in instructions and task.
38
+ - Results go to files, not into this chat. Tell the user the action is scheduled and when it will be delivered.
39
+ - Never put secrets in instructions or task: they are stored as plain text.
40
+ - Actions run only while the host program is running.
41
+ - Worker tools you may grant through `tools`: {{TOOLS}}.