google_calendar
v0.1.0 · compiled with katari 0.1.0
Overview
from the package READMEgoogle_calendar — Google Calendar tools for Katari
A single module, google_calendar: two tools the model can call — list_events and create_event —
and a notification watcher, watch — over Google's Calendar API. Pure Katari: every API call is
http.fetch with the request built and the reply parsed as json. No FFI sidecar — and no OAuth
plumbing in the program: authentication is the runtime's credentials core, reached through the stdlib's
oauth.token.
google_calendar.list_events(calendar_id, time_min, time_max, max_results?)— upcoming events in a window, trimmed to id / summary / start / end / link.google_calendar.create_event(calendar_id, summary, start, end, description?)— create an event.google_calendar.watch(calendar_id, lead_time_milliseconds, poll_interval_milliseconds, deliver_to)— a daemon that delivers each upcoming timed event todeliver_toas it enters the lead window (at-least-once, deduped per(id, start)within a single activation). Never resolves; composes underparallel [ … ].google_calendar.provider(name?)— provides the capability the tools require (get_access_token) for the extent of a continuation, by resolving the stored credential namedname(default"google") through the runtime on every ask. The provider holds no secret and keeps no cache: the runtime owns the token material, serves the stored access token while its recorded lifetime holds, and refreshes it against the stored token endpoint once due. The resolved token is astring of private— it flows only to theAuthorization: Bearerheader, never to a user-facing boundary.
Failure model
Three meanings, decided at three boundaries:
- The credential needs a human — it was never authorized, or its refresh is dead. This is never
an error:
oauth.tokenpauses the run on aprelude.oauth.authorizeescalation (the admin console andkatari answerrender it as an authorization request), and completing the browser flow resumes the run right where it stopped. Nothing to catch — a pause, not a throw. oauth.server_error(stdlib) — the token could not be resolved for a transient reason: a network error, or the token endpoint failing (5xx) while refreshing. Thrown at the provider; retry it like any transport blip.google_calendar.calendar_error = google_calendar.auth_error | google_calendar.api_error— the Calendar API's own failures, classified once:auth_error— a 401/403: Google rejected the resolved access token (revoked before its stored expiry, or scoped too narrowly). Retrying the same token does not help, but replaying the call does: the re-run resolves the token afresh, the runtime refreshes the credential once its stored lifetime passes, and a credential the runtime cannot refresh parks the run as a re-authorization prompt. No app-owned escalation is needed.api_error— any other non-2xx (a bad request, a missing calendar, a quota or server error). Usually transient; a plain backoff retry is the right response.
Setup: register the Google OAuth client, then log in
The runtime hosts the whole OAuth flow — the browser consent, the redirect callback, the token exchange, storage and refresh. You register a Google OAuth client with the runtime once; programs then name the credential and never see a token.
1. Create the OAuth client in the Google Cloud console
- In console.cloud.google.com, create (or select) a project and enable the Google Calendar API (APIs & Services → Library).
- Configure the OAuth consent screen (APIs & Services → OAuth consent screen). While the app is in Testing status, add the Google account you will authorize as a test user — and note that Google expires a Testing app's refresh tokens after 7 days, so publish the app (or use Internal on a Workspace domain) for a daemon that must keep running.
- Create the client: APIs & Services → Credentials → Create Credentials → OAuth client ID, type
Web application. Add the runtime's callback as an authorized redirect URI:
<public-url>/oauth/callback, where<public-url>is the runtime's public base URL (KATARI_PUBLIC_URL; the local default is the runtime's own address, e.g.http://localhost:3000/oauth/callback). - Note the Client ID and Client secret.
2. Register the client with the runtime
In the admin console, open your project's Credentials page and press Register client:
| Field | Value |
|---|---|
| Name | google (the provider's default credential name) |
| Issuer | https://accounts.google.com |
| Authorization endpoint | https://accounts.google.com/o/oauth2/v2/auth |
| Token endpoint | https://oauth2.googleapis.com/token |
| Client ID / Client secret | from the console (the secret is write-only — it is sealed and never echoed back) |
| Scopes | https://www.googleapis.com/auth/calendar |
| Extra authorize parameters | access_type=offline prompt=consent |
The extra parameters matter for Google specifically: access_type=offline is what makes Google issue a
refresh token (without it the credential dies when the first access token expires), and
prompt=consent makes Google re-issue one on a later re-authorization instead of silently omitting it.
3. Log in
Press Log in on the registered client's row and complete the consent screen — the runtime stores
the credential under the client's name. Or skip this: the first get_access_token of a run pauses
it on an authorization escalation, which the admin console (and katari answer) render as the same
login button; authorizing resumes the run.
For several Google accounts, register the same client under several names (e.g. google_work) and pick
one per scope: use google_calendar.provider(name = "google_work").
Usage: the tools
import google_calendar
agent upcoming(calendar_id: string, time_min: string, time_max: string) -> array[google_calendar.event] with io | prelude.throw[oauth.server_error | google_calendar.calendar_error | http.fetch_error | json.parse_error] {
use google_calendar.provider()
google_calendar.list_events(calendar_id = calendar_id, time_min = time_min, time_max = time_max)
}Hand google_calendar.list_events / google_calendar.create_event to an AI loop's tool list to let the model read
and schedule on its own. A failed call throws google_calendar.calendar_error — handle it at the app root.
If the credential is not authorized yet, the run pauses and asks instead of failing.
Usage: the watch
google_calendar.watch polls a calendar and delivers each upcoming timed event to your deliver_to agent.
Delivery is at-least-once, deduped per (event id, start time) — but only within a single watch
activation. While the watch keeps running (including across a crash-recovery restart, which resumes the
durable dedup memory) an event whose start has not moved is delivered once, and an event whose start
moved is a new pair, so it re-notifies. A replay re-run is a fresh activation whose memory starts
empty, so a prelude.replay provider around the watch (below) re-delivers every event still inside the
lead window; a deliver_to failure likewise re-delivers the events after it in that tick on the next run.
All-day events are not covered — they have no instant to place in the lead window. The watch never
resolves, so it runs as a parallel [ … ] arm (or a standalone entry); its deliver_to effects flow out
to the app's handlers unchanged.
poll_interval_milliseconds must not exceed lead_time_milliseconds: a wider interval leaves the gap
(T+lead, T+poll) covered by no window and would silently drop events starting there. That is a program
defect, rejected at entry with google_calendar.watch_misconfigured (the panic this morally is — a Katari
program cannot raise one — surfaced as a throw).
import google_calendar
import discord
@"Notify a Discord channel 10 minutes before each event, checking every minute."
agent remind(calendar_id: string, channel_id: string) -> never {
use discord.provider(token = env.get_secret(key = "DISCORD_TOKEN"))
use google_calendar.provider()
agent notify(event: google_calendar.event) -> null {
discord.send_message(channel_id = channel_id, text = f"Upcoming: ${event.summary} at ${event.start}\n${event.html_link}", files = [])
}
google_calendar.watch(
calendar_id = calendar_id,
lead_time_milliseconds = 600000, // look 10 minutes ahead
poll_interval_milliseconds = 60000, // poll once a minute
deliver_to = notify,
)
}watch has no built-in retry: a poll failure — an http error, a rejected token, a deliver_to
that throws — propagates and kills the watch, exactly as an uncaught failure in any callee does.
Resilience is composed around the watch with a prelude.replay provider, below.
Composition: resilience with prelude.replay
prelude.replay splits the retry mechanism from the failure policy. A replay provider
re-runs the rest of a block, but it knows nothing about what counts as retriable: it catches exactly one
request, replay.interrupted, and re-runs after its delay. Deciding which failures become an
interrupted is your code — an ordinary use handler (a converter) installed between the provider
and the block that turns the throws it chooses into replay.interrupted(failure = …) and re-raises the
rest.
Under oauth.token, the policy collapses to one converter. The prototype needed a second, attended
recipe — an app-owned escalation that parked a revoked refresh token for a human — because re-authorization
was the app's problem. It no longer is: a credential that needs a human pauses the run on the runtime's
prelude.oauth.authorize escalation, inside get_access_token itself. So the converter's whole job is:
- replay the transient failures (
api_error,oauth.server_error,http.fetch_error,json.parse_error) — back off and re-run; - replay
auth_errortoo — the re-run resolves the token afresh; once the rejected token's stored lifetime passes the runtime refreshes it, and if the refresh is dead the re-run pauses for re-authorization on its own. (Until that stored lifetime passes — at most about an hour — the replays re-see the same token and fail again, which is why a capped backoff likereplay.forever, notreplay.immediate, is the right mechanism here.) - re-raise the genuine defects (
watch_misconfigured— fix the constants, retrying is meaningless).
Two rules the converter must obey — both because a throw handler catches the whole throw union of
the block it guards, not a subset:
- Name every failure the block can raise. For the block below that is
google_calendar'sauth_error | api_error | watch_misconfigured, plusoauth.server_errorfrom the provider andhttp.fetch_error/json.parse_errorfrom the calls. (Adeliver_tothat itself throws — say adiscord.send_messagetransport error — adds its failure type to that union too.) - Reconstruct a re-raised failure rather than re-throwing the match scrutinee:
prelude.throw(error = error)would widen the residual back to the whole union, so each propagated case rebuilds its own value (prelude.throw(error = google_calendar.watch_misconfigured(message = message))).
The daemon, resilient: one converter over replay.forever
import google_calendar
import discord
@"The calendar reminder daemon: replay transient failures and rejected tokens, propagate the defects.
A credential that needs a human pauses the run on the runtime's authorize escalation — not handled here."
agent remind_resiliently(calendar_id: string, channel_id: string) -> never with io | prelude.throw[google_calendar.watch_misconfigured | env.missing_secret] {
use discord.provider(token = env.get_secret(key = "DISCORD_TOKEN"))
agent notify(event: google_calendar.event) -> null {
discord.send_message(channel_id = channel_id, text = f"Upcoming: ${event.summary}", files = [])
}
// MECHANISM: re-run the block after a backoff (100ms, doubling, capped at a minute) on every replay.
use replay.forever(initial_delay_milliseconds = 100.0, factor = 2.0, max_delay_milliseconds = 60000.0)
// POLICY: names every failure the block can throw, then dispatches — one defect re-raised, the rest replayed.
use handler {
request prelude.throw(error: google_calendar.auth_error | google_calendar.api_error | google_calendar.watch_misconfigured | oauth.server_error | http.fetch_error | json.parse_error) -> never {
match (error) {
case google_calendar.watch_misconfigured(message => message) -> { prelude.throw(error = google_calendar.watch_misconfigured(message = message)) } // program defect
case _ -> { replay.interrupted(failure = error) } // transient, or a rejected token → back off + replay (the re-run re-resolves)
}
}
}
// INSIDE the replay scope, so a re-run RE-ENTERS the provider and its next `get_access_token`
// re-resolves the credential through the runtime.
use google_calendar.provider()
google_calendar.watch(calendar_id = calendar_id, lead_time_milliseconds = 600000, poll_interval_milliseconds = 60000, deliver_to = notify)
}The full story: the daemon beside a bot, under one parallel
@"The bot's serve loop — the app's other arm, echoing each message in the channel."
agent discord_serve(channel_id: string) -> never {
use discord.provider(token = env.get_secret(key = "DISCORD_TOKEN"))
agent echo(channel_id: string, text: string, files: array[file]) -> null {
discord.send_message(channel_id = channel_id, text = f"echo: ${text}", files = [])
}
discord.watch_messages(channel_id = channel_id, deliver_to = echo)
}
@"The whole app: the reminder daemon and the bot serve loop, side by side and independently resilient.
A `parallel` of two arms is a pair; of two `never` arms, one that is never produced."
agent main(calendar_id: string, channel_id: string) -> [never, never] {
parallel [
remind_resiliently(calendar_id = calendar_id, channel_id = channel_id),
discord_serve(channel_id = channel_id),
]
}The auth_error / api_error split is still what makes the policy precise — but now both arms replay,
for different reasons: an api_error replay re-runs the same call past a transient fault, while an
auth_error replay is a token re-resolution whose end state, when a human really is needed, is the
runtime's own re-authorization pause. The one failure the converter re-raises is the one no replay can
change: a miswired watch.
google_calendar
29 declarationsauth_error#
datadata auth_error(message: string)An authentication failure: a Calendar API call came back 401/403 — Google rejected the access token the runtime resolved (revoked before its stored expiry, or scoped too narrowly for the call). Retrying the SAME token does not help, but replaying the call recovers without any app-owned escalation: catch it in a converter that signals replay.interrupted — the re-run asks get_access_token afresh, the runtime refreshes the credential once its stored lifetime passes, and a credential the runtime cannot refresh parks the run on a prelude.oauth.authorize escalation (the admin console's re-authorization prompt) instead of failing. message carries the status and the server's body.
Parameters
Wire view (JSON Schema)
api_error#
datadata api_error(message: string)A Calendar API call failed for a non-authentication reason: the endpoint returned a non-2xx status that is not 401/403 (a bad request, a not-found calendar, a quota or server error). message carries the status and the server's body so the model can read what went wrong and adjust. Distinct from auth_error so a program can tell a request the service faulted on (back off and replay as-is) from a token Google rejected (the replay's re-resolution is what fixes it).
Parameters
Wire view (JSON Schema)
calendar_error#
typetype calendar_error = auth_error | api_errorDefinition
watch_misconfigured#
datadata watch_misconfigured(message: string)A watch was wired with a poll interval WIDER than its lead window, so the gap (T+lead, T+poll) between one poll's window and the next is covered by no window and events starting there would be silently dropped. This is a program defect — a bad constant at the call site, not a runtime condition — and would be a panic if a Katari program could raise one; since it cannot, watch throws this at entry instead. message names both values. It is NOT a calendar_error (nothing failed at Google); fix the constants rather than composing a retry around it.
Parameters
Wire view (JSON Schema)
event#
datadata event(id: string, summary: string, start: string, end: string, html_link: string)A calendar event trimmed to what an assistant needs: the id, the summary (title), start / end (an RFC3339 timestamp for a timed event, a date for an all-day one), and the html_link that opens it in a browser.
Parameters
Wire view (JSON Schema)
timed#
datadata timed(date_time: string)A timed event's moment: an RFC3339 instant (start.dateTime / end.dateTime), e.g. "2026-07-11T15:00:00+09:00".
Parameters
Wire view (JSON Schema)
all_day#
datadata all_day(date: string)An all-day event's moment: a bare YYYY-MM-DD date (start.date / end.date), with no time of day and no timezone.
Parameters
Wire view (JSON Schema)
undated#
datadata undated()Neither dateTime nor date was present — an unexpected event shape. The explicit stand-in for a genuinely absent moment, so downstream dispatches on the variant instead of guessing from a sentinel string.
Wire view (JSON Schema)
moment#
typetype moment = timed | all_day | undatedread_moment#
agentagent read_moment(field: unknown) -> momentClassify a Calendar start / end object into the moment sum at the ONE parse boundary: its dateTime (a timed instant) if present, else its date (an all-day date), else undated. Google nests the moment as one of dateTime / date, so this dispatch — not a "" sentinel test downstream — is where timed and all-day part ways.
Parameters
Returns
Inferred type
agent(field: unknown) -> (all_day | timed | undated)
Wire view (JSON Schema)
moment_text#
agentagent moment_text(value: moment) -> stringRender a moment as the string the event carries for the model: a timed event's dateTime, an all-day event's date, or the empty string when the moment is genuinely absent (there is no instant to show — the only place a "" is honest, at the string boundary rather than as an internal sentinel).
Parameters
Returns
Inferred type
agent(value: all_day | timed | undated) -> string
Wire view (JSON Schema)
get_access_token#
requestrequest get_access_token() -> string of privateGet a usable Google access token, served by provider for the extent of a continuation — the capability the tools require (with … get_access_token). Each ask resolves the provider's named credential through the runtime (oauth.token): the runtime returns the stored token while its recorded lifetime holds and refreshes it against the stored token endpoint once due, so no token bookkeeping lives in the program. The token is a string of private — it flows only toward a submission sink (the Bearer header). A credential that needs a human (never authorized, or refresh-dead) never throws: the ask PARKS the run on a prelude.oauth.authorize escalation and resumes once the browser flow completes. A transient resolution failure throws oauth.server_error at the provider.
Returns
Wire view (JSON Schema)
provider#
agentagent provider[R, effect E](name: string ?= "google", continuation: agent(value: null) -> R with {...E, get_access_token}) -> R with E | io | prelude.throw[oauth.server_error]Provide the Google credential for the extent of the continuation: use google_calendar.provider(), or provider(name = "…") to pick a non-default credential. It serves get_access_token by resolving name through the runtime's credentials core (oauth.token) on every ask — the runtime owns the token material, its cache and its refresh, so this provider holds no secret and keeps no state. name must be registered and authorized out of band (the README's setup: register the Google OAuth client, then log in via the admin console); an unauthorized credential parks the run on a prelude.oauth.authorize escalation rather than failing, and a transient token-resolution failure throws oauth.server_error.
Generics
Parameters
Returns
Effects
Inferred type
agent(continuation: agent(value: null) -> T0 with get_access_token | E1, name?: string) -> T0 with throw[server_error] | E1 | io
Wire view — the wire shape is fixed at the call site, where the generic parameters are instantiated.
bearer#
agentagent bearer(token: string of private) -> record[string of private]The Authorization: Bearer <token> header every Calendar API call carries. The token is a string of private (the runtime-resolved credential); concatenation propagates the taint, and the header record is a private-capable submission sink, so the secret leaves only toward Google's server.
Parameters
Returns
Wire view (JSON Schema)
classify_api_error#
agentagent classify_api_error(context: string, status: integer, body: string) -> calendar_errorClassify a failed Calendar API response into the error sum, at the ONE boundary where the split is decided. A 401 or 403 is an authentication failure — Google rejected the resolved access token (revoked, or scoped too narrowly) — surfaced as auth_error, which a replay converter turns into a fresh token resolution (and, when the credential is refresh-dead, the runtime's re-authorization park). Every other non-2xx is an api_error. (403 is read as auth even though Google also returns it for quota exhaustion; the access-rejection reading is the one worth composing the re-resolution around, and a quota 403 that is replayed simply fails again.) context names the call for the message.
Parameters
Returns
Inferred type
agent(body: string, context: string, status: integer) -> (api_error | auth_error)
Wire view (JSON Schema)
pad#
agentagent pad(value: number, width: integer) -> stringParameters
Returns
Wire view (JSON Schema)
to_rfc3339#
agentagent to_rfc3339(epoch_milliseconds: number) -> stringFormat an epoch-millisecond instant as an RFC3339 UTC timestamp (YYYY-MM-DDTHH:MM:SSZ) — the shape Calendar's timeMin / timeMax require, produced from the epoch milliseconds time.now returns. The civil date is Howard Hinnant's civil_from_days algorithm (a closed-form, leap-rule-correct days -> year/month/day with no month-length tables), read in UTC; sub-second precision is dropped (the API does not need it). Assumes a non-negative instant (post-1970, which time.now always is) and a four-digit year (any realistic wall-clock instant). Pure Katari keeps the package sidecar-free; the algorithm is validated against Date.toISOString across the whole range.
Parameters
Returns
Wire view (JSON Schema)
from_rfc3339#
agentagent from_rfc3339(value: string) -> numberParse an RFC3339 timestamp back to an epoch-millisecond instant — the inverse of to_rfc3339, used by watch to place a Calendar event's start.dateTime on the same numeric line as its tick's epoch window. Reads the fixed-position civil fields (YYYY-MM-DDTHH:MM:SS) and folds them to days with Hinnant's days_from_civil (the exact inverse of to_rfc3339's civil_from_days), then applies the trailing zone: Z is UTC, and a ±HH:MM offset (always the last six characters Google emits for a timed event) is subtracted so the result is a true UTC instant regardless of the event's timezone — a string compare would be wrong, since 09:00+09:00 and 00:00Z are the same moment. Sub-second precision is ignored (it never affects a minute-or-coarser window). A malformed field degrades to 0 in the total-reader idiom, so a garbled timestamp sorts to the epoch rather than throwing.
Parameters
Returns
Wire view (JSON Schema)
calendar_get#
agentagent calendar_get(url: string, context: string) -> unknownIssue an authenticated GET to a Calendar API url and return its parsed JSON body. The one place the Bearer header, the 2xx check and the error classification live, shared by list_events and watch's pager. A non-2xx becomes classify_api_error(@context@, ...) (401/403 → auth_error, else api_error); context names the caller for the message.
Parameters
Returns
Inferred type
agent(context: string, url: string) -> unknown with get_access_token | throw[api_error | auth_error | fetch_error | parse_error] | io
Wire view (JSON Schema)
list_events#
agentagent list_events(calendar_id: string, time_min: string, time_max: string, max_results: integer ?= 10) -> array[event]Tool: list the upcoming events on a calendar. calendar_id is the calendar's id ("primary" for the user's own, or an address like "teamexample.com"); time_min@ / time_max bound the window as RFC3339 timestamps (e.g. "2026-07-11T00:00:00Z"); max_results caps how many come back. Returns the events trimmed to id / summary / start / end / link — small enough to feed straight back to the model.
Parameters
Returns
Inferred type
agent(calendar_id: string, max_results?: integer, time_max: string, time_min: string) -> array[event] with get_access_token | throw[api_error | auth_error | fetch_error | parse_error] | io
Wire view (JSON Schema)
create_event#
agentagent create_event(calendar_id: string, summary: string, start: string, end: string, description: string ?= "") -> eventTool: create an event on a calendar. calendar_id is the target calendar ("primary" for the user's own); summary is the title; start / end are RFC3339 timestamps (e.g. "2026-07-11T15:00:00Z"); description is optional detail. Returns the created event (its id and the link to open it).
Parameters
Returns
Inferred type
agent(calendar_id: string, description?: string, end: string, start: string, summary: string) -> event with get_access_token | throw[api_error | auth_error | fetch_error | parse_error] | io
Wire view (JSON Schema)
deliver_if_new#
requestrequest deliver_if_new(event: event) -> nullDeliver one event and record its (id, start) pair the INSTANT its deliver_to returns — the per-event dedup commit. Handled inside watch; the pair is looked up in the durable already_notified var (absent → deliver and append, present → skip), so a mid-tick failure never re-delivers an event already delivered in the same tick. Internal to watch; an app does not handle it.
Parameters
Returns
Wire view (JSON Schema)
retain_window#
requestrequest retain_window(keys: array[string]) -> nullPrune the dedup memory to keys — the (id, start) pairs of the current window — dropping events that have left it, so the memory stays bounded to one window. Performed once per tick before delivery. Internal to watch; an app does not handle it.
Parameters
Returns
Wire view (JSON Schema)
watch#
agentagent watch[effect E](calendar_id: string, lead_time_milliseconds: number, poll_interval_milliseconds: number, deliver_to: agent(event: event) -> null with E) -> never with E | io | get_access_token | prelude.throw[watch_misconfigured | calendar_error | http.fetch_error | json.parse_error]Watch a calendar for upcoming TIMED events and deliver each one, once per activation, as it enters the lead window — shaped like time.watch / discord.watch_messages so it composes under parallel [ … ]. Every poll_interval_milliseconds it lists the events whose START lies within lead_time_milliseconds ahead of the tick and calls deliver_to for each not yet notified. All-day events are NOT covered (they have no instant to place in the lead window); this watch is for timed events only.
Delivery is AT-LEAST-ONCE, exactly-once per (event id, start time) only WITHIN a single activation: an event whose start has not moved is delivered a single time while the watch keeps running, and an event whose start moved is a different pair, so it re-notifies. A crash-recovery restart resumes the durable dedup memory and does not re-notify; but a replay re-run (a replay.forever / replay.exponential around the watch re-invokes its continuation, giving a fresh activation whose memory starts empty) re-delivers every event still inside the lead window, and a deliver_to failure re-delivers the events after it in that tick on the next run. watch never resolves; deliver_to's effects E flow to the caller's handlers unchanged.
poll_interval_milliseconds must not exceed lead_time_milliseconds — a wider interval leaves the gap (T+lead, T+poll) covered by no window, silently dropping events that start there. That is a program defect, rejected at entry with watch_misconfigured (a panic in spirit; a Katari program cannot raise one).
No retry is built in — a poll failure (an http error, a rejected token, a deliver_to that throws) propagates and kills the watch, exactly as an uncaught failure in any callee does. Compose resilience by wrapping the watch in a replay provider (replay.forever with a converter that replays the transient blips AND auth_error — the re-run re-resolves the token, and a credential needing a human parks on the runtime's authorize escalation rather than failing). The first poll is one poll_interval in (a time.watch occurrence fires one interval after the start).
Generics
Parameters
Returns
Effects
Inferred type
agent(calendar_id: string, deliver_to: agent(event: event) -> null with E2, lead_time_milliseconds: number, poll_interval_milliseconds: number) -> never with get_access_token | throw[api_error | auth_error | watch_misconfigured | fetch_error | parse_error] | E2 | io
Wire view — the wire shape is fixed at the call site, where the generic parameters are instantiated.
list_window#
agentagent list_window(calendar_id: string, window_start: number, lead: number) -> array[event]List every timed event whose START lies in the window [window_start, window_start + lead), across ALL result pages — watch's per-poll listing. Paginates on nextPageToken so a busy window is delivered in full rather than truncated at one page. The recursion depth is the number of pages in one poll (small and bounded — a single lead window rarely spans more than a page), not the daemon's lifetime, so it does not accumulate like a per-tick loop would. Internal to watch.
Parameters
Returns
Inferred type
agent(calendar_id: string, lead: number, window_start: number) -> array[event] with get_access_token | throw[api_error | auth_error | fetch_error | parse_error] | io
Wire view (JSON Schema)
fetch_pages#
agentagent fetch_pages(base_url: string, page_token: string | null, window_start: number, lead: number, accumulated: array[event]) -> array[event] with get_access_token | io | prelude.throw[calendar_error | http.fetch_error | json.parse_error]Fetch one page of the window listing, accumulate its deliverable events, and recurse on nextPageToken until Google stops sending one. page_token is null for the first page. Internal to watch's pager (list_window).
Parameters
Returns
Effects
Inferred type
agent(accumulated: array[event], base_url: string, lead: number, page_token: null | string, window_start: number) -> array[event] with get_access_token | throw[api_error | auth_error | fetch_error | parse_error] | io
Wire view (JSON Schema)
deliverable_events#
agentagent deliverable_events(parsed: unknown, window_start: number, lead: number) -> array[event]The timed events on one events.list page whose START lies in [window_start, window_start + lead) — the client-side re-filter that corrects the coarse server query. Dispatches each event's start moment: a timed instant is parsed (from_rfc3339) and kept only if it falls in the window; an all_day or undated event is dropped (the watch covers timed events only). Internal to watch.
Parameters
Returns
Inferred type
agent(lead: number, parsed: unknown, window_start: number) -> array[event]
Wire view (JSON Schema)
render_events#
agentagent render_events(parsed: unknown) -> array[event]Every event in a Calendar events.list reply, trimmed to event. A reply that carries no items array (an error shape the caller already branched away from, or an empty calendar) degrades to an empty list rather than throwing.
Parameters
Returns
Wire view (JSON Schema)
render_event#
agentagent render_event(item: unknown) -> eventOne Calendar event object as an event. Google nests the moment under start / end as either a dateTime (timed) or a date (all-day); read_moment classifies it and moment_text renders the string the model reads.
Parameters
Returns