Livenote2 (#440)

* tasks execute immediately

* response formatting

* remove at rowbot block for single tasks

* show last ran time stamp
This commit is contained in:
arkml 2026-03-19 01:34:10 +05:30 committed by GitHub
parent 91030a5fca
commit affc9956f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 372 additions and 39 deletions

View file

@ -5,6 +5,13 @@ export function getRaw(): string {
.map(name => ` ${name}:\n type: builtin\n name: ${name}`)
.join('\n');
const now = new Date();
const defaultEnd = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const localNow = now.toLocaleString('en-US', { dateStyle: 'full', timeStyle: 'long' });
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const nowISO = now.toISOString();
const defaultEndISO = defaultEnd.toISOString();
return `---
model: gpt-5.2
tools:
@ -12,16 +19,74 @@ ${toolEntries}
---
# Task
You are an inline task execution agent. You receive a @rowboat instruction from within a knowledge note and execute it.
You are an inline task execution agent. You receive a @rowboat instruction from within a knowledge note and either execute it immediately or set it up as a recurring task.
# Instructions
# Two Modes
1. You will receive the full content of a knowledge note and a specific instruction extracted from a \`@rowboat <instruction>\` line in that note.
2. Execute the instruction using your full workspace tool set. You have access to read files, edit files, search, run commands, etc.
3. Use the surrounding note content as context for the task.
4. Your response will be inserted directly into the note below the @rowboat instruction. Write your output as note content it must read naturally as part of the document.
5. NEVER include meta-commentary, thinking out loud, or narration about what you're doing. No "Let me look that up", "Here are the details", "I found the following", etc. Just write the content itself.
6. Keep the result concise and well-formatted in markdown.
7. Do not modify the original note file the service will handle inserting your response.
## 1. One-Time Tasks (no scheduling intent)
For instructions that should be executed immediately (e.g., "summarize this note", "look up the weather"):
- Execute the instruction using your full workspace tool set
- Return the result as markdown content
- Do NOT include any schedule or instruction markers
## 2. Recurring/Scheduled Tasks (has scheduling intent)
For instructions that imply a recurring or future-scheduled task (e.g., "every morning at 8am check emails", "remind me tomorrow at 3pm"):
- Do NOT execute the task only set up the schedule
- You MUST include BOTH markers described below
- Do NOT include any other content besides the markers
# Markers for Scheduled Tasks
When the instruction has scheduling intent, your response MUST contain these markers and nothing else:
## Schedule Marker (required)
<!--rowboat-schedule:{"type":"...","label":"..."}-->
Schedule types:
1. "cron" recurring: \`<!--rowboat-schedule:{"type":"cron","expression":"<5-field cron>","startDate":"<ISO>","endDate":"<ISO>","label":"<label>"}-->\`
"startDate" defaults to now (${nowISO}). "endDate" defaults to 7 days from now (${defaultEndISO}).
Example: "every morning at 8am" \`<!--rowboat-schedule:{"type":"cron","expression":"0 8 * * *","startDate":"${nowISO}","endDate":"${defaultEndISO}","label":"runs daily at 8 AM until ${defaultEnd.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}"}-->\`
2. "window" recurring with time window: \`<!--rowboat-schedule:{"type":"window","cron":"<cron>","startTime":"HH:MM","endTime":"HH:MM","startDate":"<ISO>","endDate":"<ISO>","label":"<label>"}-->\`
3. "once" future one-time: \`<!--rowboat-schedule:{"type":"once","runAt":"<ISO 8601>","label":"<label>"}-->\`
The "label" must be a short plain-English description starting with "runs" (e.g., "runs daily at 8 AM until Mar 24").
## Instruction Marker (required for scheduled tasks)
<!--rowboat-instruction:the refined instruction text-->
This is the instruction that will be executed on each scheduled run. You may refine/clarify the original instruction to make it more specific and actionable for the background agent that will execute it. For example:
- User says "check my emails every morning" \`<!--rowboat-instruction:Check for new emails and summarize any important ones.-->\`
- User says "news about claude daily" \`<!--rowboat-instruction:Search for the latest news about Anthropic's Claude AI and list the top stories with sources.-->\`
If the instruction is already clear and actionable, you can keep it as-is.
# Context
Current local time: ${localNow}
Timezone: ${tz}
Current UTC time: ${nowISO}
# Output Rules
- For one-time tasks: write output as note content it must read naturally as part of the document. NEVER include meta-commentary. Keep concise and well-formatted in markdown.
- For scheduled tasks: output ONLY the two markers (schedule + instruction), nothing else.
- Do not modify the original note file the system handles all insertions.
# Target Regions
For recurring/scheduled tasks, the note will contain a **target region** delimited by HTML comment tags:
\`\`\`
<!--task-target:TARGETID-->
...existing content...
<!--/task-target:TARGETID-->
\`\`\`
When you see a target region associated with your task (during a scheduled run), your response MUST be the replacement content for that region. You should:
- Write content that replaces whatever is currently between the tags
- Use the existing content as context (e.g., to update rather than regenerate from scratch if appropriate)
- Do NOT include the target tags themselves in your response
`;
}

View file

@ -176,6 +176,8 @@ interface InlineTask {
startLine: number;
/** Line index of the closing ``` fence */
endLine: number;
/** Target region ID for recurring tasks */
targetId: string | null;
}
/**
@ -183,7 +185,7 @@ interface InlineTask {
* Returns { instruction, schedule } or null if not valid JSON.
* Also supports legacy @rowboat format.
*/
function parseBlockContent(contentLines: string[]): { instruction: string; schedule: InlineTaskSchedule | null; lastRunAt: string | null } | null {
function parseBlockContent(contentLines: string[]): { instruction: string; schedule: InlineTaskSchedule | null; lastRunAt: string | null; targetId: string | null } | null {
const raw = contentLines.join('\n').trim();
try {
const data = JSON.parse(raw);
@ -193,6 +195,7 @@ function parseBlockContent(contentLines: string[]): { instruction: string; sched
instruction: parsed.data.instruction,
schedule: parsed.data.schedule ? { ...parsed.data.schedule, label: parsed.data['schedule-label'] ?? '' } as InlineTaskSchedule : null,
lastRunAt: parsed.data.lastRunAt ?? null,
targetId: parsed.data.targetId ?? null,
};
}
// Fallback for blocks that have instruction but don't fully match schema
@ -201,6 +204,7 @@ function parseBlockContent(contentLines: string[]): { instruction: string; sched
instruction: data.instruction,
schedule: data.schedule ?? null,
lastRunAt: data.lastRunAt ?? null,
targetId: data.targetId ?? null,
};
}
} catch {
@ -227,7 +231,7 @@ function parseBlockContent(contentLines: string[]): { instruction: string; sched
const rawInstruction = firstRowboatLine?.trim() ?? instructionLines.join('\n').trim();
const instruction = rawInstruction.replace(/^@rowboat:?\s*/, '');
if (!instruction) return null;
return { instruction, schedule, lastRunAt: null };
return { instruction, schedule, lastRunAt: null, targetId: null };
}
/**
@ -308,16 +312,16 @@ function findPendingTasks(body: string): InlineTask[] {
const parsed = parseBlockContent(contentLines);
if (parsed) {
const { instruction, schedule, lastRunAt } = parsed;
const { instruction, schedule, lastRunAt, targetId } = parsed;
if (schedule) {
if (isScheduledTaskDue(schedule, lastRunAt)) {
tasks.push({ instruction, schedule, startLine, endLine });
tasks.push({ instruction, schedule, startLine, endLine, targetId });
}
} else {
// One-time task: skip if already ran
if (!lastRunAt) {
tasks.push({ instruction, schedule: null, startLine, endLine });
tasks.push({ instruction, schedule: null, startLine, endLine, targetId });
}
}
}
@ -339,6 +343,32 @@ function insertResultBelow(body: string, endLine: number, result: string): strin
}
/**
* Replace content inside a target region identified by targetId.
* If the target region exists, replaces its content.
* If it doesn't exist, creates the target region below the task block,
* wrapping any existing content between the block and the next block/heading.
*/
function replaceTargetRegion(body: string, targetId: string, result: string, endLine: number): string {
const openTag = `<!--task-target:${targetId}-->`;
const closeTag = `<!--/task-target:${targetId}-->`;
const openIdx = body.indexOf(openTag);
const closeIdx = body.indexOf(closeTag);
if (openIdx !== -1 && closeIdx !== -1 && closeIdx > openIdx) {
// Target region exists — replace content between the tags
const before = body.slice(0, openIdx + openTag.length);
const after = body.slice(closeIdx);
return before + '\n' + result + '\n' + after;
}
// Target region doesn't exist yet — create it below the task block's closing fence
const lines = body.split('\n');
const taggedResult = `${openTag}\n${result}\n${closeTag}`;
lines.splice(endLine + 1, 0, '', taggedResult);
return lines.join('\n');
}
/**
* Determine if a note has any "live" tell-rowboat tasks.
* A task is live if:
@ -495,7 +525,13 @@ async function processInlineTasks(): Promise<void> {
const result = await extractAgentResponse(run.id);
if (result) {
currentBody = insertResultBelow(currentBody, task.endLine, result);
if (task.targetId) {
// Recurring task with target region — replace content inside the region
currentBody = replaceTargetRegion(currentBody, task.targetId, result, task.endLine);
} else {
// No target region — insert below the block
currentBody = insertResultBelow(currentBody, task.endLine, result);
}
// Update the block JSON with lastRunAt
const timestamp = new Date().toISOString();
currentBody = updateBlockData(currentBody, task.startLine, task.endLine, timestamp);
@ -531,6 +567,85 @@ async function processInlineTasks(): Promise<void> {
}
}
/**
* Process a @rowboat instruction via the inline task agent.
* The agent can execute one-off tasks and/or detect scheduling intent.
* Returns schedule info (if any), a schedule label, and optional response text.
*/
type ScheduleWithoutLabel =
| { type: 'cron'; expression: string; startDate: string; endDate: string }
| { type: 'window'; cron: string; startTime: string; endTime: string; startDate: string; endDate: string }
| { type: 'once'; runAt: string };
export async function processRowboatInstruction(
instruction: string,
noteContent: string,
notePath: string,
): Promise<{
instruction: string;
schedule: ScheduleWithoutLabel | null;
scheduleLabel: string | null;
response: string | null;
}> {
const run = await createRun({ agentId: INLINE_TASK_AGENT });
const message = [
`Process the following @rowboat instruction from the note "${notePath}":`,
'',
`**Instruction:** ${instruction}`,
'',
'**Full note content for context:**',
'```markdown',
noteContent,
'```',
].join('\n');
await createMessage(run.id, message);
await waitForRunCompletion(run.id);
const rawResponse = await extractAgentResponse(run.id);
if (!rawResponse) {
return { instruction, schedule: null, scheduleLabel: null, response: null };
}
// Parse out the schedule marker if present (allow multiline JSON)
const scheduleMarkerRegex = /<!--rowboat-schedule:([\s\S]*?)-->/;
const scheduleMatch = rawResponse.match(scheduleMarkerRegex);
// Parse out the instruction marker if present
const instructionMarkerRegex = /<!--rowboat-instruction:([\s\S]*?)-->/;
const instructionMatch = rawResponse.match(instructionMarkerRegex);
let schedule: ScheduleWithoutLabel | null = null;
let scheduleLabel: string | null = null;
let refinedInstruction = instruction;
if (instructionMatch) {
refinedInstruction = instructionMatch[1].trim();
}
if (scheduleMatch) {
try {
const parsed = JSON.parse(scheduleMatch[1]);
if (parsed && typeof parsed === 'object' && parsed.type) {
scheduleLabel = parsed.label || null;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { label: _, ...rest } = parsed;
schedule = rest as ScheduleWithoutLabel;
}
} catch {
// Invalid JSON in marker — ignore
}
// Scheduled task — no response content (agent only returns markers)
return { instruction: refinedInstruction, schedule, scheduleLabel, response: null };
}
// One-time task — the full response is the content
const response = rawResponse.trim() || null;
return { instruction: refinedInstruction, schedule: null, scheduleLabel: null, response };
}
/**
* Classify whether an instruction contains a scheduling intent using the user's configured LLM.
* Returns a schedule object or null for one-time tasks.