evOo-local/packages/adapters/src/scripted-runtime.ts
cynarAI dbf37f86a8
feat: show routines the bot sets up from chat as one quiet line (#133)
* feat: show routines the bot sets up from chat as one quiet line

When someone mentions something recurring in passing ("every Sunday I plan the
week"), the bot now creates the routine right away and the conversation shows a
single line with the schedule, Edit and Undo, on web and mobile.

- New `event` message block for routine lines, kept in history so later turns
  know what was set up or removed.
- schedule_create reuses an active repeating routine of the same name instead
  of adding a duplicate, caps a bot at 50 active schedules, and falls back to
  default times when none was given.
- Removing a routine (Undo, the editor, or schedule_cancel) marks its lines
  removed via the new routines.noticeMessageId column.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1

* test: give the routine notice test a complete job publisher

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-04 20:36:53 +02:00

614 lines
18 KiB
TypeScript

import type {
AdapterContext,
AgentRunRequest,
AgentRuntime,
AgentRuntimeEvent,
} from "@rakazo/adapter-kit";
import { USER_FACT_CATEGORIES } from "@rakazo/contracts";
import { abortableDelay, DELEGATED_TASK_WAKE_CUE, inferHandoffTargetName } from "@rakazo/core";
import { COACH_ROUTINES } from "./coach-rhythm.js";
const running = new Map<string, AbortController>();
/** "learn about me: [category:] statement" records one fact in the user's profile. */
const LEARN_ABOUT_ME = new RegExp(
`learn about me:\\s*(?:(${USER_FACT_CATEGORIES.join("|")})\\s*:\\s*)?([\\s\\S]+)$`,
"i",
);
/** "set a goal: …", "track a habit: …", "done today: <habit>" and "write today's brief: …". */
const COACH_COMMANDS: Array<{
pattern: RegExp;
reply: string;
call: (value: string) => { name: string; args: Record<string, unknown> };
}> = [
{
pattern: /^set a goal:\s*([\s\S]+)$/i,
reply: "goal noted.",
call: (title) => ({ name: "update_goal", args: { title } }),
},
{
pattern: /^track a habit:\s*([\s\S]+)$/i,
reply: "habit noted.",
call: (title) => ({ name: "update_habit", args: { title } }),
},
{
pattern: /^done today:\s*([\s\S]+)$/i,
reply: "checked off.",
call: (habit) => ({ name: "log_check_in", args: { habit, done: true } }),
},
{
pattern: /^write today's brief:\s*([\s\S]+)$/i,
reply: "today's brief is ready.",
call: (text) => ({ name: "write_daily_brief", args: { text } }),
},
];
/** The morning check-in writes a fixed brief so tests can follow it to the Today page. */
export const SCRIPTED_MORNING_BRIEF = "One thing at a time today.";
function inferCoachScript(prompt: string): NonNullable<AgentRunRequest["script"]> | undefined {
if (prompt.startsWith(COACH_ROUTINES.morning.prompt)) {
return [
{
assistant: "good morning. today's brief is ready.",
toolCalls: [{ name: "write_daily_brief", args: { text: SCRIPTED_MORNING_BRIEF } }],
complete: true,
},
];
}
for (const command of COACH_COMMANDS) {
const value = command.pattern.exec(prompt.trim())?.[1]?.trim();
if (value) {
return [{ assistant: command.reply, toolCalls: [command.call(value)], complete: true }];
}
}
return undefined;
}
export class ScriptedAgentRuntime implements AgentRuntime {
describe() {
return {
id: "scripted",
contractVersion: "1",
adapterVersion: "0.1.0",
capabilities: { streaming: true, compaction: false, tools: true, scripted: true },
};
}
async abort(runId: string): Promise<void> {
running.get(runId)?.abort();
}
async *run(
request: AgentRunRequest,
context?: Partial<AdapterContext>,
): AsyncIterable<AgentRuntimeEvent> {
const controller = new AbortController();
running.set(request.runId, controller);
const signal = context?.signal ?? controller.signal;
try {
if (shouldFail(request.prompt)) {
throw new Error("Scripted run failure");
}
if (shouldHang(request.prompt)) {
yield { type: "progress", text: "still working…", activity: true };
while (!controller.signal.aborted && !signal.aborted) {
await abortableDelay(50, controller.signal);
if (controller.signal.aborted || signal.aborted) break;
yield { type: "progress", text: "still working…", activity: true };
}
yield { type: "done", text: "stopped" };
return;
}
const script = request.script ?? inferScript(request.prompt, request.resumeFromCheckpoint);
// Per-run call index so repeated tools (e.g. message_agent) get distinct
// executionIds — delivery keys and effect replays key off this value.
let toolCallSeq = 0;
for (const turn of script) {
if (signal.aborted || controller.signal.aborted) {
yield { type: "done", text: "stopped" };
return;
}
if (turn.assistant) {
yield { type: "progress", text: "working…", activity: true };
yield { type: "text", text: turn.assistant };
}
for (const call of turn.toolCalls ?? []) {
const executionId = `${request.runId}:${call.name}:${toolCallSeq++}`;
if (call.name === "run_subagent") {
const agentId = `${request.runId}:subagent`;
const name = String(call.args.name ?? "helper");
const task = String(call.args.task ?? request.prompt);
yield {
type: "subagent",
agentId,
name,
task,
status: "running",
progress: "working…",
};
yield {
type: "tool",
name: call.name,
args: call.args,
executionId,
};
yield {
type: "subagent",
agentId,
name,
task,
status: "completed",
result: `done. i handled: ${task.slice(0, 180)}`,
};
continue;
}
yield {
type: "tool",
name: call.name,
args: call.args,
executionId,
};
}
if (turn.ask) {
yield {
type: "ask",
text: turn.ask.text,
detail: turn.ask.detail,
actions: turn.ask.actions,
};
return;
}
if (turn.takeover) {
yield { type: "takeover", reason: turn.takeover.reason };
return;
}
if (turn.complete) {
yield {
type: "usage",
inputTokens: 12,
outputTokens: 40,
provider: "scripted",
model: "scripted",
};
yield { type: "done", text: turn.assistant };
return;
}
}
yield { type: "text", text: "done." };
yield { type: "done", text: "done." };
} finally {
controller.abort();
running.delete(request.runId);
}
}
}
export function inferScript(
prompt: string,
resumeFromCheckpoint?: string,
): NonNullable<AgentRunRequest["script"]> {
const lower = prompt.toLowerCase();
if (resumeFromCheckpoint === "takeover-skipped") {
return [
{
assistant: "login was skipped. continuing without treating sign-in as done.",
complete: true,
},
];
}
if (resumeFromCheckpoint === "takeover") {
return [
{
assistant:
"signed in. the session stays in this computer — protected input never hit the thread.",
complete: true,
},
];
}
// A finished delegated task wakes the main bot; the result inside may say anything.
if (prompt.startsWith(DELEGATED_TASK_WAKE_CUE)) {
const title = /"([^"\n]{1,120})"/.exec(prompt)?.[1] ?? "the task";
return [{ assistant: `closing the task: ${title}.`, complete: true }];
}
// Before every content-based intent so payload text cannot steal the branch.
if (lower.includes("message the bot named") || lower.includes("message bot named")) {
const name = namedBot(prompt) ?? "Peer";
const message =
/named\s+[A-Za-z0-9][A-Za-z0-9_-]{0,39}\s+(?:saying|with|:)\s*([\s\S]+)$/i
.exec(prompt)?.[1]
?.trim() ?? `Please help with: ${prompt}`;
return [
{
assistant: "messaging that bot now.",
toolCalls: [
{
name: "message_bot",
args: {
confirm_name: name,
message,
intent: "request",
},
},
],
complete: true,
},
];
}
const learned = LEARN_ABOUT_ME.exec(prompt);
if (learned?.[2]?.trim()) {
return [
{
assistant: "noted — i will keep that in your profile.",
toolCalls: [
{
name: "update_user_profile",
args: {
facts: [
{
category: (learned[1] ?? "preferences").toLowerCase(),
value: learned[2].trim(),
},
],
},
},
],
complete: true,
},
];
}
const coach = inferCoachScript(prompt);
if (coach) return coach;
// Said in passing: the bot sets the routine up without asking first.
if (lower.includes("every sunday i plan the week")) {
return [
{
assistant: "i will remind you every sunday at 6 pm.",
toolCalls: [
{
name: "schedule_create",
args: { name: "Weekly plan", prompt: "Plan the week with me.", cron: "0 18 * * 0" },
},
],
complete: true,
},
];
}
if (lower.includes("delegate to the bot named")) {
const name = namedBot(prompt) ?? "Researcher";
const brief =
/named\s+[A-Za-z0-9][A-Za-z0-9_-]{0,39}\s*(?::|saying|with)\s*([\s\S]+)$/i
.exec(prompt)?.[1]
?.trim() ?? prompt;
return [
{
assistant: "delegating that now.",
toolCalls: [
{ name: "delegate_task", args: { bot: name, title: brief.slice(0, 80), brief } },
],
complete: true,
},
];
}
if (
lower.includes("launch a cloud agent") ||
lower.includes("start a cloud agent") ||
lower.includes("cloud coding agent")
) {
return [
{
assistant: "launching a cloud agent for that.",
toolCalls: [
{
name: "cloud_agent_launch",
args: {
prompt: "Add a README with setup instructions",
repository: "https://github.com/example/demo",
openPr: true,
},
},
],
complete: true,
},
];
}
if (
lower.includes("masked secret card") ||
lower.includes("show a secret card") ||
lower.includes("request a masked api key")
) {
return [
{
assistant: "i need that value in a protected field.",
toolCalls: [
{
name: "request_secret",
args: {
label: "API key",
purpose: "api_key",
credential: {
name: "example_api",
origin: "https://api.example.test",
auth: { type: "bearer" },
},
},
},
],
},
];
}
if (
lower.includes("tappable choices") ||
lower.includes("choice buttons") ||
lower.includes("pick from these cities")
) {
return [
{
assistant: "pick one to continue.",
ask: {
text: "Which city should I use?",
detail: "Tap one option.",
actions: [
{ id: "choice-1", label: "Berlin" },
{ id: "choice-2", label: "Seoul" },
{ id: "choice-3", label: "Toronto" },
{ id: "choice-4", label: "Lisbon" },
],
},
},
];
}
if (
lower.includes("ask me") ||
lower.includes("which city") ||
lower.includes("need a decision")
) {
return [
{
assistant: "i need a decision before i continue.",
ask: { text: "Which city should I use?", detail: "Reply with one city name." },
},
];
}
if (lower.includes("take over") || lower.includes("sign in") || lower.includes("login")) {
return [
{ assistant: "i need you on the screen for a one-time sign-in. handing you the computer." },
{ takeover: { reason: "Sign in to continue. Protected input stays off the thread." } },
];
}
if (
lower.includes("observe your screen") ||
lower.includes("look at your screen") ||
lower.includes("use your screen")
) {
const typed = /type\s+([A-Za-z0-9._-]+)/i.exec(prompt)?.[1] ?? "ready";
return [
{
assistant: "using my screen now.",
toolCalls: [
{ name: "computer_observe", args: {} },
{
name: "computer_act",
args: {
// Click into the focused field first so the visible cursor has a target.
actions: [
{ kind: "click", x: 640, y: 400 },
{ kind: "type", text: typed },
],
observe: true,
},
},
],
complete: true,
},
];
}
if (
lower.includes("delete the bot named") ||
lower.includes("delete the child bot") ||
lower.includes("delete child")
) {
const name = namedBot(prompt) ?? "Scout";
return [
{
assistant: "archiving that bot.",
toolCalls: [{ name: "archive_bot", args: { confirm_name: name } }],
complete: true,
},
];
}
if (
lower.includes("create a space") ||
lower.includes("create space") ||
lower.includes("new space named") ||
lower.includes("new private space")
) {
const name = namedSpace(prompt) ?? "New space";
return [
{
assistant: "i can create that separate space after you confirm the boundary.",
toolCalls: [{ name: "create_space", args: { name } }],
complete: true,
},
];
}
if (
lower.includes("spawn a bot") ||
lower.includes("spawn a child") ||
lower.includes("create a bot named") ||
lower.includes("create a child bot")
) {
const name = namedBot(prompt) ?? "Helper";
return [
{
assistant: "creating a bot for that.",
toolCalls: [
{
name: "spawn_bot",
args: {
name,
title: `${name} specialist`,
instructions: `You are ${name}.`,
prompt: "Start on the assigned work.",
},
},
],
complete: true,
},
];
}
if (lower.includes("subagent") || lower.includes("delegate to a helper")) {
return [
{
assistant: "spinning up a helper for that.",
toolCalls: [{ name: "run_subagent", args: { name: "helper", task: prompt } }],
complete: true,
},
];
}
if (
lower.includes("hand this to") ||
lower.includes("hand off to") ||
lower.includes("handoff to") ||
(lower.includes("@writer") && lower.includes("draft"))
) {
const target = inferHandoffTargetName(prompt) ?? "Writer";
return [
{
assistant: "handing this off in the group thread.",
toolCalls: [
{
name: "handoff_to_bot",
args: {
confirm_name: target,
message: prompt,
},
},
],
complete: true,
},
];
}
if (lower.startsWith("run taught skill:") || lower.includes("this is a safe test")) {
return [
{
assistant:
"Running the taught skill using its saved playbook. I will follow the demonstrated steps and report the result.",
complete: true,
},
];
}
if (/^run\s+/.test(lower)) {
return [
{
assistant:
"Using the saved taught skill playbook for that request and following its steps.",
complete: true,
},
];
}
if (lower.includes("connector") || lower.includes("crm") || lower.includes("destination")) {
return [
{
assistant: "writing the record through the connected destination.",
toolCalls: [
{
name: "destination.write",
args: { collection: "notes", title: "Rakazo result", body: prompt },
},
],
complete: true,
},
];
}
if (lower.includes("present") && lower.includes("report")) {
const filePath =
/(?:called|named|path|file)\s+([A-Za-z0-9._/-]+)/i.exec(prompt)?.[1] ?? "report.md";
const content =
"# Report\n\n| Item | Status |\n| --- | --- |\n| Research | Done |\n| Draft | Done |\n";
return [
{ assistant: "writing the report and putting it next to our chat." },
{ toolCalls: [{ name: "write_file", args: { path: filePath, content } }] },
{
toolCalls: [{ name: "attach_file", args: { path: filePath, present: true } }],
complete: true,
},
];
}
if (lower.includes("attach") && (lower.includes("thread") || lower.includes("into the thread"))) {
const said = /says?\s+(.+)$/i.exec(prompt)?.[1]?.replace(/[.]+$/, "") ?? prompt;
const content = `${said.trim()}\n`;
const filePath =
/(?:called|named|path|file)\s+([A-Za-z0-9._/-]+)/i.exec(prompt)?.[1] ?? "notes/result.txt";
return [
{ assistant: "writing that into my home and attaching it to the thread." },
{ toolCalls: [{ name: "write_file", args: { path: filePath, content } }] },
{ toolCalls: [{ name: "attach_file", args: { path: filePath } }], complete: true },
];
}
if (
lower.includes("write") &&
(lower.includes("file") || lower.includes("home") || lower.includes("note"))
) {
const said = /says?\s+(.+)$/i.exec(prompt)?.[1]?.replace(/[.]+$/, "") ?? prompt;
const content = `${said.trim()}\n`;
const filePath =
/(?:called|named)\s+([A-Za-z0-9._/-]+)/i.exec(prompt)?.[1] ?? "notes/result.txt";
return [
{ assistant: "writing that into my home now." },
{
toolCalls: [{ name: "write_file", args: { path: filePath, content } }],
complete: true,
},
];
}
if (lower.includes("remember")) {
return [
{
assistant: "noted — i will keep that in memory.",
memory: [{ scope: "bot", path: "MEMORY.md", content: prompt, mode: "append" }],
complete: true,
},
];
}
return [
{
assistant: `on it. i will work this in the background and come back with a result.\n\n${summarize(prompt)}`,
},
{
files: [{ path: "notes/last-task.md", content: `# Task\n\n${prompt}\n` }],
complete: true,
},
];
}
function shouldFail(prompt: string): boolean {
return prompt.toLowerCase().includes("fail this run");
}
function shouldHang(prompt: string): boolean {
const lower = prompt.toLowerCase();
return (
lower.includes("keep working") ||
lower.includes("work until i stop") ||
lower.includes("until i stop you") ||
lower.includes("hang until stopped")
);
}
function namedBot(prompt: string) {
return /named\s+([A-Za-z0-9][A-Za-z0-9_-]{0,39})/i.exec(prompt)?.[1];
}
function namedSpace(prompt: string) {
return /space\s+(?:named|called)\s+["“]?([^"”\n]{1,60})/i
.exec(prompt)?.[1]
?.replace(/[.!?]+$/, "")
.trim();
}
function summarize(prompt: string): string {
return `done. i handled: ${prompt.slice(0, 180)}`;
}