mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
feat: background tasks
Adds Background Tasks — recurring background agents the user can set up to either keep a digest current (daily email summary, top HN stories, weather brief) or perform a recurring action (draft a reply, post to Slack, call an API). Each task is a persistent set of instructions plus optional triggers (schedule, time-of-day window, or matching incoming Gmail / calendar event). The agent reads the verbs in the instructions on every run and picks the right mode automatically. User-facing surfaces: - New "Background tasks" entry in the sidebar, with a table listing every task, its schedule, last run, and an active toggle. - A detail page per task with a max-width reader showing the task's current output and a control sidebar for editing instructions, triggers, and reviewing run history. - "New task" can open in a free-form box where the user describes what they want and Copilot sets it up end-to-end, or in a structured form for manual setup. - "Edit with Copilot" hand-off from the detail view, pre-seeded with the task's context. Under the hood: - The event pipeline that previously powered live-notes is now a generic consumer registry. Live-notes and background tasks both subscribe; incoming events are routed to candidates from both concurrently. - Schedule helpers and the agent-message trigger block are factored out of live-notes into shared modules. Both features use the same building blocks now. - Copilot's proactive routing is reframed: anything recurring (cadence words, watch / monitor verbs, action verbs, event-conditional asks) now flows to background tasks. Live-notes load only on explicit mention. - A small reliability fix for the run-creation fallback chain: an empty-string model/provider passed by an LLM tool call now correctly falls through to the default instead of being persisted as a real value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13fa80c687
commit
b01af12148
45 changed files with 4025 additions and 594 deletions
76
apps/x/packages/shared/src/events.ts
Normal file
76
apps/x/packages/shared/src/events.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rowboat events — the shared queue feeding the live-note + bg-task consumers.
|
||||
//
|
||||
// Producers (gmail/calendar sync) write JSON files to `$WorkDir/events/pending/`
|
||||
// using IDs from the monotonically increasing ID generator. The processor in
|
||||
// `packages/core/src/events/processor.ts` polls the directory, fans out Pass-1
|
||||
// routing across registered consumers in parallel, fires each consumer's
|
||||
// candidates sequentially, then enriches the event and moves it to `done/`.
|
||||
//
|
||||
// Schema is additive-on-optional so old events written by previous versions
|
||||
// parse cleanly. The legacy `KnowledgeEventSchema` is re-exported as an alias
|
||||
// from `./live-note.ts` for one release.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ConsumerResultSchema = z.object({
|
||||
candidateIds: z.array(z.string()),
|
||||
runIds: z.array(z.string()),
|
||||
errors: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type ConsumerResult = z.infer<typeof ConsumerResultSchema>;
|
||||
|
||||
export const RowboatEventSchema = z.object({
|
||||
id: z.string().describe('Monotonically increasing ID; also the filename in events/pending/'),
|
||||
source: z.string().describe('Producer of the event (e.g. "gmail", "calendar")'),
|
||||
type: z.string().describe('Event type (e.g. "email.synced")'),
|
||||
createdAt: z.string().describe('ISO timestamp when the event was produced'),
|
||||
payload: z.string().describe('Human-readable event body, usually markdown'),
|
||||
|
||||
/**
|
||||
* If set, the consumer-named here short-circuits Pass-1 and targets the
|
||||
* named id directly (used for re-runs from the UI). The producer is
|
||||
* unchanged from the legacy `targetFilePath` behavior but generalized.
|
||||
*/
|
||||
target: z.object({
|
||||
consumer: z.string(),
|
||||
id: z.string(),
|
||||
}).optional(),
|
||||
|
||||
/** Legacy field — preserved on read for backwards compat with events
|
||||
* written by the pre-rename code. Equivalent to
|
||||
* `target: { consumer: 'live-note', id: <value> }`. */
|
||||
targetFilePath: z.string().optional(),
|
||||
|
||||
// ----------------- Enriched on move from pending/ to done/ -----------
|
||||
|
||||
processedAt: z.string().optional(),
|
||||
|
||||
/** Per-consumer outcome map. */
|
||||
consumers: z.record(z.string(), ConsumerResultSchema).optional(),
|
||||
|
||||
/** Legacy field — preserved on read for backwards compat with events
|
||||
* enriched by the pre-rename code. */
|
||||
candidateFilePaths: z.array(z.string()).optional(),
|
||||
|
||||
/** Legacy field — preserved on read for backwards compat with events
|
||||
* enriched by the pre-rename code. */
|
||||
runIds: z.array(z.string()).optional(),
|
||||
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
export type RowboatEvent = z.infer<typeof RowboatEventSchema>;
|
||||
|
||||
/**
|
||||
* Pass-1 routing output. The `ids` strings are consumer-defined:
|
||||
* - live-note → workspace-relative paths
|
||||
* - bg-task → task slugs
|
||||
*/
|
||||
export const Pass1OutputSchema = z.object({
|
||||
ids: z.array(z.string()).describe('Identifiers of candidates whose intent and event-match criteria suggest the event might be relevant. The consumer\'s agent does Pass 2 on the event payload before acting.'),
|
||||
});
|
||||
|
||||
export type Pass1Output = z.infer<typeof Pass1OutputSchema>;
|
||||
Loading…
Add table
Add a link
Reference in a new issue