rowboat/apps/x/packages/shared/src/events.ts
Ramnique Singh b01af12148 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>
2026-05-12 17:43:25 +05:30

76 lines
3.2 KiB
TypeScript

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>;