Add createExternalResource agent tool - #427
Draft
ndisidore wants to merge 12 commits into
Draft
Conversation
Preview:
|
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Vendors advertise creatable resource types via SupportedResource.creatable; GatekeeperUser.createResource() mints a provisional identity (no provider call, no user interaction) and returns an imbued gatekeeper class the way getGatekeeperClassFor() does; Gatekeeper.submitCreationAction() queues the "create this resource" action so the provider-side creation rides the normal action-approval flow while the gatekeeper simulates the resource locally. All three are optional and additive; no existing gatekeeper changes.
Like requestConnection except it creates a brand-new resource of a creatable type through an already-connected account, and no user action gates the binding: it lands in the chat's env immediately (the createGadget pattern -- recorded output drives replay and chatScopeNames), the turn continues, and the creation is an ordinary pending action attributed to the chat, so its card rides the existing consumeCapturedActions splice. * agent.ts: tool definition, AgentHooks.createExternalResource, replay case, a system-prompt sentence. Fixable rejections (bad name, unknown vendor or type, no usable account, ambiguous accounts) return non-error tool results so the model retries in-turn; with several connected accounts the message enumerates ids so the agent can retry with accountId or ask the user. * user.ts: createResourceGatekeeper(), the urlPattern-to-capability chokepoint for creations, applying the same admin disable-set checks as getGatekeeperClassFor(). * overseer.ts: the hook implementation (vendor/creatable validation, mint, addGatekeeper, submitCreationAction through an ApprovalQueueImpl scoped to the new workpiece and the creating chat, with removeGatekeeper on failure); GatekeeperRecord.provisional drives a one-shot describe refresh (guarded on the URL changing, since an invalidated edit can apply before the creation) in applyPendingAction once the creation applies, since nothing else ever re-denormalizes resourceTitle/resourceUrl; listConnectableResources annotates creatable types. * ChatInterface.tsx: the six exhaustive tool-display switch cases. * integration tests: the fixture gatekeeper grows a creatable type with provisional URLs and a rejection-kills-the-binding path; new end-to-end coverage for fixable retry, pre-approval use, the action card, the describe refresh after approval, replay across turns, and rejection.
… scan. buildCompactionState folded createGadget/createWorktree outputs into the checkpoint's chatBindings but not createExternalResource, so once compaction crossed the creation call the binding replay re-establishes from the recorded output silently vanished. prepareChatBindings' naming chokepoint had the same gap: the quick-model namer could hand a pasted capsule a name replay would bind to a created resource. Both now mirror the replay semantics: a structured output binds, a string (rejection) output only reserves the name.
…owner. createExternalResource resolved the workspace owner's user DO, so a collaborator-driven turn enumerated the owner's connected accounts (the ambiguous-accounts error lists ids and names into the transcript) and created resources under the owner's credentials. Follow the actor-scoped pattern of listAvailableBlueprints: the hook now takes the turn's initiator and resolves their user DO. Owner-initiated turns are behavior-identical. The vendor listing stays owner-scoped -- it exposes only static vendor classes.
createExternalResource makes the gatekeeper record and its pending creation
action durable immediately, but the tool call reaches the chat log only at
commitAgentStep -- a DO restart mid-turn left an orphan gatekeeper plus an
approvable creation action bound to nothing, and the resumed turn could mint
a duplicate. Mirror the gadget pending lifecycle: stamp
GatekeeperRecord.pending = {chatId} in the same put as `provisional`, clear
it at the step barrier in addChatMessages (same transaction as the message
write), and sweep unstamped markers from reconcilePendingGadgets' schedule
and chat deletion. The sweep keeps a gatekeeper whose creation was actually
applied (approved-action fallback covers a stuck `provisional` from the
best-effort describe refresh); otherwise it rejects the queued actions and
removes the workpiece in one durable step.
- createResourceGatekeeper: check the admin disable-set after the vendor RPC against the resolved resource.urlPattern, mirroring getGatekeeperClassFor (the vendor is the authority on which type a request maps to). - applyPendingAction's post-apply refresh: re-read the gatekeeper record after the describe() await so a concurrent removeGatekeeper isn't resurrected by the stale put, and refresh creationSpec.resourceUrl too so blueprint export with suggestValue doesn't export the dead provisional URL. - ChatInterface: label a rejected creation "Tried to create external resource" instead of claiming it was created. - submitCreationAction doc: ordering is the gatekeeper's responsibility -- manual approval has no platform-side ordering guard, so the gatekeeper must reject applyAction of dependent actions until the creation applies. The test-gatekeeper fixture now models that guard like the Google gatekeeper.
ndisidore
force-pushed
the
nathan/create-external-resource
branch
from
September 4, 2026 15:04
a37f164 to
ccb9d9a
Compare
This comment was marked as outdated.
This comment was marked as outdated.
…ation failure.
Observations and hooks are stamped approved at creation, so without the type
gate a vendor authorizing an observation before its creation action would fake
an approval injection (and first-encounter delete would silence the real card).
The gatekeeper row is now read only when the state is approved, the only case
that consumes it. The awaitDecision latch a failed submitCreationAction set is
restored to its prior value: the settled action can never be decided, and
suspending the turn would hide {created: false} from the model. Also: the
transcript row now uses isCreatedResourceSuccess (an errored call rendered
"Created external resource" beside its Error badge), and the reap comment no
longer claims the action index is resolved-only (pending records index under
their type too; the state check is load-bearing).
The creation card is where replay injects the user's decision; compacting it away orphaned the decision, so in any sufficiently long chat the recorded tool result's "does not exist yet" became the model's permanent last word. Mirror the pending-connectionRequest rule: replay notes the earliest still-undecided creation card and findProtectedFromSequence keeps the boundary behind it. Once the decision lands, the next replay injects it before the projection is built from the same model messages, so the summary absorbs it and the card compacts normally. Same trade-off as connection requests: an undecided creation defers compaction until the user decides.
ndisidore
commented
Sep 4, 2026
| messages: AiChatMessage[], pendingCreationSequence?: number): number | undefined { | ||
| let protectedIndex = messages.findIndex(message => | ||
| (message.type === "connectionRequest" && message.state === "pending") || | ||
| (pendingCreationSequence !== undefined && message.sequence >= pendingCreationSequence)); |
Member
Author
There was a problem hiding this comment.
Note for reviewers: now a pending resource create also blocks compaction (following the connectionRequest pattern)
|
@ndisidore Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
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.
Adds the
createExternalResourcetool as discussed with respect to Google Drive: rather than granting whole-Drive access so the agent can make one document, the agent creates a new resource of a creatable type through an already-connected account.This adds the other half of
requestConnection: the agent mints a brand-new resource of a type the vendor marks creatable, through an account the user already connected. The binding is live immediately (simliar to whatcreateGadgetdoes), the turn continues, and the gatekeeper simulates the resource until the user approves the creation, which queues first and applies first, so dependent edits are safe to stack behind it.First consumer is Google Doc creation, stacked as #428