Repository navigation
Typed arguments and results for built-in tools, piloted on EventKit - #8
Merged
Merged
Conversation
The rewrite said no release was tagged, but 0.1.0 through 0.2.2 are. The old README had carried the same mistake.
Astra reviewed the options read-only and Fable decided: go truly async, with the first phase shipping width 1 so only deadlines and cancellation change. Written down now so the branch after typed-bridges starts from it.
…-dependent ones The registry checks a tool's statically declared permissions before its handler runs, and the bridges then checked the same permissions again with their own copy of the logic. The registry's check, the context's `resolvedPermission`, and LocationBridge each carried the broker sequence (status, record, request if undetermined, record). That sequence now lives in one place, `BridgeInvocationContext`. `requirePermission` is the enforcing form the registry uses, and it still asks the broker and records an event on every invocation, as before. A test pins that, so a later cache is a deliberate change, not a side effect. Built-in tools gain `argumentDependentPermissions(for:)`, checked after decoding, for permissions that depend on what the call asks for. Location read needs location access only in `current` mode. Calendar create vs update follows with the EventKit migration. The audit found all 29 static declarations match what their bridges enforce, and the permission-request helpers declare none, so nothing pre-checks them.
`@BuiltInCodeMode` generated typed decoding, and then almost every tool threw it away: it handed `arguments.raw` to its bridge, which parsed every argument again by string key. That meant two declarations per argument, and defaults kept both in hint prose and in code. This is the pilot that removes the duplication for one family and sets the template for the rest. The macro gains: - Defaults written as Swift initializers. `var limit: Int = 50` makes the argument optional, decode falls back to the default, and the advertised hint ends with a generated "Default: 50." Only literals and `.enumCase` are accepted; anything else, `let`, or an optional with a default is a diagnostic. - `Date` properties, decoded through `CodeModeDate`, with the same errors the bridges produced. - `providedArguments: Set<String>`, the keys a call supplied, so patch updates can tell which fields to touch without keeping raw values. The six EventKit tools drop `raw`. Their bridge methods take the tool's typed `Arguments`, and the re-parsing, the operation parsing, and the permission guards the registry already enforces are deleted. Calendar create and update choose their permission through `argumentDependentPermissions`. One intentional change: create now accepts a custom broker that reports `.writeOnly` for `.calendarWriteOnly`. The system broker already reported that as granted. Scripts see the same spellings, defaults, errors, and codes. The golden baseline diff is four hint lines gaining "Default: …". EventKitBridge.swift goes from 544 to 475 lines.
Schema derives from Swift types at runtime so shared and nested result structs compose; a @ToolResult macro generates the encoder and the schema from one declaration; the declarations render a real Promise type. JSON a script receives is unchanged.
Every built-in tool wrote a `codeModeTitle`, 120 of them, and no model ever saw one: `JavaScriptAPIReference` carries no title, and neither search nor the TypeScript declarations use it. The descriptor's title is now the last path segment, as the host-facing macro already does, and the protocol no longer asks for one. The golden baseline changes by exactly those 120 lines.
…, and move the logic into call Three changes to the EventKit family that set the template for the remaining bridge families. **One tool per helper.** `createEvent` also answered to `updateEvent`, and `createReminder` to `updateReminder` and `completeReminder`, through aliases on one tool. Hand-written JavaScript in the bootstrap injected an `operation` argument (and `isCompleted: true` for complete) before the call reached Swift, so scripts behaved, but the catalog and the declarations could not tell the helpers apart: `updateEvent` showed `identifier` as optional, and a failed `updateEvent` got `createEvent`'s repair suggestions. They are now five tools with their own `Arguments` (create requires title/start/end as `Date`; update requires `identifier`; complete requires `identifier` with `isCompleted` defaulting to true). The hand-written bindings are deleted; the generated bootstrap is the only binding for these names. Several tools now share one `CapabilityID`, so the registry keeps a list per ID, JavaScript dispatch passes the called helper name through to the registry and to error enrichment (suggestions are looked up by name first), and a host registration replaces every existing one whose names overlap, or the whole ID when it names none. `byCapability` in search holds the first helper; `byJSName` is exact. **`Patch<Value>`.** Scripts could never clear a field: the registry rejected `null` for typed arguments before any handler ran, so the "null clears" branches, and the presence tracking added for them, were unreachable. A `Patch` property is `.unchanged` when the key is missing, `.clear` on `null`, `.set` otherwise; it is advertised as nullable through the descriptor, `JavaScriptAPIReference` (decoded with a default) and the declaration (`notes?: string | null`), and the registry accepts `null` only there. Fields that cannot be cleared (title, start, end, an event's calendar) stay plain optionals, so the declaration is honest. For an `Arguments` struct with any `Patch` field the macro generates `hasChanges(_:)`, so an update with nothing to change is rejected without a hand-kept field list that would drift. **Logic in `call`.** Each tool's body moved from `EventKitBridge` into the tool; the bridge is now a namespace of the genuinely shared helpers (calendar lookup, completion, serializers). The loader no longer holds an instance. EventKitBridge.swift: 544 lines on main, 111 now. Also: hints say only what the type cannot (the macro adds "ISO 8601 timestamp." for `Date` arguments; dynamic defaults read "… when omitted"), pinned by a test; every catalog example is parsed against the live catalog; `RemindersCreateTool.isCompleted` is honoured instead of ignored; a capability-specific branch in the shared date parser is gone, so write-date errors use the standard repairable message.
Every helper was declared `Promise<CodeModeValue>`, with a hand-written
`resultSummary` sentence (120 of them, 111 distinct) that nothing checked
against the JSON the bridge built. Models guessed field names from prose,
and a host's `tsc` could not catch a wrong one.
A result is now a `@ToolResult` struct. The macro generates its encoder and
its schema from the one declaration, so they cannot disagree; an optional
`@ToolField("hint")` becomes the field's doc comment. The schema
(`CodeModeResultSchema`) is derived from Swift types at runtime rather than
read by the macro, so shared and nested results compose: `CalendarEvent`
serves list, create and update, and `[CalendarEvent]` describes itself.
Its JSON form is hand-written and compact (`"string"`, `{"array": …}`,
`{"object": [{name, schema, hint?}]}`), since it is public, persisted in
the golden and eval fixtures, and shown to models in search results.
`BuiltInCodeModeTool.call` returns `Result: CodeModeResultDescribable`.
`JSONValue` conforms with schema `.any`, and `codeModeResultSummary` is a
protocol-extension default derived from the schema, so the 14 unmigrated
families compile unchanged with their prose overrides. The declarations
render the schema as the promise's type argument and drop the `Returns:`
line when it would only repeat the type.
EventKit is migrated: `CalendarEvent`, `Reminder` and a delete result replace
the hand-built serializers; the nine summary overrides are gone. The JSON a
script receives is unchanged, including `""` where a missing string is
coalesced today; whether those should become `null` is recorded in TODO.md
as a deliberate decision still to take, along with element types for
argument arrays and named interfaces in the whole-surface declarations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes
@BuiltInCodeModeactually remove code, and sets the template the other 14 bridge families will copy. EventKit (calendar and reminders) is the pilot.Why
The macro generated typed
Argumentsdecoding, and then 109 of ~117 tools threw it away: they passedarguments.rawto a bridge that parsed every argument again by string key. Every argument was declared twice, defaults lived in both hint prose and code, and the results were hand-builtJSONValuedescribed by an unchecked sentence. Models saw every helper asPromise<CodeModeValue>.What a tool author writes now
And what the model sees:
Commits
4f9f4f6). One implementation of the broker sequence, used by the registry. Tools gainargumentDependentPermissions(for:)for permissions that depend on the call (location read incurrentmode). An audit found all 29 static declarations correct.651e054). Macro defaults as initializers (var limit: Int = 50),Dateproperties, bridges take typed input.codeModeTitleremoved (5b2e90f). 120 titles no model ever saw; derived from the path instead.Patch<T>, logic incall(b516c56).createEvent/updateEventandcreateReminder/updateReminder/completeReminderwere aliases on one tool, disambiguated by hand-written JS that injected anoperationargument. Now separate tools with correct required arguments. Several tools share oneCapabilityID, so the registry keeps a list per ID and dispatch passes the called name through (to the handler and to error suggestions).nullbefore any handler ran.Patch<T>(missing /null/ value) makes clearing real on update tools, advertised asT | nullonly where it's true. The macro generateshasChanges(_:)so an empty update is rejected without a hand-kept field list.EventKitBridge.swiftis now 149 lines of shared helpers and result structs (544 onmain).a240d0c).@ToolResultstructs, a runtime-derivedCodeModeResultSchemawith a compact public JSON form, and declarations that render it as the promise type. Unmigrated families keep-> JSONValueand their prose summary, so they compile unchanged.For the reviewer
nullnow clears a field on the update helpers:notes,location,urlandisAllDayonupdateEvent;dueDate,notes,isCompleted,priorityand the calendar onupdateReminder. Before, it was rejected.createEventwithidentifier, orupdateEventwithout one, is now anINVALID_ARGUMENTSerror naming the right helper..writeOnlyfor.calendarWriteOnly. The system broker already reported that case as granted.CapabilityRegistry.invoke(_:jsName:arguments:context:)gains an optionaljsName;CapabilityDescriptorandJavaScriptAPIReferencegainnullableArgumentsandresultSchema(both decode with defaults, so earlier encodings still load); a host registration now replaces every built-in registration whose JS names it overlaps, or the whole capability when it names none.""for absent strings. Whether those should becomenullis a deliberate decision left open inTODO.md.CapabilityID, first-registration semantics fordescriptor(for:)/registration(for:), andbyCapabilityin search holding the first helper.Verification
Each commit was reviewed by an independent quality gate (Opus) until it passed; three rounds on the split, two on typed results. On the final head, locally:
swift test: 356 + 12 + 32 tests passingscripts/check-typescript-declarations.sh: all 128 generated declarations compile undertsc --strictxcodebuild buildforgeneric/platform=iOSandgeneric/platform=visionOS: cleanNot in this PR
Migrating the other 14 families (each with its hand-written
globalThis.apple.*JS deleted), element types for argument arrays, named interfaces in the whole-surface declarations, and the async bridge work. All recorded inTODO.mdand the two plan files.