@crowdedkingdoms/crowdyjs 11.1.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +107 -0
- package/README.md +203 -0
- package/dist/crowdy-agent/browser-dispatcher.d.ts +41 -0
- package/dist/crowdy-agent/browser-dispatcher.d.ts.map +1 -0
- package/dist/crowdy-agent/browser-dispatcher.js +264 -0
- package/dist/crowdy-agent/controller.d.ts +144 -0
- package/dist/crowdy-agent/controller.d.ts.map +1 -0
- package/dist/crowdy-agent/controller.js +1239 -0
- package/dist/crowdy-agent/errors.d.ts +37 -0
- package/dist/crowdy-agent/errors.d.ts.map +1 -0
- package/dist/crowdy-agent/errors.js +107 -0
- package/dist/crowdy-agent/graphql-transport.d.ts +154 -0
- package/dist/crowdy-agent/graphql-transport.d.ts.map +1 -0
- package/dist/crowdy-agent/graphql-transport.js +854 -0
- package/dist/crowdy-agent/index.d.ts +16 -0
- package/dist/crowdy-agent/index.d.ts.map +1 -0
- package/dist/crowdy-agent/index.js +15 -0
- package/dist/crowdy-agent/registry.d.ts +29 -0
- package/dist/crowdy-agent/registry.d.ts.map +1 -0
- package/dist/crowdy-agent/registry.js +281 -0
- package/dist/crowdy-agent/schema.d.ts +72 -0
- package/dist/crowdy-agent/schema.d.ts.map +1 -0
- package/dist/crowdy-agent/schema.js +467 -0
- package/dist/crowdy-agent/studio-tools.d.ts +15 -0
- package/dist/crowdy-agent/studio-tools.d.ts.map +1 -0
- package/dist/crowdy-agent/studio-tools.js +280 -0
- package/dist/crowdy-agent/tool-descriptors.d.ts +4 -0
- package/dist/crowdy-agent/tool-descriptors.d.ts.map +1 -0
- package/dist/crowdy-agent/tool-descriptors.js +1049 -0
- package/dist/crowdy-agent/transport.d.ts +152 -0
- package/dist/crowdy-agent/transport.d.ts.map +1 -0
- package/dist/crowdy-agent/transport.js +27 -0
- package/dist/crowdy-agent/types.d.ts +328 -0
- package/dist/crowdy-agent/types.d.ts.map +1 -0
- package/dist/crowdy-agent/types.js +37 -0
- package/dist/crowdy-client.d.ts +3 -0
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +6 -0
- package/dist/crowdy-studio/agent-dom-shell.d.ts +40 -0
- package/dist/crowdy-studio/agent-dom-shell.d.ts.map +1 -0
- package/dist/crowdy-studio/agent-dom-shell.js +354 -0
- package/dist/crowdy-studio/controller.d.ts +74 -4
- package/dist/crowdy-studio/controller.d.ts.map +1 -1
- package/dist/crowdy-studio/controller.js +450 -17
- package/dist/crowdy-studio/dom-shell.d.ts +4 -1
- package/dist/crowdy-studio/dom-shell.d.ts.map +1 -1
- package/dist/crowdy-studio/dom-shell.js +6 -1
- package/dist/crowdy-studio/index.d.ts +6 -3
- package/dist/crowdy-studio/index.d.ts.map +1 -1
- package/dist/crowdy-studio/index.js +3 -0
- package/dist/crowdy-studio/models.d.ts +67 -0
- package/dist/crowdy-studio/models.d.ts.map +1 -1
- package/dist/crowdy-studio/mount.d.ts +15 -0
- package/dist/crowdy-studio/mount.d.ts.map +1 -1
- package/dist/crowdy-studio/mount.js +129 -2
- package/dist/crowdy-studio/styles.d.ts +1 -1
- package/dist/crowdy-studio/styles.d.ts.map +1 -1
- package/dist/crowdy-studio/styles.js +2 -0
- package/dist/generated/graphql.d.ts +3753 -1
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +296 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/kit/npcs.d.ts.map +1 -1
- package/dist/kit/social.d.ts.map +1 -1
- package/dist/player-host/index.d.ts +5 -0
- package/dist/player-host/index.d.ts.map +1 -0
- package/dist/player-host/index.js +3 -0
- package/dist/player-host/lease-manager.d.ts +66 -0
- package/dist/player-host/lease-manager.d.ts.map +1 -0
- package/dist/player-host/lease-manager.js +428 -0
- package/dist/player-host/schemas.d.ts +9 -0
- package/dist/player-host/schemas.d.ts.map +1 -0
- package/dist/player-host/schemas.js +347 -0
- package/dist/player-host/tools.d.ts +13 -0
- package/dist/player-host/tools.d.ts.map +1 -0
- package/dist/player-host/tools.js +79 -0
- package/dist/player-host/types.d.ts +203 -0
- package/dist/player-host/types.d.ts.map +1 -0
- package/dist/player-host/types.js +1 -0
- package/package.json +12 -2
package/MIGRATION.md
CHANGED
|
@@ -1,3 +1,110 @@
|
|
|
1
|
+
# CrowdyJS v12 — Agentic Crowdy Studio contract (BREAKING)
|
|
2
|
+
|
|
3
|
+
Version 12 establishes the greenfield public contracts
|
|
4
|
+
`crowdy.studio-agent/1`, `crowdy.agent-tools/1`, and
|
|
5
|
+
`crowdy.player-host/1`. The major bump reserves their authority, event,
|
|
6
|
+
descriptor, and browser-control semantics before rollout; changing those
|
|
7
|
+
semantics later requires another major contract version.
|
|
8
|
+
|
|
9
|
+
New package subpaths:
|
|
10
|
+
|
|
11
|
+
- `@crowdedkingdoms/crowdyjs/agent` — immutable descriptor registry, bounded
|
|
12
|
+
JSON-schema validator, stable errors, injectable durable transport,
|
|
13
|
+
ordered/reconnecting session controller, exact approvals, and execute-once
|
|
14
|
+
browser dispatch.
|
|
15
|
+
- `@crowdedkingdoms/crowdyjs/player-host` — generic host capability,
|
|
16
|
+
observation, command, and result contracts plus the revocable Play lease
|
|
17
|
+
manager/gate.
|
|
18
|
+
- `@crowdedkingdoms/crowdyjs/crowdy-studio` re-exports both surfaces and adds
|
|
19
|
+
the integrated Ask/Build/Play dock.
|
|
20
|
+
|
|
21
|
+
The reconciled Game API SDL and generated agent operations are now committed.
|
|
22
|
+
`client.crowdyStudioAgent` is a production `CrowdyAgentGraphQLTransport`
|
|
23
|
+
implementing every `CrowdyStudioAgentTransportV1` query, Relay connection,
|
|
24
|
+
mutation, heartbeat, and typed event subscription. Tests and non-GraphQL hosts
|
|
25
|
+
may still inject the interface; do not add a generic raw-GraphQL callback.
|
|
26
|
+
|
|
27
|
+
Creation now carries optional `providerDataConsent`; attach carries a stable
|
|
28
|
+
`clientInstanceId` and consumes `replayAfterSeq`; the public transport
|
|
29
|
+
`message` maps to Game API `content`; cancellation requires the exact run id;
|
|
30
|
+
and nested browser results map to `AgentToolResultEnvelopeInput`. PLAY sends a
|
|
31
|
+
two-second heartbeat only while attached, active, and visible, stopping and
|
|
32
|
+
clearing local authority on pause, disconnect, stale epoch, kill, or destroy.
|
|
33
|
+
Descriptor builds verify the full registry and canonical 28-tool Game API
|
|
34
|
+
follow-up subset (14 mandatory game plus 14 Studio/diagnostic/runtime tools)
|
|
35
|
+
against the copied digest fixture.
|
|
36
|
+
|
|
37
|
+
Mode changes now consume the server-repinned registry/policy/context fields.
|
|
38
|
+
BUILD mounts derive `projectId` from the selected saved Studio project after
|
|
39
|
+
initialization; callers should no longer guess it. An existing session for a
|
|
40
|
+
different project fails closed, and project switches require a new session
|
|
41
|
+
until Game API adds an explicit set-project mutation.
|
|
42
|
+
|
|
43
|
+
BUILD workspace leases renew every ten seconds through agent heartbeat and
|
|
44
|
+
stop on human edit, project/context change, revocation, disconnect, or destroy.
|
|
45
|
+
Backend-advertised draft/live/stop/invoke tools execute through the headless
|
|
46
|
+
Studio controller, with exact approval for live work. Run events now preserve
|
|
47
|
+
typed code/error details, aborted handlers clear local intent, and inner
|
|
48
|
+
`OUTCOME_UNKNOWN` can no longer be wrapped as outer success.
|
|
49
|
+
|
|
50
|
+
Runtime draft/live calls now require an exact full-project target plan. Live
|
|
51
|
+
execution also binds the post-autosave revision, content/module hash, and
|
|
52
|
+
pairing preference; mismatches fail before any compile/deploy. Invoke verifies
|
|
53
|
+
the running DRAFT/LIVE environment and export, while stop remains an
|
|
54
|
+
all-project safety action.
|
|
55
|
+
|
|
56
|
+
Existing manual mounts continue to work:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
await mountCrowdyStudio(host, existingOptions);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
To enable the agent dock, inject the transport and either an existing session
|
|
63
|
+
or create-session input:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
await mountCrowdyStudio(host, {
|
|
67
|
+
...existingOptions,
|
|
68
|
+
agent: {
|
|
69
|
+
transport: game.crowdyStudioAgent,
|
|
70
|
+
sessionId,
|
|
71
|
+
playerHost, // optional; required for generic Play tools
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`CrowdyStudioHandle` now exposes `agent` and `controlLeaseManager` (both `null`
|
|
77
|
+
when agent mode is not configured). `CrowdyStudioController.testDraft()` and
|
|
78
|
+
`deployLive()` now resolve typed `CrowdyStudioDeployResult` values; code that
|
|
79
|
+
ignored their previous `void` result remains valid.
|
|
80
|
+
|
|
81
|
+
Headless integrations should adopt:
|
|
82
|
+
|
|
83
|
+
- `prepareForAgentWork()` before sending a turn;
|
|
84
|
+
- `applyAtomicPatch()` / `synchronizeProject()` for complete revision-fenced
|
|
85
|
+
project updates;
|
|
86
|
+
- `CrowdyStudioSynchronizationProvider` for durable checkpoint list, atomic
|
|
87
|
+
patch, and approved restore hooks;
|
|
88
|
+
- `state.runtimeSync` instead of inferring saved-versus-running status from the
|
|
89
|
+
display phase.
|
|
90
|
+
|
|
91
|
+
Game integrations implement `PlayerHostAdapterV1`, route commands through the
|
|
92
|
+
same intent services as human input, and call
|
|
93
|
+
`AgentControlLeaseManager.preempt(reason)` synchronously on human input,
|
|
94
|
+
Escape, Stop, death, disconnect, or context/target changes. Do not adapt the
|
|
95
|
+
agent through DOM events, raw UDP/GraphQL/CrowdyJS methods,
|
|
96
|
+
`PlayerCodeBroker`, or client-mod `host_call`.
|
|
97
|
+
|
|
98
|
+
The current Game API pilot advertises its canonical 28-tool follow-up subset,
|
|
99
|
+
including all 14 mandatory game tools. CrowdyJS dispatches the game tools
|
|
100
|
+
through `PlayerHostAdapterV1`;
|
|
101
|
+
BWF Play still requires the concrete BWF adapter/shared-intent integration and
|
|
102
|
+
matching host/app policy.
|
|
103
|
+
|
|
104
|
+
The browser package contains no provider client or key. Provider routing,
|
|
105
|
+
policy, budgets, durable approvals, and server tools remain Game API
|
|
106
|
+
responsibilities.
|
|
107
|
+
|
|
1
108
|
# CrowdyJS v11.1 — responsive Crowdy Studio embedding
|
|
2
109
|
|
|
3
110
|
Crowdy Studio now sizes to its host instead of imposing a 680-pixel minimum
|
package/README.md
CHANGED
|
@@ -12,6 +12,21 @@ npm install @crowdedkingdoms/crowdyjs
|
|
|
12
12
|
|
|
13
13
|
CrowdyJS v4 targets browsers by default and uses native `fetch`, `WebSocket`, `crypto`, `btoa`, and `atob`. Node tools can still use the SDK, but must provide browser-compatible globals when opening realtime connections.
|
|
14
14
|
|
|
15
|
+
> **CrowdyJS v12 (breaking): Agentic Crowdy Studio.** v12 adds the accepted
|
|
16
|
+
> `crowdy.studio-agent/1`, `crowdy.agent-tools/1`, and
|
|
17
|
+
> `crowdy.player-host/1` browser contracts. Import the provider-neutral durable
|
|
18
|
+
> client and immutable tool registry from
|
|
19
|
+
> `@crowdedkingdoms/crowdyjs/agent`, and generic game observations/control from
|
|
20
|
+
> `@crowdedkingdoms/crowdyjs/player-host`. Crowdy Studio can mount the
|
|
21
|
+
> integrated Ask/Build/Play dock when given an injectable agent transport.
|
|
22
|
+
> There is deliberately no OpenRouter key/client, raw GraphQL executor, DOM
|
|
23
|
+
> driver, `fetch`, shell, unrestricted SDK bridge, or client-mod `host_call` in
|
|
24
|
+
> these surfaces. The reconciled Game API SDL, generated operations, and
|
|
25
|
+
> production `CrowdyAgentGraphQLTransport` are included and exposed as
|
|
26
|
+
> `client.crowdyStudioAgent`; tests may still inject
|
|
27
|
+
> `CrowdyStudioAgentTransportV1`. Existing manual Studio mounting remains
|
|
28
|
+
> unchanged when `agent` is omitted.
|
|
29
|
+
>
|
|
15
30
|
> **Server compatibility (v8.13):** v8.13 adds the **flow-correlation surface**: `gameModel.flow({ appId, flowId })` stitches one flow correlation id into a single cross-engine timeline (model events + automation runs + compute module runs, each time-ascending), and the default event/run fragments now select the nullable `flowId` field on `GmEvent` / `GmAutomationRun` / `WasmModuleRun`. Both require the 2026-07-19 `cks-game-api` dev line (the `2026-07-19-compute-fleet-hardening` migration for the columns and the `gameModelFlow` query for the timeline); older servers reject the operations with a GraphQL validation error, and everything else keeps working. `gameModel.flow` requires the app-admin `manage_apps` permission (it is a diagnostics surface). v8.13 also extends the kit invoke helpers' gameplay-verdict mapping: the typed invoke-contract violation `computeInvoke` raises (`BAD_REQUEST` with an "Invoke params violate ..." message, game-api 2026-07-19+) now maps onto `{ success: false, errorMessage }` (`kitInvoke`) / `{ success: false, reason }` (engine invokes) instead of throwing — see the exported `isKitVerdictError` predicate.
|
|
16
31
|
>
|
|
17
32
|
> **Server compatibility:** v8.12's **container query predicates** (`where`/`limit`/`offset` on `gameModel.containers`), **automation compute actions** (`actionKind: 'compute_invoke'` with `computeModuleName`/`computeExport` on `gameModel.upsertAutomation`; `property_changed` events additionally carry `oldValue`/`newValue` to compute modules), **container-change push** (`gameModel.containerChanged` — metadata-only pull-on-push over graphql-transport-ws; on Node ≤ 21 pass `webSocketImpl`), and **invoke-trigger contracts** (`contractJson` on `compute.upsertTrigger`/`moduleTriggers`, validated server-side pre-sandbox) require the 2026-07 `cks-game-api` dev line with the `2026-07-19-automation-compute-action` migration; older servers reject the new arguments/fields (omit them and everything else keeps working). (npm 8.11.0 is a partial cut of this surface — prefer 8.12.0.) v8.12 also adds the `runOptimisticAction` kit helper (client-only: the packaged optimistic apply → referee invoke → confirm/rollback loop with actionId receipts). v5.2+ targets environments on release **v0.1.19 or later** (`cks-game-api >= v0.10.3`, `cks-management-api >= v0.1.70`). The destructive mutations send an `idempotencyKey` argument that older servers don't define. v6.1's `client.gameApps.deleteGrid` additionally requires release **v0.1.33+** (`cks-game-api >= v0.12.3`). The game-model **permission effects** fields (`permissionEffects` on `gameModel.upsertFunction`/`seed`, `permissionEffectsAppliedJson` on events) require a `cks-game-api` build with the `2026-07-17-model-permission-effects` migration (v0.13.11+); older servers reject queries/mutations that include them (omit the fields and everything else keeps working). The **permission-read** surface (the `has_grid_permission`/`grid_at`/`has_chunk_permission` expression builtins the kit's `chunkPermission` locks compile to, and selector `*PermissionWhere` predicates) additionally requires `cks-game-api` **v0.13.12+**. Older `cks-game-api` builds report game-model **invoke policy denials** as `FORBIDDEN` GraphQL errors instead of resolving with `success: false`; as of this version the kit's invoke helpers map that error onto the documented `{ success: false, errorMessage }` result, so kit callers behave identically against both server generations. v8.6's **`client.compute`** (Compute Modules — server-side Rust/WASM logic) requires a `cks-game-api` build with the compute surface (the `compute*` root fields, v0.13.13+ dev line); older servers reject these operations with a GraphQL validation error, and everything else keeps working. v8.9's **realtime + live-ops surfaces** (`kit.abilities`/`movement`/`territory`/`racing`/`liveops`/`moderation`/`telemetry`, the loot engine path, `client.compute.deployTemplate` + `kit.deploy({engines})`, and the type-94..98 event parsers) complete the 30-abstraction catalog; v8.8's **session-genre engine surfaces** (`kit.instances`/`director`/`matchmaking`/`minigames`, the engine paths on matches/decks/leaderboards, `economy.orderBook`, and the type-91/92/93 event parsers) talk to the Wave 2 engine templates; capability detection keeps model-only deployments on today's behavior. v8.7's **engine kit surfaces** (`kit.mobs`, `kit.pets`, `kit.combat.attackRouted`, `kit.worldsim.forecast`, and the `kit/wire` pose/lane registry) talk to compute-module game engines built on the Wave 0/1 `cks-game-api` dev line (`crowdy-game-kit` crates); capability detection makes them degrade gracefully — model-only deployments keep today's behavior.
|
|
@@ -144,6 +159,7 @@ If `managementUrl` is omitted, the SDK falls back to `httpUrl` for backwards-com
|
|
|
144
159
|
| `client.compute` | **Compute Modules** — server-side Rust/WASM logic: author + deploy source (`upsertModule`, `deployVersion`, `waitForCompile`), triggers + policy, synchronous `invoke`, and monitoring (`moduleRuns`, `moduleStats`, `moduleLogs`, `appDiagnostics`). Modules run server-only; see [Compute Modules docs](https://docs.crowdedkingdoms.com/game-api/compute-modules). |
|
|
145
160
|
| `client.playerCompute` | Player-authored SERVER/CLIENT Rust/WASM bound to player-owned grids: deploy source, activate/deactivate, list modules/versions, and delete self-authored modules. |
|
|
146
161
|
| `client.crowdyStudio` | Cloud project, personal-library, and common-file APIs for Crowdy Studio: target-scoped files, metadata/module names, pairing preference, optimistic revisions, copy-by-value imports, and atomic metadata/file saves. Generated operations are pinned to the committed merged SDL. |
|
|
162
|
+
| `client.crowdyStudioAgent` | Generated, app-token Game API transport for durable agent sessions, Relay history/session pages, descriptors/budgets, exact approvals, browser tool results, heartbeat, control mutations, and ordered event subscriptions. |
|
|
147
163
|
| `client.playerModel` | Player-owned flexible model containers and grid-confined automations (`containers`, `createContainer`, `setProperty`, `automations`, `createAutomation`, …). |
|
|
148
164
|
| `client.marketplace` | Player-code store/install/consent flows plus player-authorized grid claims: `claimGridOwnership` preserves the existing-grid policy flow, while `claimGridChunk` atomically creates and owns one chunk under `SELF_CLAIM` and `releaseClaimedGrid` releases an eligible owner-created claim. |
|
|
149
165
|
| `client.udp` | UDP proxy subscriptions + spatial mutations (`sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`, `sendTextPacket`, `sendClientEvent`). |
|
|
@@ -248,6 +264,193 @@ presentation enum to Crowdy Studio's runtime-oriented project kind and
|
|
|
248
264
|
`CrowdyStudioRevisionConflictError` with the latest cloud project when that
|
|
249
265
|
follow-up read succeeds.
|
|
250
266
|
|
|
267
|
+
### Agent SDK, transport, and integrated dock (v12)
|
|
268
|
+
|
|
269
|
+
The v12 agent API remains provider-neutral, while CrowdyJS now ships generated
|
|
270
|
+
operations plus the production `CrowdyAgentGraphQLTransport` exposed as
|
|
271
|
+
`client.crowdyStudioAgent`:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import {
|
|
275
|
+
CROWDY_AGENT_TOOL_REGISTRY_V1,
|
|
276
|
+
CrowdyStudioAgentController,
|
|
277
|
+
} from '@crowdedkingdoms/crowdyjs/agent';
|
|
278
|
+
|
|
279
|
+
const agent = new CrowdyStudioAgentController({
|
|
280
|
+
transport: game.crowdyStudioAgent,
|
|
281
|
+
createSession: {
|
|
282
|
+
appId,
|
|
283
|
+
projectId,
|
|
284
|
+
gridId,
|
|
285
|
+
mode: 'BUILD',
|
|
286
|
+
providerDataConsent: true,
|
|
287
|
+
idempotencyKey: crypto.randomUUID(),
|
|
288
|
+
},
|
|
289
|
+
beforeAgentWork: () => studio.controller.prepareForAgentWork(),
|
|
290
|
+
});
|
|
291
|
+
await agent.initialize(); // attach epoch → durable replay/gap fill → live tail
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`CROWDY_AGENT_TOOL_REGISTRY_V1` is an immutable, digest-pinned registry of the
|
|
295
|
+
minimum Studio/project/workspace/library/template/diagnostics/runtime and
|
|
296
|
+
generic game tools. Every descriptor has exact logical and provider wire names,
|
|
297
|
+
semantic version, bounded input/output JSON schemas,
|
|
298
|
+
`additionalProperties:false`, executor, mode, risk/effect/reversibility,
|
|
299
|
+
scope, approval, idempotency, timeout, and redaction metadata. Input schemas
|
|
300
|
+
reject caller-supplied identity, epoch, lease, approval, endpoint, token, and
|
|
301
|
+
other authority fields. `CrowdyAgentToolRegistry.fromWireName` is exact and
|
|
302
|
+
case-sensitive; browser dispatch is execute-once by `toolCallId`, and ambiguous
|
|
303
|
+
effects become `OUTCOME_UNKNOWN` instead of being repeated.
|
|
304
|
+
|
|
305
|
+
`CrowdyStudioAgentTransportV1` owns no credentials and exposes only typed
|
|
306
|
+
durable orchestration operations: session/history/descriptor/budget queries;
|
|
307
|
+
create/attach/mode/event-ack/message/approval/tool-result/lease/pause/resume/
|
|
308
|
+
cancel/close mutations; and the ordered event subscription. A generated adapter
|
|
309
|
+
maps these methods to the operation names in
|
|
310
|
+
`CROWDY_AGENT_GRAPHQL_OPERATIONS_V1`:
|
|
311
|
+
|
|
312
|
+
- `crowdyStudioAgentSession`, `crowdyStudioAgentSessions`,
|
|
313
|
+
`crowdyStudioAgentHistory`, `crowdyStudioAgentToolDescriptors`, and
|
|
314
|
+
`crowdyStudioAgentBudget`;
|
|
315
|
+
- `crowdyStudioAgentCreateSession`, `AttachClient`, `SetMode`,
|
|
316
|
+
`AcknowledgeEvents`, `Heartbeat`, `SendMessage`, `ApproveTool`, `RejectTool`,
|
|
317
|
+
`ToolResult`, `GrantLease`, `RevokeLease`, `Pause`, `Resume`, `CancelRun`,
|
|
318
|
+
and `CloseSession` (all with the `crowdyStudioAgent` prefix);
|
|
319
|
+
- `crowdyStudioAgentEvents(sessionId, afterSeq, clientEpoch)`.
|
|
320
|
+
|
|
321
|
+
The controller applies only contiguous decimal-string sequences, deduplicates
|
|
322
|
+
event IDs, fills gaps from durable history, acknowledges the highest
|
|
323
|
+
contiguous cursor, fences old attach epochs, and explicitly reconnects without
|
|
324
|
+
resuming a run or Play lease. Approval methods use the exact displayed argument
|
|
325
|
+
hash. Human edit/game input calls local preemption before best-effort transport
|
|
326
|
+
cleanup. Attach uses a stable `clientInstanceId` and the server
|
|
327
|
+
`replayAfterSeq`; public message input maps to Game API `content`, cancellation
|
|
328
|
+
always sends an explicit run id, and PLAY sends a two-second heartbeat only
|
|
329
|
+
while attached, active, and visible. Heartbeat, kill, and stale-epoch failures
|
|
330
|
+
immediately clear local authority.
|
|
331
|
+
|
|
332
|
+
Mode changes adopt the complete server-repinned session (registry digest,
|
|
333
|
+
provider/app policy revisions, and context version) before loading its new
|
|
334
|
+
effective descriptors. The Studio mount resolves the selected saved project
|
|
335
|
+
after `CrowdyStudioController.initialize()`, overrides caller guesses when
|
|
336
|
+
creating a BUILD session, and rejects an existing session bound to another
|
|
337
|
+
project. Because v1 has no set-project mutation, switching projects fences the
|
|
338
|
+
agent and requires a new session/remount.
|
|
339
|
+
|
|
340
|
+
An active BUILD workspace lease is renewed every ten seconds through the same
|
|
341
|
+
authenticated heartbeat; the server returns its renewed 30-second expiry.
|
|
342
|
+
Human edits, project/context changes, lease revocation, disconnect, and destroy
|
|
343
|
+
stop renewal and abort browser handlers. Run events retain their stable
|
|
344
|
+
`code`, `reason`, and typed safe `error`.
|
|
345
|
+
|
|
346
|
+
The committed descriptor digest fixture is checked on every build against the
|
|
347
|
+
canonical registry. Coordinated refreshes first run
|
|
348
|
+
`npm run agent-descriptors:drift -- --source <game-api-fixture>`; builds then
|
|
349
|
+
recompute the full and canonical 28-tool Game API subset digest (14 mandatory
|
|
350
|
+
game plus 14 Studio/diagnostic/runtime tools) so the fixture cannot silently
|
|
351
|
+
diverge.
|
|
352
|
+
|
|
353
|
+
Mount the responsive agent dock by adding `agent`; omit it to retain the v11
|
|
354
|
+
manual UI unchanged:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
const studio = await mountCrowdyStudio(host, {
|
|
358
|
+
projectProvider: game.crowdyStudio,
|
|
359
|
+
playerCompute: game.playerCompute,
|
|
360
|
+
appId,
|
|
361
|
+
gridId,
|
|
362
|
+
agent: {
|
|
363
|
+
transport: game.crowdyStudioAgent,
|
|
364
|
+
sessionId,
|
|
365
|
+
playerHost: bwfPlayerHostAdapter,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
studio.agent?.sendMessage('Explain the current diagnostics');
|
|
370
|
+
studio.controlLeaseManager?.preempt('HUMAN_INPUT');
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The dock provides human-owned Ask/Build/Play selection, chat streaming,
|
|
374
|
+
plan/tool status, exact approval hashes, diff/checkpoint cards, budget, visible
|
|
375
|
+
lease scope/expiry, Pause/Resume/Stop, and checkpoint-restore requests. It uses
|
|
376
|
+
text nodes for all untrusted content, ARIA status/log/group labels, visible
|
|
377
|
+
focus, keyboard submission, and a container-query layout that moves the agent
|
|
378
|
+
dock below narrow embeds.
|
|
379
|
+
|
|
380
|
+
The headless `CrowdyStudioController` now also exposes:
|
|
381
|
+
|
|
382
|
+
- `prepareForAgentWork()` to flush autosave and fail closed on conflict/offline;
|
|
383
|
+
- `applyAtomicPatch()` and `synchronizeProject()` for all-or-none multi-file
|
|
384
|
+
updates, human-edit preemption, and Monaco synchronization;
|
|
385
|
+
- `refreshCheckpoints()` / `restoreCheckpoint()` through the optional
|
|
386
|
+
transport-neutral `CrowdyStudioSynchronizationProvider` (the provider is
|
|
387
|
+
required for agent writes/restores and must make checkpoint + revision
|
|
388
|
+
changes durable and atomic);
|
|
389
|
+
- typed draft/live results plus `runtimeSync` (`RUNNING_SAVED`,
|
|
390
|
+
`RUNNING_STALE`, `STOPPED`, or `NEVER_RUN`).
|
|
391
|
+
|
|
392
|
+
### PlayerHostAdapter and Blocks with Friends integration
|
|
393
|
+
|
|
394
|
+
`@crowdedkingdoms/crowdyjs/player-host` exports
|
|
395
|
+
`PlayerHostAdapterV1`, capability/observation/command/result types and schemas,
|
|
396
|
+
`AgentControlLeaseManager`, and `createPlayerHostAgentTools`. A game adapter
|
|
397
|
+
implements only:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
interface PlayerHostAdapterV1 {
|
|
401
|
+
readonly contractVersion: 'crowdy.player-host/1';
|
|
402
|
+
capabilities(): Promise<PlayerHostCapabilitiesV1>;
|
|
403
|
+
observe(request: ObserveRequestV1): Promise<GameObservationV1>;
|
|
404
|
+
dispatch(command: GameCommandV1, gate: ValidatedGateV1):
|
|
405
|
+
Promise<GameCommandResultV1>;
|
|
406
|
+
clearAgentIntent(reason: CrowdyAgentPreemptionReason): void;
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
For Blocks with Friends, implement this interface over the same typed intent
|
|
411
|
+
methods used by `PlayerController`, `ActionService`, `InventoryService`,
|
|
412
|
+
`FishingService`, `RideService`, and existing referee/model calls. Construct
|
|
413
|
+
`createPlayerHostAgentTools(adapter)` and pass `adapter` as the mount's
|
|
414
|
+
`playerHost`. The returned lease manager enforces TTL, scopes, client epoch,
|
|
415
|
+
host/controlled-entity revision, observation freshness, modal/death state,
|
|
416
|
+
per-command rate limits, approvals, and tool-call dedup. Call
|
|
417
|
+
`leaseManager.preempt('HUMAN_INPUT')` synchronously on key/mouse/touch input,
|
|
418
|
+
`ESCAPE` on Escape, and the corresponding reason on death, target/context
|
|
419
|
+
change, disconnect, or Stop. `clearAgentIntent` must immediately zero movement,
|
|
420
|
+
look, and pending action intent. BWF does not integrate through raw CrowdyJS,
|
|
421
|
+
DOM input, UDP packets, `PlayerCodeBroker`, or client-mod host calls.
|
|
422
|
+
|
|
423
|
+
BWF should pass only `game.crowdyStudioAgent` and its
|
|
424
|
+
`BwfPlayerHostAdapter` to the Studio mount; the mount derives the selected
|
|
425
|
+
project id. Its adapter must implement the exact 14 mandatory game surfaces,
|
|
426
|
+
classify combat as conditional approval, return typed failed/denied/unknown
|
|
427
|
+
outcomes, and make `clearAgentIntent` synchronously cancel shared movement and
|
|
428
|
+
action intents when the dispatcher aborts.
|
|
429
|
+
|
|
430
|
+
The reconciled Game API development pilot advertises the exact 28-tool
|
|
431
|
+
follow-up subset, including all 14 mandatory game descriptors. CrowdyJS routes
|
|
432
|
+
all game tools through
|
|
433
|
+
`PlayerHostAdapterV1`, the scoped lease gate, conditional combat approval,
|
|
434
|
+
safety stop, heartbeat fencing, and typed result continuation. BWF Play now
|
|
435
|
+
remains blocked only on the concrete BWF adapter/shared-intent wiring and its
|
|
436
|
+
host/app policy configuration.
|
|
437
|
+
|
|
438
|
+
The same browser dispatcher routes backend-advertised `runtime.test_draft`,
|
|
439
|
+
`runtime.deploy_live`, `runtime.invoke`, and `runtime.stop` through the
|
|
440
|
+
headless Studio controller. Draft testing is routine BUILD work; live deploy
|
|
441
|
+
and LIVE invoke require the exact approval grant; stop remains an idempotent
|
|
442
|
+
safety action. Inner game `FAILED`, `DENIED`, or `OUTCOME_UNKNOWN` results are
|
|
443
|
+
promoted to matching outer terminal tool results and are never reported as
|
|
444
|
+
successful.
|
|
445
|
+
|
|
446
|
+
Runtime execution is plan-bound: requested targets must exactly equal the
|
|
447
|
+
selected project’s authoritative target set, so a full-stack project cannot run
|
|
448
|
+
CLIENT after SERVER-only authorization. Live deploy additionally rechecks the
|
|
449
|
+
saved revision, canonical project content hash (including module names/files),
|
|
450
|
+
and pairing preference after autosave. Invoke checks the exact running
|
|
451
|
+
DRAFT/LIVE environment and export; stop always stops the complete selected
|
|
452
|
+
project as a safety action rather than interpreting a partial target.
|
|
453
|
+
|
|
251
454
|
| `createWorldSession(client, appId, config)` (from `@crowdedkingdoms/crowdyjs/stores`) | World Stores: opt-in, SDK-managed game state — typed codecs (`structCodec` binary DSL), your own actor with a 5 Hz send loop (`session.self`), a remote-actor registry with lanes/history/staleness (`session.actors`), attributed send errors (`session.errors`), a chunk/voxel cache with realtime merge + worldgen write-back (`session.chunks`), channel/direct-message inboxes + a typed event router, host tracking, typed save/avatar state, and a game-model container mirror. Only configured stores exist (compile-time + runtime); unimported stores tree-shake away. |
|
|
252
455
|
| `client.kit(appId)` | Game Kit: ready-made mappings of game concepts onto the game model — `kit.inventory`, `kit.objects` (lockable doors/chests with custom permissions), `kit.npcs`, `kit.plots` (buy/rent land with transactional, replication-enforced grid grants), and the genre layers `kit.economy` (wallets/shops/trades/market), `kit.progression` (xp/skills/achievements/rating), `kit.loot`, `kit.quests`, `kit.combat`, `kit.matches` (session lobbies/turns/scores with notify-to-pull channels), `kit.decks` (hidden hands), `kit.worldsim` (clock/nodes/crops/waves), `kit.social` (parties/guilds/chat over teams+channels), `kit.leaderboards`, `kit.features` (tier gates), and the engine-aware helpers `kit.mobs` (refereed attacks, defs/slots, contact-damage parsing), `kit.pets` (adopt/summon/dismiss/rename), `kit.instances` (private world slices, seeded runs), `kit.director` (encounter runs), `kit.matchmaking` (queues/proposals/rating), `kit.minigames` (invoke-loop wrapper), `kit.economy.orderBook` (escrowed bid/ask market), engine paths on `kit.matches`/`kit.decks`/`kit.leaderboards`, `kit.quests` tutorial sequencing, `kit.engines` (compute capability detection) with `kit/wire` (the engine pose codec, `engineLanes()`, and the 77/90/91/92/93 event parsers) — plus blueprint builders + `kit.deploy(...)` for the admin "load the rules" step. |
|
|
253
456
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CrowdyAgentMode, CrowdyAgentToolInvocationV1, CrowdyAgentToolResultV1 } from './types.js';
|
|
2
|
+
import { CrowdyAgentToolRegistry } from './registry.js';
|
|
3
|
+
export interface CrowdyAgentBrowserToolContextV1 {
|
|
4
|
+
readonly invocation: CrowdyAgentToolInvocationV1;
|
|
5
|
+
readonly signal: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
export type CrowdyAgentBrowserToolHandlerV1 = (argumentsValue: Readonly<Record<string, unknown>>, context: CrowdyAgentBrowserToolContextV1) => unknown | Promise<unknown>;
|
|
8
|
+
export type CrowdyAgentBrowserToolHandlersV1 = Readonly<Record<string, CrowdyAgentBrowserToolHandlerV1>>;
|
|
9
|
+
export interface CrowdyAgentBrowserDispatcherOptionsV1 {
|
|
10
|
+
readonly registry: CrowdyAgentToolRegistry;
|
|
11
|
+
readonly handlers: CrowdyAgentBrowserToolHandlersV1;
|
|
12
|
+
readonly getSessionId?: () => string | null;
|
|
13
|
+
readonly getClientEpoch: () => string | null;
|
|
14
|
+
readonly getContextVersion: () => string;
|
|
15
|
+
readonly getMode?: () => CrowdyAgentMode;
|
|
16
|
+
readonly now?: () => number;
|
|
17
|
+
readonly maxRememberedCalls?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Exact browser tool router with execute-once semantics. It has no fallback
|
|
21
|
+
* executor: unknown names, raw SDK access, and missing handlers fail closed.
|
|
22
|
+
*/
|
|
23
|
+
export declare class CrowdyAgentBrowserToolDispatcher {
|
|
24
|
+
private readonly options;
|
|
25
|
+
private readonly records;
|
|
26
|
+
private readonly active;
|
|
27
|
+
private readonly now;
|
|
28
|
+
constructor(options: CrowdyAgentBrowserDispatcherOptionsV1);
|
|
29
|
+
dispatch(invocation: CrowdyAgentToolInvocationV1): Promise<CrowdyAgentToolResultV1>;
|
|
30
|
+
has(toolCallId: string): boolean;
|
|
31
|
+
/** Abort pending browser work during human or context preemption. */
|
|
32
|
+
cancelActive(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Cache lifetime is the attached session. Call only after that session is
|
|
35
|
+
* closed or fenced; clearing during a live session could repeat an effect.
|
|
36
|
+
*/
|
|
37
|
+
clearClosedSession(): void;
|
|
38
|
+
private execute;
|
|
39
|
+
private validateEnvelope;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=browser-dispatcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-dispatcher.d.ts","sourceRoot":"","sources":["../../src/crowdy-agent/browser-dispatcher.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,eAAe,EACf,2BAA2B,EAC3B,uBAAuB,EACxB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAExD,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,UAAU,EAAE,2BAA2B,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED,MAAM,MAAM,+BAA+B,GAAG,CAC5C,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACjD,OAAO,EAAE,+BAA+B,KACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,MAAM,MAAM,gCAAgC,GAAG,QAAQ,CACrD,MAAM,CAAC,MAAM,EAAE,+BAA+B,CAAC,CAChD,CAAC;AAEF,MAAM,WAAW,qCAAqC;IACpD,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,gCAAgC,CAAC;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC5C,QAAQ,CAAC,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC7C,QAAQ,CAAC,iBAAiB,EAAE,MAAM,MAAM,CAAC;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,eAAe,CAAC;IACzC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC;AAOD;;;GAGG;AACH,qBAAa,gCAAgC;IAK/B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsC;IAC7D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEN,OAAO,EAAE,qCAAqC;IAe3E,QAAQ,CACN,UAAU,EAAE,2BAA2B,GACtC,OAAO,CAAC,uBAAuB,CAAC;IA6CnC,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAIhC,qEAAqE;IACrE,YAAY,IAAI,IAAI;IAIpB;;;OAGG;IACH,kBAAkB,IAAI,IAAI;YAKZ,OAAO;IA6IrB,OAAO,CAAC,gBAAgB;CAyFzB"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { CrowdyAgentError, CrowdyAgentOutcomeUnknownError, toAgentError, } from './errors.js';
|
|
2
|
+
import { canonicalJson, sha256Digest } from './schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* Exact browser tool router with execute-once semantics. It has no fallback
|
|
5
|
+
* executor: unknown names, raw SDK access, and missing handlers fail closed.
|
|
6
|
+
*/
|
|
7
|
+
export class CrowdyAgentBrowserToolDispatcher {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.records = new Map();
|
|
11
|
+
this.active = new Map();
|
|
12
|
+
this.now = options.now ?? Date.now;
|
|
13
|
+
for (const name of Object.keys(options.handlers)) {
|
|
14
|
+
const matches = options.registry
|
|
15
|
+
.list({ executor: 'BROWSER' })
|
|
16
|
+
.filter(({ descriptor }) => descriptor.name === name);
|
|
17
|
+
if (matches.length === 0) {
|
|
18
|
+
throw new CrowdyAgentError('AGENT_TOOL_UNKNOWN', `Browser handler ${name} is not an exact registered browser tool`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
dispatch(invocation) {
|
|
23
|
+
if (typeof invocation.toolCallId !== 'string' ||
|
|
24
|
+
invocation.toolCallId.length < 1 ||
|
|
25
|
+
invocation.toolCallId.length > 128) {
|
|
26
|
+
return this.execute(invocation);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
// Bound arguments before canonical fingerprinting or cache insertion.
|
|
30
|
+
this.options.registry.validateInput(invocation.name, invocation.version, invocation.arguments);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return this.execute(invocation);
|
|
34
|
+
}
|
|
35
|
+
const fingerprint = invocationFingerprint(invocation);
|
|
36
|
+
const previous = this.records.get(invocation.toolCallId);
|
|
37
|
+
if (previous) {
|
|
38
|
+
if (previous.fingerprint !== fingerprint) {
|
|
39
|
+
return Promise.reject(new CrowdyAgentError('AGENT_IDEMPOTENCY_CONFLICT', `Tool call ${invocation.toolCallId} was replayed with different arguments`));
|
|
40
|
+
}
|
|
41
|
+
return previous.promise;
|
|
42
|
+
}
|
|
43
|
+
const max = this.options.maxRememberedCalls ?? 2048;
|
|
44
|
+
if (this.records.size >= max) {
|
|
45
|
+
return Promise.reject(new CrowdyAgentError('AGENT_RATE_LIMITED', 'Browser execute-once cache is full; attach a fresh session before continuing'));
|
|
46
|
+
}
|
|
47
|
+
const promise = this.execute(invocation);
|
|
48
|
+
this.records.set(invocation.toolCallId, { fingerprint, promise });
|
|
49
|
+
return promise;
|
|
50
|
+
}
|
|
51
|
+
has(toolCallId) {
|
|
52
|
+
return this.records.has(toolCallId);
|
|
53
|
+
}
|
|
54
|
+
/** Abort pending browser work during human or context preemption. */
|
|
55
|
+
cancelActive() {
|
|
56
|
+
for (const controller of this.active.values())
|
|
57
|
+
controller.abort();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Cache lifetime is the attached session. Call only after that session is
|
|
61
|
+
* closed or fenced; clearing during a live session could repeat an effect.
|
|
62
|
+
*/
|
|
63
|
+
clearClosedSession() {
|
|
64
|
+
this.cancelActive();
|
|
65
|
+
this.records.clear();
|
|
66
|
+
}
|
|
67
|
+
async execute(invocation) {
|
|
68
|
+
const startedMs = this.now();
|
|
69
|
+
const startedAt = new Date(startedMs).toISOString();
|
|
70
|
+
let entry;
|
|
71
|
+
let activeAbort = null;
|
|
72
|
+
try {
|
|
73
|
+
this.validateEnvelope(invocation, startedMs);
|
|
74
|
+
entry = this.options.registry.require(invocation.name, invocation.version);
|
|
75
|
+
if (entry.descriptor.executor !== 'BROWSER') {
|
|
76
|
+
throw new CrowdyAgentError('AGENT_TOOL_UNKNOWN', `${invocation.name} is not a browser-executed tool`);
|
|
77
|
+
}
|
|
78
|
+
if (entry.descriptorDigest !== invocation.descriptorDigest) {
|
|
79
|
+
throw new CrowdyAgentError('AGENT_CONTEXT_STALE', `${invocation.name} descriptor digest changed`);
|
|
80
|
+
}
|
|
81
|
+
if (this.options.getMode &&
|
|
82
|
+
!entry.descriptor.modes.includes(this.options.getMode())) {
|
|
83
|
+
throw new CrowdyAgentError('AGENT_SCOPE_DENIED', `${invocation.name} is unavailable in the selected mode`);
|
|
84
|
+
}
|
|
85
|
+
if (entry.descriptor.approval.policy === 'REQUIRED' &&
|
|
86
|
+
!invocation.approvalGrant) {
|
|
87
|
+
throw new CrowdyAgentError('AGENT_APPROVAL_REQUIRED', `${invocation.name} requires exact human approval`);
|
|
88
|
+
}
|
|
89
|
+
this.options.registry.validateInput(invocation.name, invocation.version, invocation.arguments);
|
|
90
|
+
if (!isRecord(invocation.arguments)) {
|
|
91
|
+
throw new CrowdyAgentError('AGENT_TOOL_INPUT_INVALID', 'Browser tool arguments must be an object');
|
|
92
|
+
}
|
|
93
|
+
const handler = this.options.handlers[invocation.name];
|
|
94
|
+
if (!handler) {
|
|
95
|
+
throw new CrowdyAgentError('AGENT_HOST_UNAVAILABLE', `No browser host implements ${invocation.name}`);
|
|
96
|
+
}
|
|
97
|
+
const deadlineMs = Date.parse(invocation.deadline);
|
|
98
|
+
const remaining = Math.min(entry.descriptor.timeoutMs, Math.max(0, deadlineMs - startedMs));
|
|
99
|
+
const abort = new AbortController();
|
|
100
|
+
activeAbort = abort;
|
|
101
|
+
this.active.set(invocation.toolCallId, abort);
|
|
102
|
+
const output = await runWithDeadline(Promise.resolve(handler(invocation.arguments, {
|
|
103
|
+
invocation,
|
|
104
|
+
signal: abort.signal,
|
|
105
|
+
})), remaining, abort);
|
|
106
|
+
if (abort.signal.aborted) {
|
|
107
|
+
throw new CrowdyAgentError('AGENT_CANCELLED', 'Browser tool was cancelled before its result was accepted');
|
|
108
|
+
}
|
|
109
|
+
if (invocation.contextVersion !== this.options.getContextVersion()) {
|
|
110
|
+
throw new CrowdyAgentError('AGENT_CONTEXT_STALE', 'Browser tool context changed before its result was accepted');
|
|
111
|
+
}
|
|
112
|
+
this.options.registry.validateOutput(invocation.name, invocation.version, output);
|
|
113
|
+
return {
|
|
114
|
+
protocolVersion: 'crowdy.tool-result/1',
|
|
115
|
+
toolCallId: invocation.toolCallId,
|
|
116
|
+
status: 'SUCCEEDED',
|
|
117
|
+
output,
|
|
118
|
+
observedContextVersion: this.options.getContextVersion(),
|
|
119
|
+
startedAt,
|
|
120
|
+
finishedAt: new Date(this.now()).toISOString(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const registered = this.options.registry.get(invocation.name, invocation.version);
|
|
125
|
+
const timedOut = error instanceof CrowdyAgentError && error.code === 'AGENT_TOOL_TIMEOUT';
|
|
126
|
+
const ambiguous = error instanceof CrowdyAgentOutcomeUnknownError ||
|
|
127
|
+
(timedOut &&
|
|
128
|
+
registered !== undefined &&
|
|
129
|
+
['TOOL_CALL_ONCE', 'NON_RETRYABLE'].includes(registered.descriptor.idempotency.class));
|
|
130
|
+
return {
|
|
131
|
+
protocolVersion: 'crowdy.tool-result/1',
|
|
132
|
+
toolCallId: invocation.toolCallId,
|
|
133
|
+
status: ambiguous
|
|
134
|
+
? 'OUTCOME_UNKNOWN'
|
|
135
|
+
: timedOut
|
|
136
|
+
? 'TIMED_OUT'
|
|
137
|
+
: error instanceof CrowdyAgentError &&
|
|
138
|
+
error.code === 'AGENT_CANCELLED'
|
|
139
|
+
? 'CANCELLED'
|
|
140
|
+
: 'FAILED',
|
|
141
|
+
error: toAgentError(ambiguous
|
|
142
|
+
? new CrowdyAgentOutcomeUnknownError(error instanceof Error ? error.message : undefined)
|
|
143
|
+
: error, timedOut ? 'AGENT_TOOL_TIMEOUT' : 'AGENT_TOOL_FAILED'),
|
|
144
|
+
observedContextVersion: this.options.getContextVersion(),
|
|
145
|
+
startedAt,
|
|
146
|
+
finishedAt: new Date(this.now()).toISOString(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
if (activeAbort)
|
|
151
|
+
this.active.delete(invocation.toolCallId);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
validateEnvelope(invocation, now) {
|
|
155
|
+
if (invocation.protocolVersion !== 'crowdy.tool-call/1') {
|
|
156
|
+
throw new CrowdyAgentError('AGENT_TOOL_INPUT_INVALID', 'Unsupported browser tool-call protocol');
|
|
157
|
+
}
|
|
158
|
+
for (const [field, value, max] of [
|
|
159
|
+
['sessionId', invocation.sessionId, 128],
|
|
160
|
+
['runId', invocation.runId, 128],
|
|
161
|
+
['toolCallId', invocation.toolCallId, 128],
|
|
162
|
+
['contextVersion', invocation.contextVersion, 128],
|
|
163
|
+
]) {
|
|
164
|
+
if (typeof value !== 'string' ||
|
|
165
|
+
value.length < 1 ||
|
|
166
|
+
value.length > max) {
|
|
167
|
+
throw new CrowdyAgentError('AGENT_TOOL_INPUT_INVALID', `${field} is outside protocol bounds`, { field });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const currentSessionId = this.options.getSessionId?.();
|
|
171
|
+
if (currentSessionId && invocation.sessionId !== currentSessionId) {
|
|
172
|
+
throw new CrowdyAgentError('AGENT_SESSION_NOT_FOUND', 'Browser tool dispatch belongs to a different session');
|
|
173
|
+
}
|
|
174
|
+
if (typeof invocation.descriptorDigest !== 'string' ||
|
|
175
|
+
typeof invocation.argumentHash !== 'string' ||
|
|
176
|
+
!/^sha256:[0-9a-f]{64}$/u.test(invocation.descriptorDigest) ||
|
|
177
|
+
!/^sha256:[0-9a-f]{64}$/u.test(invocation.argumentHash) ||
|
|
178
|
+
(invocation.approvalGrant !== undefined &&
|
|
179
|
+
(typeof invocation.approvalGrant !== 'string' ||
|
|
180
|
+
invocation.approvalGrant.length < 1)) ||
|
|
181
|
+
(invocation.approvalGrant?.length ?? 0) > 512 ||
|
|
182
|
+
(invocation.idempotencyKey !== undefined &&
|
|
183
|
+
(typeof invocation.idempotencyKey !== 'string' ||
|
|
184
|
+
invocation.idempotencyKey.length < 1)) ||
|
|
185
|
+
(invocation.idempotencyKey?.length ?? 0) > 240) {
|
|
186
|
+
throw new CrowdyAgentError('AGENT_TOOL_INPUT_INVALID', 'Browser tool dispatch contains invalid digest or capability metadata');
|
|
187
|
+
}
|
|
188
|
+
const currentEpoch = this.options.getClientEpoch();
|
|
189
|
+
if (!currentEpoch ||
|
|
190
|
+
!invocation.clientEpoch ||
|
|
191
|
+
invocation.clientEpoch !== currentEpoch) {
|
|
192
|
+
throw new CrowdyAgentError('AGENT_CLIENT_EPOCH_STALE', 'Browser tool dispatch belongs to a stale client epoch');
|
|
193
|
+
}
|
|
194
|
+
if (invocation.contextVersion !== this.options.getContextVersion()) {
|
|
195
|
+
throw new CrowdyAgentError('AGENT_CONTEXT_STALE', 'Browser tool dispatch belongs to a stale app, project, or game context');
|
|
196
|
+
}
|
|
197
|
+
if (typeof invocation.deadline !== 'string' ||
|
|
198
|
+
invocation.deadline.length < 20 ||
|
|
199
|
+
invocation.deadline.length > 40) {
|
|
200
|
+
throw new CrowdyAgentError('AGENT_TOOL_INPUT_INVALID', 'Browser tool deadline is invalid');
|
|
201
|
+
}
|
|
202
|
+
const deadline = Date.parse(invocation.deadline);
|
|
203
|
+
if (!Number.isFinite(deadline) || deadline <= now) {
|
|
204
|
+
throw new CrowdyAgentError('AGENT_TOOL_TIMEOUT', 'Browser tool deadline has expired');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function invocationFingerprint(invocation) {
|
|
209
|
+
return sha256Digest(canonicalJson({
|
|
210
|
+
protocolVersion: invocation.protocolVersion,
|
|
211
|
+
sessionId: invocation.sessionId,
|
|
212
|
+
runId: invocation.runId,
|
|
213
|
+
toolCallId: invocation.toolCallId,
|
|
214
|
+
name: invocation.name,
|
|
215
|
+
version: invocation.version,
|
|
216
|
+
descriptorDigest: invocation.descriptorDigest,
|
|
217
|
+
arguments: invocation.arguments,
|
|
218
|
+
argumentHash: invocation.argumentHash,
|
|
219
|
+
contextVersion: invocation.contextVersion,
|
|
220
|
+
...(invocation.clientEpoch
|
|
221
|
+
? { clientEpoch: invocation.clientEpoch }
|
|
222
|
+
: {}),
|
|
223
|
+
...(invocation.leaseId ? { leaseId: invocation.leaseId } : {}),
|
|
224
|
+
...(invocation.approvalGrant
|
|
225
|
+
? { approvalGrant: invocation.approvalGrant }
|
|
226
|
+
: {}),
|
|
227
|
+
...(invocation.idempotencyKey
|
|
228
|
+
? { idempotencyKey: invocation.idempotencyKey }
|
|
229
|
+
: {}),
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
function runWithDeadline(operation, timeoutMs, abort) {
|
|
233
|
+
if (timeoutMs <= 0) {
|
|
234
|
+
abort.abort();
|
|
235
|
+
return Promise.reject(new CrowdyAgentError('AGENT_TOOL_TIMEOUT', 'Browser tool deadline expired'));
|
|
236
|
+
}
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
let timedOut = false;
|
|
239
|
+
const onAbort = () => {
|
|
240
|
+
if (!timedOut) {
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
reject(new CrowdyAgentError('AGENT_CANCELLED', 'Browser tool was cancelled by human or context preemption'));
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
abort.signal.addEventListener('abort', onAbort, { once: true });
|
|
246
|
+
const timer = setTimeout(() => {
|
|
247
|
+
timedOut = true;
|
|
248
|
+
abort.abort();
|
|
249
|
+
reject(new CrowdyAgentError('AGENT_TOOL_TIMEOUT', `Browser tool exceeded its ${timeoutMs}ms deadline`));
|
|
250
|
+
}, timeoutMs);
|
|
251
|
+
operation.then((value) => {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
abort.signal.removeEventListener('abort', onAbort);
|
|
254
|
+
resolve(value);
|
|
255
|
+
}, (error) => {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
abort.signal.removeEventListener('abort', onAbort);
|
|
258
|
+
reject(error);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function isRecord(value) {
|
|
263
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
264
|
+
}
|