* 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>
614 lines
18 KiB
TypeScript
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)}`;
|
|
}
|