Conversation
Preview:
|
a5eceb8 to
8604cbd
Compare
This comment was marked as outdated.
This comment was marked as outdated.
8604cbd to
19d6322
Compare
This comment was marked as outdated.
This comment was marked as outdated.
| const summary = getToolCallSummary(toolCalls[0], outputOf); | ||
| // Label by the last call: for same-target retries the final outcome wins (a failed create | ||
| // retried successfully is "Created", not "Tried"). | ||
| const summary = getToolCallSummary(toolCalls[toolCalls.length - 1], outputOf); |
There was a problem hiding this comment.
so createExternalResource is the one tool where a "failure" is just a normal string result the model reads and retries on
every other tool gets away with a dumb count label because the error machinery covers their failures separately. we can't just set tc.error on rejections either - the tool call itself succeeded, it returned guidance the model is supposed to act on, and flagging that as an error would paint the designed fix-and-retry flow red like something broke.
so if we did "created 2 external resources" when one was actually rejected, the group header would straight up lie about the thing the user is being asked to approve - hence this one case counts successes
open to other options here
19d6322 to
e9ee3a1
Compare
This comment was marked as outdated.
This comment was marked as outdated.
0fd3f8c to
c0eee25
Compare
c0eee25 to
25cb704
Compare
|
Posted 2 actionable inline findings. |
Thread a flat scalar map through createExternalResource that the kernel carries but never reads: the vendor documents accepted keys on the creatable type, validates them, and reflects consequential ones (e.g. placement) on its approval card.
25cb704 to
969f2bc
Compare
There was a problem hiding this comment.
Devin Review found 2 potential issues.
3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| } catch (error) { | ||
| this.logger.warn("failed to refresh created resource description after apply", { | ||
| event: "gatekeeper.created.describe.refresh.failed", | ||
| gatekeeperId: record.gatekeeperId, error, | ||
| }); |
There was a problem hiding this comment.
🟡 Created resource keeps dead URL
If both post-approval describe() attempts fail, the catch leaves provisional set without scheduling another retry. A creation with no later action keeps its dead URL in cards and blueprint exports indefinitely.
Prompt for agents
The post-creation description refresh in packages/workshop-backend/src/overseer.ts only retries immediately once. If both attempts fail and no later action is applied to this gatekeeper, the provisional marker and URL remain durable forever. Add a durable or scheduled retry path that re-reads the gatekeeper record, refreshes its description after the creation action is approved, updates the creation action snapshot and creationSpec URL, and remains safe if the gatekeeper is removed concurrently.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Eh, this is documented best-effort with a bounded cosmetic residual. it requires describe() failing twice, milliseconds after applyAction to the same vendor succeeded
| let record = this.impl.storage.gatekeepers.get(action.gatekeeperId); | ||
| if (record?.creation?.actionId === id) { | ||
| let siblings = Array.from( | ||
| this.impl.storage.actions.pendingByGatekeeper.get(action.gatekeeperId)) | ||
| .filter(sibling => sibling.type === "action"); | ||
| for (let sibling of siblings) this.#markActionRejected(sibling, profile); |
There was a problem hiding this comment.
🟡 Rejected creation leaves broken bindings
When rejection follows a merged gadget binding, rejectAction preserves the gatekeeper and every binding edge. The gadget exposes a nonexistent resource until someone manually removes that binding.
Prompt for agents
In packages/workshop-backend/src/overseer.ts, rejecting a creation retains its gatekeeper so chat replay can explain the dead chat binding. However, permanent gadget binding edges may already target that gatekeeper if chat changes were merged before the action decision. On creation rejection, remove or otherwise invalidate those gadget edges and restart affected gadgets while retaining whatever chat-scoped tombstone is needed for replay. Add coverage for setGadgetBinding, merge, then reject creation.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
accepted tradeoff. the outcome is the platform's uniform broken-connection behavior: the gadget's calls fail with the vendor's curated DEAD: … do not retry explanation
| // agent should adjust and retry in the same turn. | ||
| async createExternalResource(chatId: number, input: CreateExternalResourceInput, | ||
| initiator: AiChatAuthorInfo): Promise<CreatedResourceOutput | string> { | ||
| let vendors = await this.#listGatekeeperVendorsCached(); |
There was a problem hiding this comment.
Medium: This catalog is cached from the workspace owner, but the mint below deliberately resolves initiator.id. GatekeeperVendor.getSupportedResources({ userId }) is allowed to be user-specific, so a build collaborator can be rejected as unknown/not-creatable solely because the owner cannot see a type that the collaborator and their connected account support (and the inverse can advertise owner-only types). Resolve creation discovery and validation through the initiator’s User DO rather than the owner cache.
There was a problem hiding this comment.
Right now catalogs don't vary per user so this point is moot.
|
Posted 2 actionable inline findings. |
| // for as long as the collaborator keeps reconnecting. Quarantine in the same synchronous | ||
| // block instead (unverified sessions can't reach the id, see assertGatekeeperUsable) and | ||
| // restart once the log backs the creation (addChatMessages' unstamp branch). | ||
| if (this.#hasCollaboratorSession("build")) { |
There was a problem hiding this comment.
The comment above has more context on this.
the restart is a full DO abort, but the tool call that recorded this mint isn't durable until the step barrier commits - abort before that and the workspace comes back with no memory of the mint, the resumed turn re-creates it, and you're in a remint loop. so instead in these cases we defer the restart until the call is actually in the log
no collaborator session, no quarantine, no restart - all this gets skipped
| try { | ||
| let userStub = wrapDoStubForTelemetry( | ||
| this.users.get(this.users.idFromName(initiator.id)), this.logger); | ||
| minted = await userStub.createResourceGatekeeper( |
There was a problem hiding this comment.
Flagging again for reviewers: this goes through the turn initiator’s User DO on purpose, since connected accounts are per-user. a collaborator-driven chat creates the resource with that collaborator’s selected account, while gadget/scheduled turns resolve the initiator to the gadget owner, so the normal scheduled-task case still uses the owner’s account. the returned gatekeeper class is permanently imbued with that account’s credentials - whoever later clicks approve does not swap in their own credentials
I'm not deadset on this approach so I welcome pushback here
| * (e.g. its authorization does not cover the needed scopes) or when `input.options` carries | ||
| * unknown or invalid entries (see ResourceCreationOptions); callers surface the message. | ||
| */ | ||
| createResource?(resourceUrlPattern: string, |
There was a problem hiding this comment.
this method is intentionally not the provider-side create call - it only mints a provisional local capability. the real create is queued as the first normal approval action, while the gatekeeper simulates the resource so the agent can keep working and queue dependent edits. that’s what lets something like “create today’s doc, then append to it” happen in one turn without bypassing approval. once that first action is approved, the same gatekeeper starts talking to the real resource instead
| /** | ||
| * On success, the full tool result: the created gatekeeper workpiece, its provisional | ||
| * resourceUrl, and the message shown to the model — replay re-binds and re-renders from it | ||
| * instead of re-creating, like createGadget. A string output is a fixable rejection returned |
There was a problem hiding this comment.
Shouldn't a rejection use the common error property instead?
(I'm not sure why requestConnection does it differently -- maybe it should also change to use error.)
| * `error` is set on the call), so error-based filters do not catch it. Every consumer that scans | ||
| * recorded tool calls must gate on this, not on `output !== undefined`. | ||
| */ | ||
| export function isCreatedResourceSuccess(output: CreatedResourceOutput | string | undefined) |
There was a problem hiding this comment.
I'm dubious this helper is worth defining, but it could definitely go away if we change to use error for errors (then output would be output?: CreatedResourceOutput so could trivially be tested with if (tool.output)).
| class: DurableObjectClass<Gatekeeper<any>>; | ||
| resource: SupportedResource; | ||
| /** Provisional URL of the new resource; replaced by the real URL once created. */ | ||
| resourceUrl: string; |
There was a problem hiding this comment.
What is the use of a provisional URL? It's fake, you can't actually visit it, and in most cases it won't even match the final URL. Could we just drop this property altogether?
| * and removes the gatekeeper WITHOUT delivering rejectAction(): the facet's storage is | ||
| * destroyed with it, and any state staged outside the facet must tolerate orphaned entries. | ||
| */ | ||
| submitCreationAction?(approvalQueue: RpcStub<ApprovalQueue>): Promise<void>; |
There was a problem hiding this comment.
<thinking>
Hmm, as currently designed,ResourceCreationOptionswould have to be embedded into the props of the gatekeeper class returned bycreateResource(). This strikes me as a bit odd -- those props are permanent, but creation options may not be needed anymore after creation time. Perhaps the options should be passed here instead?I'm also realizing that in the existing design,
getGatekeeperClassFor()needs to embed the resource URL into the props. This is problematic because a gatekeeper created bycreateResource()doesn't necessarily have a URL until finalized, but we would probably like to share the same implementation class between created resources and existing ones... but you can't change the props after the fact.I wonder if we need to transition the gatekeeper API to be two-step for all gatekeepers, that is, you obtain the class you want first, then you start the facet and call a method on the class to initialize it. When connecting to an existing resource, you'd pass the URL on that method. For creations, you'd pass the ResourceCreationOptions.
Existing gatekeepers that embed their URL into
propswould need a migration to move the information into local storage instead.Alternatively, I guess another approach we could take is to keep connecting to existing resources as-is, but for creations, the
propsstarts out containing null values, but when the creation is finalized, there's some API for the facet to return a replacementDurableObjectClassto use from that point forward. So that way it actually is able to embed the URL into thepropsafter the fact.OK, I guess I like this idea a bit better because it allows read-only gatekeepers to remain totally stateless. (Whereas if we move props to dynamic storage, the facet MUST initialize its sqlite database to store the resource URL at least...)
</thinking>
OK, I think what we actually want here is:
- Remove this method. I think it's not really needed: The
createResource()call could return the appropriate ActionDescription. - Add a different method,
applyCreation(), to be called to actually create the resource, instead of havingapplyAction()do it. This new method should return a new version of theDurableObjectClass(and finalresourceUrl), and the facet should then be restarted using that new class before anything else happens.
I kind of wonder if applyCreation() should be hooked with an opportunity for the user to be presented with some UI to choose final creation options. E.g. for GitHub, this could give the user a chance to change the repo name and select an org to put it in -- things the agent might not have known if it was given generic instructions to create a repo.
| * entries with an agent-readable message, and MUST reflect consequential entries (e.g. placement) | ||
| * in the creation action's description so the user approves what will actually happen. | ||
| */ | ||
| export type ResourceCreationOptions = Record<string, string | number | boolean>; |
There was a problem hiding this comment.
How does the agent discover what options are available?
| * (e.g. its authorization does not cover the needed scopes) or when `input.options` carries | ||
| * unknown or invalid entries (see ResourceCreationOptions); callers surface the message. | ||
| */ | ||
| createResource?(resourceUrlPattern: string, |
There was a problem hiding this comment.
Maybe worth being extra-clear in the comments that this method does not actually create anything or have any side effects. It just returns a class. You have to call methods on the facet for anything to happen.
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.🔏 One thing worth noting: thus far we have been following that we use the minting account's credentials, and for creations that account is the turn initiator's. This means a collaborator-driven creation lands in the collaborator's provider account. If we want this to be the gadget owner's we can make that swap.
Auto approval is not supported yet.
First consumer is Google Doc creation, stacked as #428