Repository navigation
Move the turn engine into @openbot/toolkit, and split the largest server files - #784
Conversation
…interrupt's metadata
…used before it starts
… run-turn stops importing copilot.ts
…rc/toolkit, apart from the computer tools
…src/toolkit, apart from the plugin store
…ead of reading the lifecycle and controls globals
…ead of the audit store
…data key into src/toolkit
…nentCatalog port, and keep AuditSink on the server
…story helpers into src/toolkit
…de server/src/toolkit
…e(), with the lock held by a lease
…lse's, not on any failed renewal
…t when its lock is renewed as somebody else's
…erive the sweep's abandoned-run cutoff from it
…tever a hop asks for
…ead its Anthropic fallback from the shared spec
…everal call sites share
…e adapter into composio-vendor.ts
…roup, mounted in their original order
…approvals/resume-sweep.ts
…approvals/person-response-sweep.ts
…of index.ts into http-server.ts
… agents/handoff-wiring.ts
…er out of index.ts into agents/coordination-wiring.ts
…, where no cycle prevents it
# Conflicts: # server/src/index.ts # server/tests/passwords-sign-in.test.ts
davidmckayv
left a comment
There was a problem hiding this comment.
Requesting changes for one blocker: Desktop can't install a release that contains this. The refactor itself checks out.
What I verified on this branch
-
The moves are verbatim. A scripted syntax-tree comparison against
main:- all 123 plugin-store units and all 61 Composio units match;
- the 21 routes match on method, path, middleware and order, and their 32 handlers match;
http-server.tsmatches main'sBun.serveblock apart from two re-wrapped lines;- 99 of the 132 modified TS files change nothing but import paths.
-
index.tsstartup is unchanged. The 8 top-level awaits keep their order, and none of the three reassigned variables is passed into a wiring module by value. -
settle()matches main. It reproduces main's order of throws, error objects and side effects, apart from the intended lease change. -
The tests catch regressions. I re-broke each fix: the lease stopping on any failure, the handoff ignoring lock loss, admission after the lock, and two outcomes in
settleswapped. The tests caught every one. -
The merge from
mainlost nothing. #776, #751, #775 and #780 are intact. -
Same dependencies. The lockfile resolves the same package@version set as
main; three packages only swapped which version is hoisted. -
Both Dockerfiles build and start, and every toolkit and shared module resolves inside them. CI skipped
imageandcomponent dockerfiles, which run only with thefull-cilabel, so I built both by hand. -
It works live. I booted it on a local stack and drove it in the browser:
- a chat turn;
- a two-Bot group turn through the toolkit runner;
- the computer stream websocket, both frames and input;
- live updates over
/api/channels/events; - demonstration recording capture.
All of it worked, which covers the two paths the description lists as unverified. I didn't exercise an approval resume.
Blocker
- Desktop install fails.
- Desktop copies a fixed list of folders from the release tarball (
desktop/src-tauri/src/deployment.rs:175-187), andtoolkitisn't on it. - It then runs
bun install --frozen-lockfileat the root, which now stops witherror: Workspace not found "toolkit". Even past that, the server's@openbot/toolkit/*imports wouldn't resolve. - I reproduced it from the copied tree; main's tree installs fine. CI misses it because the startup jobs use the full checkout.
- Fix: add
"toolkit"toALSO_COPIED. Also addsharedandtoolkittoPACKAGESindesktop/src-tauri/src/preparation.rs:14, so a dependency change in either triggers a reinstall.
- Desktop copies a fixed list of folders from the release tarball (
Should fix
- The lease fails open, with no time limit (inline on
toolkit/src/lease.ts). - The toolkit core can import CopilotKit code through its own package name (inline on
biome.json).
Nits
runtimeModelis now computed beforenormalizeModelBaseUrls()runs (inline).- Setting the headless lock lifetime back to 20s fails no test.
routine-run-turn.test.ts:766andthread-lock.test.ts:157compare against the constant itself, so a literal120would pin it.
…rprint shared and toolkit's dependencies
…ugh the package's own name
…eads them, as on main
…ifetime, not only on a conflict
davidmckayv
left a comment
There was a problem hiding this comment.
Thanks, all five check out on 135cbe2.
- Desktop:
toolkitis copied into a deployment,sharedandtoolkitare inPACKAGES, and the fixture lays out both folders. - Lease: this is what I suggested. A renewal that lands late extends the lock only from when it was asked for, which is the right cautious choice. With the lifetime check removed, two lease tests and the hung-renewal handoff test fail.
- Lint: the self-reference probe is now flagged.
- Model URLs:
normalizeModelBaseUrls()runs right beforeruntimeModelagain. - Lock lifetime: 120 is pinned as a literal in both tests.
The lease, settle, thread-lock, handoff and turn-runner suites pass locally: 34 toolkit tests and 74 server tests.
Optional follow-up, not blocking: a lapse is only noticed on a tick, so a turn can run up to one interval past the lifetime: 15s for a turn, 30s for a handoff. Comparing against lifetimeMs - everyMs would stop it before the lock lapses rather than after.
Approving. app (macos), app (windows) and the Python harness job were still running when I looked.
…ot yet re-homed WIP COMMIT ON A SCRATCH BRANCH. Do not merge to arcade-provider. Takes main's side for the four conflicted files so the tree carries main's new module layout: plugin-store/*, route-groups/*, composio-client/* and composio-vendor.ts. This DROPS the arcade work that lived in the monoliths (15 store methods, ~1634 lines of routes, the broker contract changes) and it is re-homed in the commits that follow. Remaining typecheck errors (23) are the shared contracts our branch changed and main's modules do not yet carry: brokerProvider on PluginStoreOptions, ConnectedAppBroker's grantIsLocal/authorizations/grantsFor/authProviderOf, authorizationId on the consent answer, and BrokerApp's classifiedCount, permissionCount and scopedActionCount. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports `git diff bb62c65 a54ba3a -- server/src/plugins/routes.ts` (+1,569/-65) onto the route-groups split from #784. A port, not a fix: no behaviour changes. Method: the diff was split into 40 hunks; 34 matched exactly one route-group module by pre-image and were applied in place. The other 6 (imports, module helpers, the activeProvider/activeVendor/appOfUrl closures, and the final hunk carrying the new routes) were placed by hand. A line check finds every added line in the new modules (13 differ only by import path or an `export` prefix) and no removed line surviving. - context.ts: logReturnUrlRefusal, directoryUnavailable (replaces DIRECTORY_UNAVAILABLE), signInUrlFor, AUTHORIZATION_REFUSALS, sameAccount, VendorTimeoutError, the deadline constants, withVendorDeadline (executor shape kept verbatim), settledStatus; PluginRouteContext gains activeProvider, activeVendor, appOfUrl. - routes.ts: builds those three (with the construction-time throw) and re-exports AUTHORIZATION_REFUSALS / AuthorizationRefusal. - servers.ts: `/` gains authorizesPerAction + provider; remove's vendor sentences and guarded forgetRule; account-mode uses appOfUrl. - composio-apps.ts: deadline-wrapped listApps, directoryUnavailable, provider-named sentences, appOfUrl. - connections.ts: connect-path sentences and deadlines, brokeredAccountFor readOnly, GET /servers/:id/grants, and after disconnect: GET /servers/:id/connection/siblings, POST + DELETE /servers/:id/authorize, GET /arcade/verify. Guard entries re-keyed (path only): pending-authorization-audit-ledger, 7 CALL_SITES keys and 1 NON_WRITES file, routes.ts -> route-groups/connections.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ver files (CopilotKit#784) * test(server): pin a headless turn that waits on a person through the interrupt's metadata * test(server): pin how a hop treats a failed lock renewal and a Bot paused before it starts * refactor: make shared/ the @openbot/shared workspace and import it by name * refactor(server): lift the hop's thread lock out of mountCopilotRuntime and test it * refactor(server): give the thread-history helpers their own module so run-turn stops importing copilot.ts * refactor(server): move the headless tool contract and middleware to src/toolkit, apart from the computer tools * refactor(server): move the GrantedTool contract and parametersFor to src/toolkit, apart from the plugin store * refactor(server): inject a turn's admission into the turn runner instead of reading the lifecycle and controls globals * refactor(server): hand the headless component tools an AuditSink instead of the audit store * refactor(server): define AuditInitiator in src/toolkit and re-export it from audit.ts * refactor(server): move the approval continuation and the waiting metadata key into src/toolkit * refactor(server): give a headless turn its components through a ComponentCatalog port, and keep AuditSink on the server * refactor(server): move the headless turn runner, its types and the history helpers into src/toolkit * chore(lint): forbid server, CopilotKit, Drizzle and Hono imports inside server/src/toolkit * refactor(toolkit): decide how a headless turn ended in one pure settle(), with the lock held by a lease * refactor: make the toolkit its own @openbot/toolkit workspace, outside the server * fix(toolkit): stop a turn only when its lock is renewed as somebody else's, not on any failed renewal * fix(server): admit a hop before it locks the conversation, and stop it when its lock is renewed as somebody else's * refactor(toolkit): name a headless turn's limits in TURN_LIMITS and derive the sweep's abandoned-run cutoff from it * docs(server): say that the platform applies its own lock lifetime whatever a hop asks for * refactor(server): name the built-in Bot's OpenAI fallback model and read its Anthropic fallback from the shared spec * refactor(server): name the OAuth and delivery request timeouts that several call sites share * fix(toolkit): ask for the platform's own 120-second lock lifetime on a headless turn * refactor(server): move Composio's vendor shapes and parsers out of the adapter into composio-vendor.ts * refactor(server): split the plugin routes into one module per route group, mounted in their original order * refactor(server): move the approval resume loop out of index.ts into approvals/resume-sweep.ts * refactor(server): move the person-response loop out of index.ts into approvals/person-response-sweep.ts * refactor(server): move the Bun server and its websocket dispatch out of index.ts into http-server.ts * refactor(server): move starting handoff delivery out of index.ts into agents/handoff-wiring.ts * refactor(server): move the coordination tools and their per-run wrapper out of index.ts into agents/coordination-wiring.ts * refactor(server): split the plugin store into its model, a shared context and five method groups * refactor(server): split the Composio client into a shared context, its actions and four broker method groups * refactor(toolkit): move the native approval wrapper into the toolkit core * refactor(toolkit): add the copilotkit adapters folder, starting with the thread lock, and keep the core free of CopilotKit * refactor(toolkit): move the CopilotKit agent classes into the toolkit, with the server's run hooks passed in * refactor(server): declare what the approval service reads before it in index.ts * refactor(server): move the approval service's construction out of index.ts into approvals/wiring.ts * refactor(server): move the responsibility engine's construction out of index.ts into responsibilities/wiring.ts * style(server): format copilot.ts's imports after moving the agent classes * refactor(server): declare what index.ts's statements read before them, where no cycle prevents it * docs(changelog): say how Bot turns hold their conversation more carefully * fix(desktop): copy the toolkit workspace into a deployment, and fingerprint shared and toolkit's dependencies * fix(lint): stop the toolkit core importing its copilotkit folder through the package's own name * fix(server): normalise the model base URLs before the runtime model reads them, as on main * fix(toolkit): lose a lease whose renewals have failed for its whole lifetime, not only on a conflict * test: pin the 120-second lock lifetime with a literal, not the constant it checks * fix(toolkit): give up on a lease one interval before its lock lapses, not after
…t + server 大文件拆分 - plugins/store|routes 保我方版(OpenConnector brokerFor 多路),上游拆分版目录未采纳 - toolkit BuiltInAgentHooks 增可选 wrapRun(fork observation threading 接缝) - copilot.ts 4 类用 toolkit 版,observation 经 wrapRun 注入 - headoff-delivery 内联体删除(上游已抽 handoff-wiring) - GrantedTool 等符号改 @openbot/toolkit/* 包路径 Co-Authored-By: Claude Code <noreply@anthropic.com>
上游 CopilotKit#784 将 turn 引擎移入 @openbot/toolkit 并拆分超大文件后,本分支仍在旧路径演化, 本次合并把 ME 语义移植到新结构上: - toolkit/run-turn: createTurnRunner 支持 intelligence/runner 惰性访问器(ME15 #487), 与 admission 并存;takeLease 在回合内解析 client - toolkit/copilotkit/thread-lock: createIntelligenceThreadLock 接受惰性 client, 逐次解析,保持 ME15 零 client 启动 - server/index: 八处 turn 装配改用 requireRoutineIntelligence/requireRoutineAgentRunner; openMuseTaskRunner 走 governedOpenMuseTaskTurn ?? 选择器(ME12 #484); handoff 经 startHandoffDelivery 传入 governedHandoffDelivery(ME11 #483); approvals 走 HEAD 的 createServerApprovalService,intelligence 条件构造 - server/mastra/handoff-delivery: import 指向 toolkit 真实导出 - tests/config/e2e-bun-suites.json: 按 HEAD 分区结构补入 8 个新测试清单项 typecheck:server/app 合并引入错误 0(存量红 94+156 项与 HEAD 逐字节相同文件) Co-Authored-By: Claude Code <noreply@anthropic.com>
What this changes
A refactor of the server toward packages: the code that runs a Bot's turn becomes its own workspace,
@openbot/toolkit;shared/becomes@openbot/shared; and the largest files are split. Three behaviour fixes to how turns hold their conversation's lock ride along, each in its own commit. They're described below and in the changelog.Draft, because it's big (42 commits on
main, 196 files) and is meant to be reviewed commit by commit, or split into the stacked PRs listed under Plan.Benefits
The turn engine is safer to change
toolkit/, outside the server.toolkit/src/copilotkit/, and a second rule keeps the toolkit core free of CopilotKit.main, loadingrun-turn.tspulled in 49 server modules, including the database schema and all ofcopilot.ts.settle(evidence) → TurnOutcome(completed | failed | lockLost | timedOut | suspended | interrupted | empty), andconclude()is the only place an outcome becomes a reply or an error.TurnAdmission,ComponentCatalog,ThreadLock) instead of read from module globals.Behaviour fixes
keepLease) stops a turn only when Intelligence answers 409THREAD_LOCK_RENEWAL_FAILED, meaning the lock is now someone else's. A stopped turn is retried from the start, so a blip could make a Bot repeat a side effect.No more 5,000-line files
mainplugins/store.tsplugin-store/(model, shared context, five method groups)plugins/composio-adapter.tscomposio-vendor.tsandcomposio-client/index.tsplugins/routes.tsroute-groups/(six groups)copilot.tstoolkit/src/copilotkit/agents.ts)Each split was checked mechanically:
@composio/coreindex.tsis less fragile. It had 29 places that read a name declared further down. That worked only because the read happened later, at run time, and an extraction could turn any of them into a startup crash. They're down to 12. The ones left are real cycles (for exampleactorAgentResolver↔copilotRuntime), a hoisted function, or would mean moving 100–200 lines of startup for one reference.Clearer contracts
@openbot/sharedhas explicitexports. Nothing imports it through../../../sharedany more.TURN_LIMITS) and the shared OAuth and delivery timeouts are named. The routines sweep calculates its "abandoned run" cutoff from the turn timeout instead of keeping a hidden copy.More tests: 46 new: the uncovered waiting-on-interrupt branch, the handoff lock adapter, admission, the lease rule,
settle,turnMessages/turnInput, and the provider-switch model fallbacks.How to read it
The commits follow the phases, each ending where the next starts:
feb91d1117481b1ffcbb9db0shared/→@openbot/sharedc33f0d3c…684325dcTurnAdmissiona9f5aee8…1b0329baserver/src/toolkit/, add the boundary rulec90311efsettle(), the lease, pureturnMessages/turnInput654f3174toolkit/becomes the@openbot/toolkitworkspace9f378bf0afb21822a1ceb39f…dc3d7523722e2b09…876a2aeeindex.tsreordering, CopilotKit agents into the toolkitd6e9f0baefd8ef8amain, changelogMost commits are mechanical moves. Where code moved, it moved verbatim: the store and the Composio client were split with the TypeScript compiler's syntax tree, which keeps each node's exact text and comments. The commits that change behaviour are
9f378bf0,afb21822anddc3d7523.Plan
Next, in this refactor
copilot.ts. Moving them in behind the ports already sketched (PromptContext,ModelConfig,ToolProvider) would let the toolkit take "this Bot, this person, this message" and return a finished turn.index.ts(2505),plugin-store/connections.ts(2495),app.ts(2005),plugin-store/servers.ts(1907) andcopilot.ts's agent building.After that: Slack, Teams and any other platform, through the Channels SDK
Reading OpenTag and
@copilotkit/channels0.11.0 for this work turned up two things:The OpenTag side of the pairing isn't public. OpenBot's pairing expects an
openbot.sendercontext entry and/openbot/deliverand/openbot/membershipendpoints. No public CopilotKit repository implements them, OpenTag included; upstream OpenTag sends a free-text sender label and has neither route. Theconfirm_writeapproval card does match.The runtime OpenBot already runs can host Channels.
@copilotkit/runtime1.77.0 acceptschannelson the same Intelligence runtime OpenBot mounts. OpenBot could therefore declare its own Channel:identifyUserwould be the existing binding lookup, plus same-domain email auto-linking.agentwould be the person's Bot.onInterrupt, and charts throughchannels-ui.That would remove the OpenTag relay, the shared secret and most of
delivery/opentag*.ts. It would also bring the SDK's Discord, Telegram and WhatsApp adapters. The open gap is messages a Bot starts on its own: the SDK says "Proactive delivery to subscribed conversations is not yet wired".The next step is a spike: one Channel inside
mountCopilotRuntime, and checking whether a linked person can chat, approve an action from a card and get a chart back, with no OpenTag in between.Where it runs
main. Turns still serialise on Intelligence's thread lock. The only change is when a turn gives up: on a 409 renewal (the lock is now another replica's or the person's), not on a network error. A handoff that loses its lock fails into the existing work-queue retry.SET … NX). Nothing new.http-server.ts.Boundary and audit
agent.handoff_failed), and a capability refusal is still final.Changelog
Unreleasedhas "Bot turns hold their conversation more carefully", covering the three behaviour fixes and the lock lifetime. The rest moves code without changing behaviour.Proof
Run on this branch after merging
main:Tests ran against a dedicated pgvector database, migrated to
0056. The shared local Postgres runs out of connections under the full suite.Other checks along the way:
index.tsmove, the server booted against the test database and served/healthand/api/mewith no "before initialization" errors. Handoff delivery was enabled, so its startup ran.settleorder each failed the new tests, as intended.@copilotkit/runtimeand./copilotkit/*were rejected, a copilotkit-folder import of server code was rejected, and the allowed imports passed.@openbot/toolkitand@openbot/sharedresolve inside it with their own dependencies.Not verified: the computer stream and channel websocket paths through the moved
http-server.ts. No test reaches them throughindex.ts, and the boot check doesn't open a socket.