@avocadostudio-ai/orchestrator-core 0.29.0 → 0.30.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 (84) hide show
  1. package/dist/agents/assistant/assemble.d.ts +29 -0
  2. package/dist/agents/assistant/assemble.js +151 -0
  3. package/dist/agents/assistant/builtin-skills.d.ts +1 -0
  4. package/dist/agents/assistant/builtin-skills.js +149 -0
  5. package/dist/agents/assistant/connectors/analytics.d.ts +27 -0
  6. package/dist/agents/assistant/connectors/analytics.js +109 -0
  7. package/dist/agents/assistant/connectors/drive.d.ts +15 -0
  8. package/dist/agents/assistant/connectors/drive.js +203 -0
  9. package/dist/agents/assistant/connectors/google-access.d.ts +11 -0
  10. package/dist/agents/assistant/connectors/google-access.js +21 -0
  11. package/dist/agents/assistant/connectors/pagespeed.d.ts +39 -0
  12. package/dist/agents/assistant/connectors/pagespeed.js +103 -0
  13. package/dist/agents/assistant/connectors/types.d.ts +31 -0
  14. package/dist/agents/assistant/connectors/types.js +1 -0
  15. package/dist/agents/assistant/connectors.d.ts +14 -0
  16. package/dist/agents/assistant/connectors.js +66 -0
  17. package/dist/agents/assistant/context.d.ts +14 -0
  18. package/dist/agents/assistant/context.js +67 -0
  19. package/dist/agents/assistant/media-sink.d.ts +12 -0
  20. package/dist/agents/assistant/media-sink.js +19 -0
  21. package/dist/agents/assistant/next-steps.d.ts +10 -0
  22. package/dist/agents/assistant/next-steps.js +34 -0
  23. package/dist/agents/assistant/permissions.d.ts +62 -0
  24. package/dist/agents/assistant/permissions.js +76 -0
  25. package/dist/agents/assistant/planner-tool.d.ts +9 -0
  26. package/dist/agents/assistant/planner-tool.js +79 -0
  27. package/dist/agents/assistant/router.d.ts +20 -0
  28. package/dist/agents/assistant/router.js +69 -0
  29. package/dist/agents/assistant/routines.d.ts +85 -0
  30. package/dist/agents/assistant/routines.js +142 -0
  31. package/dist/agents/assistant/site-connections.d.ts +17 -0
  32. package/dist/agents/assistant/site-connections.js +51 -0
  33. package/dist/agents/assistant/skills.d.ts +31 -0
  34. package/dist/agents/assistant/skills.js +79 -0
  35. package/dist/agents/learning.js +21 -7
  36. package/dist/agents/runtime/apply.d.ts +29 -0
  37. package/dist/agents/runtime/apply.js +72 -0
  38. package/dist/agents/runtime/approve.d.ts +71 -0
  39. package/dist/agents/runtime/approve.js +145 -0
  40. package/dist/agents/runtime/definition.d.ts +88 -0
  41. package/dist/agents/runtime/definition.js +12 -0
  42. package/dist/agents/runtime/engine.d.ts +88 -0
  43. package/dist/agents/runtime/engine.js +322 -0
  44. package/dist/agents/runtime/model.d.ts +34 -0
  45. package/dist/agents/runtime/model.js +80 -0
  46. package/dist/agents/runtime/registry.d.ts +22 -0
  47. package/dist/agents/runtime/registry.js +14 -0
  48. package/dist/agents/runtime/service.d.ts +62 -0
  49. package/dist/agents/runtime/service.js +217 -0
  50. package/dist/agents/runtime/toolkit.d.ts +64 -0
  51. package/dist/agents/runtime/toolkit.js +479 -0
  52. package/dist/agents/search/follow-up.d.ts +11 -0
  53. package/dist/agents/search/follow-up.js +76 -0
  54. package/dist/agents/search/opportunities.d.ts +42 -0
  55. package/dist/agents/search/opportunities.js +153 -0
  56. package/dist/agents/search/tools.d.ts +31 -0
  57. package/dist/agents/search/tools.js +134 -0
  58. package/dist/agents/tick.d.ts +3 -0
  59. package/dist/agents/tick.js +12 -0
  60. package/dist/checks/session-runner.js +14 -3
  61. package/dist/connections/google-auth.d.ts +55 -0
  62. package/dist/connections/google-auth.js +159 -0
  63. package/dist/connections/google-oauth.d.ts +60 -0
  64. package/dist/connections/google-oauth.js +207 -0
  65. package/dist/connections/search-console.d.ts +76 -0
  66. package/dist/connections/search-console.js +129 -0
  67. package/dist/connections/secrets.d.ts +15 -0
  68. package/dist/connections/secrets.js +88 -0
  69. package/dist/durable/in-memory-durable-store.d.ts +15 -1
  70. package/dist/durable/in-memory-durable-store.js +110 -1
  71. package/dist/durable/sqlite-durable-store.d.ts +19 -1
  72. package/dist/durable/sqlite-durable-store.js +198 -0
  73. package/dist/durable/types.d.ts +84 -0
  74. package/dist/handler/auth.js +7 -1
  75. package/dist/handler/create-orchestrator.d.ts +6 -1
  76. package/dist/handler/create-orchestrator.js +171 -3
  77. package/dist/http/agent-runtime-actions.d.ts +58 -0
  78. package/dist/http/agent-runtime-actions.js +247 -0
  79. package/dist/http/assistant-actions.d.ts +83 -0
  80. package/dist/http/assistant-actions.js +366 -0
  81. package/dist/http/connections-actions.d.ts +41 -0
  82. package/dist/http/connections-actions.js +145 -0
  83. package/dist/nlp/deterministic-planner-suggestions.js +23 -2
  84. package/package.json +3 -3
@@ -0,0 +1,34 @@
1
+ const MAX_OPTIONS = 3;
2
+ const MAX_CHARS = 80;
3
+ const clean = (value) => (typeof value === "string" ? value.replace(/\s+/g, " ").trim().slice(0, MAX_CHARS) : "");
4
+ export const nextStepsTool = {
5
+ name: "next_steps",
6
+ description: "Offer the person up to three next steps as buttons under your answer. Call it in the same message as your answer, never alone. Write each the way they would type it, specific to this answer (usually what you just offered to do). Add `routine` only when this question is worth asking again on a schedule, such as a check of how the site is doing: write it with its cadence, e.g. 'Check Google clicks every Monday'. Never offer a routine for a one-off change.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ options: { type: "array", items: { type: "string" }, description: "Up to three, each a short sentence in the person's language." },
11
+ routine: { type: "string", description: "Optional: the same question on a schedule, with its cadence." }
12
+ },
13
+ required: ["options"]
14
+ },
15
+ run: async (input) => {
16
+ const options = (Array.isArray(input.options) ? input.options : []).map(clean).filter(Boolean).slice(0, MAX_OPTIONS);
17
+ const routine = clean(input.routine);
18
+ return { result: "Shown to the person.", trace: JSON.stringify({ options, ...(routine ? { routine } : {}) }) };
19
+ }
20
+ };
21
+ /** The chips from a run's trace lines: the last next_steps call's, or none. */
22
+ export function nextStepsFrom(calls) {
23
+ const call = [...calls].reverse().find((c) => c.name === "next_steps");
24
+ if (!call)
25
+ return null;
26
+ try {
27
+ const parsed = JSON.parse(call.result);
28
+ const options = Array.isArray(parsed.options) ? parsed.options.filter((o) => typeof o === "string") : [];
29
+ return { options, ...(typeof parsed.routine === "string" && parsed.routine ? { routine: parsed.routine } : {}) };
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import type { RuntimeTool } from "../runtime/definition.ts";
3
+ export type Permission = "allow" | "ask" | "never";
4
+ export declare const sitePermissionsSchema: z.ZodObject<{
5
+ propose: z.ZodDefault<z.ZodEnum<{
6
+ never: "never";
7
+ ask: "ask";
8
+ }>>;
9
+ remember: z.ZodDefault<z.ZodEnum<{
10
+ never: "never";
11
+ allow: "allow";
12
+ }>>;
13
+ ask: z.ZodDefault<z.ZodEnum<{
14
+ never: "never";
15
+ allow: "allow";
16
+ }>>;
17
+ report: z.ZodDefault<z.ZodEnum<{
18
+ never: "never";
19
+ allow: "allow";
20
+ }>>;
21
+ outbound: z.ZodDefault<z.ZodEnum<{
22
+ never: "never";
23
+ }>>;
24
+ connectors: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
25
+ never: "never";
26
+ allow: "allow";
27
+ }>>>;
28
+ }, z.core.$strip>;
29
+ export type SitePermissions = z.infer<typeof sitePermissionsSchema>;
30
+ export declare const DEFAULT_PERMISSIONS: SitePermissions;
31
+ /** What a routine may narrow. Same keys; each value can only be the same or stricter. */
32
+ export declare const routinePermissionsSchema: z.ZodObject<{
33
+ propose: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
34
+ never: "never";
35
+ ask: "ask";
36
+ }>>>;
37
+ remember: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
38
+ never: "never";
39
+ allow: "allow";
40
+ }>>>;
41
+ ask: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
42
+ never: "never";
43
+ allow: "allow";
44
+ }>>>;
45
+ report: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
46
+ never: "never";
47
+ allow: "allow";
48
+ }>>>;
49
+ outbound: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
50
+ never: "never";
51
+ }>>>;
52
+ connectors: z.ZodOptional<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
53
+ never: "never";
54
+ allow: "allow";
55
+ }>>>>;
56
+ }, z.core.$strip>;
57
+ export type RoutinePermissions = z.infer<typeof routinePermissionsSchema>;
58
+ /** The site's settings, narrowed by a routine's. Never wider than the site's. */
59
+ export declare function narrow(site: SitePermissions, routine?: RoutinePermissions): SitePermissions;
60
+ /** The tools a run is offered: everything whose permission is not `never`. */
61
+ export declare function permittedTools(tools: RuntimeTool[], permissions: SitePermissions): RuntimeTool[];
62
+ export declare function connectorPermitted(id: string, permissions: SitePermissions): boolean;
@@ -0,0 +1,76 @@
1
+ /*
2
+ * Permissions: one setting per kind of action (assistant concept §3).
3
+ *
4
+ * allow — do it, and record it;
5
+ * ask — hold it for the owner in the Inbox;
6
+ * never — the tool is not offered to the model at all.
7
+ *
8
+ * What exists today is narrower than the concept and says so in its types:
9
+ * a change can be asked for or refused, not applied unasked (earned autonomy
10
+ * comes later), and nothing outbound exists yet, so it is never. A routine can
11
+ * only narrow the site's settings, never widen them; and only the owner, in
12
+ * Settings, changes the site's.
13
+ */
14
+ import { z } from "zod";
15
+ export const sitePermissionsSchema = z.object({
16
+ /** Suggest changes, which wait in the Inbox. */
17
+ propose: z.enum(["ask", "never"]).default("ask"),
18
+ /** Record facts for later runs. */
19
+ remember: z.enum(["allow", "never"]).default("allow"),
20
+ /** Pause a background run to ask the owner a question. */
21
+ ask: z.enum(["allow", "never"]).default("allow"),
22
+ /** Leave a report in the Inbox. */
23
+ report: z.enum(["allow", "never"]).default("allow"),
24
+ /** Send anything off the site. Nothing does yet. */
25
+ outbound: z.enum(["never"]).default("never"),
26
+ /** Per connector id; absent means allow. */
27
+ connectors: z.record(z.string(), z.enum(["allow", "never"])).default({})
28
+ });
29
+ export const DEFAULT_PERMISSIONS = sitePermissionsSchema.parse({});
30
+ /** What a routine may narrow. Same keys; each value can only be the same or stricter. */
31
+ export const routinePermissionsSchema = sitePermissionsSchema.partial();
32
+ const RANK = { never: 0, ask: 1, allow: 2 };
33
+ const stricter = (a, b) => (b !== undefined && RANK[b] < RANK[a] ? b : a);
34
+ /** The site's settings, narrowed by a routine's. Never wider than the site's. */
35
+ export function narrow(site, routine) {
36
+ if (!routine)
37
+ return site;
38
+ const connectors = { ...site.connectors };
39
+ for (const [id, value] of Object.entries(routine.connectors ?? {})) {
40
+ connectors[id] = stricter(connectors[id] ?? "allow", value);
41
+ }
42
+ return {
43
+ propose: stricter(site.propose, routine.propose),
44
+ remember: stricter(site.remember, routine.remember),
45
+ ask: stricter(site.ask, routine.ask),
46
+ report: stricter(site.report, routine.report),
47
+ outbound: "never",
48
+ connectors
49
+ };
50
+ }
51
+ function permissionFor(effect, permissions) {
52
+ switch (effect) {
53
+ case "read":
54
+ return "allow";
55
+ case "propose":
56
+ return permissions.propose;
57
+ case "apply":
58
+ // Only the chat assistant is given an apply tool, and only while a person is asking.
59
+ return "allow";
60
+ case "remember":
61
+ return permissions.remember;
62
+ case "ask":
63
+ return permissions.ask;
64
+ case "report":
65
+ return permissions.report;
66
+ case "outbound":
67
+ return permissions.outbound;
68
+ }
69
+ }
70
+ /** The tools a run is offered: everything whose permission is not `never`. */
71
+ export function permittedTools(tools, permissions) {
72
+ return tools.filter((tool) => permissionFor(tool.effect ?? "read", permissions) !== "never");
73
+ }
74
+ export function connectorPermitted(id, permissions) {
75
+ return (permissions.connectors[id] ?? "allow") !== "never";
76
+ }
@@ -0,0 +1,9 @@
1
+ import type { ChatPipelineContext } from "../../chat/chat-pipeline.ts";
2
+ import type { RuntimeTool } from "../runtime/definition.ts";
3
+ /** Each host hands over its chat pipeline context once, at startup. */
4
+ export declare function setAssistantPlanner(ctx: ChatPipelineContext | null): void;
5
+ export declare function assistantPlannerAvailable(): boolean;
6
+ /** What the editor sends a chat turn (manifest, capabilities, locale), kept for the assistant's edits. */
7
+ export declare function rememberPlannerBody(scopeKey: string, body: Record<string, unknown>): void;
8
+ export declare function plannerBodyFrom(raw: unknown): Record<string, unknown>;
9
+ export declare const editPageTool: RuntimeTool;
@@ -0,0 +1,79 @@
1
+ let pipelineCtx = null;
2
+ const plannerBodies = new Map();
3
+ const MAX_REMEMBERED = 200;
4
+ /** Each host hands over its chat pipeline context once, at startup. */
5
+ export function setAssistantPlanner(ctx) {
6
+ pipelineCtx = ctx;
7
+ }
8
+ export function assistantPlannerAvailable() {
9
+ return pipelineCtx !== null;
10
+ }
11
+ /** What the editor sends a chat turn (manifest, capabilities, locale), kept for the assistant's edits. */
12
+ export function rememberPlannerBody(scopeKey, body) {
13
+ plannerBodies.delete(scopeKey);
14
+ plannerBodies.set(scopeKey, body);
15
+ if (plannerBodies.size > MAX_REMEMBERED)
16
+ plannerBodies.delete(plannerBodies.keys().next().value);
17
+ }
18
+ /** Fields of a chat body the planner may be given from the editor; nothing that steers execution. */
19
+ const PLANNER_FIELDS = ["componentsManifest", "siteCapabilities", "sitePurpose", "siteHosting", "businessContext", "siteContext", "locale", "modelKey", "provider"];
20
+ export function plannerBodyFrom(raw) {
21
+ if (!raw || typeof raw !== "object")
22
+ return {};
23
+ const source = raw;
24
+ return Object.fromEntries(PLANNER_FIELDS.filter((key) => source[key] !== undefined).map((key) => [key, source[key]]));
25
+ }
26
+ export const editPageTool = {
27
+ name: "edit_page",
28
+ effect: "apply",
29
+ description: "Edit one page by instruction, through the editor's own page editor: add, remove, move or restyle blocks, change images, translate, or any edit that is more than replacing a field's text. Give the page slug and one plain instruction, as the person would say it. It applies at once, with undo. Something that deletes content comes back held for the person's confirmation: tell them, and call again with confirm_plan set to the id you were given once they agree.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ page: { type: "string", description: "The page slug, e.g. /events/." },
34
+ instruction: { type: "string", description: "One edit, in plain words, e.g. 'Add a FAQ section with three questions about parking'." },
35
+ confirm_plan: { type: "string", description: "Only to apply an edit that came back held: its plan id." }
36
+ },
37
+ required: ["page", "instruction"]
38
+ },
39
+ run: async (input, ctx) => {
40
+ if (!pipelineCtx)
41
+ return { result: "Page edits are not available here. Use apply_changes for text, or say what you would change.", isError: true };
42
+ const slug = typeof input.page === "string" ? input.page : "";
43
+ const instruction = typeof input.instruction === "string" ? input.instruction.trim() : "";
44
+ if (!slug.startsWith("/") || !instruction)
45
+ return { result: "Give a page slug and an instruction.", isError: true };
46
+ const confirm = typeof input.confirm_plan === "string" && input.confirm_plan ? input.confirm_plan : undefined;
47
+ const siteId = ctx.scopeKey.includes("::") ? ctx.scopeKey.split("::")[0] : undefined;
48
+ // Imported here: the chat pipeline reaches the agents through its own imports.
49
+ const { runChatPipeline } = await import("../../chat/chat-pipeline.js");
50
+ const result = await runChatPipeline(pipelineCtx, {
51
+ ...plannerBodies.get(ctx.scopeKey),
52
+ session: ctx.scopeKey,
53
+ ...(siteId ? { siteId } : {}),
54
+ slug,
55
+ message: instruction,
56
+ ...(confirm ? { executionMode: "apply_pending_plan", pendingPlanId: confirm } : {})
57
+ });
58
+ const payload = (result.payload ?? {});
59
+ const changes = (payload.changes ?? []).slice(0, 12);
60
+ const list = changes.length ? `\nChanges:\n${changes.map((c) => `- ${c}`).join("\n")}` : "";
61
+ switch (payload.status) {
62
+ case "applied":
63
+ return { result: `Done on ${payload.updatedSlug ?? slug}: ${payload.summary ?? ""}${list}`, trace: `edited ${payload.updatedSlug ?? slug}: ${changes.length} changes` };
64
+ case "plan_ready":
65
+ return {
66
+ result: `Held for confirmation, because it ${payload.destructiveReasons?.join("; ") || "removes content"}: ${payload.summary ?? ""}${list}\nAsk the person. If they agree, call edit_page again with the same page and confirm_plan "${payload.pendingPlanId ?? ""}".`,
67
+ trace: `held an edit on ${slug} for confirmation`
68
+ };
69
+ case "needs_clarification":
70
+ return { result: `The page editor needs to know more: ${payload.summary ?? ""}`, trace: `the page editor asked a question about ${slug}` };
71
+ default:
72
+ return {
73
+ result: `The page editor did not make this edit (${payload.status ?? `HTTP ${result.code}`}): ${payload.summary ?? JSON.stringify(payload.validationErrors ?? "")}`,
74
+ isError: true,
75
+ trace: `the page editor refused an edit on ${slug}`
76
+ };
77
+ }
78
+ }
79
+ };
@@ -0,0 +1,20 @@
1
+ import type { ScheduleTrigger } from "../schedule.ts";
2
+ export type Route = {
3
+ route: "planner";
4
+ } | {
5
+ route: "assistant";
6
+ message: string;
7
+ } | {
8
+ route: "routine";
9
+ schedule: ScheduleTrigger;
10
+ request: string;
11
+ };
12
+ export type RouteOptions = {
13
+ /**
14
+ * The chat's last reply was the assistant's. An edit that points back at it
15
+ * ("rewrite them", "change both") continues that conversation instead of
16
+ * taking the planner's shortcut.
17
+ */
18
+ followUp?: boolean;
19
+ };
20
+ export declare function routeMessage(message: string, options?: RouteOptions): Route;
@@ -0,0 +1,69 @@
1
+ const DAYS = [
2
+ [/\b(sunday|sonntags?)\b/i, 0],
3
+ [/\b(monday|montags?)\b/i, 1],
4
+ [/\b(tuesday|dienstags?)\b/i, 2],
5
+ [/\b(wednesday|mittwochs?)\b/i, 3],
6
+ [/\b(thursday|donnerstags?)\b/i, 4],
7
+ [/\b(friday|freitags?)\b/i, 5],
8
+ [/\b(saturday|samstags?)\b/i, 6]
9
+ ];
10
+ const DAY_NAMES = "monday|tuesday|wednesday|thursday|friday|saturday|sunday";
11
+ const TAG_NAMEN = "montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag";
12
+ const CADENCE = new RegExp([
13
+ `\\b(every|each)\\s+(day|morning|week|${DAY_NAMES})\\b`,
14
+ "\\b(daily|weekly)\\b",
15
+ `\\bjeden\\s+(tag|morgen|${TAG_NAMEN})\\b`,
16
+ "\\bjede\\s+woche\\b",
17
+ `\\b(${TAG_NAMEN})s\\b`,
18
+ "\\b(täglich|wöchentlich)\\b"
19
+ ].join("|"), "i");
20
+ /** A message that starts like a direct edit of the page: the planner's fast path. */
21
+ const EDIT_START = /^\s*(please\s+|bitte\s+|can you\s+|could you\s+|kannst du\s+|könntest du\s+)?(change|make|set|replace|add|insert|append|remove|delete|rewrite|write|translate|update|move|swap|shorten|lengthen|fix|rename|create|generate|put|turn|duplicate|clone|copy|reorder|sort|align|center|centre|resize|crop|restyle|style|colou?r|increase|decrease|expand|simplify|improve|show|hide|use|link|undo|redo|ändere|änder|mach|setze|ersetze|füge|entferne|lösche|übersetze|schreib|schreibe|erstelle|verschiebe|kürze|verlängere|tausche|dupliziere|kopiere|verbessere|zeige|verstecke|blende)\b/i;
22
+ /** An edit across the whole site needs finding every mention first: the assistant's. */
23
+ const WHOLE_SITE = /\b(everywhere|on (all|every) pages?|across the (whole )?site|sitewide|site-wide)\b|(?:^|\s)(überall|auf allen seiten|auf der ganzen website)(?=$|\s|[.,!?])/i;
24
+ const MENTION = /^\s*@(assistant|assistent)\b[:,]?\s*/i;
25
+ /** Words that say "the thing we just did", with nothing of their own to repeat. */
26
+ const FILLER = /\b(please|bitte|do|this|that|it|again|run|repeat|the same|same|mach|das|dies|es|wieder|nochmal|wiederholen|so|also|and|und|at|um|\d{1,2}(:00)?( ?uhr| ?h)?)\b/gi;
27
+ function scheduleOf(message) {
28
+ if (!CADENCE.test(message))
29
+ return null;
30
+ const hourMatch = /\b(?:at|um)\s+(\d{1,2})(?::00)?\s*(?:uhr|h|am|pm)?\b/i.exec(message);
31
+ let hour = hourMatch ? Number(hourMatch[1]) : 6;
32
+ if (hourMatch && /pm/i.test(hourMatch[0]) && hour < 12)
33
+ hour += 12;
34
+ if (hour > 23)
35
+ hour = 6;
36
+ if (/\b(every|each)\s+(day|morning)\b|\bdaily\b|\bjeden\s+(tag|morgen)\b|\btäglich\b/i.test(message)) {
37
+ return { kind: "schedule", every: "day", hour, weekday: 1 };
38
+ }
39
+ const weekday = DAYS.find(([pattern]) => pattern.test(message))?.[1] ?? 1;
40
+ return { kind: "schedule", every: "week", hour, weekday };
41
+ }
42
+ /** What the routine should do: the message minus its cadence, when that leaves a request of its own. */
43
+ function requestOf(message) {
44
+ const rest = message
45
+ .replace(CADENCE, " ")
46
+ .replace(/\b(?:at|um)\s+\d{1,2}(?::00)?\s*(?:uhr|h|am|pm)?\b/gi, " ")
47
+ .replace(/^[\s,.:;-]+|[\s,.:;-]+$/g, "")
48
+ .replace(/\s+/g, " ")
49
+ .trim();
50
+ const meaningful = rest.replace(FILLER, " ").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
51
+ return meaningful.split(" ").filter(Boolean).length >= 3 ? rest : "";
52
+ }
53
+ /** "them", "those", "die": an edit that points back at what the assistant just said. */
54
+ const BACK_REFERENCE = /\b(them|those|these|that|both|all of them|the ones?|die|diese|das|dies|beide|alle)\b/i;
55
+ export function routeMessage(message, options = {}) {
56
+ const text = message.trim();
57
+ if (!text)
58
+ return { route: "planner" };
59
+ const mention = MENTION.exec(text);
60
+ const body = mention ? text.slice(mention[0].length).trim() : text;
61
+ const schedule = scheduleOf(body);
62
+ if (schedule)
63
+ return { route: "routine", schedule, request: requestOf(body) };
64
+ if (mention)
65
+ return { route: "assistant", message: body || text };
66
+ if (EDIT_START.test(body) && !WHOLE_SITE.test(body) && !(options.followUp && BACK_REFERENCE.test(body)))
67
+ return { route: "planner" };
68
+ return { route: "assistant", message: body };
69
+ }
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ import type { DurableStore } from "../../durable/types.ts";
3
+ export declare const ROUTINE_PREFIX = "routine:";
4
+ export declare const routineSchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ name: z.ZodString;
7
+ job: z.ZodDefault<z.ZodString>;
8
+ request: z.ZodString;
9
+ skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
10
+ connectors: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ schedule: z.ZodOptional<z.ZodObject<{
12
+ kind: z.ZodLiteral<"schedule">;
13
+ every: z.ZodEnum<{
14
+ day: "day";
15
+ week: "week";
16
+ }>;
17
+ hour: z.ZodDefault<z.ZodNumber>;
18
+ weekday: z.ZodDefault<z.ZodNumber>;
19
+ }, z.core.$strip>>;
20
+ events: z.ZodDefault<z.ZodArray<z.ZodEnum<{
21
+ publish: "publish";
22
+ }>>>;
23
+ may: z.ZodOptional<z.ZodObject<{
24
+ propose: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
25
+ never: "never";
26
+ ask: "ask";
27
+ }>>>;
28
+ remember: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
29
+ never: "never";
30
+ allow: "allow";
31
+ }>>>;
32
+ ask: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
33
+ never: "never";
34
+ allow: "allow";
35
+ }>>>;
36
+ report: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
37
+ never: "never";
38
+ allow: "allow";
39
+ }>>>;
40
+ outbound: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
41
+ never: "never";
42
+ }>>>;
43
+ connectors: z.ZodOptional<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
44
+ never: "never";
45
+ allow: "allow";
46
+ }>>>>;
47
+ }, z.core.$strip>>;
48
+ budget: z.ZodDefault<z.ZodObject<{
49
+ usdPerRun: z.ZodNumber;
50
+ usdPerMonth: z.ZodNumber;
51
+ }, z.core.$strip>>;
52
+ tier: z.ZodDefault<z.ZodEnum<{
53
+ fast: "fast";
54
+ balanced: "balanced";
55
+ reasoning: "reasoning";
56
+ }>>;
57
+ maxSteps: z.ZodDefault<z.ZodNumber>;
58
+ fromRunId: z.ZodOptional<z.ZodString>;
59
+ }, z.core.$strip>;
60
+ export type Routine = z.infer<typeof routineSchema>;
61
+ export type RoutineInput = z.input<typeof routineSchema>;
62
+ export type SiteRoutine = Routine & {
63
+ origin: "template" | "site";
64
+ enabled: boolean;
65
+ since: number;
66
+ };
67
+ /** The templates. Off until a site switches one on. */
68
+ export declare const ROUTINE_TEMPLATES: Routine[];
69
+ export declare function getRoutineTemplate(id: string): Routine | undefined;
70
+ export declare function isSiteRoutineId(id: string): boolean;
71
+ /** A new id for a site routine, readable in logs: `routine:monday-search-check-3f9a`. */
72
+ export declare function newRoutineId(name: string): string;
73
+ /** Every routine for a site: the templates with the site's switch, then its own, by name. */
74
+ export declare function listSiteRoutines(store: DurableStore, scopeKey: string): Promise<SiteRoutine[]>;
75
+ export declare function getSiteRoutine(store: DurableStore, scopeKey: string, id: string): Promise<SiteRoutine | undefined>;
76
+ /** Create or replace a site routine. Templates are code and cannot be saved over. */
77
+ export declare function saveSiteRoutine(store: DurableStore, scopeKey: string, input: RoutineInput, options?: {
78
+ enabled?: boolean;
79
+ by?: string;
80
+ }): Promise<SiteRoutine | {
81
+ error: string;
82
+ }>;
83
+ export declare function removeSiteRoutine(store: DurableStore, scopeKey: string, id: string): Promise<boolean>;
84
+ /** "every Monday at 06:00 UTC", for a routine's line on the Agents page and in chat. */
85
+ export declare function describeSchedule(routine: Pick<Routine, "schedule" | "events">): string;
@@ -0,0 +1,142 @@
1
+ /*
2
+ * Routines: the assistant working without being asked (assistant concept §2.5).
3
+ *
4
+ * A routine is a saved request with a trigger, the skills its job always
5
+ * needs, the connectors it may use, permissions that can only narrow the
6
+ * site's, and a budget. What used to be a coded agent is a routine template:
7
+ * the Search analyst is "weekly search review" with two skills and Search
8
+ * Console, nothing more.
9
+ *
10
+ * Templates are code and keep their old agent ids (`findings-review`,
11
+ * `search-analyst`), so runs, settings and Inbox items recorded before the
12
+ * split still resolve. A site's own routines — most often made from a chat
13
+ * answer with "do this every Monday" — are stored as agent settings rows with
14
+ * an id starting `routine:` and the routine as the row's spec, which both
15
+ * durable stores already keep.
16
+ */
17
+ import { randomBytes } from "node:crypto";
18
+ import { z } from "zod";
19
+ import { scheduleTriggerSchema } from "../schedule.js";
20
+ import { DEFAULT_AGENT_BUDGET } from "../runtime/definition.js";
21
+ import { routinePermissionsSchema } from "./permissions.js";
22
+ export const ROUTINE_PREFIX = "routine:";
23
+ export const routineSchema = z.object({
24
+ id: z.string().min(1).max(80),
25
+ name: z.string().trim().min(1).max(80),
26
+ /** What it is for, in one line. Shown on the Agents page. */
27
+ job: z.string().trim().max(240).default(""),
28
+ /** The request each run starts from, as the owner would say it. */
29
+ request: z.string().trim().min(1).max(2000),
30
+ /** Skills loaded up front. Others can still be loaded with use_skill. */
31
+ skills: z.array(z.string()).max(8).default([]),
32
+ /**
33
+ * Connectors it may use. Absent: every connected one the site permits.
34
+ * Listed: only these, and the routine cannot run until they are connected.
35
+ */
36
+ connectors: z.array(z.string()).max(8).optional(),
37
+ schedule: scheduleTriggerSchema.optional(),
38
+ events: z.array(z.enum(["publish"])).default([]),
39
+ may: routinePermissionsSchema.optional(),
40
+ budget: z
41
+ .object({ usdPerRun: z.number().positive().max(5), usdPerMonth: z.number().positive().max(200) })
42
+ .default(DEFAULT_AGENT_BUDGET),
43
+ tier: z.enum(["fast", "balanced", "reasoning"]).default("balanced"),
44
+ maxSteps: z.number().int().min(1).max(40).default(20),
45
+ /** The chat run it was made from, when it was. */
46
+ fromRunId: z.string().optional()
47
+ });
48
+ const template = (input) => routineSchema.parse(input);
49
+ /** The templates. Off until a site switches one on. */
50
+ export const ROUTINE_TEMPLATES = [
51
+ template({
52
+ id: "findings-review",
53
+ name: "Findings review",
54
+ job: "Each week, reviews the open findings, proposes the fixes it is sure of, and asks when it is not.",
55
+ request: "Review this site's open findings. Summarise what matters this week, propose fixes for the ones you are sure of, and ask the owner if one decision blocks you.",
56
+ skills: ["findings-triage"],
57
+ connectors: [],
58
+ schedule: { kind: "schedule", every: "week", weekday: 1, hour: 7 }
59
+ }),
60
+ template({
61
+ id: "search-analyst",
62
+ name: "Weekly search review",
63
+ job: "Each week, reads Google Search Console and proposes the few title, description and content changes that should bring the most clicks.",
64
+ request: "Weekly search review. Find the few changes that should bring this site the most clicks from Google, and propose them.",
65
+ skills: ["search-analysis", "search-snippets"],
66
+ connectors: ["search-console"],
67
+ schedule: { kind: "schedule", every: "week", weekday: 1, hour: 6 }
68
+ })
69
+ ];
70
+ export function getRoutineTemplate(id) {
71
+ return ROUTINE_TEMPLATES.find((routine) => routine.id === id);
72
+ }
73
+ export function isSiteRoutineId(id) {
74
+ return id.startsWith(ROUTINE_PREFIX);
75
+ }
76
+ /** A new id for a site routine, readable in logs: `routine:monday-search-check-3f9a`. */
77
+ export function newRoutineId(name) {
78
+ const slug = name
79
+ .toLowerCase()
80
+ .normalize("NFKD")
81
+ .replace(/[^a-z0-9]+/g, "-")
82
+ .replace(/^-+|-+$/g, "")
83
+ .slice(0, 40);
84
+ return `${ROUTINE_PREFIX}${slug || "routine"}-${randomBytes(2).toString("hex")}`;
85
+ }
86
+ /** Every routine for a site: the templates with the site's switch, then its own, by name. */
87
+ export async function listSiteRoutines(store, scopeKey) {
88
+ const rows = await store.listAgentSettings(scopeKey);
89
+ const byId = new Map(rows.map((row) => [row.agentId, row]));
90
+ const templates = ROUTINE_TEMPLATES.map((routine) => {
91
+ const row = byId.get(routine.id);
92
+ return { ...routine, origin: "template", enabled: row?.enabled ?? false, since: row?.updatedAt ?? 0 };
93
+ });
94
+ const own = [];
95
+ for (const row of rows) {
96
+ if (!isSiteRoutineId(row.agentId) || !row.spec)
97
+ continue;
98
+ // A stored routine this build cannot read is left out, not run half-understood.
99
+ const parsed = routineSchema.safeParse({ ...row.spec, id: row.agentId });
100
+ if (!parsed.success)
101
+ continue;
102
+ own.push({ ...parsed.data, origin: "site", enabled: row.enabled, since: row.updatedAt });
103
+ }
104
+ own.sort((a, b) => a.name.localeCompare(b.name));
105
+ return [...templates, ...own];
106
+ }
107
+ export async function getSiteRoutine(store, scopeKey, id) {
108
+ return (await listSiteRoutines(store, scopeKey)).find((routine) => routine.id === id);
109
+ }
110
+ /** Create or replace a site routine. Templates are code and cannot be saved over. */
111
+ export async function saveSiteRoutine(store, scopeKey, input, options = {}) {
112
+ if (!isSiteRoutineId(input.id))
113
+ return { error: `a site routine's id starts with "${ROUTINE_PREFIX}"` };
114
+ const parsed = routineSchema.safeParse(input);
115
+ if (!parsed.success)
116
+ return { error: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ") };
117
+ const row = await store.putAgentSetting({
118
+ scopeKey,
119
+ agentId: parsed.data.id,
120
+ enabled: options.enabled ?? true,
121
+ ...(options.by ? { owner: options.by } : {}),
122
+ spec: parsed.data
123
+ });
124
+ return { ...parsed.data, origin: "site", enabled: row.enabled, since: row.updatedAt };
125
+ }
126
+ export async function removeSiteRoutine(store, scopeKey, id) {
127
+ if (!isSiteRoutineId(id))
128
+ return false;
129
+ return store.deleteAgentSetting(scopeKey, id);
130
+ }
131
+ /** "every Monday at 06:00 UTC", for a routine's line on the Agents page and in chat. */
132
+ export function describeSchedule(routine) {
133
+ const parts = [];
134
+ if (routine.schedule) {
135
+ const hour = `${String(routine.schedule.hour).padStart(2, "0")}:00 UTC`;
136
+ const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
137
+ parts.push(routine.schedule.every === "day" ? `every day at ${hour}` : `every ${days[routine.schedule.weekday]} at ${hour}`);
138
+ }
139
+ if (routine.events.includes("publish"))
140
+ parts.push("after every publish");
141
+ return parts.join(", ") || "only when started by hand";
142
+ }
@@ -0,0 +1,17 @@
1
+ import type { DurableStore } from "../../durable/types.ts";
2
+ export declare const ASSISTANT_ROW = "assistant";
3
+ /** The value a set secret field reads as in a site's connections. */
4
+ export declare const SECRET_SET = "(set)";
5
+ /** The secrets.ts name a connector's secret field is kept under. */
6
+ export declare const connectorSecretName: (connectorId: string, key: string) => string;
7
+ export type SiteConnections = Record<string, Record<string, string>>;
8
+ /**
9
+ * The site's connection settings, plus `google.signedIn` when the site has
10
+ * connected a Google account (the token itself stays in secrets.ts; only its
11
+ * presence is read here, so a status can be answered without decrypting).
12
+ */
13
+ export declare function readSiteConnections(store: DurableStore, scopeKey: string): Promise<SiteConnections>;
14
+ /** Replace one connection's settings for a site. Empty values are dropped. */
15
+ export declare function writeSiteConnection(store: DurableStore, scopeKey: string, id: string, values: Record<string, unknown>, by?: string): Promise<void>;
16
+ /** Whether a site can reach Google: its own sign-in, or the deployment's service account. */
17
+ export declare function googleSignedIn(all: SiteConnections): boolean;
@@ -0,0 +1,51 @@
1
+ export const ASSISTANT_ROW = "assistant";
2
+ /** The value a set secret field reads as in a site's connections. */
3
+ export const SECRET_SET = "(set)";
4
+ /** The secrets.ts name a connector's secret field is kept under. */
5
+ export const connectorSecretName = (connectorId, key) => `connector:${connectorId}:${key}`;
6
+ const MAX_VALUE = 500;
7
+ /**
8
+ * The site's connection settings, plus `google.signedIn` when the site has
9
+ * connected a Google account (the token itself stays in secrets.ts; only its
10
+ * presence is read here, so a status can be answered without decrypting).
11
+ */
12
+ export async function readSiteConnections(store, scopeKey) {
13
+ const rows = await store.listAgentSettings(scopeKey).catch(() => []);
14
+ const signedIn = rows.some((row) => row.agentId === "secret:google-refresh-token");
15
+ const raw = rows.find((row) => row.agentId === ASSISTANT_ROW)?.spec?.connections;
16
+ const out = signedIn ? { google: { signedIn: "1" } } : {};
17
+ // A secret field that is set reads as SECRET_SET: enough for a status, nothing to leak.
18
+ for (const row of rows) {
19
+ const match = /^secret:connector:([\w-]+):([\w-]+)$/.exec(row.agentId);
20
+ if (match)
21
+ out[match[1]] = { ...out[match[1]], [match[2]]: SECRET_SET };
22
+ }
23
+ if (!raw || typeof raw !== "object")
24
+ return out;
25
+ for (const [id, values] of Object.entries(raw)) {
26
+ if (!values || typeof values !== "object")
27
+ continue;
28
+ out[id] = { ...out[id], ...Object.fromEntries(Object.entries(values).filter((entry) => typeof entry[1] === "string")) };
29
+ }
30
+ return out;
31
+ }
32
+ /** Replace one connection's settings for a site. Empty values are dropped. */
33
+ export async function writeSiteConnection(store, scopeKey, id, values, by) {
34
+ const rows = await store.listAgentSettings(scopeKey);
35
+ const row = rows.find((r) => r.agentId === ASSISTANT_ROW);
36
+ const clean = Object.fromEntries(Object.entries(values)
37
+ .filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0)
38
+ .map(([key, value]) => [key, value.trim().slice(0, MAX_VALUE)]));
39
+ const connections = { ...(row?.spec?.connections ?? {}), [id]: clean };
40
+ await store.putAgentSetting({
41
+ scopeKey,
42
+ agentId: ASSISTANT_ROW,
43
+ enabled: true,
44
+ ...(by ? { owner: by } : {}),
45
+ spec: { ...(row?.spec ?? {}), connections }
46
+ });
47
+ }
48
+ /** Whether a site can reach Google: its own sign-in, or the deployment's service account. */
49
+ export function googleSignedIn(all) {
50
+ return all.google?.signedIn === "1";
51
+ }