805 Commits
Author SHA1 Message Date
Hare 016dbd7cb1 fix: preserve worker run generations across restore 2026-09-15 01:17:30 +09:00
Hare 7210d3c202 fix: reject failed worker restores before attach 2026-09-15 00:47:12 +09:00
Hare 572204b49a fix: resolve CLI connections before async runtime 2026-09-15 00:28:25 +09:00
Hare 86dd67a24c fix: skip stopped worker protocol subscriptions 2026-09-14 23:31:14 +09:00
Hare beeba1fdfc fix: migrate legacy worker snapshots 2026-09-14 22:28:01 +09:00
Hare 41b7b289d0 fix: serialize Workdir lease admission with writes 2026-09-14 20:32:21 +09:00
Hare 24237249d1 Merge remote-tracking branch 'origin/develop' into work/T-604-workdir-symlink-policy 2026-09-14 20:05:34 +09:00
Hare e448073b98 fix: fence resolved Workdir lease aliases 2026-09-14 20:05:27 +09:00
Hare c08152d069 fix: share workspace creation wire contract 2026-09-14 19:45:18 +09:00
Hare 3995561220 Merge remote-tracking branch 'refs/remotes/origin/develop' into work/T-468-remove-local-workspace-authority 2026-09-14 19:26:44 +09:00
Hare aea51caeb4 fix: resolve workspaces through backend repositories 2026-09-14 19:24:44 +09:00
Hare 3eca380bd8 Merge remote-tracking branch 'origin/develop' into work/T-604-workdir-symlink-policy 2026-09-14 19:12:17 +09:00
Hare 8a3e06bc81 feat: add selective Workdir symlink policies 2026-09-14 19:09:12 +09:00
Hare c4274c42cb fix: align plugin help with offline commands 2026-09-14 18:52:00 +09:00
Hare a61ad15767 refactor: remove ambient plugin authority 2026-09-14 18:40:42 +09:00
Hare 7f1e374fd7 fix: remove repository-local workspace selection 2026-09-14 18:31:04 +09:00
Hare e3f5445a02 fix: move workspace metadata authority to server database 2026-09-14 18:31:04 +09:00
Hare d2cb50d081 fix: fence runtime removal races and retries 2026-09-13 02:54:01 +09:00
Hare 6c609808c9 fix: use authoritative runtime removal result 2026-09-13 02:30:24 +09:00
Hare 2d4c7b383a feat: add guarded runtime removal operation 2026-09-13 02:30:15 +09:00
Hare c21ed7dff2 fix: preserve credential candidates across Workdir retries 2026-09-13 01:24:26 +09:00
Hare 448e392a0e fix: persist Workdir credential candidate snapshots 2026-09-13 01:14:39 +09:00
Hare d97c40d6af fix: reject plain HTTP repository sources 2026-09-13 01:12:01 +09:00
Hare 2d512b6be6 fix: remove stale HTTP repository access branch 2026-09-13 01:04:46 +09:00
Hare f061a95b48 fix: validate repository URI before source kind 2026-09-13 00:45:43 +09:00
Hare eefdef1bef fix: lock verified runtime public keys 2026-09-13 00:32:44 +09:00
Hare 7675f81999 fix: reject plain HTTP repository sources 2026-09-13 00:29:16 +09:00
Hare 8fb592071f fix: fence stale repository SSH probes 2026-09-13 00:27:32 +09:00
Hare 2528312142 fix: lock verified Runtime public keys in settings 2026-09-13 00:23:25 +09:00
Hare 08d7965ea8 fix: centralize repository access expiry 2026-09-13 00:20:25 +09:00
Hare e0badad91f fix: invalidate stale repository SSH probes 2026-09-13 00:17:12 +09:00
Hare f0a91ce2d8 fix: centralize repository access expiry 2026-09-13 00:11:03 +09:00
Hare 24cab83f48 test: cover repository credential rotate visibility 2026-09-13 00:05:14 +09:00
Hare 33a2b5d702 fix: unify workspace request routing 2026-09-12 23:49:13 +09:00
Hare 7f807004ad Update AGENTS.md 2026-09-12 22:47:35 +09:00
Hare 5564425488 fix: retry retained workdir removal 2026-09-12 13:30:25 +09:00
Hare 4a89c04732 fix: allow corrupted workdir removal 2026-09-12 01:43:37 +09:00
Hare ec5a403ec6 feat: replace linked worktrees with runtime clones 2026-09-12 01:43:21 +09:00
Hare f6ce1df766 feat: support workspace-managed SSH repository access 2026-09-11 22:55:06 +09:00
Hare 9d7ddcc04a fix: enforce Workspace Runtime binding lifecycle 2026-09-09 13:20:00 +09:00
Hare 3df611636b feat: complete Workspace Runtime management flow 2026-09-09 09:38:26 +09:00
Hare d0999326bd fix: use Backend audience for embedded Runtime requests 2026-09-09 09:38:26 +09:00
Hare 6fbc65476c fix: migrate standalone Worker manifest snapshots 2026-09-09 01:22:59 +09:00
Hare fcc7d79d80 feat: authorize scoped symlink paths lexically 2026-09-09 00:51:21 +09:00
Hare 18fd6a1f5e fix: restore remote Runtime management contracts 2026-09-09 00:26:04 +09:00
Hare a072562034 chore: merge develop into hare/develop 2026-09-08 13:04:10 +09:00
Hare 2b4a2bc688 fix: fail closed on missing workspace capability 2026-09-08 12:40:04 +09:00
Hare 3344d9f8b2 refactor: remove server-global runtime trust 2026-09-08 12:19:39 +09:00
Hare fae36d220d fix: complete Runtime verification cutover 2026-09-08 10:11:18 +09:00
Hare 7b6a84a550 feat: project Runtime verification state 2026-09-08 09:22:44 +09:00
Hare f29c343879 feat: verify Workspace-signed Runtime bindings 2026-09-08 08:13:58 +09:00
Hare f5e9f49a13 fix: bind Runtime WebSockets to egress policy 2026-09-08 05:29:53 +09:00
Hare 73a35599d2 fix: complete configured Runtime onboarding 2026-09-08 05:15:14 +09:00
Hare 5080d7860e fix: preserve Runtime binding trust boundaries 2026-09-08 04:54:23 +09:00
Hare 7fb1d4056c feat: add manual Runtime trust setup UI 2026-09-08 04:22:45 +09:00
Hare 243a081874 feat: add configured Workspace Runtime bindings 2026-09-08 04:22:32 +09:00
Hare 04924cf796 fix: bound Runtime issuer trust surfaces 2026-09-08 02:31:58 +09:00
Hare 8f0917b8bc docs: remove obsolete local profile override guidance 2026-09-08 02:13:26 +09:00
Hare d4ad46127a feat: add GPT-6 Astra model catalog entry 2026-09-08 02:13:26 +09:00
Hare fba5ecf54c fix: harden Workspace issuer bootstrap 2026-09-08 02:00:19 +09:00
Hare e035df9e7b feat: add Runtime Workspace issuer trust 2026-09-08 01:54:10 +09:00
Hare 3baf0b6358 feat: integrate Workspace signing identity authority 2026-09-08 00:48:45 +09:00
Hare 4de04e42b5 fix: harden identity publication recovery 2026-09-08 00:30:38 +09:00
Hare ebec98a14c feat: add Workspace signing identity authority 2026-09-07 23:52:20 +09:00
Hare f966470d33 fix: acknowledge durable submissions before run preparation 2026-09-07 21:38:19 +09:00
Hare c76ede2ab4 fix: allow slow initial input commits 2026-09-07 20:21:22 +09:00
Hare 2fd043b634 fix: expand worker creation timeout budgets 2026-09-07 19:43:26 +09:00
Hare fb6bbe9145 fix: wait for worker shutdown before deletion 2026-09-07 19:31:32 +09:00
Hare d1585d7483 fix: route worker failure logs through tracing 2026-09-07 18:14:07 +09:00
Hare 13d853217d fix: log failed worker creations to stdout 2026-09-07 04:56:54 +09:00
Hare cc27d57e4a fix: reclaim failed worker creations 2026-09-07 04:44:19 +09:00
Hare f5c5ea5a0b fix: route worker consoles through logical resources 2026-09-07 03:53:34 +09:00
Hare f1baea1705 feat: distribute latest workspace config to runtimes 2026-09-07 02:40:03 +09:00
Hare 31f7d39647 feat: add Server database migration command 2026-09-06 16:17:48 +09:00
Hare c5a834bfd2 fix: restore composable Workspace schema migrations 2026-09-06 16:17:40 +09:00
Hare 1eef9b75ee Merge branch 'develop' into hare/develop 2026-09-06 13:39:37 +09:00
Hare ba9c885f52 feat: integrate Worker state authority 2026-09-06 11:00:00 +09:00
Hare 88683a8d8f test: provide authoritative websocket snapshot 2026-09-06 10:41:20 +09:00
Hare 0c48c5dee3 fix: fail closed without worker snapshots 2026-09-06 10:33:45 +09:00
Hare 101a0acb6b fix: terminalize removed worker reservations 2026-09-06 10:32:45 +09:00
Hare 2d1956b653 fix: separate worker state from runtime lifecycle 2026-09-06 10:05:54 +09:00
Hare b7bba8b53a fix: preserve workspace deletion checkpoints 2026-09-06 10:02:18 +09:00
Hare 7b25b767f8 fix: checkpoint workspace cleanup children 2026-09-06 09:35:44 +09:00
Hare 4631b95144 fix: fence workspace deletion side effects 2026-09-06 09:21:37 +09:00
Hare 38627c498b fix: harden workspace deletion recovery 2026-09-06 08:57:54 +09:00
Hare 282a8d31b5 fix: enforce monotonic worker state projection 2026-09-06 07:56:25 +09:00
Hare ab4fb4c1ee fix: enforce workspace deletion blockers 2026-09-06 07:54:52 +09:00
Hare e3e9e83bc1 feat: add guarded workspace deletion workflow 2026-09-06 07:22:33 +09:00
Hare 4269ebec04 chore: merge develop into worker state snapshot 2026-09-06 07:17:40 +09:00
Hare e8b9adcde4 feat: add revisioned worker execution state 2026-09-06 06:54:29 +09:00
Hare 668a9062b3 fix: fence runtime detail route state 2026-09-06 06:11:23 +09:00
Hare 5fd2ccf084 fix: gate runtime key reveal and revoke confirmation 2026-09-06 05:50:29 +09:00
Hare 5686bbc9fd fix: preview runtime trust rotation fingerprint 2026-09-06 05:25:03 +09:00
Hare 2cd57a32b2 fix: align runtime trust schema and built-in controls 2026-09-06 05:17:27 +09:00
Hare 89f4f99622 Merge branch 'develop' into work/T-595-runtime-trust-key-management
# Conflicts:
#	web/workspace/deno.json
2026-09-06 05:07:26 +09:00
Hare 78d571ed14 feat: add workspace runtime trust key management 2026-09-06 05:06:37 +09:00
Hare e5332f4a7f feat: route SubWorker workdir execution through parent 2026-09-06 04:28:52 +09:00
Hare 3ed1545c3c feat: integrate worker submit queue 2026-09-06 04:24:07 +09:00
Hare 9da20d15da fix: compose passive notify with queued submit 2026-09-06 04:15:30 +09:00
Hare 052d60bd7d fix: fence late command events and spawn rollback 2026-09-06 04:10:23 +09:00
Hare 2456d6fda5 fix: preserve pending activation order on restore 2026-09-06 04:00:02 +09:00
Hare e7803d1aba fix: qualify runtime binding helper 2026-09-06 03:52:42 +09:00
Hare ca5fddf89b fix: fence recursive SubWorker shutdown 2026-09-06 03:48:54 +09:00
Hare 88fad3893e Merge commit '5e9f7a7dc3d1169118412376f16b72850aab63f2' into work/T-593-workspace-runtime-bindings
# Conflicts:
#	crates/workspace-server/src/server.rs
2026-09-06 03:46:09 +09:00
Hare 82f9b0e48c chore: merge current develop into T-588 2026-09-06 03:45:30 +09:00
Hare ddb4c1454d fix: enable running composer delivery 2026-09-06 03:32:23 +09:00
Hare 7b1cf854f2 fix: serialize scoped command teardown 2026-09-06 03:29:50 +09:00
Hare 51c6d7f835 fix: require authenticated runtime bindings 2026-09-06 03:28:19 +09:00
Hare 9d55ce0a87 fix: reconcile interrupted upload pins 2026-09-06 03:15:44 +09:00
Hare 0f8d61188a fix: order SubWorker cleanup before Workdir release 2026-09-06 03:12:55 +09:00
Hare 7363dffb9d fix: gate runtime projection broadcasts 2026-09-06 03:00:36 +09:00
Hare 8e4b7deaa4 fix: preserve authenticated submit source 2026-09-06 02:56:51 +09:00
Hare ec845cbc25 fix: fence revoked runtime bindings 2026-09-06 02:52:15 +09:00
Hare e7079e223f fix: close scoped SubWorker command authority 2026-09-06 02:37:13 +09:00
Hare dea5bd581d fix: secure submit activation handoff 2026-09-06 02:31:10 +09:00
Hare 75b85b46d1 feat: scope runtime bindings by workspace 2026-09-06 02:30:13 +09:00
Hare 68f00bc948 refactor: broker SubWorker Workdir tools through parent 2026-09-06 02:17:44 +09:00
Hare 5e9f7a7dc3 feat: add versioned runtime connection ping 2026-09-06 02:17:26 +09:00
Hare b038f022d3 fix: harden submit queue durability 2026-09-06 01:35:26 +09:00
Hare cf7515fb35 fix: keep tool overview neutral on failures 2026-09-06 01:20:39 +09:00
Hare bb4c1dfe4f docs: update flow submit protocol example 2026-09-05 23:25:25 +09:00
Hare 72b56964c3 chore: refresh T-588 onto develop 2026-09-05 23:19:52 +09:00
Hare 5b0a6691f8 fix: fence pending queue controls 2026-09-05 23:16:19 +09:00
Hare 1239c638a5 Merge branch 'develop' into hare/develop 2026-09-05 22:39:57 +09:00
Hare d2fa0787d8 fix: retain child-local bash output scope 2026-09-05 22:29:19 +09:00
Hare a7056702e8 fix: keep worker-host paths out of remote delegations 2026-09-05 22:18:46 +09:00
Hare bb56283063 feat: persist worker submit activation queue 2026-09-05 22:06:59 +09:00
Hare 130ef1f0fe fix: align Backend worker picker columns 2026-09-05 21:19:18 +09:00
Hare 724205b1df feat: create Backend workers from bare TUI launch 2026-09-05 21:19:12 +09:00
Hare 69824ea45d fix: defer resident Memory loading until first run 2026-09-05 07:52:47 +09:00
Hare 15bc299987 fix: align integrated validation contracts 2026-09-05 06:49:06 +09:00
Hare 87bdb0c6ed chore: integrate develop into hare/develop candidate 2026-09-05 04:15:47 +09:00
Hare aa96bbedbc fix: harden Memory restore and scheduling 2026-09-05 00:24:00 +09:00
Hare 4df277c81f fix: preserve Memory consolidation enablement 2026-09-04 23:53:59 +09:00
Hare 12646b6ca0 refactor: install Memory prompt contributions through Feature 2026-09-04 23:40:12 +09:00
Hare 1e674d70c2 refactor: unify Memory feature configuration authority 2026-09-04 22:55:37 +09:00
Hare 5ee77698db fix: gate workspace memory lifecycle authority 2026-09-04 20:53:27 +09:00
Hare d1f5661881 test: cover memory lifecycle outcomes 2026-09-04 20:11:29 +09:00
Hare 532d078720 test: exercise memory lifecycle background behavior 2026-09-04 19:41:04 +09:00
Hare 27e5df106f fix: complete memory lifecycle feature boundaries 2026-09-04 19:16:01 +09:00
Hare 33d98868c3 refactor: move memory extraction into lifecycle feature 2026-09-04 18:44:50 +09:00
Hare fb13e53cb5 test: cover start race at rewrite barrier 2026-09-04 16:53:35 +09:00
Hare 60a5495ccd fix: hold feature task barrier across rewrites 2026-09-04 16:34:15 +09:00
Hare f1dc90621c fix: fence feature task shutdown races 2026-09-04 16:13:35 +09:00
Hare eecb116709 fix: bound feature lifecycle execution 2026-09-04 16:04:10 +09:00
Hare 783d25b1c4 fix: project ticket blockers from current DTO shape 2026-09-04 15:47:11 +09:00
Hare af06eecfd0 feat: add executable feature lifecycle hooks 2026-09-04 14:50:44 +09:00
Hare 9bd08a3a5b fix: bound Skill API response handling 2026-09-04 13:55:13 +09:00
Hare 3d66247e11 test: include Skill API contract suite 2026-09-04 13:36:36 +09:00
Hare 4390554477 feat: share Skill REST DTO authority 2026-09-04 13:30:39 +09:00
Hare 74457db4eb Merge commit '89856eb7c3dd6b659ff952d7e3be4a8c0885bf98' into work/T-584-agen-typed-interceptor 2026-09-04 13:01:55 +09:00
Hare 5d61da481b fix: consume internal tool batch cancellation 2026-09-04 13:01:37 +09:00
Hare 89856eb7c3 chore: replace legacy SQLite migrations with baselines 2026-09-04 12:48:45 +09:00
Hare 64c268582d fix: keep runtime SSH sockets below path limits 2026-09-04 12:48:25 +09:00
Hare bb6558e7bf fix: log Worker execution restore failures 2026-09-04 12:48:15 +09:00
Hare 42d109cae3 Merge commit '4ca8ea1694d88db501c48dd4947fdbd6f2828136' into work/T-584-agen-typed-interceptor 2026-09-04 12:28:30 +09:00
Hare 9b48b1ff5d fix: terminalize parallel tool siblings on interceptor stop 2026-09-04 12:27:59 +09:00
Hare 4ca8ea1694 feat: merge REST DTO updates from develop 2026-09-03 19:09:12 +09:00
Hare c10d6c6914 fix: keep Cargo artifacts local to workdirs 2026-09-03 19:06:57 +09:00
Hare 3a94c845cf fix: harden auth response validation 2026-09-03 18:08:43 +09:00
Hare bc810beb3b feat: centralize auth REST contracts 2026-09-03 17:35:24 +09:00
Hare eac4a0c071 fix: harden interceptor lifecycle contracts 2026-09-03 16:46:02 +09:00
Hare c1dfb1add5 Merge remote-tracking branch 'refs/remotes/origin/develop' into work/T-566-memory-rest-dto 2026-09-03 16:07:53 +09:00
Hare e67f9bee08 Merge branch 'develop' into hare/develop 2026-09-03 16:00:05 +09:00
Hare 7c1d81cee9 chore: optimize build 2026-09-03 15:59:40 +09:00
Hare 5cec2eef60 feat: unify Memory REST DTO authority 2026-09-03 15:54:48 +09:00
Hare e62c7cf4f5 Merge commit '7abc6aca45a82665d33115612148536f3a5dd275' into work/T-584-agen-typed-interceptor 2026-09-03 15:47:21 +09:00
Hare 0245980ea5 refactor: finalize interceptor lifecycle boundaries 2026-09-03 15:33:15 +09:00
Hare 7abc6aca45 Merge commit '80221289935e227820aec902988a4f987273e05e' into work/T-549-provider-published-ref
# Conflicts:
#	crates/worker-runtime/src/runtime.rs
2026-09-03 15:25:34 +09:00
Hare f1bcd41ad9 fix: route embedded repository ref observation 2026-09-03 15:05:30 +09:00
Hare 8022128993 Merge branch 'refs/heads/develop' into work/T-565-worker-launch-rest-dto 2026-09-03 14:54:19 +09:00
Hare 68b1aa64e9 feat: type async interceptor failures 2026-09-03 14:46:07 +09:00
Hare f3af8f21dc fix: validate canonical Worker launch segments 2026-09-03 14:43:53 +09:00
Hare 7c056b1db8 fix: publish merge request ref diagnostic DTO 2026-09-03 14:34:09 +09:00
Hare da14c82f71 fix: expose provider ref blockers without failing reads 2026-09-03 14:25:51 +09:00
Hare 1f32c693df refactor: share Worker launch REST DTOs 2026-09-03 14:23:33 +09:00
Hare 74bfbe941e fix: detach stopped worker execution handles 2026-09-03 13:50:25 +09:00
Hare 2884c08466 fix: harden provider ref review boundaries 2026-09-03 13:49:20 +09:00
Hare 85e1ea320a feat: unify worker lifecycle restore semantics 2026-09-03 13:23:31 +09:00
Hare 7be428d8bf fix: require provider-published merge request refs 2026-09-03 12:56:17 +09:00
Hare 56798f9fb4 feat: observe repository refs through runtime providers 2026-09-03 12:56:06 +09:00
Hare 6b1b8a8846 fix: migrate legacy repository references 2026-09-03 11:09:55 +09:00
Hare 9fb1b90856 Merge remote-tracking branch 'refs/remotes/origin/hare/develop' into develop 2026-09-03 09:47:53 +09:00
Hare 30d4023475 fix: preserve blocked TUI submissions 2026-09-03 08:57:28 +09:00
Hare 70432f3d12 fix: retain attachments through TUI retry 2026-09-03 08:44:28 +09:00
Hare 7ee6c307fc fix: require exact attachment acceptance 2026-09-03 08:38:15 +09:00
Hare 36cfbbe6d2 fix: preserve attachment replay evidence 2026-09-03 08:21:11 +09:00
Hare a2e1a3d939 fix: preserve attachment lifecycle boundaries 2026-09-03 08:10:35 +09:00
Hare 9dc8d9a77a fix: fence attachment grants and lifecycle cleanup 2026-09-03 07:22:07 +09:00
Hare 3f6bb65eb1 test: align fork checkpoint regression 2026-09-03 05:55:48 +09:00
Hare 8a70f3cb26 fix: harden uploaded attachment retention and replay 2026-09-03 05:50:47 +09:00
Hare 6c5b8315a3 chore: remove generated test bindings 2026-09-03 05:07:14 +09:00
Hare 690ed0f121 fix: fence uploaded attachment lifecycle 2026-09-03 05:07:03 +09:00
Hare fd60c2b8be fix: render TUI attachment notices with supported alert level 2026-09-03 05:03:49 +09:00
Hare e27b4feb25 feat: upload client-local files from Web and TUI 2026-09-03 04:59:35 +09:00
Hare 09a33e7283 feat: add session-owned uploaded file attachments 2026-09-03 04:58:11 +09:00
Hare d87441448e test: normalize formatted workdir TypeScript 2026-09-03 03:18:09 +09:00
Hare f783f10f6e Merge branch 'work/companion' into hare/develop 2026-09-03 03:06:10 +09:00
Hare 70bdb2d723 test: align client workdir fixture with repository key 2026-09-03 03:03:32 +09:00
Hare 4c1ef04378 fix: show workspace list throughout global sidebar 2026-09-03 02:49:58 +09:00
Hare a595af133c feat: merge durable Workdir updates from develop
# Conflicts:
#	docs/README.md
#	docs/design/durable-operations.md
#	web/workspace/deno.json
2026-09-03 02:47:50 +09:00
Hare f74f3cd133 fix: reject conflicting repository import intent 2026-09-03 02:44:24 +09:00
Hare bcd4848458 Merge branch 'work/companion' into hare/develop 2026-09-03 02:39:54 +09:00
Hare fc05bf9711 fix: restore cursor after inline terminal menus 2026-09-03 02:34:27 +09:00
Hare 63ad590262 fix: remove implicit repository startup selection 2026-09-02 20:00:12 +09:00
Hare 0fd1193b6b fix: harden repository key projections 2026-09-02 19:53:18 +09:00
Hare d996822957 feat: list accessible workspaces in global sidebar 2026-09-02 19:22:45 +09:00
Hare 96349721cb feat: project repository keys across public clients 2026-09-02 19:03:55 +09:00
Hare 8344921b65 feat: add Composer input history navigation 2026-09-02 18:33:43 +09:00
Hare c97b3b7b77 feat: introduce workspace-scoped repository keys 2026-09-02 16:43:28 +09:00
Hare e00e675ed1 feat: add Worker actions to workspace sidebar 2026-09-02 16:24:29 +09:00
Hare 538da1f2b2 fix: validate workspace profile responses 2026-09-02 15:24:33 +09:00
Hare bad37ddc7d feat: centralize workspace profile DTOs 2026-09-02 15:24:12 +09:00
Hare 175eda9f29 fix: authenticate remote worker removal through workspace router 2026-09-02 14:43:49 +09:00
Hare 14c806d38f docs: define durable operation boundaries 2026-09-02 14:43:49 +09:00
Hare 925100fb82 fix: stabilize configuration editor completion 2026-09-02 14:18:51 +09:00
Hare b29b003ea3 feat: define orchestrator cleanup ownership 2026-09-02 02:26:35 +09:00
Hare faa727965b fix: fence orphan recovery claims 2026-09-02 01:59:32 +09:00
Hare d2ffbf2c40 fix: guard Workdir removal recovery ownership 2026-09-02 01:40:48 +09:00
Hare 5418fad7d7 fix: serialize Workdir removal attempts 2026-09-02 01:00:24 +09:00
Hare 510795f1c5 fix: fence Workdir removal retries 2026-09-02 00:25:21 +09:00
Hare 9e0d499987 Merge remote-tracking branch 'origin/develop' into work/T-578-durable-workdir-removal 2026-09-02 00:01:26 +09:00
Hare e96fde0632 feat: add durable Workdir removal authority 2026-09-02 00:01:16 +09:00
Hare eea79dead4 fix: bound Web UX review evidence and cleanup 2026-09-01 23:44:24 +09:00
Hare c4a3f4ba1e fix: harden Web UX capture profiles 2026-09-01 23:00:28 +09:00
Hare 4a4a01b730 feat: add repeatable Web UX inspection workbench 2026-09-01 22:48:26 +09:00
Hare a664e72488 feat: merge Composer paste handling from develop 2026-09-01 22:06:01 +09:00
Hare 21317123a4 fix: deduplicate CodeMirror completion state 2026-09-01 22:03:25 +09:00
Hare 409245cb52 fix: fence disabled Composer mutations 2026-09-01 21:00:26 +09:00
Hare 1aeb6fdb35 fix: enforce Composer read-only state 2026-09-01 20:48:16 +09:00
Hare 816fa96e07 fix: restore workers with remote workdir attachments 2026-09-01 20:41:37 +09:00
Hare 8fbe4218c6 fix: delete complete Composer selections 2026-09-01 20:34:23 +09:00
Hare 00c8df0fc9 fix: preserve exact short paste text 2026-09-01 20:19:53 +09:00
Hare c52c7ead19 feat: add atomic paste chips to Web Composer 2026-09-01 19:50:26 +09:00
Hare d1e8a827c2 fix: preserve configuration completion context 2026-09-01 18:59:16 +09:00
Hare 12d96fb03d fix: preserve short paste line endings 2026-09-01 18:49:44 +09:00
Hare 171a191873 test: run shared composer paste contract 2026-09-01 18:27:21 +09:00
Hare 47dabd8793 fix: keep short composer pastes as text 2026-09-01 18:23:23 +09:00
Hare 2bb661f1cf fix: complete paste artifact storage contract 2026-09-01 17:51:24 +09:00
Hare 04e296a4ef feat: store large paste inputs as artifacts 2026-09-01 17:26:58 +09:00
Hare 4bba227af5 fix: remove legacy resume flag 2026-09-01 16:10:59 +09:00
Hare 5cc78d63c6 fix: merge Companion repository response validation 2026-09-01 13:21:13 +09:00
Hare 323f5dc09c fix: validate repository settings wire response 2026-09-01 13:13:01 +09:00
Hare fb97edfe95 fix: validate workspace schema before owner migration 2026-09-01 12:41:14 +09:00
Hare 070f62ef12 fix: harden workspace owner migration preflight 2026-09-01 12:22:29 +09:00
Hare 981749aa3d feat: require account owners for workspaces 2026-09-01 12:10:52 +09:00
Hare e01b46b30a chore: shard Cargo build cache by workspace 2026-09-01 09:33:47 +09:00
Hare a1b659c45d chore: merge develop into hare/develop
# Conflicts:
#	crates/client/src/lib.rs
#	web/workspace/deno.json
2026-09-01 09:15:10 +09:00
Hare 37a012ef92 chore: configure shared Cargo build caching 2026-09-01 09:04:00 +09:00
Hare 4927e8a843 chore: merge current develop into workspace catalog DTO source
# Conflicts:
#	crates/workspace-api/src/lib.rs
#	crates/workspace-server/src/server.rs
#	web/workspace/deno.json
#	web/workspace/src/lib/workspace/sidebar/types.ts
2026-09-01 08:17:25 +09:00
Hare 025d6ddb47 refactor: make workspace API own workdir projections 2026-09-01 03:16:31 +09:00
Hare 01a4dfd5d3 fix: remove unused workspace API dependency 2026-09-01 02:52:11 +09:00
Hare 1d7158a0bf chore: merge current develop into T-563 source 2026-09-01 02:39:08 +09:00
Hare 9de2afbfc6 fix: validate ticket repository summaries 2026-09-01 02:37:44 +09:00
Hare 9013754a3a refactor: name the workspace catalog list response 2026-09-01 02:24:11 +09:00
Hare 21eea0b104 fix: consume shared workdir create contract 2026-09-01 02:14:57 +09:00
Hare 15e8d7365c test: run workspace DTO boundary regressions 2026-09-01 02:05:07 +09:00
Hare 88e3bf7065 fix: preserve optional workdir wire fields 2026-09-01 02:00:19 +09:00
Hare 2765138bf3 chore: merge current develop into Companion DTO work
# Conflicts:
#	web/workspace/deno.json
2026-09-01 01:59:39 +09:00
Hare 6b20ceac46 fix: validate workspace repository browser payloads 2026-09-01 01:49:38 +09:00
Hare d748274905 refactor: centralize workspace repository response DTOs 2026-09-01 01:49:25 +09:00
Hare e1578217d5 fix: preserve workdir status display contract 2026-09-01 01:45:32 +09:00
Hare 3481682cb4 feat: validate shared workdir REST contracts in web 2026-09-01 01:42:17 +09:00
Hare 996b7f2468 refactor: centralize workdir REST DTOs 2026-09-01 01:42:05 +09:00
Hare 879993b9b1 fix: accept zero-length Companion transcripts 2026-09-01 01:41:16 +09:00
Hare 6604154e3f test: cover repository access loader failures 2026-09-01 01:39:55 +09:00
Hare a9ad42a970 fix: align Companion REST DTO contracts 2026-09-01 01:29:04 +09:00
Hare 8b3d1302c6 fix: share repository access API contracts 2026-09-01 01:22:27 +09:00
Hare ac9269d6ce fix: omit local bash spill path from remote commands 2026-08-31 23:14:33 +09:00
Hare 8ffb716817 fix: preserve workdir operation error classifications 2026-08-31 23:14:23 +09:00
Hare 23f671fa48 fix: remove redundant settings overview 2026-08-31 20:09:29 +09:00
Hare d7cdcde443 fix: preserve standalone scope conflict details 2026-08-31 19:28:09 +09:00
Hare 95a81faf63 chore: merge hare/develop into companion work 2026-08-31 18:45:32 +09:00
Hare bb8bb6d099 feat: restore interactive standalone profile selection 2026-08-31 18:43:46 +09:00
Hare 310801a29b feat: add repository settings registration 2026-08-31 18:20:17 +09:00
Hare 7d09b20445 feat: add workspace repository registration API 2026-08-31 18:12:08 +09:00
Hare 456a06f194 fix: propagate browser actor to workspace routes 2026-08-31 16:55:45 +09:00
Hare 62eaefb1fa feat: spill long bash output to worker temp storage 2026-08-31 16:52:27 +09:00
Hare 10264b4019 Merge branch 'work/companion' into hare/develop 2026-08-31 15:51:36 +09:00
Hare ab9765d91d feat: refine workspace navigation chrome 2026-08-31 15:50:49 +09:00
Hare e7f4c6864f fix: make WorkerId the standalone primary identity 2026-08-31 14:37:46 +09:00
Hare bde1dea2a5 fix: require confirmation before Ctrl-X shutdown 2026-08-31 13:07:39 +09:00
Hare 13a021c480 feat: add TUI run status spinner 2026-08-31 12:46:47 +09:00
Hare a7f09fad98 fix: route standalone protocol through client transports 2026-08-31 12:11:35 +09:00
Hare 10eaf4a5fb refactor: name worker event channels by role 2026-08-31 11:21:14 +09:00
Hare bcada300e3 fix: mark standalone TUI connection ready 2026-08-31 09:44:13 +09:00
Hare 9756174676 chore: merge standalone feature into companion integration
# Conflicts:
#	crates/client/src/target.rs
#	crates/client/src/ticket_role.rs
#	crates/manifest/src/profile.rs
#	crates/tui/src/dashboard/tests.rs
#	crates/tui/src/worker_list.rs
#	crates/workspace-server/src/hosts.rs
#	crates/yoi/src/main.rs
2026-08-31 02:01:38 +09:00
Hare 6dd8461a46 chore: merge develop into companion integration
# Conflicts:
#	crates/workspace-server/src/server.rs
2026-08-31 01:51:13 +09:00
Hare 12cc2eb0e9 feat: unify workspace settings and runtime resources 2026-08-31 01:32:31 +09:00
Hare 38dd717aa6 test: lock discovery control grant separation 2026-08-30 23:07:27 +09:00
Hare 6640c902de feat: add privileged workspace worker discovery 2026-08-30 22:48:26 +09:00
Hare 583c343d08 fix: reject duplicate backend selectors 2026-08-30 21:57:50 +09:00
Hare dfc48f7a05 fix: preserve login backend selection 2026-08-30 21:48:07 +09:00
Hare cd9d854595 fix: reject standalone runtime selectors 2026-08-30 21:36:33 +09:00
Hare 766cbd17e5 refactor: centralize worker control routing 2026-08-30 21:26:29 +09:00
Hare 6df95bf981 fix: make workers help backend specific 2026-08-30 21:26:27 +09:00
Hare 6398ca0893 docs: clarify standalone resume help 2026-08-30 21:16:45 +09:00
Hare 62372a48cc fix: parse product help before target resolution 2026-08-30 21:08:08 +09:00
Hare 733632509a fix: keep product help target independent 2026-08-30 20:57:16 +09:00
Hare e1c13ec314 chore: merge final develop into T-556 2026-08-30 20:49:02 +09:00
Hare 928ff0eabe fix: isolate standalone target resolution 2026-08-30 20:45:40 +09:00
Hare 406559b13d refactor: remove legacy local client authority 2026-08-30 20:35:26 +09:00
Hare 20c16aa6fd chore: merge current develop into T-556 2026-08-30 20:32:20 +09:00
Hare 44ba5fd6d4 fix: expose canonical SubWorker control tools 2026-08-30 20:25:17 +09:00
Hare 745c6adbf2 fix: apply Grep filters to direct files 2026-08-30 20:15:01 +09:00
Hare a9aa09636f fix: preserve Workdir operation error classification 2026-08-30 20:07:46 +09:00
Hare 6945986dd1 fix: use canonical standalone session path 2026-08-30 19:53:31 +09:00
Hare 80c1f48f0e fix: allow Grep to target a file 2026-08-30 19:50:57 +09:00
Hare 31e18205f0 fix: fail closed on unknown standalone leases 2026-08-30 19:44:15 +09:00
Hare e84a9d3f9b fix: synchronize Resume busy state from protocol events 2026-08-30 14:11:12 +09:00
Hare 133feb8c76 feat: add standalone resume picker 2026-08-30 13:46:24 +09:00
Hare c5fd9c01e5 feat: persist standalone sessions for restore 2026-08-30 13:46:16 +09:00
Hare a7bf5ceac3 test: update client session snapshot fixture 2026-08-30 13:23:56 +09:00
Hare 74139aeb7e chore: merge develop into hare/develop 2026-08-30 13:19:24 +09:00
Hare 0cae4fd05c fix: retry device login code collisions 2026-08-30 12:55:12 +09:00
Hare 4b3b4fda61 feat: connect standalone host to local TUI 2026-08-30 12:40:22 +09:00
Hare adb684a6bf fix: preserve typed user input snapshots 2026-08-30 12:33:15 +09:00
Hare 8493472983 refactor: require annotated session log history 2026-08-30 12:18:44 +09:00
Hare 862eeb7add fix: route runtime workers through shared bootstrap 2026-08-30 11:54:20 +09:00
Hare 4d9b211d69 feat: add in-process standalone worker host 2026-08-30 11:29:59 +09:00
Hare ebb272324c Merge branch 'work/T-552-ticket-relation-projection' into hare/develop 2026-08-30 10:15:33 +09:00
Hare c0290512b3 fix: align Ticket relation projection 2026-08-30 10:15:23 +09:00
Hare 4ec56fe41e Merge remote-tracking branch 'origin/develop' into work/T-545-canonical-session-snapshot 2026-08-30 09:42:57 +09:00
Hare f8a7c46cf9 fix(session): preflight migration and retain timestamps 2026-08-30 09:42:46 +09:00
Hare 2bab8a9bb6 fix: distinguish Ticket activity notifications 2026-08-30 09:00:48 +09:00
Hare 89e6a6215a Merge remote-tracking branch 'origin/develop' into work/T-545-canonical-session-snapshot
# Conflicts:
#	web/workspace/src/lib/workspace/console/model.ts
2026-08-30 00:42:29 +09:00
Hare 88be87e03e feat(session): expose canonical public snapshots 2026-08-30 00:41:51 +09:00
Hare 22867faa9c test: align Profile consumers with explicit default 2026-08-30 00:37:52 +09:00
Hare 16c0fc704d feat: unify builtin Profile resolution authority 2026-08-30 00:28:58 +09:00
Hare 32fdd076bf feat: authenticate Backend target requests 2026-08-29 23:41:55 +09:00
Hare 402ae0d466 fix: replay annotated history in Web Console 2026-08-29 23:10:32 +09:00
Hare acb3c6d68b fix: separate Ticket keys from assignment identity 2026-08-29 22:52:03 +09:00
Hare 40ac83e632 chore: merge develop into hare/develop 2026-08-29 13:25:32 +09:00
Hare 58da395941 fix: merge workdir registry cleanup 2026-08-29 13:21:40 +09:00
Hare 0e3ef94c9e feat: merge web console tool call presentation 2026-08-29 13:21:35 +09:00
Hare 1f68dfc2b5 fix: group grep content output by file 2026-08-28 02:05:12 +09:00
Hare 84977a464c feat: improve web console tool call presentation 2026-08-28 00:05:00 +09:00
Hare 62ada5eaa4 fix: bound safe-boundary pause escalation 2026-08-27 23:57:20 +09:00
Hare f5ff0b7c13 fix: confirm bash cancellation cleanup 2026-08-27 23:01:06 +09:00
Hare 3337cafcdf feat: own cancellable tool execution lifecycle 2026-08-27 23:01:00 +09:00
Hare e87784118b fix: preserve resumable paused tool calls 2026-08-27 21:48:59 +09:00
Hare 40fada28ea fix: preserve confirmed output on interceptor abort 2026-08-27 21:30:04 +09:00
Hare 58cc94d4b7 fix: terminalize interrupted tool executions 2026-08-27 20:54:13 +09:00
Hare ccabea59c9 fix: terminalize parallel tool outputs on completion 2026-08-27 19:16:49 +09:00
Hare 8cc1dc042d fix: remove deleted workdirs from registry 2026-08-27 18:36:17 +09:00
Hare 183c37446e fix: fence running snapshots on input commit 2026-08-27 15:42:20 +09:00
Hare 7aa06afc45 chore: preserve reviewed source lineage
# Conflicts:
#	crates/agen/README.md
#	crates/agen/examples/engine_cancel_demo.rs
#	crates/agen/examples/engine_cli.rs
#	crates/agen/src/engine.rs
#	crates/agen/tests/annotated_history_test.rs
#	crates/agen/tests/callback_test.rs
#	crates/agen/tests/engine_fixtures.rs
#	crates/agen/tests/engine_state_test.rs
#	crates/agen/tests/parallel_execution_test.rs
#	crates/agen/tests/reasoning_round_trip_test.rs
#	crates/session-store/tests/session_test.rs
#	crates/worker/src/worker.rs
2026-08-27 15:13:27 +09:00
Hare 1515a2fb86 fix: reconcile typed history with run exits 2026-08-27 15:12:05 +09:00
Hare ec798c58d7 fix: preserve annotated history through rewind 2026-08-27 14:54:24 +09:00
Hare e365189276 feat: add provenance-aware worker history 2026-08-27 14:54:24 +09:00
Hare 116d610ad0 fix: project Ticket mutation outputs to human keys 2026-08-27 14:18:48 +09:00
Hare 75c570962d Merge commit '7edc588202dfbfd4c834f677f510ddda7f3d6451' into work/00001M10HW6BV-model-facing-resource-projection 2026-08-27 13:46:19 +09:00
Hare cae8ac1799 fix: allow missing Objective query snippets 2026-08-27 13:46:10 +09:00
Hare 917cc222a3 fix: resolve relation summaries to Ticket keys 2026-08-27 13:22:08 +09:00
Hare 7edc588202 chore: refresh T-528 after T-541 2026-08-27 13:09:44 +09:00
Hare c83461508b Merge commit '21b3dd1da1b1bbf18799a0623bf67dbe6266067c' into work/00001M10HW6BV-model-facing-resource-projection 2026-08-27 13:09:15 +09:00
Hare 651d64f34d Merge branch 'work/00001M10N3Z0H-ticket-detail-route-sync' into hare/develop 2026-08-27 12:57:52 +09:00
Hare b98d4b59f5 fix: synchronize reused ticket detail routes 2026-08-27 12:55:01 +09:00
Hare 374449e663 feat: add provenance-aware worker history 2026-08-27 12:51:01 +09:00
Hare 4c876a201b fix: validate projected resource keys canonically 2026-08-27 12:49:59 +09:00
Hare 21b3dd1da1 Merge commit '0496cd907bc7bb96e9aa1c6d385bedb616bf3233' into work/00001M10FJVA2-orchestrator-queue-notice 2026-08-27 12:46:27 +09:00
Hare 5ca0ea9228 fix: sanitize orchestrator queue attention 2026-08-27 12:46:10 +09:00
Hare d5c3a68a37 fix: use Ticket resource keys in handoffs 2026-08-27 12:45:08 +09:00
Hare 2b33b9158d chore: refresh T-528 against develop 2026-08-27 12:45:01 +09:00
Hare b31642e284 feat: project Ticket and Objective human references 2026-08-27 12:39:28 +09:00
Hare 3a7a3307ef fix: map internal worker terminal lifecycles 2026-08-27 12:27:55 +09:00
Hare 0496cd907b fix: use Ticket resource keys in handoffs 2026-08-27 12:24:19 +09:00
Hare 975b4fa700 feat: add typed engine run exits 2026-08-27 11:42:49 +09:00
Hare 060f280fdf fix: allow parent commands during write delegation 2026-08-27 11:27:27 +09:00
Hare 7aaf189247 fix: scope delegated commands by cwd 2026-08-27 11:11:06 +09:00
Hare df6d99c07d Merge branch 'develop' into hare/develop 2026-08-27 09:21:58 +09:00
Hare 63306cf017 feat: integrate runtime Git cache materialization 2026-08-27 08:30:38 +09:00
Hare 08be5e85e4 fix: preserve command sessions across access refresh 2026-08-27 08:09:22 +09:00
Hare 9843510e1f fix: remove conflicting coder worker control provider 2026-08-26 23:52:25 +09:00
Hare 83bda3dfb2 fix: restore companion subworker control 2026-08-26 22:06:34 +09:00
Hare 0ab15aa227 chore: merge hare/develop into develop 2026-08-26 19:44:31 +09:00
Hare 29c2fb8e06 fix: refresh repository access for worker workdirs 2026-08-26 19:27:10 +09:00
Hare 4ebc465e8d fix: constrain repository SSH commands to authorized source 2026-08-26 19:02:20 +09:00
Hare 4b132a21e9 fix: bind repository SSH secrets to one-shot resources 2026-08-26 18:28:22 +09:00
Hare b644971d45 fix: preauthorize repository access without persisting secrets 2026-08-26 17:23:43 +09:00
Hare df34533765 fix: broker read-only repository SSH operations 2026-08-26 16:48:37 +09:00
Hare 6f4efb36bb fix: order optional feature service bindings 2026-08-26 16:28:54 +09:00
Hare 108d5b14d7 fix: withhold write-capable credentials from read-only sessions 2026-08-26 16:23:48 +09:00
Hare f633b86b35 chore: integrate current develop for review 2026-08-26 16:12:48 +09:00
Hare 1873e18f8e fix: enforce attachment access transitions 2026-08-26 16:10:39 +09:00
Hare 3cdcbb47bf fix: preserve selector retries and workdir observations 2026-08-26 16:05:14 +09:00
Hare aaa9c7987c fix: reject ambiguous feature service providers 2026-08-26 16:01:06 +09:00
Hare 048007a042 test: complete tool output fixture 2026-08-26 15:47:34 +09:00
Hare cb35b40b9d feat: plan feature service installation 2026-08-26 15:47:27 +09:00
Hare 3b71fe03b4 fix: include merge request status regression in web tests 2026-08-26 15:27:19 +09:00
Hare 471db64bcc fix: preserve repository access across retries 2026-08-26 15:21:52 +09:00
Hare 5e2234763c fix: distinguish completed review requests in merge request status 2026-08-26 15:20:13 +09:00
Hare 1d140be715 feat: align merge request review workflow with selector refs 2026-08-26 15:03:26 +09:00
Hare ffb2a34ae5 fix: enforce repository access and cache boundaries 2026-08-26 14:39:37 +09:00
Hare ccf7de1a55 fix: preserve run budget across segment forks 2026-08-26 14:13:43 +09:00
Hare ccf3c80d29 fix: clear abandoned run budget before compaction 2026-08-26 14:03:07 +09:00
Hare 3a3c89e0b4 feat: materialize repositories through runtime Git cache 2026-08-26 13:54:25 +09:00
Hare 17c629136a fix: scope max turns to logical runs 2026-08-26 13:47:35 +09:00
Hare 52a5c4141f chore: merge current develop into Ticket source 2026-08-26 12:24:14 +09:00
Hare c9ba27c333 fix: update RepositoryRecord test URI access 2026-08-26 11:50:41 +09:00
Hare 46f6e2c58b fix: complete Repository access management contracts 2026-08-26 11:38:23 +09:00
Hare d1f47e5a22 fix: align Repository access DCDL fields 2026-08-26 11:06:19 +09:00
Hare 65de94bad3 feat: expose compaction as an internal worker 2026-08-26 10:39:43 +09:00
Hare 4e935c6203 test: refresh clean-build fixtures and assertions 2026-08-26 10:16:18 +09:00
Hare eac07cf5a8 fix: provide subworker control service 2026-08-26 10:16:10 +09:00
Hare 260259d461 fix: return ticket queue outcomes to web clients 2026-08-26 10:16:03 +09:00
Hare a9fb092834 feat: add Repository access settings UI 2026-08-26 08:51:23 +09:00
Hare 3a21a68792 feat: add Workspace Repository SSH secret authority 2026-08-26 08:51:15 +09:00
Hare 9d572d18bc fix: accept reserved workers during initial flow resolution 2026-08-26 07:52:04 +09:00
Hare f7852e8034 fix: remove misleading runtime capability projections 2026-08-26 06:19:33 +09:00
Hare 8c075de147 fix: allow remote runtime workdir creation 2026-08-26 05:13:21 +09:00
Hare 864367f4f5 fix: remove repository-local server configuration paths 2026-08-26 03:56:35 +09:00
Hare 33db2ea7f4 fix: unify browser origin configuration 2026-08-26 02:37:14 +09:00
Hare 8396d09891 fix: limit coder ticket comments to handoffs 2026-08-25 13:21:47 +09:00
Hare bf7171924d feat: integrate dependency queue planning 2026-08-25 13:08:09 +09:00
Hare 097c363fbc fix: block internal dependency cycles in projections 2026-08-25 12:54:47 +09:00
Hare 7dd8809e38 fix: fail closed without queue target authority 2026-08-25 12:44:03 +09:00
Hare 1749757036 fix: align queue eligibility with target authority 2026-08-25 12:24:31 +09:00
Hare 5857e6121c fix: preserve dependency queue atomicity 2026-08-25 12:05:43 +09:00
Hare a41147916b fix: confirm queue closures across clients 2026-08-25 11:51:40 +09:00
Hare cabe38db1d fix: align queue projections with dependency closure 2026-08-25 11:19:27 +09:00
Hare f079479160 feat: queue ready dependency closures atomically 2026-08-25 10:53:31 +09:00
Hare 87e160a01a test: track current migration version in dry run 2026-08-25 10:29:48 +09:00
Hare f94d829bf8 feat: integrate hare/develop 2026-08-25 10:29:41 +09:00
Hare 9a05bfa0c3 feat: allow ticket implementation cancellation 2026-08-25 09:52:21 +09:00
Hare c1d46859a3 fix: block cleanup for assigned workers 2026-08-25 09:39:33 +09:00
Hare 87ecbcb113 fix: validate non-worker ticket assignments 2026-08-25 08:09:31 +09:00
Hare 1fb2949561 fix: give reviewer write-scoped command tools 2026-08-25 04:45:17 +09:00
Hare 11be777fc0 fix: verify signed Workspace query targets 2026-08-25 01:12:24 +09:00
Hare ff94161fc0 test: authenticate workdir route fixtures 2026-08-25 00:54:28 +09:00
Hare c2ab9a950f feat: integrate Workspace API authorization 2026-08-25 00:54:13 +09:00
Hare 8e26a0f5a8 chore: merge latest develop into Ticket source
# Conflicts:
#	crates/workspace-server/src/store.rs
2026-08-24 23:24:12 +09:00
Hare d1c15ee295 fix: protect direct auth mutation routes 2026-08-24 14:58:34 +09:00
Hare 00a96234c6 fix: enforce CSRF on server mutations 2026-08-24 14:50:15 +09:00
Hare fc3b663510 fix: authorize Runtime profile archive fetches 2026-08-24 14:39:16 +09:00
Hare e4e045d059 fix: preserve Workdir provider rejection codes 2026-08-24 14:30:34 +09:00
Hare 436feaf33d fix: close remaining Workspace auth gaps 2026-08-24 14:28:44 +09:00
Hare 29f450b962 fix: map missing default Runtime to bad request 2026-08-24 14:16:38 +09:00
Hare db343893c8 fix: secure HTTPS browser session cookies 2026-08-24 14:11:51 +09:00
Hare 554906ec02 fix: treat legacy Runtime config as unconfigured 2026-08-24 14:07:42 +09:00
Hare 18c37f4842 fix: persist Workdir Runtime failure classifications 2026-08-24 13:59:52 +09:00
Hare 83382b824a fix: protect legacy Workspace API routes 2026-08-24 13:56:46 +09:00
Hare 6203316aa1 fix: defer local repository validation to runtimes 2026-08-24 13:46:02 +09:00
Hare d57b4d1d5e fix: align ticket queue projection test 2026-08-24 13:42:39 +09:00
Hare 379ae214fc feat: queue tickets with dependency context 2026-08-24 13:40:33 +09:00
Hare 163a403636 feat: support typed remote repository sources 2026-08-24 13:35:19 +09:00
Hare 53edaadc3a feat: authenticate Workspace API requests 2026-08-24 13:32:55 +09:00
Hare 3c2664c3ce feat: sign Runtime workspace requests 2026-08-24 13:32:49 +09:00
Hare d9048954a5 fix: reject unsupported Workdir runtimes before delegation 2026-08-24 13:24:59 +09:00
Hare 4f84dfd73f feat: resolve default Runtime for Workdir creation 2026-08-24 13:15:18 +09:00
Hare d69c367285 feat: prepare agen crates for publication 2026-08-23 03:41:51 +09:00
Hare aa8dd4f89e test: align workspace server fixtures with current authority 2026-08-23 02:19:36 +09:00
Hare b12e0785e2 fix: migration error 2026-08-23 02:10:03 +09:00
Hare 24291a4545 fix: resolve ticket assignment target integration 2026-08-23 00:23:38 +09:00
Hare 92d073ff36 fix: accept queued lifecycle assignments 2026-08-22 23:58:55 +09:00
Hare bc57ef38c1 fix: guard state change event transitions 2026-08-22 23:48:11 +09:00
Hare ae8a0316d8 fix: guard state field workflow transitions 2026-08-22 23:41:35 +09:00
Hare 2cfbb1caea fix: make role assignment retries idempotent 2026-08-22 23:34:52 +09:00
Hare 703398bd2c fix: reject invalid coder principals safely 2026-08-22 23:27:27 +09:00
Hare 10c80ae514 fix: close ticket start authority gaps 2026-08-22 23:17:44 +09:00
Hare 285763f4ae fix: enforce ticket assignment mutation fences 2026-08-22 23:06:28 +09:00
Hare 8030045602 feat: generalize ticket role assignments 2026-08-22 22:48:56 +09:00
Hare 8c85b93e7d feat: integrate ticket notify delivery warnings 2026-08-22 21:59:21 +09:00
Hare 4017992c7d fix: share ticket query route contracts 2026-08-22 20:20:38 +09:00
Hare 9dc6d8f144 fix: preserve backend product authority boundaries 2026-08-22 20:03:27 +09:00
Hare 779ced82b1 fix: allow workspace clients through ticket REST operations 2026-08-22 19:43:18 +09:00
Hare 30e4985f9a feat: route product commands through backend targets 2026-08-22 19:16:14 +09:00
Hare 1a1e3c286f feat: integrate workspace memory settings authority 2026-08-22 00:34:30 +09:00
Hare 71c906e04d fix: reject legacy workspace worker restore 2026-08-22 00:28:33 +09:00
Hare e6bfb27fa9 fix: make workdir command completion stateful 2026-08-22 00:20:45 +09:00
Hare 2a3ece0364 fix: validate workspace memory settings authority 2026-08-21 22:16:41 +09:00
Hare 61d174b174 fix: enforce workspace memory settings contract 2026-08-21 22:05:53 +09:00
Hare c4814115de feat: bind memory language to workspace settings 2026-08-21 21:50:12 +09:00
Hare f84377b2fe feat: integrate ticket notify delivery warnings 2026-08-21 20:32:26 +09:00
Hare 19f506f8bc fix: restore reviewer ticket and merge request reads 2026-08-21 20:23:26 +09:00
Hare 0a9d09b958 test: cover complete notification delivery failure 2026-08-21 19:19:26 +09:00
Hare 83c6d290b4 fix: complete notification warning context 2026-08-21 19:11:16 +09:00
Hare 831db34acf fix: surface failed Ticket notification delivery 2026-08-21 19:05:53 +09:00
Hare 4a276b0af0 fix: scope console and workspace shell widths 2026-08-21 18:34:40 +09:00
Hare c2b82a2591 fix: contain ticket operation rail layout 2026-08-21 18:30:38 +09:00
Hare 99daaf31b6 fix: widen workspace shell to 920px 2026-08-21 18:08:53 +09:00
Hare 94e51ea6d1 fix: clean up console task summary 2026-08-21 18:06:55 +09:00
Hare f2d2ab0102 fix: center workspace shell at 720px 2026-08-21 18:04:48 +09:00
Hare 6f42f23d2b fix: enlarge overview activity summaries 2026-08-21 17:40:33 +09:00
Hare 8f54fa2a00 fix: remove console composer overhang 2026-08-21 17:37:37 +09:00
Hare c110965911 fix: remove redundant console chrome 2026-08-21 17:26:47 +09:00
Hare 2a48dfc41a fix: set bash output to compact terminal sizing 2026-08-21 15:22:52 +09:00
Hare 0451142d41 fix: align bash sizing with xterm defaults 2026-08-21 15:18:32 +09:00
Hare 65f18b0cdb fix: match bash output to terminal cell sizing 2026-08-21 15:11:15 +09:00
Hare ea31c7ca81 fix: tighten bash terminal line spacing 2026-08-21 13:46:52 +09:00
Hare dd47dba5b0 feat: render bash ANSI colors in web console 2026-08-21 13:36:03 +09:00
Hare cbedc76d06 fix: simplify live bash output presentation 2026-08-21 13:12:50 +09:00
Hare a4aa1a1848 fix: preserve command telemetry through workdir delegation 2026-08-21 13:01:44 +09:00
Hare ee8ee360ef fix: share workspace runtime worker contracts 2026-08-21 12:35:58 +09:00
Hare 72cae33ea6 chore: merge develop into work/companion 2026-08-21 11:38:47 +09:00
Hare 0cd5ca11cc feat: expose merge requests as workspace resources 2026-08-21 11:31:45 +09:00
Hare ccb9d03865 feat: add workspace switcher menu 2026-08-21 11:30:23 +09:00
Hare 41cd2d044a fix: verify resource key schema drift 2026-08-21 10:32:25 +09:00
Hare 85d1815dcf feat: adopt Workspace resource keys 2026-08-21 10:20:32 +09:00
Hare 9989aed916 Merge branch 'work/companion' into develop 2026-08-21 07:21:09 +09:00
Hare 3f2ba9df47 fix: migration error 2026-08-21 07:20:48 +09:00
Hare cd9f0f009e chore: merge develop into work/companion
# Conflicts:
#	crates/tui/src/app.rs
2026-08-21 07:18:07 +09:00
Hare b49abce798 fix: locate multiline Console Tasks source 2026-08-21 07:13:46 +09:00
Hare 51b381a701 feat: switch Web SubWorker views 2026-08-21 07:12:37 +09:00
Hare bea121ade0 feat: switch TUI SubWorker views 2026-08-21 07:12:20 +09:00
Hare 18112d29a6 feat: project SubWorker activity in sidebar 2026-08-21 07:12:04 +09:00
Hare 8bedfcda84 fix: remove caller worker revision fence 2026-08-21 07:11:45 +09:00
Hare 402543d617 Merge remote-tracking branch 'origin/develop' into develop 2026-08-21 05:37:57 +09:00
Hare a21ef31ee7 fix: make authentication workspace independent 2026-08-21 05:04:17 +09:00
Hare e8c159247a fix: serve chooser before workspace creation 2026-08-21 04:51:44 +09:00
Hare 5dd9392575 fix: serve workspace chooser at root 2026-08-21 04:45:05 +09:00
Hare da2296bc9c fix: restore workspace routing contracts 2026-08-21 04:36:40 +09:00
Hare 8e4f35eb3f fix: update TUI command snapshot fixture 2026-08-21 04:21:18 +09:00
Hare 9917c09b19 feat: integrate workspace switching 2026-08-21 04:18:32 +09:00
Hare 4445501f6c fix: recover command snapshots after provider lag 2026-08-21 04:09:42 +09:00
Hare 6fe36f3e46 feat: integrate command output streaming 2026-08-21 03:54:47 +09:00
Hare cfb173c570 fix: preserve command stream boundaries and lifecycle evidence 2026-08-21 03:49:45 +09:00
Hare ad729af592 feat: add web workspace chooser and creation 2026-08-20 18:34:28 +09:00
Hare 17abe1c40c feat: add scoped TUI workspace selection 2026-08-20 18:34:18 +09:00
Hare 4b8dc302ee fix: fence workspace bootstrap and resource routing 2026-08-20 17:52:23 +09:00
Hare cf2e74404d feat: dispatch registered workspaces independently 2026-08-20 17:28:14 +09:00
Hare a0e161c653 feat: integrate workspace-scoped resource references 2026-08-20 16:10:42 +09:00
Hare 614157424f fix: remove cargo check warnings 2026-08-20 15:16:29 +09:00
Hare f5f80fcd48 feat: add web console modes and run status 2026-08-20 15:02:24 +09:00
Hare 7dd4539f50 feat: add compact TUI activity overview 2026-08-20 15:02:03 +09:00
Hare e66876249e fix: remove redundant system reminder wrappers 2026-08-20 15:01:43 +09:00
Hare d39eb43419 fix: track retained assignment parents 2026-08-20 11:38:13 +09:00
Hare 674b897321 fix: validate assignment runtime snapshots 2026-08-20 11:28:41 +09:00
Hare d7e54ed181 fix: preserve retained assignment history 2026-08-20 11:21:31 +09:00
Hare 3e833b5295 fix: finalize stopped subworker sessions 2026-08-20 11:19:02 +09:00
Hare 9194f0a1ba test: complete ticket reference fixture 2026-08-20 11:19:02 +09:00
Hare 6945b7b3c3 fix: bind assignment events to tickets 2026-08-20 11:14:33 +09:00
Hare 560226dea2 fix: enforce assignment workspace references 2026-08-20 11:06:31 +09:00
Hare 4583b512b3 docs: add workspace schema migration runbook 2026-08-20 10:43:05 +09:00
Hare 223a6ed011 feat: enforce workspace-scoped resource references 2026-08-20 10:42:57 +09:00
Hare a82234a75e docs: report subworker feature installation failure 2026-08-20 10:33:12 +09:00
Hare 92594488da feat: stream workdir command output to consoles 2026-08-20 10:32:58 +09:00
Hare 2315c69f0a fix(worker-runtime): complete embedded store identity migration 2026-08-20 08:44:09 +09:00
Hare 9d003a5c98 fix: exclude stale worker sockets from migration 2026-08-20 07:51:34 +09:00
Hare 53ec914a52 docs: report subworker control dependency failure 2026-08-20 07:21:03 +09:00
Hare d052cedc7d fix: make worker identity migrations dry-runnable 2026-08-20 07:21:03 +09:00
Hare de72afd9a1 fix: clear stale internal worker in-flight state 2026-08-20 06:18:21 +09:00
Hare 80ffff642f fix: resolve ticket keys for nested merge request routes 2026-08-20 02:39:52 +09:00
Hare 97960d4e3f fix: resolve human keys at resource boundaries 2026-08-20 02:30:09 +09:00
Hare a96038d79f feat: add canonical human-key resource routes 2026-08-20 02:17:21 +09:00
Hare 1ca36d6b66 feat: add workspace resource human keys 2026-08-20 02:17:00 +09:00
Hare bb8eda379f fix: migrate all persisted Workspace Worker references 2026-08-20 00:49:21 +09:00
Hare 2858e8ceba fix: align Workdir occupancy UI with stable worker IDs 2026-08-19 23:15:34 +09:00
Hare e35b5797a3 feat: promote workers to Workspace-owned UUIDv7 identities 2026-08-19 23:04:02 +09:00
Hare 25baeedc03 Merge branch 'work/companion' into develop 2026-08-19 21:22:04 +09:00
Hare 17d81e29cc chore: merge authoritative develop into ticket source 2026-08-19 12:05:54 +09:00
Hare 08bce5b630 chore: merge current develop into ticket source 2026-08-19 11:54:35 +09:00
Hare 84f1b229ba fix: validate remote workdir grants before leasing 2026-08-19 11:42:49 +09:00
Hare 856ea7119a fix: clarify orchestrator branch integration 2026-08-19 11:24:55 +09:00
Hare 5f2798458e fix: align delegated paths with provider resolution 2026-08-19 11:22:56 +09:00
Hare af3decce51 fix: preserve nested workdir delegation chains 2026-08-19 11:04:40 +09:00
Hare 1cb6cd4e98 fix: enforce delegated workdir scope after resolution 2026-08-19 10:43:37 +09:00
Hare 21bd089a23 feat: delegate subworker access through workdir sessions 2026-08-19 09:45:56 +09:00
Hare 88f463e633 feat: add scoped workdir delegation sessions 2026-08-19 09:45:49 +09:00
Hare ce62e09919 feat: stream internal subworker output through parent 2026-08-19 09:24:14 +09:00
Hare fe74d7c4b8 chore: update ticket source from develop 2026-08-19 08:38:13 +09:00
Hare 73902b03b6 fix: preserve live errors across segment rotation 2026-08-19 08:29:36 +09:00
Hare 9aeaa52bdb fix: retain worker run errors in console history 2026-08-19 08:12:55 +09:00
Hare 89ee5e48a5 fix: use active prompt projection route 2026-08-19 07:03:15 +09:00
Hare 5b5396599d fix: restore pending workspace prompt catalog 2026-08-19 06:52:32 +09:00
Hare 1e33b2945c fix: requeue uncommitted notifications 2026-08-19 06:45:45 +09:00
Hare 50b05051bb feat: persist prompt render provenance 2026-08-19 06:40:29 +09:00
Hare 4208b6228e fix: fence prompt projection freshness 2026-08-19 06:22:41 +09:00
Hare bb558bad2b fix: share compiled prompt projection cache 2026-08-19 06:19:31 +09:00
Hare fcc7c49ff1 fix: enforce prompt projection workspace scope 2026-08-19 06:07:24 +09:00
Hare fb5f49d2a2 test: align pending orchestrator restore contract 2026-08-19 06:05:43 +09:00
Hare 82eaa986d8 fix: scope prompt projection cache fills 2026-08-19 06:03:18 +09:00
Hare 17d6789b41 feat: propagate workspace prompt revisions 2026-08-19 06:01:03 +09:00
Hare d24d50cac9 feat: refresh prompts at operation boundaries 2026-08-19 06:00:57 +09:00
Hare 44b3c78761 fix: reject unrecoverable pending worker restore 2026-08-19 05:02:50 +09:00
Hare 08daf782b9 fix: accept queued tickets after coder spawn 2026-08-19 04:55:29 +09:00
Hare d7e35ea9ee fix: keep restore independent of live prompts 2026-08-19 04:41:38 +09:00
Hare 39aa465a51 fix: keep config bundles transport-only 2026-08-19 04:36:38 +09:00
Hare 382b5e57f2 fix: carry prompt projection source identity 2026-08-19 04:30:00 +09:00
Hare 0d011ea0cd fix: fence prompt cache source revisions 2026-08-19 04:25:13 +09:00
Hare b740b2d1e2 fix: remove config bundle restore dependency 2026-08-19 04:23:37 +09:00
Hare c97bde9ee0 fix: isolate workspace profile launch projections 2026-08-18 22:47:34 +09:00
Hare bb742e253d feat: allow companion to manage workdirs 2026-08-18 10:47:31 +09:00
Hare a10507c54f merge: integrate orchestration
# Conflicts:
#	crates/flow/src/builtin.rs
#	crates/manifest/src/profile.rs
#	crates/worker/src/prompt/catalog.rs
#	crates/worker/src/prompt/system.rs
#	resources/flows/coder-review.dcdl
#	resources/prompts/role/coder.md
#	resources/prompts/role/orchestrator.md
#	web/workspace/src/lib/workspace/console/worker-console.ui.test.ts
#	web/workspace/src/lib/workspace/styles/tickets.css
#	web/workspace/src/routes/w/[workspaceId]/tickets/+page.svelte
#	web/workspace/src/routes/w/[workspaceId]/tickets/+page.ts
2026-08-18 09:55:27 +09:00
Hare 6a607ccbed feat: integrate bounded workspace query pagination 2026-08-18 09:03:59 +09:00
Hare 4ac79b3665 fix: publish coder revisions before review 2026-08-18 08:14:58 +09:00
Hare a4fdf9cc45 docs: record test code quality audit 2026-08-18 08:14:58 +09:00
Hare 981c422122 fix: preserve completed merge request replays 2026-08-18 08:14:43 +09:00
Hare ab0f57c00a fix: include incoming Ticket relation filters 2026-08-18 08:14:05 +09:00
Hare b723c64fa1 fix: preserve accepted Ticket query filters 2026-08-18 08:06:21 +09:00
Hare f86ae6d52f feat: connect selector-based merge request flow 2026-08-18 08:06:02 +09:00
Hare 9a548d2b5e fix: require observed merge target completion 2026-08-18 08:05:57 +09:00
Hare 4e738ac5eb fix: query authoritative merge request review events 2026-08-18 07:57:20 +09:00
Hare a6f3e30652 fix: preserve Ticket lane order across pages 2026-08-18 07:50:42 +09:00
Hare 6ca5dfbe11 feat: paginate Ticket board lanes independently 2026-08-18 03:08:39 +09:00
Hare bc835b8503 feat: add bounded Ticket and Objective query pages 2026-08-18 03:08:30 +09:00
Hare 46767daf49 test: remove duplicated resource content assertions 2026-08-18 02:20:32 +09:00
Hare 99170d47ab fix: delegate queued tickets through merge 2026-08-18 02:20:32 +09:00
Hare 796fa2ee85 feat: integrate ticket ready target guards 2026-08-18 01:12:32 +09:00
Hare 200c24bc00 fix: avoid unbounded ticket board queries 2026-08-18 00:46:31 +09:00
Hare 0a2b24bf5e fix: ignore transient vite config bundles 2026-08-17 23:33:12 +09:00
Hare eec2be87ad feat: paginate ticket workflow lanes 2026-08-17 23:33:12 +09:00
Hare 71cd58f868 feat: scope merge request tools by profile flags 2026-08-17 22:53:09 +09:00
Hare 58ed04ac59 Merge branch 'orchestration' into develop 2026-08-17 21:50:21 +09:00
Hare c6a476d65d feat: derive ticket readiness from merge requests 2026-08-17 21:26:42 +09:00
Hare 4c31ea2228 fix: check queue state before local readiness 2026-08-17 14:59:24 +09:00
Hare f9e5fca67d fix: preserve intake and companion ticket workflows 2026-08-17 14:44:59 +09:00
Hare a2cd860199 feat: gate ticket readiness on validated targets 2026-08-17 14:23:47 +09:00
Hare f5fdce2d07 fix: migrate legacy merge request schema marker 2026-08-17 11:54:24 +09:00
Hare 8c60921c99 chore: merge orchestration into develop 2026-08-17 11:02:47 +09:00
Hare 956b453e6a fix: preserve long-running workdir requests 2026-08-17 11:01:33 +09:00
Hare 063f203efe merge: integrate selector-thread merge request authority 2026-08-17 10:34:01 +09:00
Hare 5f44b10ff9 fix: revalidate merge authority in completion transaction 2026-08-17 08:15:13 +09:00
Hare e3dc8ee327 fix: validate merge authority before ref updates 2026-08-17 08:04:33 +09:00
Hare 645458498a fix: fence merge request approval and grant lifecycle 2026-08-17 07:57:23 +09:00
Hare 6c4119e2c6 fix: separate merge request authority from thread projection 2026-08-17 07:46:48 +09:00
Hare 7e9cae5f39 fix: bound merge request thread bodies 2026-08-17 07:44:08 +09:00
Hare 1fe1b7463f fix: align merge requests with selector thread contract 2026-08-17 07:41:03 +09:00
Hare 9e48cae759 feat: expose selector based merge request threads 2026-08-17 07:09:20 +09:00
Hare aeb2727bea feat: replace merge request revision authority 2026-08-17 07:09:10 +09:00
Hare 298c20012a fix: bind relation and event query evidence 2026-08-17 06:57:29 +09:00
Hare 7507412f1c fix: query authoritative Ticket content and relations 2026-08-17 06:46:43 +09:00
Hare 45b7d0764d fix: complete query and workflow alias cutover 2026-08-17 06:35:24 +09:00
Hare 7508d428b0 docs: report MR target discovery blocker 2026-08-17 05:39:18 +09:00
Hare 977c8e7b21 fix: align read projections with query contracts 2026-08-17 05:37:06 +09:00
Hare 4964583868 feat: cut over Ticket and Objective read tools 2026-08-17 05:18:38 +09:00
Hare 14aa1aabea feat: add bounded Ticket and Objective read APIs 2026-08-17 05:18:27 +09:00
Hare 76c427c887 fix: remove redundant merge request diff digest 2026-08-17 04:13:14 +09:00
Hare ff7d874138 fix: rebuild merge request schema authority 2026-08-17 02:51:53 +09:00
Hare e881c8fad8 fix: remove speculative worker control delegation 2026-08-17 02:51:18 +09:00
Hare 06cc6056e5 chore: merge orchestration into develop 2026-08-17 01:09:51 +09:00
Hare c15999b7a6 fix: classify commit messages by change type 2026-08-17 01:09:12 +09:00
Hare 0b954c1ab6 fix: scope merge request foreign key checks 2026-08-17 01:09:05 +09:00
Hare eb0dd67d16 server: recover completed Worker transfers 2026-08-17 00:48:37 +09:00
Hare 97828f8bd5 server: fence Worker control operation identity 2026-08-17 00:38:48 +09:00
Hare 8be2cfd2a3 worker: unify granted Worker control 2026-08-17 00:18:49 +09:00
Hare f41ab0e277 worker: persist known-worker control grants 2026-08-16 23:08:32 +09:00
Hare 46a44b232b merge-request: normalize target observation refs 2026-08-16 21:03:30 +09:00
Hare 2cf4c57813 merge-request: verify guarded target updates 2026-08-16 21:03:30 +09:00
Hare 41534b215a merge-request: harden target update recovery 2026-08-16 21:03:30 +09:00
Hare 5ea2792df7 merge-request: finalize one guarded merge outcome 2026-08-16 21:03:30 +09:00
Hare 1479148f84 merge-request: select one final integration result 2026-08-16 21:03:30 +09:00
Hare 7256d80514 merge-request: preserve migration foreign keys 2026-08-16 21:03:30 +09:00
Hare 8e73d755d3 merge-request: harden latest schema migration 2026-08-16 21:03:30 +09:00
Hare 027f60d262 merge-request: add target and merge result authority 2026-08-16 21:03:30 +09:00
Hare 4f042cae84 tui: scope composer history under client data 2026-08-16 18:24:34 +09:00
Hare 0b8924eda7 merge-request: checkpoint restartable migration 2026-08-15 21:26:02 +09:00
Hare eaaf2f6dcc workdir: display working directory status 2026-08-15 21:26:02 +09:00
Hare f5a0e14991 docs: require root cargo check 2026-08-15 21:26:02 +09:00
Hare 11e4d536c5 ticket: support relation removal 2026-08-15 17:05:47 +09:00
Hare 0fd2486baf Merge ticket 00001M00PZS9R paused Worker Resume 2026-08-15 03:55:20 +09:00
Hare 8294a476d2 runtime: allow safe paused resume on orchestration 2026-08-15 03:49:53 +09:00
Hare 9922b654d7 Merge ticket 00001KZVPX841 workspace Workdir contract 2026-08-15 03:38:24 +09:00
Hare b910efd945 workdir: centralize workspace inventory contract 2026-08-15 03:29:53 +09:00
Hare 20854edca2 Merge ticket 00001KZ9SR97B notification projection 2026-08-15 02:57:46 +09:00
Hare 2a23a5d574 server: minimize ticket notification context 2026-08-15 02:34:28 +09:00
Hare 4cf34375a8 config: commit without preview 2026-08-15 02:06:46 +09:00
Hare 404809ab6e config: format sources before commit 2026-08-15 01:28:25 +09:00
Hare c5c89795e1 config: require schema assertion on main 2026-08-15 00:21:46 +09:00
Hare 99b08e0f0c web: protect config schema wrapper 2026-08-14 23:38:51 +09:00
Hare 83a99541b2 web: repair config source editor 2026-08-14 23:28:14 +09:00
Hare 1ed835fb79 Merge branch 'orchestration' into develop
# Conflicts:
#	web/workspace/deno.json
2026-08-14 14:49:01 +09:00
Hare ace134e2f0 runtime: deliver immutable prompt bundle on initial spawn 2026-08-14 14:24:59 +09:00
Hare ecbd003579 worker: track work as natural task steps 2026-08-14 14:23:58 +09:00
Hare 93e784a3ed prompt: route source assertions through effective catalog 2026-08-14 14:02:01 +09:00
Hare a1f6a6bad5 prompt: close whitespace-control include validation gap 2026-08-14 13:58:11 +09:00
Hare a6f92104fa server: project Workspace prompts into Worker config bundles 2026-08-14 13:45:14 +09:00
Hare 0ad7d6d210 worker: replace filesystem prompts with effective DCDL catalog 2026-08-14 13:45:03 +09:00
Hare 48ff977d06 config-source: validate static template catalog projections 2026-08-14 13:44:51 +09:00
Hare 93eea24420 web: show Worker tasks in Console 2026-08-14 12:46:59 +09:00
Hare e582babae3 runtime: avoid duplicate Notify observations 2026-08-14 12:46:49 +09:00
Hare 46ffde19a6 config-source: bind import cache keys to content 2026-08-14 12:06:10 +09:00
Hare 6446eb1302 server: project Skills from active virtual config 2026-08-14 11:55:33 +09:00
Hare ad0f6c68d5 config-source: project Markdown imports as values 2026-08-14 11:55:10 +09:00
Hare 6bcd59fcc6 test: cover Decodal 0.4 parity contracts 2026-08-14 11:00:50 +09:00
Hare 1fad5fc8ed config: migrate workspace evaluation to Decodal 0.4 2026-08-14 10:53:22 +09:00
Hare 5c57e10de9 web: link profiles to shared config editor 2026-08-14 10:03:36 +09:00
Hare c0f4e80320 server: preserve profile config authority at spawn 2026-08-14 09:54:11 +09:00
Hare 4857910410 web: retire profile-specific editor 2026-08-14 09:19:13 +09:00
Hare c2510495ef server: derive profiles from virtual config 2026-08-14 09:19:04 +09:00
Hare f8baa1edb7 server: compose Workspace config schemas 2026-08-14 05:38:21 +09:00
Hare 7a8bd7717e server: require main config entrypoint 2026-08-14 04:36:26 +09:00
Hare c98048b97b Merge branch 'work/00001KY8KRJKK-virtual-config-tree' into develop 2026-08-14 03:20:59 +09:00
Hare 38dad4e865 server: authorize orchestrator MR completion 2026-08-14 03:14:08 +09:00
Hare d039359386 web: refine workspace sidebar navigation 2026-08-14 02:20:50 +09:00
Hare 8c68129d69 web: reapply original changes after config conflict 2026-08-14 01:48:36 +09:00
Hare 348ac51011 config: add revision reads and conflict recovery 2026-08-14 01:41:23 +09:00
Hare 544abbbf56 config: enforce browser preflight and generated DTOs 2026-08-14 01:27:29 +09:00
Hare fa9bd8207f config: complete virtual tree editor contract 2026-08-14 01:07:14 +09:00
Hare 0a7e67e373 config: validate every Decodal source before commit 2026-08-14 00:45:20 +09:00
Hare 9cd1c1c448 runtime: update Decodal import loader test 2026-08-14 00:40:39 +09:00
Hare dfea679f0b web: update Decodal CodeMirror baseline 2026-08-14 00:36:39 +09:00
Hare 0689116e4d server: update schema migration expectations 2026-08-14 00:33:00 +09:00
Hare 02a09ae2d7 test: verify browser WASM config parity 2026-08-14 00:31:22 +09:00
Hare f9d328f2db worker: add typed feature services for coder spawn 2026-08-14 00:02:13 +09:00
Hare 8c6dcb9483 web: edit virtual config sources with WASM core 2026-08-13 22:34:56 +09:00
Hare c8b57a6a5d server: persist evaluated virtual config trees 2026-08-13 22:26:11 +09:00
Hare 7aa4d3067e config: add canonical Decodal source core 2026-08-13 22:13:33 +09:00
Hare e47eca53a2 Merge branch 'orchestration' into develop 2026-08-13 17:50:33 +09:00
Hare 8d061a8734 fix: guard orchestrator Coder assignment spawn 2026-08-13 17:50:27 +09:00
Hare 9c8ab6441c Merge branch 'work/00001KZVN5J0S-ticket-list-bulk-projection' into orchestration 2026-08-13 05:23:19 +09:00
Hare dfede080d2 ticket: deduplicate bulk relation blockers 2026-08-13 04:56:52 +09:00
Hare cf32d871de server: bulk project ticket list 2026-08-13 04:21:10 +09:00
Hare fe373b5656 runtime: remove embedded worker socket transport 2026-08-13 03:25:57 +09:00
Hare a44a4bc4f8 docs: record ticket audit and restart failures 2026-08-13 02:01:23 +09:00
Hare cb683beef8 dev: gate dogfood restarts with isolated smoke 2026-08-13 02:01:17 +09:00
Hare 1d4ffa875a runtime: restore persisted workers without adapter panic 2026-08-13 02:01:13 +09:00
Hare 297a7ddd9d merge: integrate orchestration merge request lifecycle
# Conflicts:
#	resources/flows/coder-review.dcdl
2026-08-12 18:30:08 +09:00
Hare be698c2239 Merge branch 'work/00001KZPQW4GJ-worker-remove-v3' into orchestration-merge-request-domain-final 2026-08-12 17:49:35 +09:00
Hare e80581d139 worker: preserve retry cleanup stage 2026-08-12 17:40:11 +09:00
Hare 4e7eaac7d5 worker: retry attachment cleanup stages 2026-08-12 17:31:29 +09:00
Hare 92f7f3fca3 Merge branch 'work/00001KZPQW4GJ-worker-remove-v3' into orchestration-merge-request-domain 2026-08-12 17:18:04 +09:00
Hare 1e10cdecc8 docs: report stale Flow verifier capture 2026-08-12 17:16:57 +09:00
Hare ce6b8f65cc Merge branch 'work/00001KZPQW4GJ-worker-remove-v3' into orchestration-merge-request-domain 2026-08-12 17:10:29 +09:00
Hare f60c2d5834 worker: resume failed removal operation 2026-08-12 17:09:39 +09:00
Hare 795f26fb51 Merge branch 'work/00001KZPQW4GJ-worker-remove-v3' into orchestration-merge-request-domain 2026-08-12 17:01:24 +09:00
Hare 8ae930c5fc worker: add guarded WorkerRemove lifecycle 2026-08-12 17:00:32 +09:00
Hare ebe0f93744 Merge branch 'work/00001KZRWSNNE-runtime-worker-source' into orchestration-merge-request-domain 2026-08-12 04:11:22 +09:00
Hare 8cc0aaf8d2 runtime: prove Worker mutation source authority 2026-08-12 04:03:09 +09:00
Hare 86be3a6865 Merge branch 'work/00001KZPQSFQD-worker-retention' into orchestration-merge-request-domain 2026-08-12 00:47:46 +09:00
Hare 5e5ce73fd0 runtime: harden retention reconciliation and retry 2026-08-12 00:46:39 +09:00
Hare 01971807cb Merge branch 'work/00001KZPQSFQD-worker-retention' into orchestration-merge-request-domain 2026-08-11 23:59:19 +09:00
Hare da8313fa1a runtime: add revisioned worker retention authority 2026-08-11 23:57:37 +09:00
Hare 09fd17e38b Merge branch 'work/00001KZPS59RK-worker-aggregate' into orchestration-merge-request-domain 2026-08-11 22:39:30 +09:00
Hare 34f8949e85 runtime: canonicalize Worker aggregates 2026-08-11 22:38:29 +09:00
Hare 33d3d1a43f Merge branch 'work/00001KZNCCD9C-merge-request-domain' into orchestration-merge-request-domain 2026-08-11 21:17:35 +09:00
Hare 1814c35701 feat: add merge request review authority 2026-08-11 20:21:37 +09:00
Hare 6df5fe5b2a flow: require coder work branch commits 2026-08-11 19:39:56 +09:00
Hare c292c01da3 fix: keep active workers visible in sidebar 2026-08-11 15:32:50 +09:00
Hare 133fbbe038 fix: keep reviewer routing owned by coder 2026-08-11 15:02:13 +09:00
Hare 7f0d025312 server: scope repository identity by workspace 2026-08-11 05:44:48 +09:00
Hare 9dccf99c50 ticket: own typed schema migrations 2026-08-11 05:00:40 +09:00
Hare 1d498d13c3 fix: remove secure-context websocket id dependency 2026-08-11 04:58:47 +09:00
Hare e88b0cda18 fix: restore production orchestrator spawn 2026-08-11 04:58:34 +09:00
Hare 8d2b8b690f fix: make image tool results durably prunable 2026-08-11 02:44:37 +09:00
Hare 38b8f26a50 feat: add scoped multimodal image attachments 2026-08-10 21:49:39 +09:00
Hare 64ced7dbad worker: make coder review routing durable 2026-08-10 02:01:08 +09:00
Hare b9dadb6a08 worker: support typed initial submit on spawn 2026-08-09 23:19:29 +09:00
Hare 068ba9afa5 flow: own worker flow state in runtime sessions 2026-08-09 18:04:32 +09:00
Hare c0532fda4e runtime: project granted worker sessions 2026-08-07 16:31:30 +09:00
Hare ff50baec99 worker: split session capture observation features 2026-08-07 16:31:15 +09:00
Hare a9bb806387 subworker: harden internal session lifecycle 2026-08-07 13:36:44 +09:00
Hare 8fe0525295 worker: exercise project reviewer internal spawn 2026-08-07 02:20:52 +09:00
Hare e6703ed18a worker: preserve prompt overrides in internal children 2026-08-07 02:15:00 +09:00
Hare ee75272917 worker: materialize legacy child cleanup only when needed 2026-08-07 02:05:43 +09:00
Hare 620ecbafcb worker: cover legacy sub-worker restore cleanup 2026-08-07 02:04:42 +09:00
Hare cf394403a6 worker: retire process sub-worker registry authority 2026-08-07 02:03:09 +09:00
Hare 8f0d7fa3c0 worker: revoke parent writes before child run 2026-08-07 01:54:31 +09:00
Hare cc7266c801 worker: cover parent teardown scope reclaim 2026-08-07 01:52:07 +09:00
Hare e0ac732769 worker: verify internal sub-worker scope reclaim 2026-08-07 01:48:25 +09:00
Hare c79db24016 worker: run sub-workers as internal sessions 2026-08-07 01:44:04 +09:00
Hare 485918ebe3 worker: route sub-worker tools through internal sessions 2026-08-06 23:15:30 +09:00
Hare d9f399b97b worker: add internal worker session actor 2026-08-06 22:57:42 +09:00
Hare b8a5c60ff9 server: diagnose and fix worker spawn failures 2026-08-06 22:39:24 +09:00
Hare 2d4b93dde7 worker: cover cancelled memory extraction caller 2026-08-06 22:02:57 +09:00
Hare bd893f271a worker: treat rolled-back extraction as cancelled 2026-08-06 09:49:48 +09:00
Hare 2c8e617b2a worker: run internal extraction through Worker 2026-08-06 08:54:40 +09:00
Hare 8207e560e9 worker: compensate spawn finalize failures 2026-08-06 07:45:18 +09:00
Hare 22b6f4e71d worker: allow companion to manage workers 2026-08-05 23:20:38 +09:00
Hare 5c5921fcd2 docs: report LAN websocket UUID failure 2026-08-05 21:31:46 +09:00
Hare a2781f57e9 worker: structure runtime worker identities 2026-08-05 21:26:55 +09:00
Hare fd391ef705 fix: recover partial session log writes 2026-08-05 18:15:49 +09:00
Hare 36df79e561 runtime: allow cross-runtime workdir sessions 2026-08-05 16:02:16 +09:00
Hare 636dc14616 web: refresh worker sidebar snapshots 2026-08-05 05:46:06 +09:00
Hare fb115fbb7e worker: unify workdir attachments 2026-08-05 05:30:58 +09:00
Hare ba009c0a20 worker: separate subworkers from workspace workers 2026-08-05 03:36:59 +09:00
Hare 50726e4cf3 web: show system items in worker console 2026-08-05 03:36:50 +09:00
Hare f98a123e40 runtime: route notifications through worker inbox 2026-08-05 01:44:57 +09:00
Hare dd2ca54874 web: launch embedded workspace orchestrator 2026-08-04 19:52:54 +09:00
Hare da90ac74b4 worker: add workspace workdir management feature 2026-08-04 19:28:59 +09:00
Hare e6a2da548f fs: extract provider operations into shared crate 2026-08-04 02:03:22 +09:00
Hare e5f0c4168f workdir: add authenticated runtime session transport 2026-08-03 23:37:15 +09:00
Hare 05e8b00bf0 workdir: separate identity from worker session 2026-08-03 18:12:47 +09:00
Hare 0ffaa6c741 workspace: remove worker credential refresh flow 2026-08-03 17:08:23 +09:00
Hare ddadc830ac workdir: add network-capable operation boundary 2026-08-03 16:14:01 +09:00
Hare 0fa36395e7 web: show worker status in sidebar 2026-08-02 04:18:16 +09:00
Hare 606cd5fa31 web: subscribe console on route changes 2026-08-02 03:43:53 +09:00
Hare e5f3c20f64 server: restore weak workspace web access 2026-08-02 03:24:54 +09:00
Hare e530150e43 auth: bootstrap legacy workspace ownership 2026-08-02 00:26:28 +09:00
Hare 0f9f06048a runtime: remove legacy event polling authority 2026-08-01 22:55:58 +09:00
Hare 24f7267d55 web: share workspace multiplexer with console 2026-08-01 21:07:45 +09:00
Hare 11f26a1090 server: multiplex worker protocol subscriptions 2026-08-01 21:07:45 +09:00
Hare 72cef5ed9e web: subscribe sidebar to workspace workers 2026-08-01 20:12:27 +09:00
Hare b977c4cbad server: stream workspace worker subscriptions 2026-08-01 20:12:27 +09:00
Hare 5d4deb258a server: expose subscription runtime catalog 2026-08-01 19:34:50 +09:00
Hare f46c82d171 server: broker embedded runtime subscriptions 2026-08-01 19:23:49 +09:00
Hare 42c69f9f6c protocol: identify workspace subscription runtimes 2026-08-01 19:11:41 +09:00
Hare 9308f93de7 server: broker runtime event subscriptions 2026-08-01 19:04:05 +09:00
Hare ecd5751a67 docs: report spawn worker delegation scope 2026-08-01 18:34:32 +09:00
Hare ec262f0238 runtime: serve selective event subscriptions 2026-08-01 18:34:32 +09:00
Hare 21cd672f64 protocol: define multiplexer subscriptions 2026-08-01 18:34:32 +09:00
Hare 9dfcddf40b docs: report spawned worker launcher mismatch 2026-08-01 17:24:10 +09:00
Hare 81e631e640 auth: enforce workspace worker credentials over ticket REST 2026-08-01 17:23:59 +09:00
Hare 3412f1c0ed merge: integrate ticket assignment notifications
# Conflicts:
#	crates/workspace-server/src/store.rs
2026-07-31 23:59:09 +09:00
Hare 48556e1a2c feat: add workspace breadcrumbs 2026-07-31 23:48:46 +09:00
Hare 06a98fe30c workdir: report current selector and ref 2026-07-31 23:48:46 +09:00
Hare 4cf110b28e runtime: replay Worker creation before workdir conflicts 2026-07-31 23:28:07 +09:00
Hare df34d43a23 server: close Ticket lifecycle crash gaps 2026-07-31 23:12:05 +09:00
Hare e9a6269d9f server: address Ticket orchestration review 2026-07-31 22:15:55 +09:00
Hare 005f6cb498 server: route Ticket mutation notifications 2026-07-31 20:15:11 +09:00
Hare 32d3f8f75e Merge branch 'merge/00001KYRRDVH9-ticket-panel' into develop 2026-07-31 02:54:15 +09:00
Hare cb4b588598 fix: remove companion filter from worker list 2026-07-31 02:53:36 +09:00
Hare 20a0f7a454 merge: integrate ticket panel workflow 2026-07-31 02:22:49 +09:00
Hare e720af38a6 workspace: add ticket panel workflow 2026-07-31 01:12:10 +09:00
Hare 1c2d284578 workspace: add typed Ticket API and targets 2026-07-30 22:13:01 +09:00
761 changed files with 212872 additions and 72691 deletions
+4 -5
View File
@@ -70,13 +70,12 @@ Intake は以下を行う。
利用可能なら、以下の typed Ticket tools を使う。
- `TicketList`: 既存 Ticket の一覧・重複確認。
- `TicketShow`: 関連 Ticket の詳細確認。
- `QueryTicket`: 既存 Ticket の一覧・重複確認。
- `ShowTicket`: 関連 Ticket の詳細確認。
- `TicketCreate`: 合意済み Ticket の作成。
- `TicketComment`: 既存 Ticket refinement / decision / plan の記録。
- `TicketDoctor`: 必要に応じた整合性確認。
Intake は `TicketReview`, `TicketWorkflowState`, `TicketClose` を通常使わない。review / state transition / close は Orchestrator または reviewer / maintainer workflow の責務である。
Intake は `MergeRequest*`, `TicketWorkflowState`, `TicketClose` を通常使わない。review authority は assigned Coder が起動した read-only direct-child Reviewer の immutable Merge Request attempt に属し、completion / merge / close は各guarded workflowの責務である。
Ticket tools が利用できない環境では、勝手に file write で代替しない。ユーザーまたは Orchestrator に「Ticket tools がないため materialize できない」と報告し、必要なら `yoi ticket` を使える人間/親 workflow に戻す。
@@ -96,7 +95,7 @@ Ticket tools が利用できない環境では、勝手に file write で代替
### 2. 既存 Ticket を確認する
`TicketList` / `TicketShow` で duplicate / related work を探す。
`QueryTicket` / `ShowTicket` で duplicate / related work を探す。
確認観点:
+12 -11
View File
@@ -36,8 +36,8 @@ TicketCreate / TicketComment
Orchestrator は以下を行う。
- Ticket を `TicketShow` で読む。
- 必要に応じて関連 Ticket を `TicketList` / `TicketShow` で確認する。
- Ticket を `ShowTicket` で読む。
- 必要に応じて関連 Ticket を `QueryTicket` / `ShowTicket` で確認する。
- Ticket body / thread / artifacts / resolution / review / implementation report を読む。
- Ticket が Objective context と結びついている場合は、Objective を medium-term goal / motivation / strategy / success criteria / decision context として読む。ただし Objective context は判断背景であり、Ticket body/thread/artifacts や explicit Ticket relations / OrchestrationPlan records を読む代替ではない。
- repository 状態、関連 docs/code、既存 worktree、visible Pods を必要に応じて明示的に確認する。
@@ -49,6 +49,7 @@ Orchestrator は以下を行う。
- 既存 umbrella/progress-container Ticket が concrete follow-up Ticket / Objective context で置き換え済みなら、superseded/decomposed として退役・close する routing を検討する。
- implementation-ready の場合は `multi-agent-workflow` に渡す `IntentPacket` を作る。
- implementation-ready かつ Ticket が `queued` の場合は、worktree 作成 / implementation Pod `SpawnPod` / coder routing などの side effect の前に、既存の typed Ticket backend/tool path で `queued -> inprogress` を記録する。
- 人間による `ready -> queued` は、記録済み Ticket scopeについて、実装、current MRのguarded merge、completion記録、Ticket closeまでをWorkspace Orchestratorへ委任するdurable gateである。Orchestratorは`queued -> inprogress`を受理した後、current-ref approval、repository evidence、blocking relations、merge CASを確認して完了まで進め、別のmerge確認を待たない。Ticketがseparate approval gateを明記する場合、またはqueued scope外の新しい判断が必要な場合だけ停止する。
- 明示的な queue review 中に、他にも queued Ticket が見え、capacity が空いている場合は、各 Ticket について relation / orchestration-plan / dirty state / visible Pods / worktree / conflict risk を確認し、独立して受理できるものを同じ routing pass で追加の `queued -> inprogress` 候補にする。
- queued Ticket を capacity が見える状態で idle のまま残す場合は、dependency / conflict / capacity / missing planning decision / dirty workspace / reviewer-coder bottleneck / migration boundary / human gate のいずれかに絞った bounded reason を Ticket thread または `TicketOrchestrationPlanRecord` に残す。
- `ready` または `queued` に concrete missing decision / information がある場合だけ、typed state-change/routing event 付きで `planning` に戻す。その event/comment には missing item、checked context、implementation latitude では足りない理由、次の planning question/action を含める。
@@ -61,7 +62,7 @@ Orchestrator は以下を行う。
- 人間/上位 Orchestrator の許可または明示的な routing acceptance なしに coder / reviewer Pod や read-only investigation helper Pod を起動しない。
- unqueued Ticket を capacity 埋めのために開始しない。parallel start の候補は、個別に `queued` であり、人間が routing を許可済みの Ticket に限る。
- 設計境界の未決定を勝手に implementation-ready として固定しない。
- merge / close / cleanup 権限を持たない場面で勝手に完了処理しない
- Ticketが`ready -> queued`されておらず完了権限を委任されていない場合、またはTicketがseparate approval gateを明記する場合に、勝手にmerge / close / cleanupしない。queued delegationとguarded completion evidenceが揃っている場合は、追加のhuman gateを作らず完了まで進める
- Ticket tools があるからといって arbitrary filesystem write を行わない。
- broad multi-Ticket effort のために新しい umbrella/progress-container Ticket を作らない。
- parent/child、sub-ticket、umbrella、part-of、contains などの hierarchy/container relation を split/refinement の代替として扱わない。
@@ -73,21 +74,21 @@ Orchestrator は以下を行う。
利用可能なら、以下を使う。
- `TicketList`: routing 候補関連 Ticket の確認
- `TicketShow`: 対象 Ticket の body / thread / artifacts / resolution / typed relation metadata と derived inverse/blocker view を確認。
- `QueryTicket`: routing 候補関連 Ticket、project-level forward relation (`depends_on` / `blocks` / `related` / `supersedes` / `duplicate_of`) と derived blocker summary を bounded filter/projection で確認する。`depends_on` と incoming unresolved `blocks` は queue/acceptance blocker であり、`related` は blocker ではない。`supersedes` / `duplicate_of` は visible diagnostic として扱い、自動的な lifecycle 変更や scheduler 判断にはしない
- `ShowTicket`: 対象 Ticket の body / thread / artifacts / resolution / typed relation metadata、linked Objective、assignment、implementation/review evidence を確認する
- `TicketComment`: routing decision / intent packet / blocked reason / next question の記録。
- `TicketWorkflowState`: `queued -> inprogress` acceptance、`inprogress -> done`、または concrete missing decision/information reason を伴う `ready|queued -> planning` に使う。
- `TicketRelationQuery`: project-level の forward relation (`depends_on` / `blocks` / `related` / `supersedes` / `duplicate_of`) を読む。`depends_on` と incoming unresolved `blocks` queue/acceptance blocker であり、`related` は blocker ではない。`supersedes` / `duplicate_of` は visible diagnostic として扱い、自動的な lifecycle 変更や scheduler 判断にはしない
- `TicketDependencyCheck`: queue/acceptance 直前の typed dependency readiness guard に使う
- `TicketRelationRecord` / `TicketRelationRemove`: ユーザー合意済みの durable project relation を明示的に更新する場合だけ使う。
- `TicketOrchestrationPlanQuery`: 対象 Ticket や関連 Ticket の ordering / blocker / conflict / waiting-capacity / accepted-plan 記録を読む。queued acceptance 前に必ず確認する。
- `TicketOrchestrationPlanRecord`: Orchestrator が routing 中に project-relevant な ordering / dependency / conflict / capacity/waiting / accepted-plan decision を残す。これは queue reorder、自動起動、state 変更ではない。
- `TicketClose`: 完了権限と resolution が揃っている場合だけ使う。
- `TicketDoctor`: routing 前後の整合性確認。
`TicketCreate` は通常 Intake の責務だが、routing 中に follow-up Ticket が必要だと判断した場合は、ユーザー/上位 Orchestrator の合意後にだけ使う。
## Queued acceptance contract
- `queued -> inprogress` acceptance の直前に `TicketShow` / `TicketRelationQuery` の relation blockers を再確認する。unresolved `depends_on` や incoming unresolved `blocks` が残る場合は implementation side effect を始めず、理由を thread に残して `planning` へ戻すか blocked diagnostic として停止する。
- `queued -> inprogress` acceptance の直前に `ShowTicket` / `QueryTicket` の relation blocker projection を再確認する。unresolved `depends_on` や incoming unresolved `blocks` が残る場合は implementation side effect を始めず、理由を thread に残して `planning` へ戻すか blocked diagnostic として停止する。
- Relation metadata は project-level constraint であり、OrchestrationPlan は runtime ordering/capacity decision である。relation を OrchestrationPlan で代替しないし、OrchestrationPlan を durable dependency authority として扱わない。
`state = queued` は、Ticket が routing 対象として人間により Orchestrator へ渡された状態である。Orchestrator は queued notification を受けたら、Ticket、workspace state、対象 Ticket の `TicketOrchestrationPlanQuery` 記録、risk domain に応じた bounded project context を読んで、次のどちらかを行う。
@@ -100,7 +101,7 @@ Orchestrator は以下を行う。
Parallel acceptance pass:
- 明示的な queue review 中に複数の queued Ticket が見える場合、Orchestrator は最初の1件の完了待ちを default にしない。各 Ticket について Ticket body/thread/artifacts、TicketRelationQuery、TicketOrchestrationPlanQuery、workspace/worktree dirty state、visible Pods、既存 branches、conflict/dependency notes を確認する。
- 明示的な queue review 中に複数の queued Ticket が見える場合、Orchestrator は最初の1件の完了待ちを default にしない。各 Ticket について Ticket body/thread/artifacts、QueryTicket の relation/blocker projection、TicketOrchestrationPlanQuery、workspace/worktree dirty state、visible Pods、既存 branches、conflict/dependency notes を確認する。
- 追加で開始してよいのは、blocking relation/dependency がなく、`do_not_parallelize` または applicable conflict record がなく、source/write surfaces が disjoint または conflict risk が小さく機械的で、coder/reviewer follow-up capacity があり、acceptance basis となる Ticket thread/plan/workspace records を side effect 前に記録・commit でき、別 worktree/branch/scope を切れる Ticket だけである。
- capacity が見えるのに queued Ticket を idle にする場合は、dependency / conflict / capacity / missing planning decision / dirty workspace / reviewer-coder bottleneck / migration boundary / human gate のいずれかの bounded reason を記録する。
- この pass は scheduler、background runner、resource graph solver、automatic queue drain loop ではない。unqueued Ticket を開始せず、各 Ticket の `queued -> inprogress` acceptance を個別に記録する。
@@ -269,8 +270,8 @@ Action:
### 1. 状態確認
- `git state --short --branch`
- `TicketShow <target>`
- 関連 Ticket の `TicketList` / `TicketShow`
- `ShowTicket <target>`
- 関連 Ticket の `QueryTicket` / `ShowTicket`
- 必要に応じて docs/code/workflow/history
- 必要に応じて visible Pods / worktrees / branches
+24 -30
View File
@@ -1,38 +1,19 @@
すでにシステムのドッグフーディングに成功しているが、一旦安定した旧バージョンで、ブラウザ/TUI Client/backend/runtimeの分離とチームスペースとしてのworkspaceを作るObjectiveを進めている。
すでにシステムのドッグフーディングに成功しており、ブラウザ/TUI Client/backend/runtimeの分離とチームスペースとしてのworkspaceの実装を進めている。
## このシステムに置ける設計要旨
- プロンプトはすべて resources/promptsに集約している。管理効率の向上と同時に、ユーザーがオーバーライドする形式でもある。
- プロンプトはすべて`resources/prompts`に集約している。管理効率の向上のためであると同時に、ユーザーがオーバーライドする形式でもある。
- 変更量を最小にするために設計を歪めたり、設計問題に対して不必要な後方互換性を作らない。長期的なメンテナンスと型安全性を追求すること。
### LLM コンテキストの加工原則
LLM に投げる context への割り込みは、大きく2種類に分かれる。**前者は許されるが、後者は禁止**
LLM に投げる context はappend-onlyが基本であり、またその永続化形式からAPIコールの形式を純粋に再現可能である必要が有る
Workerの状態から純粋に再現可能で、且つ揮発性の無い操作であることが望ましい。(pruning、tool result の content 切り詰め、prompt cache anchor の付与等)
原則として、コンテキストは積み重ねるものであり、一時的にメッセージを差し込むことや、過去のメッセージを改ざんすることはKVキャッシュのヒット率を下げる。
一時的にメッセージを差し込む等の、揮発性の有るコンテキストの改変や、過去のメッセージを改ざんすることは基本的に禁止されている
これを行うと、 LLM はそのコンテキストに基づいて生成を行う一方、次以降のターンでhistoryに残らないため、「自分がなぜその発言/tool call をしたか」の根拠が消えるうえ、prompt cache のヒット率も低下させることになる。
**禁止**: ターンを跨ぐことができない情報に基づいて、history に記録せずに context だけにコンテンツを差し込むこと。これをやると LLM はそれに反応して生成を行う一方、次以降のターンでhistoryに残らないため、「自分がなぜその発言/tool call をしたか」の根拠が消えるうえ、prompt cache のヒット率も低下させることになる。
新しい input を context に乗せたいなら、必ず先に `worker.history` に append して commit すること。`history.json` への永続化はそこから自動的についてくる。Notify / WorkerEvent / `<system-reminder>` 系はこの原則で扱う。
また、キャッシュを破壊するタイミングは正確にコントロールされる必要があり、キャッシュ破壊とトークン消費のトレードオフに基づいて慎重に設計されるべきである。
---
## 実際のセッションを読んでデバッグする
`~/.yoi/sessions`にすべてのセッションがある。jsonlなので、いい感じにBashで読むこと。
---
## Git操作
明示的に指示されない限り、読み取り以外の操作は控えること。
基本はworktree上の一時的なブランチでコミットを重ね、メインブランチに取り込む運用をしている。
Orchestrator の cwd が orchestration 用ブランチ/worktree の場合、通常作業では親ブランチの dirty state を気にしない。
コミットメッセージは適当に`<prefix>: *簡潔な1行*`で書いている。
外部の参考プロジェクトは必要に応じてローカルの外部 checkout からReadすること。
過去のコンテキストの圧縮は、キャッシュ破壊とトークン消費のトレードオフであり、必要であれば行っている。
しかし、キャッシュを破壊するタイミングと頻度は正確にコントロールされる必要があり、実際のセッションデータの解析に基づいて慎重に設計されるべきである。
---
@@ -43,17 +24,26 @@ Orchestrator の cwd が orchestration 用ブランチ/worktree の場合、通
```sh
cargo test -p <crate> --lib <test-or-module-filter>
cargo test -p <crate> --test <test-target> <test-filter>
cargo check -p <crate> -p <dependent-crate>
```
完了前には変更内容に応じて、対象crate全体のtest、影響するfeature構成、依存crateのcheckを追加する。通常の差分検証には以下を使う。
完了前には、workspace rootで必ず`cargo check`を実行する。rootの`cargo check`
`default-members`に含まれるTUIやServerを含む通常のcompile closureを確認するため、公開型の
変更ごとにLLMがreverse dependencyを推測して`-p`を列挙する運用にはしない。
```sh
cargo check
cargo test -p <changed-crate>
cargo fmt --all -- --check
git diff --check HEAD
```
workspace全体、E2E、Nix/Docker buildなどの重い検証は、変更した境界を狭い検証では証明できない場合や明示的に要求された場合に選ぶ。実行した検証が何を証明するのかを意識し、広い検証を形式的に回すだけにしない。
変更したcrate全体のtestに加え、影響するfeature構成やtest-only targetがある場合は、その検証を
追加する。`cargo check`はtestを実行せず、通常有効でないfeatureまでは確認しないため、semanticな
証明とfeature境界の検証はtargeted test/checkで補う。
workspace全体のtest、`--all-targets`、E2E、Nix/Docker buildなどの重い検証は、変更した境界を
通常のroot checkと狭い検証では証明できない場合や、明示的に要求された場合に選ぶ。実行した検証が
何を証明するのかを意識し、広い検証を形式的に回すだけにしない。
---
@@ -61,7 +51,6 @@ workspace全体、E2E、Nix/Docker buildなどの重い検証は、変更した
Yoi Worker で作業する場合、Ticket の authority・ライフサイクル・操作方法は Yoi
system instructions と、その Worker に提供された typed Ticket tools に従うこと。
backend や CLI の具体的な手順はこのリポジトリの `AGENTS.md` では重複して定義しない。
Codex など typed Ticket tools が提供されていないクライアントでは、`yoi ticket`
CLI や保存先の直接操作で Ticket tools を代替しないこと。Ticket の作成・更新は
@@ -70,3 +59,8 @@ Yoi Worker に委ねる。
---
YoiでYoiを開発している際、AI自身のフィードバックを元に改善を回すために `docs/report/`ディレクトリに感じた障壁や改善案等を書き残す形にした。 明確に力不足な点/ツールの問題があった場合や、ユーザーからの指示があった際に作ること。
---
絶対に自身が動作しているプロセスを止めないこと。
マージ後のドッグフーディング環境の更新は必ずユーザーの操作で行う。
Generated
+825 -695
View File
File diff suppressed because it is too large Load Diff
+41 -8
View File
@@ -2,9 +2,10 @@
resolver = "2"
members = [
"crates/client",
"crates/llm-engine",
"crates/llm-engine-macros",
"crates/agen",
"crates/agen-macros",
"crates/session-store",
"crates/standalone",
"crates/secrets",
"crates/manifest",
"crates/mcp",
@@ -17,18 +18,26 @@ members = [
"crates/session-analytics",
"crates/lint-common",
"crates/tools",
"crates/fs-operation",
"crates/flow",
"crates/config-source",
"crates/config-source-wasm",
"crates/workdir",
"crates/tui",
"crates/memory",
"crates/ticket",
"crates/merge-request",
"crates/project-record",
"crates/workspace-api",
"crates/workspace-server",
"tests/e2e",
]
default-members = [
"crates/client",
"crates/llm-engine",
"crates/llm-engine-macros",
"crates/agen",
"crates/agen-macros",
"crates/session-store",
"crates/standalone",
"crates/secrets",
"crates/manifest",
"crates/mcp",
@@ -41,10 +50,17 @@ default-members = [
"crates/session-analytics",
"crates/lint-common",
"crates/tools",
"crates/fs-operation",
"crates/flow",
"crates/config-source",
"crates/config-source-wasm",
"crates/workdir",
"crates/tui",
"crates/memory",
"crates/ticket",
"crates/merge-request",
"crates/project-record",
"crates/workspace-api",
"crates/workspace-server",
]
@@ -52,27 +68,39 @@ default-members = [
edition = "2024"
license = "MIT"
[profile.dev]
debug = "line-tables-only"
[profile.dev.package."*"]
debug = false
[workspace.dependencies]
# Internal crates
client = { path = "crates/client" }
llm-engine = { path = "crates/llm-engine", version = "0.2" }
llm-engine-macros = { path = "crates/llm-engine-macros", version = "0.2" }
agen = { path = "crates/agen", version = "0.2" }
agen-macros = { path = "crates/agen-macros", version = "0.2" }
manifest = { path = "crates/manifest" }
mcp = { path = "crates/mcp" }
lint-common = { path = "crates/lint-common" }
memory = { path = "crates/memory" }
merge-request = { path = "crates/merge-request" }
ticket = { path = "crates/ticket" }
project-record = { path = "crates/project-record" }
worker = { path = "crates/worker" }
worker-runtime = { path = "crates/worker-runtime" }
workspace-api = { path = "crates/workspace-api" }
yoi-plugin-pdk = { path = "crates/plugin-pdk" }
yoi = { path = "crates/yoi" }
protocol = { path = "crates/protocol" }
session-metrics = { path = "crates/session-metrics" }
session-analytics = { path = "crates/session-analytics" }
session-store = { path = "crates/session-store" }
standalone = { path = "crates/standalone" }
secrets = { path = "crates/secrets" }
tools = { path = "crates/tools" }
config-source = { path = "crates/config-source" }
fs-operation = { path = "crates/fs-operation" }
workdir = { path = "crates/workdir" }
tui = { path = "crates/tui" }
yoi-workspace-server = { path = "crates/workspace-server" }
@@ -82,7 +110,9 @@ yoi-workspace-server = { path = "crates/workspace-server" }
async-trait = "0.1"
axum = "0.8"
base64 = "0.22.1"
decodal = "0.1.1"
decodal = "0.4.0"
decodal-language-service = "0.4.0"
decodal-language-tools = "0.4.0"
fs4 = "0.13"
futures = "0.3"
libc = "0.2"
@@ -91,9 +121,10 @@ serde = "1.0"
serde_json = "1.0"
serde_yaml = "0.9.34"
tar = "0.4"
rusqlite = { version = "0.37", features = ["bundled"] }
rusqlite = { version = "0.37", features = ["backup", "bundled"] }
ring = "0.17.14"
sha2 = "0.11"
ssh-key = { version = "0.6.7", features = ["ed25519", "encryption"] }
tempfile = "3.27"
thiserror = "2.0"
tokio = "1.52"
@@ -101,6 +132,8 @@ tokio-tungstenite = "0.29"
tower = "0.5"
toml = "1.1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
url = "2.5"
uuid = "1.23"
zeroize = "1"
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
+1 -1
View File
@@ -21,7 +21,7 @@ services:
- "8787"
volumes:
- server-data:/server-data
- ./docker/workspace:/workspace:ro
- /etc/yoi/server.toml:/server-config/server.toml:ro
webui:
image: yoi-webui:latest
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "agen-macros"
description = "Procedural macros for declaring agen tools"
version = "0.2.0"
edition.workspace = true
rust-version = "1.85"
license.workspace = true
readme = "README.md"
repository = "https://gitea.hareworks.net/Hare/yoi"
homepage = "https://gitea.hareworks.net/Hare/yoi"
documentation = "https://docs.rs/agen-macros"
keywords = ["llm", "agent", "tools", "macros"]
categories = ["development-tools::procedural-macro-helpers"]
include = ["src/**", "README.md", "LICENSE"]
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
[package.metadata.docs.rs]
all-features = true
+7
View File
@@ -0,0 +1,7 @@
Copyright 2026 Hare
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+32
View File
@@ -0,0 +1,32 @@
# agen-macros
Procedural macros used by [`agen`](https://crates.io/crates/agen) to declare LLM tools from Rust methods.
Applications should normally depend only on `agen` and import its re-exports:
```rust
use agen::tool_registry;
#[derive(Clone)]
struct Tools;
#[tool_registry]
impl Tools {
/// Returns the supplied text.
#[tool]
async fn echo(
&self,
#[description = "Text to return"] text: String,
) -> Result<String, std::io::Error> {
Ok(text)
}
}
```
`#[tool_registry]` generates the argument schema, a `Tool` implementation, and an `<method>_definition` constructor. It rejects arguments of its own, duplicate `#[tool]` markers, malformed or duplicate `#[description = "..."]` attributes, and non-identifier argument patterns.
Generated code targets the canonical `::agen` path and uses implementation dependencies re-exported by `agen`; consumers do not need direct `serde`, `schemars`, `serde_json`, or `async-trait` dependencies. Renaming the `agen` dependency in `Cargo.toml` is not currently supported.
This companion package is published before the matching `agen` release. Its public contract is the generated API consumed by `agen`, and its minor version compatibility follows the `agen` 0.2 series.
Licensed under the [MIT License](https://gitea.hareworks.net/Hare/yoi/src/branch/develop/LICENSE).
+482
View File
@@ -0,0 +1,482 @@
//! Procedural macros for declaring [`agen`](https://docs.rs/agen) tools.
//!
//! [`tool_registry`] expands methods marked with `#[tool]` into `agen::tool::Tool`
//! implementations and tool definitions. Applications normally use the re-exports from
//! `agen`; this companion crate exists so those macros can be published and versioned
//! independently.
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
Attribute, FnArg, ImplItem, ItemImpl, Lit, Meta, Pat, ReturnType, Type, parse_macro_input,
spanned::Spanned,
};
/// Generates tools for methods marked with `#[tool]` in an `impl` block.
///
/// Method doc comments become the tool description. An argument can use
/// `#[description = "..."]` to supply its JSON Schema description.
///
/// ```ignore
/// #[derive(Clone)]
/// struct MyApp;
///
/// #[agen::tool_registry]
/// impl MyApp {
/// /// Retrieves a user by ID.
/// #[tool]
/// async fn get_user(
/// &self,
/// #[description = "The user ID"] user_id: String,
/// ) -> Result<String, std::io::Error> {
/// todo!()
/// }
/// }
/// ```
///
/// This generates a `ToolGetUser` wrapper, a `GetUserArgs` schema type, and
/// `MyApp::get_user_definition()`.
#[proc_macro_attribute]
pub fn tool_registry(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr = proc_macro2::TokenStream::from(attr);
let impl_block = parse_macro_input!(item as ItemImpl);
expand_tool_registry(attr, impl_block)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn expand_tool_registry(
attr: proc_macro2::TokenStream,
mut impl_block: ItemImpl,
) -> syn::Result<proc_macro2::TokenStream> {
if !attr.is_empty() {
return Err(syn::Error::new(
attr.span(),
"tool_registry does not accept arguments",
));
}
let self_ty = impl_block.self_ty.as_ref().clone();
let mut generated_items = Vec::new();
for item in &mut impl_block.items {
let ImplItem::Fn(method) = item else {
continue;
};
let tool_attrs: Vec<_> = method
.attrs
.iter()
.filter(|attr| attr.path().is_ident("tool"))
.collect();
if tool_attrs.len() > 1 {
return Err(syn::Error::new_spanned(
tool_attrs[1],
"duplicate #[tool] attribute",
));
}
let Some(tool_attr) = tool_attrs.first() else {
continue;
};
if !matches!(tool_attr.meta, Meta::Path(_)) {
return Err(syn::Error::new_spanned(
tool_attr,
"#[tool] does not accept arguments",
));
}
method.attrs.retain(|attr| !attr.path().is_ident("tool"));
generated_items.push(generate_tool_impl(&self_ty, method)?);
for input in &mut method.sig.inputs {
if let FnArg::Typed(pat_type) = input {
pat_type
.attrs
.retain(|attr| !attr.path().is_ident("description"));
}
}
}
Ok(quote! {
#impl_block
#(#generated_items)*
})
}
fn extract_doc_comment(attrs: &[Attribute]) -> String {
let mut lines = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc")
&& let Meta::NameValue(meta) = &attr.meta
&& let syn::Expr::Lit(expr_lit) = &meta.value
&& let Lit::Str(lit_str) = &expr_lit.lit
{
let line = lit_str.value();
let trimmed = line.strip_prefix(' ').unwrap_or(&line);
lines.push(trimmed.to_string());
}
}
lines.join("\n")
}
fn extract_description_attr(attrs: &[Attribute]) -> syn::Result<Option<String>> {
let mut description = None;
for attr in attrs
.iter()
.filter(|attr| attr.path().is_ident("description"))
{
let value = match &attr.meta {
Meta::NameValue(meta) => match &meta.value {
syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
Lit::Str(value) => value.value(),
_ => {
return Err(syn::Error::new_spanned(
attr,
"description must be a string literal",
));
}
},
_ => {
return Err(syn::Error::new_spanned(
attr,
"description must be a string literal",
));
}
},
_ => {
return Err(syn::Error::new_spanned(
attr,
"expected #[description = \"...\"]",
));
}
};
if description.replace(value).is_some() {
return Err(syn::Error::new_spanned(
attr,
"duplicate #[description] attribute",
));
}
}
Ok(description)
}
fn argument_ident(pat: &Pat) -> syn::Result<&syn::Ident> {
match pat {
Pat::Ident(pat_ident) => Ok(&pat_ident.ident),
_ => Err(syn::Error::new_spanned(
pat,
"tool arguments must use simple identifier patterns",
)),
}
}
fn is_tool_execution_context_type(ty: &Type) -> bool {
let Type::Path(path) = ty else {
return false;
};
path.path
.segments
.last()
.is_some_and(|segment| segment.ident == "ToolExecutionContext")
}
fn generate_tool_impl(
self_ty: &Type,
method: &syn::ImplItemFn,
) -> syn::Result<proc_macro2::TokenStream> {
let sig = &method.sig;
let method_name = &sig.ident;
let tool_name = method_name.to_string();
let pascal_name = to_pascal_case(&method_name.to_string());
let tool_struct_name = format_ident!("Tool{}", pascal_name);
let args_struct_name = format_ident!("{}Args", pascal_name);
let definition_name = format_ident!("{}_definition", method_name);
let description = extract_doc_comment(&method.attrs);
let description = if description.is_empty() {
format!("Tool: {}", tool_name)
} else {
description
};
let method_args: Vec<_> = sig
.inputs
.iter()
.filter_map(|arg| match arg {
FnArg::Typed(pat_type) => Some(pat_type),
FnArg::Receiver(_) => None,
})
.collect();
let json_args: Vec<_> = method_args
.iter()
.copied()
.filter(|pat_type| !is_tool_execution_context_type(pat_type.ty.as_ref()))
.collect();
let arg_fields: Vec<_> = json_args
.iter()
.map(|pat_type| {
let field_name = argument_ident(pat_type.pat.as_ref())?;
let ty = &pat_type.ty;
let description = extract_description_attr(&pat_type.attrs)?;
Ok(if let Some(description) = description {
quote! {
#[schemars(description = #description)]
pub #field_name: #ty
}
} else {
quote! {
pub #field_name: #ty
}
})
})
.collect::<syn::Result<_>>()?;
let call_args: Vec<_> = method_args
.iter()
.map(|pat_type| {
if is_tool_execution_context_type(pat_type.ty.as_ref()) {
Ok(quote! { ctx.clone() })
} else {
let ident = argument_ident(pat_type.pat.as_ref())?;
Ok(quote! { args.#ident })
}
})
.collect::<syn::Result<_>>()?;
let method_call = if call_args.is_empty() {
quote! { self.ctx.#method_name() }
} else {
quote! { self.ctx.#method_name(#(#call_args),*) }
};
let awaiter = if sig.asyncness.is_some() {
quote! { .await }
} else {
quote! {}
};
let result_handling = if is_result_type(&sig.output) {
quote! {
match result {
Ok(val) => Ok(format!("{:?}", val).into()),
Err(error) => Err(::agen::tool::ToolError::ExecutionFailed(format!("{}", error))),
}
}
} else {
quote! {
Ok(format!("{:?}", result).into())
}
};
let args_struct_def = quote! {
#[derive(
::agen::__private::serde::Deserialize,
::agen::__private::schemars::JsonSchema,
)]
#[serde(crate = "::agen::__private::serde")]
#[schemars(crate = "::agen::__private::schemars")]
struct #args_struct_name {
#(#arg_fields),*
}
};
let execute_body = if json_args.is_empty() {
quote! {
let _: #args_struct_name = ::agen::__private::serde_json::from_str(input_json)
.unwrap_or(#args_struct_name {});
let result = #method_call #awaiter;
#result_handling
}
} else {
quote! {
let args: #args_struct_name = ::agen::__private::serde_json::from_str(input_json)
.map_err(|error| ::agen::tool::ToolError::InvalidArgument(error.to_string()))?;
let result = #method_call #awaiter;
#result_handling
}
};
Ok(quote! {
#args_struct_def
#[derive(Clone)]
pub struct #tool_struct_name {
ctx: #self_ty,
}
#[::agen::__private::async_trait::async_trait]
impl ::agen::tool::Tool for #tool_struct_name {
async fn execute(
&self,
input_json: &str,
ctx: ::agen::tool::ToolExecutionContext,
) -> Result<::agen::tool::ToolOutput, ::agen::tool::ToolError> {
let _ = &ctx;
#execute_body
}
}
impl #self_ty {
/// Returns a tool definition for registration with an `agen::Engine`.
pub fn #definition_name(&self) -> ::agen::tool::ToolDefinition {
let ctx = self.clone();
::std::sync::Arc::new(move || {
let schema = ::agen::__private::schemars::schema_for!(#args_struct_name);
let meta = ::agen::tool::ToolMeta::new(#tool_name)
.description(#description)
.input_schema(
::agen::__private::serde_json::to_value(schema)
.unwrap_or_else(|_| ::agen::__private::serde_json::json!({})),
);
let tool: ::std::sync::Arc<dyn ::agen::tool::Tool> =
::std::sync::Arc::new(#tool_struct_name { ctx: ctx.clone() });
(meta, tool)
})
}
}
})
}
fn is_result_type(return_type: &ReturnType) -> bool {
match return_type {
ReturnType::Default => false,
ReturnType::Type(_, ty) => {
if let Type::Path(type_path) = ty.as_ref()
&& let Some(segment) = type_path.path.segments.last()
{
return segment.ident == "Result";
}
false
}
}
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect()
}
/// Marker attribute interpreted by [`tool_registry`].
#[proc_macro_attribute]
pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
marker_attribute("tool", attr, item)
}
/// Argument description marker interpreted by [`tool_registry`].
///
/// Use it as `#[description = "The argument description"]` on a tool method argument.
#[proc_macro_attribute]
pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
marker_attribute("description", attr, item)
}
fn marker_attribute(name: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
if attr.is_empty() {
item
} else {
syn::Error::new(
proc_macro2::Span::call_site(),
format!("{name} is a marker interpreted by #[tool_registry]"),
)
.into_compile_error()
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
use syn::parse_quote;
#[test]
fn rejects_tool_registry_arguments() {
let implementation: ItemImpl = parse_quote!(impl Registry {});
let error = expand_tool_registry(quote!(unexpected), implementation).unwrap_err();
assert!(error.to_string().contains("does not accept arguments"));
}
#[test]
fn rejects_duplicate_tool_markers() {
let implementation: ItemImpl = parse_quote! {
impl Registry {
#[tool]
#[tool]
fn inspect(&self) {}
}
};
let error = expand_tool_registry(quote!(), implementation).unwrap_err();
assert!(error.to_string().contains("duplicate #[tool]"));
}
#[test]
fn rejects_invalid_description_attributes() {
let implementation: ItemImpl = parse_quote! {
impl Registry {
#[tool]
fn inspect(&self, #[description] input: String) {}
}
};
let error = expand_tool_registry(quote!(), implementation).unwrap_err();
assert!(error.to_string().contains("expected #[description"));
}
#[test]
fn rejects_duplicate_description_attributes() {
let implementation: ItemImpl = parse_quote! {
impl Registry {
#[tool]
fn inspect(
&self,
#[description = "first"]
#[description = "second"]
input: String,
) {}
}
};
let error = expand_tool_registry(quote!(), implementation).unwrap_err();
assert!(error.to_string().contains("duplicate #[description]"));
}
#[test]
fn generated_code_uses_only_agen_runtime_paths() {
let implementation: ItemImpl = parse_quote! {
impl Registry {
#[tool]
fn inspect(&self, input: String) -> Result<String, Error> {
unreachable!()
}
}
};
let expanded = expand_tool_registry(quote!(), implementation)
.unwrap()
.to_string();
assert!(expanded.contains(":: agen :: tool :: Tool"));
assert!(expanded.contains(":: agen :: __private :: serde_json"));
assert!(expanded.contains(":: agen :: __private :: serde"));
assert!(expanded.contains(":: agen :: __private :: schemars"));
}
}
@@ -1,17 +1,27 @@
[package]
name = "llm-engine"
description = "A library for building autonomous LLM-powered systems"
name = "agen"
description = "Provider-neutral orchestration for tool-using LLM applications"
version = "0.2.1"
edition.workspace = true
rust-version = "1.86"
license.workspace = true
readme = "README.md"
repository = "https://gitea.hareworks.net/Hare/yoi"
homepage = "https://gitea.hareworks.net/Hare/yoi"
documentation = "https://docs.rs/agen"
keywords = ["llm", "agent", "tools", "streaming", "orchestration"]
categories = ["api-bindings", "asynchronous"]
include = ["src/**", "tests/**", "examples/*.rs", "docs/**", "README.md", "LICENSE"]
autoexamples = false
[features]
default = []
codex = ["dep:base64", "dep:chrono"]
codex = ["dep:chrono"]
[dependencies]
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
schemars = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
async-trait = { workspace = true }
@@ -21,15 +31,25 @@ tokio-util = "0.7"
reqwest = { version = "0.13", default-features = false, features = ["stream", "json", "native-tls", "http2"] }
eventsource-stream = "0.2"
zstd = "0.13"
base64 = { version = "0.22.1", optional = true }
base64 = "0.22.1"
chrono = { version = "0.4", default-features = false, features = ["serde", "clock"], optional = true }
llm-engine-macros = { workspace = true }
agen-macros = { workspace = true }
[dev-dependencies]
clap = { version = "4.5", features = ["derive", "env"] }
schemars = { workspace = true }
tempfile = { workspace = true }
dotenv = "0.15"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
trybuild = "1.0.116"
wiremock = "0.6.5"
[[example]]
name = "engine_cancel_demo"
path = "examples/engine_cancel_demo.rs"
[[example]]
name = "engine_cli"
path = "examples/engine_cli.rs"
[package.metadata.docs.rs]
all-features = true
+7
View File
@@ -0,0 +1,7 @@
Copyright 2026 Hare
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+91
View File
@@ -0,0 +1,91 @@
# agen
`agen` is a provider-neutral Rust engine for streaming LLM applications that use tools. It owns the turn loop, typed conversation history, provider wire-format adapters, tool execution, interceptors, usage accounting, and cache-aware state transitions.
> `agen` is pre-1.0. Public APIs may change between minor releases.
## Installation
```toml
[dependencies]
agen = "0.2.1"
```
The default feature set is intentionally empty. Enable the experimental Codex/ChatGPT authentication adapter when needed:
```toml
agen = { version = "0.2.1", features = ["codex"] }
```
`agen` requires Rust 1.86 or newer. The companion `agen-macros` package requires Rust 1.85 or newer.
## Quick start
Supply an implementation of [`LlmClient`](https://docs.rs/agen/latest/agen/llm_client/trait.LlmClient.html), keep conversation history in your application, then run a turn. The first call consumes the mutable engine and returns a cache-locked engine for later turns.
```no_run
use agen::{Engine, EngineError, History};
use agen::llm_client::LlmClient;
async fn conversation<C: LlmClient>(client: C) -> Result<(), EngineError> {
let mut history = History::new();
let output = Engine::new(client)
.system_prompt("You are a concise assistant.")
.run(&mut history, "Explain typed state in one sentence.")
.await;
let mut engine = output.engine;
let _result = engine.run(&mut history, "Give a Rust example.").await;
Ok(())
}
```
## Declaring tools
The tool macros are re-exported by `agen`; applications do not need direct dependencies on `serde`, `schemars`, `serde_json`, or `async-trait` for generated code.
```rust
use agen::tool_registry;
#[derive(Clone)]
struct Tools;
#[tool_registry]
impl Tools {
/// Returns the supplied text.
#[tool]
async fn echo(
&self,
#[description = "Text to return"] text: String,
) -> Result<String, std::io::Error> {
Ok(text)
}
}
let definition = Tools.echo_definition();
assert_eq!(definition().0.name, "echo");
```
The generated API uses the canonical crate name `agen`. Renaming the `agen` dependency in `Cargo.toml` is not currently supported by these macros.
## Features
| Feature | Default | Adds |
|---|---:|---|
| `codex` | No | Experimental Codex/ChatGPT auth-file loading and token refresh support |
The base crate includes provider-neutral transport and Anthropic, OpenAI-compatible, Gemini, and Ollama wire-format schemes. See [`llm_client`](https://docs.rs/agen/latest/agen/llm_client/) for the client boundary.
## Architecture and API scope
The current public modules cover the engine, typed history, client transport/schemes, timeline events, tools, interceptors, pruning, token estimation, and usage records. Their relationships are described in [Architecture](https://gitea.hareworks.net/Hare/yoi/src/branch/develop/crates/agen/docs/architecture.md); behavioral requirements are summarized in [Requirements](https://gitea.hareworks.net/Hare/yoi/src/branch/develop/crates/agen/docs/requirements.md).
Low-level modules remain public in the 0.2 series because downstream Yoi components implement custom clients, event handlers, pruning policies, and tool registries against them. This surface is versioned as pre-1.0 API rather than declared stable.
## Packaging and security
The published package contains source, public documentation, curated examples, and deterministic tests/fixtures. Credentialed fixture-recording utilities are intentionally excluded. Examples that contact a provider read credentials from environment variables and never embed production credentials.
## License
Licensed under the [MIT License](https://gitea.hareworks.net/Hare/yoi/src/branch/develop/LICENSE).
+62
View File
@@ -0,0 +1,62 @@
# agen architecture
`agen` separates orchestration, event projection, and provider transport so applications can replace an LLM client without changing the turn loop or tool model.
```text
┌────────────────────────────────────────────┐
│ Engine │
│ turn loop · interceptors · tool execution │
│ typed state: Mutable → Locked → Mutable │
└─────────────────────┬──────────────────────┘
┌─────────────────────▼──────────────────────┐
│ Timeline │
│ event dispatch · block collectors │
└─────────────────────┬──────────────────────┘
┌─────────────────────▼──────────────────────┐
│ LlmClient │
│ transport · provider wire-format schemes │
└────────────────────────────────────────────┘
```
## Main modules
| Module | Responsibility |
|---|---|
| `engine` | Turn execution, pause/resume, retries, tool integration, and callbacks |
| `state` | Sealed `Mutable` and `Locked` type-state markers |
| `interceptor` | Application-owned control decisions at orchestration boundaries |
| `tool` / `tool_server` | Tool metadata, registration, execution, and bounded output |
| `timeline` | Streaming event dispatch, handlers, and block assembly |
| `llm_client` | Provider-neutral request, response, auth, transport, and scheme contracts |
| `providers` | Optional higher-level provider adapters such as the `codex` feature |
| `prune` / `token_counter` | Cache-aware history reduction and token estimation |
| `usage_record` | Request and token usage accounting |
## Request flow
```text
Engine history
→ provider-neutral Request
→ Scheme::build_request
→ Provider transport
```
## Response flow
```text
streaming response bytes
→ Scheme event parsing
→ unified Event values
→ Timeline handlers and collectors
→ Engine history/tool decisions
```
## Type state and cache protection
`Engine<C, Mutable>` permits configuration and history editing. `Engine::run` or `Engine::lock` commits the current prefix and produces `Engine<C, Locked>`. The locked engine may append turns without mutating the committed prefix. `Engine::unlock` explicitly returns to mutable state when an application accepts losing that cache guarantee.
## Public surface
The 0.2 series exposes the low-level client, timeline, tool, pruning, and usage modules because custom clients and orchestration hosts build directly on them. These APIs are intentionally provider-neutral but remain pre-1.0 and may change in later minor releases.
+39
View File
@@ -0,0 +1,39 @@
# agen requirements
## R1: Turn execution and continuation
- `Engine::run` starts a turn and loops through provider output and tool calls.
- An `Interceptor` may continue, cancel, or pause work at defined orchestration boundaries.
- `Engine::resume` continues paused generation without fabricating another user message.
- Cancellation and provider errors are represented as typed `EngineError` values.
## R2: Explicit cache-preserving state
- `Engine<C, Mutable>` permits configuration and history edits.
- `Engine::run` or `Engine::lock` transitions to `Engine<C, Locked>` and records the committed prefix.
- A locked engine appends turns but cannot mutate that prefix through mutable-only APIs.
- `Engine::unlock` explicitly abandons the lock before configuration or history changes.
## R3: Tool declarations and execution
- `#[tool_registry]` generates a schema and `Tool` implementation for methods marked `#[tool]`.
- `#[description = "..."]` supplies argument descriptions in generated JSON Schema.
- Generated code resolves its runtime and helper dependencies through `::agen`.
- Invalid and duplicate marker attributes produce compile errors rather than panics.
- Tools execute through `ToolServer` with typed context, errors, and output limits.
## R4: Provider-neutral orchestration
- `LlmClient` is the boundary between the engine and provider-specific transport.
- Request/response schemes translate provider wire formats into shared request and event types.
- Interceptors, tool execution, timeline collection, and pruning stay above the provider transport.
- Provider-specific capabilities are optional features when they require additional policy or dependencies.
## R5: Publication quality
- crates.io metadata includes license, repository, documentation, README, categories, keywords, and MSRV.
- The default feature set and each optional feature compile and test independently.
- Macro expansion compiles in a downstream-style integration test without direct helper dependencies.
- rustdoc builds without dependency documentation.
- Package contents are explicitly bounded and exclude credentialed fixture-recording utilities.
- `cargo package` and `cargo publish --dry-run` are run for `agen-macros` before `agen` because the main package depends on its companion package.
@@ -2,9 +2,9 @@
//!
//! Example of cancelling from another thread during streaming
use llm_engine::llm_client::scheme::{Scheme, anthropic::AnthropicScheme};
use llm_engine::llm_client::transport::{HttpTransport, ResolvedAuth};
use llm_engine::{Engine, EngineResult};
use agen::llm_client::scheme::{Scheme, anthropic::AnthropicScheme};
use agen::llm_client::transport::{HttpTransport, ResolvedAuth};
use agen::{Engine, EngineRunExit, RunInterruptionReason};
use std::time::Duration;
#[tokio::main]
@@ -29,6 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let base_url = scheme.default_base_url().to_string();
let client = HttpTransport::new(scheme, model, base_url, ResolvedAuth::ApiKey(api_key), cap);
let engine = Engine::new(client);
let mut history = agen::History::new();
println!("🚀 Starting Engine...");
println!("💡 Will cancel after 2 seconds\n");
@@ -45,16 +46,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("📡 Sending request to LLM...");
match engine.run("Tell me a very long story about a brave knight. Make it as detailed as possible with many paragraphs.").await {
Ok(out) => match out.result {
EngineResult::Finished => println!("✅ Task completed normally"),
EngineResult::Paused => println!("⏸️ Task paused"),
EngineResult::LimitReached => println!("🔒 Turn limit reached"),
EngineResult::Yielded => println!("↩️ Task yielded"),
},
Err(e) => {
println!("❌ Task error: {}", e);
let output = engine.run(&mut history, "Tell me a very long story about a brave knight. Make it as detailed as possible with many paragraphs.").await;
match output.result {
EngineRunExit::Finished => println!("✅ Task completed normally"),
EngineRunExit::Paused => println!("⏸️ Task paused"),
EngineRunExit::Yielded => println!("↩️ Task yielded"),
EngineRunExit::Interrupted(RunInterruptionReason::LimitReached) => {
println!("🔒 Turn limit reached")
}
EngineRunExit::Interrupted(reason) => println!("❌ Task interrupted: {reason:?}"),
}
println!("\n✨ Demo complete!");
@@ -38,10 +38,9 @@ use async_trait::async_trait;
use tracing::info;
use tracing_subscriber::EnvFilter;
use clap::{Parser, ValueEnum};
use llm_engine::{
Engine,
interceptor::{Interceptor, PostToolAction, ToolResultInfo},
use agen::{
Engine, EngineRunExit, RunInterruptionReason,
interceptor::{Interceptor, InterceptorResult, PostToolAction, ToolResultInfo},
llm_client::{
LlmClient,
capability::{CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport},
@@ -51,12 +50,9 @@ use llm_engine::{
transport::{HttpTransport, ResolvedAuth},
},
timeline::{Handler, TextBlockEvent, TextBlockKind, ToolUseBlockEvent, ToolUseBlockKind},
tool_registry,
};
use llm_engine_macros::tool_registry;
// Required imports for macro expansion
use schemars;
use serde;
use clap::{Parser, ValueEnum};
// =============================================================================
// Provider Definition
@@ -284,7 +280,10 @@ impl ToolResultPrinterPolicy {
#[async_trait]
impl Interceptor for ToolResultPrinterPolicy {
async fn post_tool_call(&self, info: &mut ToolResultInfo) -> PostToolAction {
async fn post_tool_call(
&self,
info: &ToolResultInfo<'_, ()>,
) -> InterceptorResult<PostToolAction> {
let name = self
.call_names
.lock()
@@ -298,7 +297,7 @@ impl Interceptor for ToolResultPrinterPolicy {
println!(" Result ({}): ✅ {}", name, info.result.summary);
}
PostToolAction::Continue
Ok(PostToolAction::Continue)
}
}
@@ -455,6 +454,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create Engine
let mut engine = Engine::new(client);
let mut history = agen::History::new();
let tool_call_names = Arc::new(Mutex::new(HashMap::new()));
@@ -480,12 +480,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// One-shot mode
if let Some(prompt) = args.prompt {
match engine.run(&prompt).await {
Ok(_) => {}
Err(e) => {
eprintln!("\n❌ Error: {}", e);
std::process::exit(1);
}
let output = engine.run(&mut history, &prompt).await;
if let EngineRunExit::Interrupted(RunInterruptionReason::Unexpected(error)) = output.result
{
eprintln!("\n❌ Error: {error}");
}
return Ok(());
@@ -504,13 +502,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
let mut locked = match engine.run(first_input).await {
Ok(out) => out.engine,
Err(e) => {
eprintln!("\n❌ Error: {}", e);
return Ok(());
}
};
let output = engine.run(&mut history, first_input).await;
let mut locked = output.engine;
loop {
print!("\n👤 You: ");
@@ -529,11 +522,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
break;
}
match locked.run(input).await {
Ok(_) => {}
Err(e) => {
eprintln!("\n❌ Error: {}", e);
}
if let EngineRunExit::Interrupted(RunInterruptionReason::Unexpected(error)) =
locked.run(&mut history, input).await
{
eprintln!("\n❌ Error: {error}");
}
}
@@ -19,11 +19,11 @@
mod recorder;
mod scenarios;
use clap::{Parser, ValueEnum};
use llm_engine::llm_client::scheme::{
use agen::llm_client::scheme::{
Scheme, anthropic::AnthropicScheme, gemini::GeminiScheme, openai_chat::OpenAIScheme,
};
use llm_engine::llm_client::transport::{HttpTransport, ResolvedAuth};
use agen::llm_client::transport::{HttpTransport, ResolvedAuth};
use clap::{Parser, ValueEnum};
fn make_transport<S: Scheme>(scheme: S, model: &str, auth: ResolvedAuth) -> HttpTransport<S> {
let cap = scheme.default_capability();
@@ -7,8 +7,8 @@ use std::io::{BufWriter, Write};
use std::path::Path;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use agen::llm_client::{LlmClient, Request};
use futures::StreamExt;
use llm_engine::llm_client::{LlmClient, Request};
/// Recorded event
#[derive(Debug, serde::Serialize, serde::Deserialize)]
@@ -2,7 +2,7 @@
//!
//! Defines requests and output file names for each scenario
use llm_engine::llm_client::{Request, ToolDefinition};
use agen::llm_client::{Request, ToolDefinition};
/// Test scenario
pub struct TestScenario {
File diff suppressed because it is too large Load Diff
@@ -32,7 +32,7 @@ pub trait Kind {
/// # Examples
///
/// ```ignore
/// use llm_engine::timeline::{Handler, TextBlockEvent, TextBlockKind};
/// use agen::timeline::{Handler, TextBlockEvent, TextBlockKind};
///
/// struct TextCollector {
/// texts: Vec<String>,
+199
View File
@@ -0,0 +1,199 @@
//! Typed conversation history containers.
//!
//! Agen keeps provider-visible [`Item`](crate::Item) values separate from any
//! host-domain provenance. The host chooses the annotation type `A`, while Agen
//! preserves each item and annotation as one entry for clone/truncate/restore
//! style history operations.
use serde::{Deserialize, Serialize};
use crate::Item;
/// One conversation-history entry with host-owned annotation.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HistoryEntry<A = ()> {
/// Provider/model-visible conversation item.
pub item: Item,
/// Host-domain metadata kept with the item and never projected to providers.
pub annotation: A,
}
impl<A> HistoryEntry<A> {
/// Build an entry from an item and its annotation.
pub fn new(item: Item, annotation: A) -> Self {
Self { item, annotation }
}
/// Split the entry into its item and annotation.
pub fn into_parts(self) -> (Item, A) {
(self.item, self.annotation)
}
}
impl HistoryEntry<()> {
/// Build a unit-annotated entry.
pub fn from_item(item: Item) -> Self {
Self {
item,
annotation: (),
}
}
}
/// Conversation history with one annotation per item.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct History<A = ()> {
entries: Vec<HistoryEntry<A>>,
}
impl<A> History<A> {
/// Create an empty history.
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
/// Build history from already annotated entries, preserving order.
pub fn from_entries(entries: Vec<HistoryEntry<A>>) -> Self {
Self { entries }
}
/// Replace all entries as one restore/rebuild operation and return the old entries.
pub fn replace_entries(&mut self, entries: Vec<HistoryEntry<A>>) -> Vec<HistoryEntry<A>> {
std::mem::replace(&mut self.entries, entries)
}
/// Borrow annotated entries.
pub fn entries(&self) -> &[HistoryEntry<A>] {
&self.entries
}
/// Mutably borrow annotated entries for host-owned rebuild operations.
pub fn entries_mut(&mut self) -> &mut [HistoryEntry<A>] {
&mut self.entries
}
/// Consume the history into annotated entries.
pub fn into_entries(self) -> Vec<HistoryEntry<A>> {
self.entries
}
/// Number of entries.
pub fn len(&self) -> usize {
self.entries.len()
}
/// Whether the history is empty.
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Iterate over annotated entries.
pub fn iter(&self) -> impl ExactSizeIterator<Item = &HistoryEntry<A>> {
self.entries.iter()
}
/// Iterate over provider-visible items only.
pub fn items(&self) -> impl ExactSizeIterator<Item = &Item> {
self.entries.iter().map(|entry| &entry.item)
}
/// Clone provider-visible items into a request-local projection.
pub fn items_cloned(&self) -> Vec<Item> {
self.items().cloned().collect()
}
/// Append an already annotated entry.
pub fn push_entry(&mut self, entry: HistoryEntry<A>) {
self.entries.push(entry);
}
/// Append many already annotated entries.
pub fn extend_entries(&mut self, entries: impl IntoIterator<Item = HistoryEntry<A>>) {
self.entries.extend(entries);
}
/// Commit one item through a trusted annotation callback before it becomes live.
///
/// The callback may durably persist the item and returns the annotation that
/// must be stored with it. If the callback fails, the history is left unchanged.
pub fn append_with(
&mut self,
item: Item,
annotate: &mut impl FnMut(&Item) -> Result<A, String>,
) -> Result<(), String> {
let annotation = annotate(&item)?;
self.entries.push(HistoryEntry { item, annotation });
Ok(())
}
/// Commit items through a trusted annotation callback before they become live.
///
/// Items before a failure remain appended; the failing item and later items do
/// not enter history. This mirrors append-only durable logs where each accepted
/// item is already committed before the next item is attempted.
pub fn extend_with(
&mut self,
items: impl IntoIterator<Item = Item>,
annotate: &mut impl FnMut(&Item) -> Result<A, String>,
) -> Result<(), String> {
for item in items {
self.append_with(item, annotate)?;
}
Ok(())
}
/// Truncate entries, preserving item+annotation pairing for retained entries.
pub fn truncate(&mut self, len: usize) {
self.entries.truncate(len);
}
/// Clear all entries.
pub fn clear(&mut self) {
self.entries.clear();
}
}
impl History<()> {
/// Build unit-annotated history from provider-visible items.
pub fn from_items(items: Vec<Item>) -> Self {
Self {
entries: items.into_iter().map(HistoryEntry::from_item).collect(),
}
}
/// Replace history from provider-visible items using unit annotations.
pub fn replace_items(&mut self, items: Vec<Item>) -> Vec<HistoryEntry<()>> {
self.replace_entries(items.into_iter().map(HistoryEntry::from_item).collect())
}
/// Append one item with unit annotation.
pub fn push(&mut self, item: Item) {
self.entries.push(HistoryEntry::from_item(item));
}
/// Append items with unit annotations.
pub fn extend_items(&mut self, items: impl IntoIterator<Item = Item>) {
self.entries
.extend(items.into_iter().map(HistoryEntry::from_item));
}
}
impl<A> IntoIterator for History<A> {
type Item = HistoryEntry<A>;
type IntoIter = std::vec::IntoIter<HistoryEntry<A>>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
impl<'a, A> IntoIterator for &'a History<A> {
type Item = &'a HistoryEntry<A>;
type IntoIter = std::slice::Iter<'a, HistoryEntry<A>>;
fn into_iter(self) -> Self::IntoIter {
self.entries.iter()
}
}
+423
View File
@@ -0,0 +1,423 @@
//! Interceptor - control flow delegation for the Engine execution loop
//!
//! Defines the [`Interceptor`] trait that callers implement to inject
//! orchestration decisions (approval, skip, pause, abort) into the Engine's
//! turn loop without the Engine knowing about host-application concepts.
use std::sync::Arc;
use async_trait::async_trait;
use crate::Item;
use crate::engine::EngineRunExit;
use crate::history::HistoryEntry;
use crate::tool::{Tool, ToolCall, ToolExecutionContext, ToolMeta, ToolResult};
// =============================================================================
// Typed lifecycle metadata and failures
// =============================================================================
/// Maximum UTF-8 byte length retained for interceptor diagnostics.
pub const MAX_INTERCEPTOR_DIAGNOSTIC_BYTES: usize = 1024;
/// Stable category for the source of an interceptor failure.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterceptorErrorCategory {
Policy,
Dependency,
ContractViolation,
Internal,
}
impl std::fmt::Display for InterceptorErrorCategory {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Policy => "policy",
Self::Dependency => "dependency",
Self::ContractViolation => "contract_violation",
Self::Internal => "internal",
})
}
}
/// A typed, bounded failure returned by an [`Interceptor`] implementation.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{category}: {diagnostic}")]
pub struct InterceptorError {
category: InterceptorErrorCategory,
diagnostic: String,
}
impl InterceptorError {
pub fn new(category: InterceptorErrorCategory, diagnostic: impl Into<String>) -> Self {
let mut diagnostic = diagnostic.into();
if diagnostic.len() > MAX_INTERCEPTOR_DIAGNOSTIC_BYTES {
let mut end = MAX_INTERCEPTOR_DIAGNOSTIC_BYTES;
while !diagnostic.is_char_boundary(end) {
end -= 1;
}
diagnostic.truncate(end);
}
Self {
category,
diagnostic,
}
}
pub fn category(&self) -> InterceptorErrorCategory {
self.category
}
pub fn diagnostic(&self) -> &str {
&self.diagnostic
}
}
/// The lifecycle phase at which an interceptor callback executes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum InterceptorPhase {
#[default]
PromptSubmit,
PendingHistoryAppends,
PreLlmRequest,
PreToolCall,
PostToolCall,
AssistantTurnEnd,
RunExit,
}
impl std::fmt::Display for InterceptorPhase {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::PromptSubmit => "prompt_submit",
Self::PendingHistoryAppends => "pending_history_appends",
Self::PreLlmRequest => "pre_llm_request",
Self::PreToolCall => "pre_tool_call",
Self::PostToolCall => "post_tool_call",
Self::AssistantTurnEnd => "assistant_turn_end",
Self::RunExit => "run_exit",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct InterceptorRunId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct InterceptorTurnId(pub u64);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum InterceptorCallId {
Llm(u64),
Tool(String),
}
/// Saturating public counter used by interceptor contexts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct InterceptorCounter(u32);
impl InterceptorCounter {
pub fn from_usize(value: usize) -> Self {
Self(u32::try_from(value).unwrap_or(u32::MAX))
}
pub fn get(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct InterceptorCounters {
pub invocation: InterceptorCounter,
pub engine_turn: InterceptorCounter,
pub run_turn: InterceptorCounter,
pub llm_call: InterceptorCounter,
pub tool_batch: InterceptorCounter,
pub tool_call: InterceptorCounter,
}
/// Identity, phase, and bounded counters common to every lifecycle callback.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct InterceptorInvocation {
pub run_id: InterceptorRunId,
pub turn_id: Option<InterceptorTurnId>,
pub call_id: Option<InterceptorCallId>,
pub phase: InterceptorPhase,
pub counters: InterceptorCounters,
}
/// An interceptor failure bound to the exact Engine lifecycle phase that ran it.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{phase} interceptor failed: {error}")]
pub struct InterceptorFailure {
phase: InterceptorPhase,
#[source]
error: InterceptorError,
}
impl InterceptorFailure {
pub(crate) fn new(phase: InterceptorPhase, error: InterceptorError) -> Self {
Self { phase, error }
}
pub fn phase(&self) -> InterceptorPhase {
self.phase
}
pub fn error(&self) -> &InterceptorError {
&self.error
}
}
pub type InterceptorResult<T> = Result<T, InterceptorError>;
// =============================================================================
// Lifecycle Contexts
// =============================================================================
pub struct PromptSubmitContext<'a, A = ()> {
pub invocation: InterceptorInvocation,
pub item: &'a mut Item,
pub history: &'a [HistoryEntry<A>],
}
pub struct PendingHistoryAppendsContext<'a, A = ()> {
pub invocation: InterceptorInvocation,
pub history: &'a [HistoryEntry<A>],
}
pub struct PreLlmRequestContext<'a, A = ()> {
pub invocation: InterceptorInvocation,
pub items: &'a mut Vec<Item>,
pub history: &'a [HistoryEntry<A>],
}
pub struct AssistantTurnEndContext<'a, A = ()> {
pub invocation: InterceptorInvocation,
pub assistant_entries: &'a [HistoryEntry<A>],
pub history: &'a [HistoryEntry<A>],
pub tool_calls: &'a [ToolCall],
}
pub struct RunExitContext<'a, A = ()> {
pub invocation: InterceptorInvocation,
pub exit: &'a EngineRunExit,
pub history: &'a [HistoryEntry<A>],
}
// =============================================================================
// Action Enums
// =============================================================================
/// Action after prompt submission.
#[derive(Debug, Clone, PartialEq)]
pub enum PromptAction {
/// Proceed normally.
Continue,
/// Cancel with a reason.
Cancel(String),
/// Proceed, and append these items to history right after the user
/// message. Mirrors [`TurnEndAction::ContinueWithMessages`] for the
/// submit edge: lets the upper layer attach resolver-produced
/// system messages (e.g. `@<path>` file content) so they sit
/// adjacent to the user message that referenced them.
ContinueWith(Vec<Item>),
}
/// Action before an LLM request.
#[derive(Debug, Clone, PartialEq)]
pub enum PreRequestAction {
/// Proceed normally.
Continue,
/// Proceed after appending these items to durable engine history.
///
/// This is for upper-layer budget/status nudges that the model may react
/// to: the items are committed before the request so later turns can see
/// why the engine changed course.
ContinueWith(Vec<Item>),
/// Yield after appending these items to durable engine history.
///
/// This is for host-mediated pre-request appends that must be visible to
/// usage accounting and compaction checks before the current LLM request is
/// allowed to proceed.
YieldWith(Vec<Item>),
/// Cancel with a reason (treated as an error).
Cancel(String),
/// Yield control to the caller for external processing.
///
/// The Engine exits the turn loop cleanly with `EngineResult::Yielded`.
/// The caller is expected to resume execution later.
Yield,
}
/// Action before a tool call.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PreToolAction {
/// Proceed with execution.
Continue,
/// Skip this tool call (do not execute).
Skip,
/// Do not execute the tool call; commit this synthetic result instead.
///
/// This preserves provider-visible `tool_use` / `tool_result` pairing
/// without aborting the whole turn.
SyntheticResult(ToolResult),
/// Abort the entire run.
Abort(String),
/// Pause execution (can be resumed later).
Pause,
}
/// Action after a tool call.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PostToolAction {
/// Proceed normally.
Continue,
/// Abort the entire run.
Abort(String),
}
/// Action at the end of a turn (when LLM produces no tool calls).
#[derive(Debug, Clone)]
pub enum TurnEndAction {
/// Accept the Engine's natural next phase: execute tools, or finish when none exist.
Finish,
/// Commit additional messages, then continue through the natural next phase.
ContinueWithMessages(Vec<Item>),
/// Pause execution (can be resumed later).
Pause,
}
// =============================================================================
// Context Types
// =============================================================================
/// Context for pre-tool-call decisions.
pub struct ToolCallInfo<'a, A = ()> {
pub invocation: InterceptorInvocation,
pub history: &'a [HistoryEntry<A>],
pub call: ToolCall,
/// Tool meta information.
pub meta: ToolMeta,
/// Tool instance (for state access).
pub tool: Arc<dyn Tool>,
/// Response-local execution context for this call.
pub context: ToolExecutionContext,
}
/// Context for post-tool-call decisions.
pub struct ToolResultInfo<'a, A = ()> {
pub invocation: InterceptorInvocation,
pub history: &'a [HistoryEntry<A>],
pub call: ToolCall,
/// Committed terminal tool execution result.
pub result: ToolResult,
/// Tool meta information.
pub meta: ToolMeta,
/// Tool instance (for state access).
pub tool: Arc<dyn Tool>,
/// Response-local execution context for this call.
pub context: ToolExecutionContext,
}
// =============================================================================
// Interceptor Trait
// =============================================================================
/// Intercepts the Engine execution loop at key decision points.
///
/// Every lifecycle method is asynchronous and returns [`InterceptorResult`],
/// keeping implementation failure separate from the method's control-flow
/// action. The Engine reports a failure as a typed run interruption annotated
/// with the exact [`InterceptorPhase`] that failed.
///
/// All methods have default implementations that let the Engine proceed
/// without intervention. Callers provide richer implementations for approval
/// flows, permission checks, and other trusted host adaptation.
#[async_trait]
pub trait Interceptor<A: Send + Sync = ()>: Send + Sync {
/// Called after receiving user input, before adding it to Engine history.
async fn on_prompt_submit(
&self,
_context: PromptSubmitContext<'_, A>,
) -> InterceptorResult<PromptAction> {
Ok(PromptAction::Continue)
}
/// Items that should be **committed to `engine.history`** just
/// before the next LLM request. Returned items are `extend`ed into
/// the persistent history (and therefore picked up by the per-turn
/// clone that backs the LLM request, plus the usual
/// history-persistence path).
///
/// Use this for inputs that arrive from outside the LLM and need
/// to be reflected in the on-disk history — notifications,
/// external events, system reminders. Do **not** use
/// [`Self::pre_llm_request`] for that purpose: it mutates a
/// per-request clone, so any committed assistant response that
/// reacts to the injection would have no visible trigger on the
/// next turn (or after resume / compaction).
///
/// `pre_llm_request` remains the right place for purely
/// reproducible per-request transformations (pruning, content
/// trimming, cache anchors) that depend only on the existing
/// history.
async fn pending_history_appends(
&self,
_context: PendingHistoryAppendsContext<'_, A>,
) -> InterceptorResult<Vec<Item>> {
Ok(Vec::new())
}
/// Called before each LLM request. The context starts as a clone
/// of `engine.history` (after `pending_history_appends` and the
/// Engine's own prune projection have been applied).
///
/// Direct mutations to `context` remain request-local and are not persisted.
/// If an interceptor derives a human/model-visible nudge from the current
/// request context, return [`PreRequestAction::ContinueWith`] so the Engine
/// commits it to history before the request is sent.
async fn pre_llm_request(
&self,
_context: PreLlmRequestContext<'_, A>,
) -> InterceptorResult<PreRequestAction> {
Ok(PreRequestAction::Continue)
}
/// Called before each tool is executed.
async fn pre_tool_call(
&self,
_info: &mut ToolCallInfo<'_, A>,
) -> InterceptorResult<PreToolAction> {
Ok(PreToolAction::Continue)
}
/// Called after each tool reaches one terminal result and that result is committed.
async fn post_tool_call(
&self,
_info: &ToolResultInfo<'_, A>,
) -> InterceptorResult<PostToolAction> {
Ok(PostToolAction::Continue)
}
/// Called after every terminal assistant response is committed and before
/// the Engine decides whether to execute tools, continue, or finish.
async fn on_assistant_turn_end(
&self,
_context: AssistantTurnEndContext<'_, A>,
) -> InterceptorResult<TurnEndAction> {
Ok(TurnEndAction::Finish)
}
/// Called once for the terminal outcome of each public run or resume call.
async fn on_run_exit(&self, _context: RunExitContext<'_, A>) -> InterceptorResult<()> {
Ok(())
}
}
/// Default interceptor: no intervention. Engine proceeds through the loop
/// without any external control flow decisions.
pub(crate) struct DefaultInterceptor;
#[async_trait]
impl<A: Send + Sync> Interceptor<A> for DefaultInterceptor {}
+54
View File
@@ -0,0 +1,54 @@
#![doc = include_str!("../README.md")]
mod engine;
mod handler;
mod history;
mod message;
pub(crate) mod callback;
pub mod event;
pub mod interceptor;
pub mod llm_client;
pub mod providers;
pub mod prune;
pub mod state;
pub mod timeline;
pub mod token_counter;
pub mod tool;
pub mod tool_server;
pub mod usage_record;
pub use agen_macros::{description, tool, tool_registry};
pub use callback::{TextBlockScope, ThinkingBlockScope, ToolUseBlockScope};
pub use engine::{
Engine, EngineConfig, EngineError, EngineResult, EngineRunExit, EngineRunOutput,
LlmRetryNotice, RunInterruptionReason, ToolRegistryError,
};
pub use handler::ToolUseBlockStart;
pub use history::{History, HistoryEntry};
pub use interceptor::{
AssistantTurnEndContext, Interceptor, InterceptorCallId, InterceptorCounter,
InterceptorCounters, InterceptorError, InterceptorErrorCategory, InterceptorFailure,
InterceptorInvocation, InterceptorPhase, InterceptorResult, InterceptorRunId,
InterceptorTurnId, MAX_INTERCEPTOR_DIAGNOSTIC_BYTES, PendingHistoryAppendsContext,
PreLlmRequestContext, PromptSubmitContext, RunExitContext,
};
pub use message::{ContentPart, Item, Message, Role};
pub use tool::{
ToolCall, ToolExecutionContext, ToolExecutionHandle, ToolExecutionPolicy,
ToolExecutionTerminal, ToolExecutionTerminalFuture, ToolOutputLimits, ToolResult,
ToolResultDisposition,
};
pub use usage_record::UsageRecord;
/// Implementation dependencies used by code generated from `agen` macros.
///
/// This module is not a stable user-facing API. It is public only because macro expansion
/// happens in the downstream crate.
#[doc(hidden)]
pub mod __private {
pub use async_trait;
pub use schemars;
pub use serde;
pub use serde_json;
}
@@ -30,7 +30,7 @@ pub enum AuthRequirement {
/// リクエスト毎に認証ヘッダを動的に組み立てるプロバイダ。
///
/// access token が refresh で更新されたり、複数ヘッダを同時に注入する
/// 必要があるケースで使う。実体は呼び出し側に置き、llm-engine
/// 必要があるケースで使う。実体は呼び出し側に置き、agen
/// trait を知るだけ。
///
/// 返したヘッダはそのまま `HeaderMap` に挿入される。`Authorization`
@@ -18,6 +18,9 @@ pub enum ClientError {
message: String,
retry_after: Option<Duration>,
},
/// The provider rejected the request because it exceeded the model context window.
/// Classified only from a structured provider error code, never message text.
ContextWindowExceeded,
/// A request lifecycle phase exceeded its hard timeout.
Timeout {
phase: &'static str,
@@ -48,6 +51,7 @@ impl fmt::Display for ClientError {
}
write!(f, ": {}", message)
}
ClientError::ContextWindowExceeded => write!(f, "Model context window reached"),
ClientError::Timeout { phase, timeout } => {
write!(f, "{phase} timed out after {}s", timeout.as_secs())
}
@@ -112,7 +116,10 @@ pub fn is_retryable(error: &ClientError) -> bool {
ClientError::Api { status: None, .. } => false,
ClientError::Timeout { .. } => true,
ClientError::Http(e) => e.is_connect() || e.is_timeout(),
ClientError::Json(_) | ClientError::Sse(_) | ClientError::Config(_) => false,
ClientError::ContextWindowExceeded
| ClientError::Json(_)
| ClientError::Sse(_)
| ClientError::Config(_) => false,
}
}
@@ -8,7 +8,7 @@ use std::time::Duration;
/// 指数バックオフ + ジッター + 累積タイムアウトを表すポリシー。
///
/// `Default` は llm-engine 全体の固定値を返す。呼び出し側からの上書きが
/// `Default` は agen 全体の固定値を返す。呼び出し側からの上書きが
/// 必要になったら拡張する。
#[derive(Debug, Clone)]
pub struct RetryPolicy {
@@ -5,10 +5,13 @@
use serde::Serialize;
use serde_json::Value;
use crate::llm_client::{
Request,
capability::{ModelCapability, ReasoningControl, ReasoningSupport},
types::{Item, Role, ToolDefinition, parse_tool_arguments},
use crate::{
llm_client::{
Request,
capability::{ModelCapability, ReasoningControl, ReasoningSupport},
types::{ContentPart, Item, Role, ToolDefinition, image_data_url, parse_tool_arguments},
},
tool::Attachment,
};
use super::OpenAIScheme;
@@ -134,7 +137,7 @@ impl OpenAIScheme {
}
// Convert items to messages
messages.extend(self.convert_items_to_messages(&request.items));
messages.extend(self.convert_items_to_messages(&request.items, capability.vision));
let tools = request.tools.iter().map(|t| self.convert_tool(t)).collect();
@@ -185,12 +188,38 @@ impl OpenAIScheme {
/// - Assistant messages have role "assistant"
/// - Tool calls are within assistant messages as tool_calls array
/// - Tool results have role "tool" with tool_call_id
fn convert_items_to_messages(&self, items: &[Item]) -> Vec<OpenAIMessage> {
fn flush_pending_tool_result_images(
messages: &mut Vec<OpenAIMessage>,
pending_images: &mut Vec<OpenAIContentPart>,
) {
if !pending_images.is_empty() {
messages.push(OpenAIMessage {
role: "user".to_string(),
content: Some(OpenAIContent::Parts(std::mem::take(pending_images))),
tool_calls: vec![],
tool_call_id: None,
name: None,
});
}
}
fn convert_items_to_messages(
&self,
items: &[Item],
supports_images: bool,
) -> Vec<OpenAIMessage> {
let mut messages = Vec::new();
let mut pending_tool_calls: Vec<OpenAIToolCall> = Vec::new();
let mut pending_assistant_text: Option<String> = None;
let mut pending_tool_result_images: Vec<OpenAIContentPart> = Vec::new();
for item in items {
if !matches!(item, Item::ToolResult { .. }) {
Self::flush_pending_tool_result_images(
&mut messages,
&mut pending_tool_result_images,
);
}
match item {
Item::Message { role, content, .. } => {
// Flush pending tool calls
@@ -205,16 +234,17 @@ impl OpenAIScheme {
Role::Assistant => "assistant",
Role::System => "system",
};
let text_content: String = content
.iter()
.map(|p| p.as_text())
.collect::<Vec<_>>()
.join("");
let message_content = OpenAIContent::Text(
content
.iter()
.map(ContentPart::as_text)
.collect::<Vec<_>>()
.join(""),
);
messages.push(OpenAIMessage {
role: openai_role.to_string(),
content: Some(OpenAIContent::Text(text_content)),
content: Some(message_content),
tool_calls: vec![],
tool_call_id: None,
name: None,
@@ -244,19 +274,35 @@ impl OpenAIScheme {
call_id,
summary,
content,
attachments,
..
} => {
// Flush pending tool calls before tool result
// OpenAI requires every parallel tool result before a new user message.
self.flush_pending_assistant(
&mut messages,
&mut pending_tool_calls,
&mut pending_assistant_text,
);
let text = match content {
let mut text = match content {
Some(c) => format!("{summary}\n{c}"),
None => summary.clone(),
};
if supports_images {
pending_tool_result_images.extend(attachments.iter().map(|attachment| {
let Attachment::Image(image) = attachment;
OpenAIContentPart::ImageUrl {
image_url: ImageUrl {
url: image_data_url(image.mime_type(), image.data()),
},
}
}));
} else if !attachments.is_empty() {
text.push_str(&format!(
"\n[{} image attachment(s) omitted: model does not support images]",
attachments.len()
));
}
messages.push(OpenAIMessage {
role: "tool".to_string(),
content: Some(OpenAIContent::Text(text)),
@@ -284,6 +330,7 @@ impl OpenAIScheme {
&mut pending_tool_calls,
&mut pending_assistant_text,
);
Self::flush_pending_tool_result_images(&mut messages, &mut pending_tool_result_images);
messages
}
@@ -334,6 +381,13 @@ mod tests {
}
}
fn vision_cap() -> ModelCapability {
ModelCapability {
vision: true,
..cap()
}
}
#[test]
fn test_build_simple_request() {
let scheme = OpenAIScheme::new();
@@ -439,4 +493,94 @@ mod tests {
assert_eq!(body.messages[1].tool_calls.len(), 1);
assert_eq!(body.messages[2].role, "tool");
}
#[test]
fn parallel_tool_results_precede_durable_image_projection() {
let scheme = OpenAIScheme::new();
let image = std::sync::Arc::<[u8]>::from(&b"\x89PNG\r\n\x1a\nbody"[..]);
let request = Request::new()
.item(Item::tool_call("call_image", "ViewImage", "{}"))
.item(Item::tool_call("call_text", "Read", "{}"))
.item(Item::tool_result_item_with_attachments(
"call_image",
"Attached image",
None,
false,
vec![crate::tool::Attachment::Image(
crate::tool::ImageAttachment::new("image/png", image),
)],
))
.item(Item::tool_result_item(
"call_text",
"Read text",
None,
false,
));
let json = serde_json::to_value(
&scheme
.build_request("gpt-4o", &request, &vision_cap())
.messages,
)
.unwrap();
assert_eq!(json[0]["role"], "assistant");
assert_eq!(json[1]["role"], "tool");
assert_eq!(json[2]["role"], "tool");
assert_eq!(json[3]["role"], "user");
assert_eq!(json[3]["content"][0]["type"], "image_url");
}
#[test]
fn durable_tool_image_is_deterministically_lowered_to_following_user_content() {
let scheme = OpenAIScheme::new();
let image = std::sync::Arc::<[u8]>::from(&b"\x89PNG\r\n\x1a\nbody"[..]);
let attachment = crate::tool::Attachment::Image(crate::tool::ImageAttachment::new(
"image/png",
image.clone(),
));
let item = Item::tool_result_item_with_attachments(
"call_image",
"Attached image",
None,
false,
vec![attachment],
);
let persisted = serde_json::to_string(&item).unwrap();
assert!(persisted.contains("attachments"));
let restored: Item = serde_json::from_str(&persisted).unwrap();
let request = Request::new()
.item(Item::tool_call(
"call_image",
"ViewImage",
r#"{"path":"a.png"}"#,
))
.item(restored);
let body = scheme.build_request("gpt-4o", &request, &vision_cap());
let json = serde_json::to_value(&body.messages).unwrap();
let rebuilt = serde_json::to_value(
&scheme
.build_request("gpt-4o", &request, &vision_cap())
.messages,
)
.unwrap();
assert_eq!(rebuilt, json);
assert_eq!(json[0]["role"], "assistant");
assert_eq!(json[1]["role"], "tool");
assert_eq!(json[2]["role"], "user");
assert_eq!(json[2]["content"][0]["type"], "image_url");
assert!(
json[2]["content"][0]["image_url"]["url"]
.as_str()
.unwrap()
.starts_with("data:image/png;base64,")
);
let mut no_vision = cap();
no_vision.vision = false;
let disabled =
serde_json::to_string(&scheme.build_request("gpt-4o", &request, &no_vision)).unwrap();
assert!(!disabled.contains("data:image"));
}
}
@@ -4,8 +4,8 @@
//! function_call item が first-class で、SSE イベントも `response.*` 名前空間で
//! 流れる。
//!
//! - リクエスト JSON 生成: [`request`]
//! - SSE イベントパース → [`Event`](crate::llm_client::event::Event) 変換: [`events`]
//! - リクエスト JSON 生成: `request`
//! - SSE イベントパース → [`Event`](crate::llm_client::event::Event) 変換: `events`
mod capability;
mod events;
@@ -7,14 +7,31 @@
use serde::{Serialize, Serializer};
use serde_json::Value;
use crate::llm_client::{
Request,
capability::{ModelCapability, ReasoningControl, ReasoningSupport},
types::{ContentPart, Item, Role, ToolDefinition, parse_tool_arguments},
use crate::{
llm_client::{
Request,
capability::{ModelCapability, ReasoningControl, ReasoningSupport},
types::{ContentPart, Item, Role, ToolDefinition, image_data_url, parse_tool_arguments},
},
tool::Attachment,
};
use super::OpenAIResponsesScheme;
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub(crate) enum FunctionCallOutputBody {
Text(String),
ContentItems(Vec<FunctionCallOutputContentItem>),
}
#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum FunctionCallOutputContentItem {
InputText { text: String },
InputImage { image_url: String },
}
/// `/v1/responses` のリクエスト body。
#[derive(Debug, Serialize)]
pub(crate) struct ResponsesRequest {
@@ -91,9 +108,7 @@ pub(crate) enum InputItem {
/// function tool の結果(user 側)。
FunctionCallOutput {
call_id: String,
/// Responses は文字列 or 構造化 output を許すが、ここでは
/// `summary` + `content` を改行連結した文字列で送る。
output: String,
output: FunctionCallOutputBody,
},
/// reasoning item。`encrypted_content` があれば必ず添える。
Reasoning {
@@ -118,6 +133,7 @@ pub(crate) enum InputItem {
pub(crate) enum InputContent {
/// user / developer 側のテキスト
InputText { text: String },
/// user 側の画像
/// assistant 側のテキスト
OutputText { text: String },
}
@@ -173,7 +189,7 @@ impl OpenAIResponsesScheme {
request: &Request,
capability: &ModelCapability,
) -> ResponsesRequest {
let input = convert_items_to_input(&request.items);
let input = convert_items_to_input(&request.items, capability.vision);
let tools = request.tools.iter().map(convert_tool).collect();
// Reasoning 投影: capability が Effort / Both をサポートし、かつ
@@ -234,7 +250,7 @@ impl OpenAIResponsesScheme {
}
/// `Item` 列を `input[]` に変換する。
fn convert_items_to_input(items: &[Item]) -> Vec<InputItem> {
fn convert_items_to_input(items: &[Item], supports_images: bool) -> Vec<InputItem> {
let mut out = Vec::with_capacity(items.len());
for item in items {
match item {
@@ -247,7 +263,7 @@ fn convert_items_to_input(items: &[Item]) -> Vec<InputItem> {
};
let parts: Vec<InputContent> = content
.iter()
.map(|p| match p {
.map(|part| match part {
ContentPart::Text { text } => text_variant(text.clone()),
ContentPart::Refusal { refusal } => text_variant(refusal.clone()),
})
@@ -275,15 +291,33 @@ fn convert_items_to_input(items: &[Item]) -> Vec<InputItem> {
call_id,
summary,
content,
attachments,
..
} => {
let text = match content {
Some(c) => format!("{summary}\n{c}"),
None => summary.clone(),
};
let output = if attachments.is_empty() {
FunctionCallOutputBody::Text(text)
} else if supports_images {
let mut parts = vec![FunctionCallOutputContentItem::InputText { text }];
parts.extend(attachments.iter().map(|attachment| {
let Attachment::Image(image) = attachment;
FunctionCallOutputContentItem::InputImage {
image_url: image_data_url(image.mime_type(), image.data()),
}
}));
FunctionCallOutputBody::ContentItems(parts)
} else {
FunctionCallOutputBody::Text(format!(
"{text}\n[{} image attachment(s) omitted: model does not support images]",
attachments.len()
))
};
out.push(InputItem::FunctionCallOutput {
call_id: call_id.clone(),
output: text,
output,
});
}
Item::Reasoning {
@@ -690,4 +724,51 @@ mod tests {
assert_eq!(json["tools"][0]["type"], "function");
assert_eq!(json["tools"][0]["name"], "t");
}
#[test]
fn durable_tool_image_uses_function_call_output_content_items() {
let scheme = OpenAIResponsesScheme::new();
let image = std::sync::Arc::<[u8]>::from(&b"\x89PNG\r\n\x1a\nbody"[..]);
let item = Item::tool_result_item_with_attachments(
"call_image",
"Attached image",
None,
false,
vec![crate::tool::Attachment::Image(
crate::tool::ImageAttachment::new("image/png", image),
)],
);
let persisted = serde_json::to_string(&item).unwrap();
let restored: Item = serde_json::from_str(&persisted).unwrap();
let req = Request::new()
.item(Item::tool_call(
"call_image",
"ViewImage",
r#"{"path":"a.png"}"#,
))
.item(restored);
let body = scheme.build_request("gpt-5", &req, &cap_with_reasoning());
let json = serde_json::to_value(&body).unwrap();
assert_eq!(json["input"][1]["type"], "function_call_output");
assert_eq!(json["input"].as_array().unwrap().len(), 2);
assert_eq!(json["input"][1]["output"][0]["type"], "input_text");
assert_eq!(json["input"][1]["output"][1]["type"], "input_image");
assert!(
json["input"][1]["output"][1]["image_url"]
.as_str()
.unwrap()
.starts_with("data:image/png;base64,")
);
let rebuilt =
serde_json::to_value(scheme.build_request("gpt-5", &req, &cap_with_reasoning()))
.unwrap();
assert_eq!(rebuilt["input"], json["input"]);
let mut no_vision = cap_with_reasoning();
no_vision.vision = false;
let disabled =
serde_json::to_string(&scheme.build_request("gpt-5", &req, &no_vision)).unwrap();
assert!(!disabled.contains("data:image"));
}
}
@@ -431,13 +431,7 @@ fn api_error_code(error: &ClientError) -> Option<&str> {
}
fn is_context_length_exceeded(error: &ClientError) -> bool {
match error {
ClientError::Api { code, message, .. } => {
code.as_deref() == Some("context_length_exceeded")
|| message.contains("context_length_exceeded")
}
_ => false,
}
matches!(error, ClientError::ContextWindowExceeded)
}
async fn response_with_timeout(
@@ -487,6 +481,9 @@ async fn classify_error_response(resp: reqwest::Response) -> ClientError {
.and_then(|v| v.as_str())
.unwrap_or(&text)
.to_string();
if code.as_deref() == Some("context_length_exceeded") {
return ClientError::ContextWindowExceeded;
}
ClientError::Api {
status: Some(status),
code,
@@ -9,12 +9,19 @@
use std::{fmt, sync::Arc};
use crate::tool::{Attachment, ToolResultDisposition};
use base64::Engine as _;
use serde::{Deserialize, Serialize};
fn is_false(value: &bool) -> bool {
!*value
}
pub(crate) fn image_data_url(media_type: &str, data: &[u8]) -> String {
let encoded = base64::engine::general_purpose::STANDARD.encode(data);
format!("data:{media_type};base64,{encoded}")
}
// ============================================================================
// Item - The core unit of conversation
// ============================================================================
@@ -62,7 +69,7 @@ impl fmt::Debug for RequestTrace {
/// # Examples
///
/// ```ignore
/// use llm_engine::Item;
/// use agen::Item;
///
/// let user = Item::user_message("Hello!");
/// let assistant = Item::assistant_message("Hi there!");
@@ -114,9 +121,15 @@ pub enum Item {
/// Detailed output (removed by pruning when old enough)
#[serde(default, skip_serializing_if = "Option::is_none")]
content: Option<String>,
/// Typed terminal state used for replay and recovery.
#[serde(default, skip_serializing_if = "ToolResultDisposition::is_success")]
disposition: ToolResultDisposition,
/// Whether the tool result represents an execution error.
#[serde(default, skip_serializing_if = "is_false")]
is_error: bool,
/// Durable binary details (removed with `content` by normal pruning).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
attachments: Vec<Attachment>,
},
/// Reasoning/thinking item
@@ -251,12 +264,56 @@ impl Item {
content: Option<String>,
is_error: bool,
) -> Self {
Self::tool_result_item_with_disposition_and_attachments(
call_id,
summary,
content,
if is_error {
ToolResultDisposition::Error
} else {
ToolResultDisposition::Success
},
Vec::new(),
)
}
/// Create a tool result item with durable, prunable structured attachments.
pub fn tool_result_item_with_attachments(
call_id: impl Into<String>,
summary: impl Into<String>,
content: Option<String>,
is_error: bool,
attachments: Vec<Attachment>,
) -> Self {
Self::tool_result_item_with_disposition_and_attachments(
call_id,
summary,
content,
if is_error {
ToolResultDisposition::Error
} else {
ToolResultDisposition::Success
},
attachments,
)
}
pub fn tool_result_item_with_disposition_and_attachments(
call_id: impl Into<String>,
summary: impl Into<String>,
content: Option<String>,
disposition: ToolResultDisposition,
attachments: Vec<Attachment>,
) -> Self {
let is_error = !disposition.is_success();
Self::ToolResult {
id: None,
call_id: call_id.into(),
summary: summary.into(),
content,
disposition,
is_error,
attachments,
}
}
@@ -461,7 +518,7 @@ impl ContentPart {
}
}
/// Get the text content regardless of type
/// Get a textual projection of the content part.
pub fn as_text(&self) -> &str {
match self {
Self::Text { text } => text,
@@ -110,25 +110,30 @@ impl Default for PruneConfig {
}
}
/// Set `content = None` on each `Item::ToolResult` at the given indices.
/// Remove detailed text and attachments from each `Item::ToolResult` at the given indices.
///
/// Returns the number of items that were actually modified — items that
/// are already content-less are counted as 0. Intended for use on a
/// request-context clone (never on a persistent history).
/// The mandatory summary remains. Returns the number of items that were actually
/// modified — results that already contain no detail are counted as 0. Intended
/// for use on a request-context clone (never on a persistent history).
pub fn project(items: &mut [Item], indices: &[usize]) -> usize {
let mut count = 0;
for &i in indices {
if let Item::ToolResult { content, .. } = &mut items[i]
&& content.is_some()
if let Item::ToolResult {
content,
attachments,
..
} = &mut items[i]
&& (content.is_some() || !attachments.is_empty())
{
*content = None;
attachments.clear();
count += 1;
}
}
count
}
/// Indices of `Item::ToolResult { content: Some(_), .. }` that lie before
/// Indices of detailed `Item::ToolResult` values that lie before
/// the suffix protected by `protected_tokens`. Pure: does not mutate `items`.
///
/// Returns an empty vector when token estimates are unavailable (`NoData`) or
@@ -159,8 +164,10 @@ pub fn evaluate_candidates(
.enumerate()
.filter_map(|(i, item)| match item {
Item::ToolResult {
content: Some(_), ..
} => Some(i),
content,
attachments,
..
} if content.is_some() || !attachments.is_empty() => Some(i),
_ => None,
})
.collect();
@@ -373,6 +380,38 @@ mod tests {
}
}
#[test]
fn project_drops_image_detail_but_keeps_summary_and_persistent_source() {
let original = vec![Item::tool_result_item_with_attachments(
"call_image",
"Attached image/png image (12 bytes)",
None,
false,
vec![crate::tool::Attachment::Image(
crate::tool::ImageAttachment::new("image/png", b"image-body".to_vec()),
)],
)];
let mut request_context = original.clone();
let estimates = uniform_estimates(&original, 100);
assert_eq!(prunable_indices(&original, 0, &estimates), vec![0]);
assert_eq!(project(&mut request_context, &[0]), 1);
assert!(matches!(
&request_context[0],
Item::ToolResult {
summary,
content: None,
attachments,
..
} if summary == "Attached image/png image (12 bytes)" && attachments.is_empty()
));
assert!(matches!(
&original[0],
Item::ToolResult { attachments, .. } if attachments.len() == 1
));
}
#[test]
fn project_skips_already_pruned_items() {
// indices points at an item whose content is already None.
@@ -19,12 +19,12 @@ mod private {
/// - Editing message history (add, delete, clear)
/// - Registering tools and hooks
///
/// Can transition to [`Locked`] state via `Engine::lock()`.
/// Can transition to [`Locked`] state via `Engine::lock(&history)`.
///
/// # Examples
///
/// ```ignore
/// use llm_engine::Engine;
/// use agen::Engine;
///
/// let mut engine = Engine::new(client)
/// .system_prompt("You are helpful.");
@@ -348,7 +348,7 @@ where
/// # Examples
///
/// ```ignore
/// use llm_engine::{Timeline, Handler, TextBlockKind, TextBlockEvent};
/// use agen::{Timeline, Handler, TextBlockKind, TextBlockEvent};
///
/// struct MyHandler;
/// impl Handler<TextBlockKind> for MyHandler {
@@ -3,11 +3,18 @@
//! Traits for defining tools callable by LLM.
//! Usually auto-implemented using the `#[tool]` macro.
use std::collections::HashMap;
use std::sync::Arc;
use std::{
collections::HashMap,
fmt,
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
use serde_json::Value;
use thiserror::Error;
@@ -23,6 +30,12 @@ pub enum ToolError {
/// Internal error
#[error("Internal error: {0}")]
Internal(String),
/// Cooperative cancellation completed with bounded terminal output.
#[error("Tool execution cancelled")]
Cancelled(ToolOutput),
/// Execution was interrupted with a confirmed bounded terminal output.
#[error("Tool execution interrupted")]
Interrupted(ToolOutput),
}
// =============================================================================
@@ -89,19 +102,112 @@ pub(crate) fn truncate_content(content: &mut String, limit: usize) {
content.push_str(&suffix_template.replace("%BYTES%", &dropped.to_string()));
}
#[derive(Clone, PartialEq, Eq)]
pub struct ImageAttachment {
mime_type: String,
data: Arc<[u8]>,
}
impl ImageAttachment {
pub fn new(mime_type: impl Into<String>, data: impl Into<Arc<[u8]>>) -> Self {
Self {
mime_type: mime_type.into(),
data: data.into(),
}
}
pub fn mime_type(&self) -> &str {
&self.mime_type
}
pub fn data(&self) -> &[u8] {
&self.data
}
}
impl fmt::Debug for ImageAttachment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageAttachment")
.field("mime_type", &self.mime_type)
.field("bytes", &self.data.len())
.finish()
}
}
#[derive(Serialize, Deserialize)]
struct ImageAttachmentWire {
mime_type: String,
data: String,
}
impl Serialize for ImageAttachment {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
ImageAttachmentWire {
mime_type: self.mime_type.clone(),
data: STANDARD.encode(self.data.as_ref()),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ImageAttachment {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let wire = ImageAttachmentWire::deserialize(deserializer)?;
let data = STANDARD.decode(wire.data).map_err(D::Error::custom)?;
Ok(Self::new(wire.mime_type, data))
}
}
/// Durable binary detail emitted by a tool and handled by normal ToolResult pruning.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
pub enum Attachment {
Image(ImageAttachment),
}
/// Terminal disposition of one started tool call.
///
/// `Cancelled` means the tool confirmed cancellation. `OutcomeUnknown` means
/// execution stopped without confirmation, so neither completion nor side
/// effects may be inferred.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ToolResultDisposition {
#[default]
Success,
Error,
Interrupted,
Cancelled,
OutcomeUnknown,
}
impl ToolResultDisposition {
pub const fn is_success(&self) -> bool {
matches!(self, Self::Success)
}
}
/// Tool execution result.
///
/// Every output has a mandatory `summary` (1-2 lines) that persists in
/// conversation history even after pruning. The optional `content` carries
/// full details and is removed by the Prune mechanism when the context
/// grows too large.
/// conversation history even after pruning. Optional text and binary details are
/// committed to history and may later be omitted only by normal ToolResult pruning.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolOutput {
/// Short summary (1-2 lines). Always remains in history.
pub summary: String,
/// Detailed output. Removed by Prune when old enough.
/// Detailed text output. Removed by Prune when old enough.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// Durable binary details handled by the same pruning lifecycle as `content`.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<Attachment>,
}
impl From<String> for ToolOutput {
@@ -110,6 +216,7 @@ impl From<String> for ToolOutput {
ToolOutput {
summary: s,
content: None,
attachments: Vec::new(),
}
} else {
let lines = s.lines().count();
@@ -118,6 +225,7 @@ impl From<String> for ToolOutput {
ToolOutput {
summary,
content: Some(s),
attachments: Vec::new(),
}
}
}
@@ -249,6 +357,12 @@ impl ToolExecutionContext {
}
}
/// Identifies one live execution attempt without making the batch id a durable
/// replay or idempotency authority.
pub fn execution_id(&self) -> String {
format!("{}:{}", self.batch_id, self.call_id)
}
/// Context for direct, non-engine calls in unit tests and low-level callers.
pub fn direct() -> Self {
Self::new("direct", "direct", 0)
@@ -261,6 +375,142 @@ impl Default for ToolExecutionContext {
}
}
/// The provider-confirmed terminal result of one started tool execution.
///
/// `OutcomeUnknown` is reserved for an execution task that had to be force-closed
/// or failed before the provider could confirm its terminal result.
#[derive(Debug)]
pub enum ToolExecutionTerminal {
Confirmed(Result<ToolOutput, ToolError>),
OutcomeUnknown,
}
/// The completion future paired with a [`ToolExecutionHandle`]. Dropping this
/// future does not drop the provider execution: the spawned execution remains
/// owned by its handle until it completes or is explicitly force-closed.
pub struct ToolExecutionTerminalFuture {
task: tokio::task::JoinHandle<Result<ToolOutput, ToolError>>,
}
impl Future for ToolExecutionTerminalFuture {
type Output = ToolExecutionTerminal;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match Pin::new(&mut self.task).poll(cx) {
Poll::Ready(Ok(result)) => Poll::Ready(ToolExecutionTerminal::Confirmed(result)),
Poll::Ready(Err(_)) => Poll::Ready(ToolExecutionTerminal::OutcomeUnknown),
Poll::Pending => Poll::Pending,
}
}
}
/// Live ownership and control for one started tool execution.
///
/// Execution, cancellation, and terminal confirmation remain provider-owned:
/// this handle starts `Tool::execute`, delegates cooperative cancellation to
/// `Tool::cancel_execution`, and treats execution-future completion as the
/// provider's terminal confirmation. Agen may force-close only after its caller's
/// deadline expires, at which point the outcome is necessarily unknown.
#[derive(Clone)]
pub struct ToolExecutionHandle {
inner: Arc<ToolExecutionHandleInner>,
}
struct ToolExecutionHandleInner {
tool: Arc<dyn Tool>,
context: ToolExecutionContext,
abort: tokio::task::AbortHandle,
}
impl Drop for ToolExecutionHandleInner {
fn drop(&mut self) {
// Losing the final live owner is an explicit forced close, never a
// best-effort detached provider future.
self.abort.abort();
}
}
impl fmt::Debug for ToolExecutionHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ToolExecutionHandle")
.field("call_id", &self.inner.context.call_id)
.field("batch_id", &self.inner.context.batch_id)
.finish_non_exhaustive()
}
}
impl ToolExecutionHandle {
pub fn start(
tool: Arc<dyn Tool>,
input_json: String,
context: ToolExecutionContext,
) -> (Self, ToolExecutionTerminalFuture) {
let execution_tool = Arc::clone(&tool);
let execution_context = context.clone();
let task =
tokio::spawn(
async move { execution_tool.execute(&input_json, execution_context).await },
);
let abort = task.abort_handle();
(
Self {
inner: Arc::new(ToolExecutionHandleInner {
tool,
context,
abort,
}),
},
ToolExecutionTerminalFuture { task },
)
}
pub fn context(&self) -> &ToolExecutionContext {
&self.inner.context
}
pub async fn cancel_before(&self, deadline: tokio::time::Instant) -> Result<(), ToolError> {
match tokio::time::timeout_at(
deadline,
self.inner.tool.cancel_execution(&self.inner.context),
)
.await
{
Ok(result) => result,
Err(_) => Err(ToolError::Internal(format!(
"tool cancellation request exceeded its deadline for call {}",
self.inner.context.call_id
))),
}
}
pub fn force_close(&self) {
self.inner.abort.abort();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ToolExecutionPolicy {
/// Time a pause waits for already-started providers to reach a natural safe
/// boundary before escalating to explicit cooperative cancellation.
pub pause_safe_boundary_timeout: std::time::Duration,
/// Maximum time allowed for a provider to accept one cooperative
/// cancellation request.
pub cancellation_request_timeout: std::time::Duration,
/// Maximum time allowed for all providers to confirm terminal results after
/// cancellation has been requested.
pub terminal_confirmation_timeout: std::time::Duration,
}
impl Default for ToolExecutionPolicy {
fn default() -> Self {
Self {
pause_safe_boundary_timeout: std::time::Duration::from_millis(100),
cancellation_request_timeout: std::time::Duration::from_millis(100),
terminal_confirmation_timeout: std::time::Duration::from_millis(500),
}
}
}
// =============================================================================
// Tool trait
// =============================================================================
@@ -291,7 +541,7 @@ impl Default for ToolExecutionContext {
/// # Manual Implementation
///
/// ```ignore
/// use llm_engine::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolDefinition, ToolOutput};
/// use agen::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolDefinition, ToolOutput};
/// use std::sync::Arc;
///
/// struct MyTool { counter: std::sync::atomic::AtomicUsize }
@@ -329,6 +579,26 @@ pub trait Tool: Send + Sync {
input_json: &str,
ctx: ToolExecutionContext,
) -> Result<ToolOutput, ToolError>;
/// Request cooperative cancellation for one started call.
///
/// Implementations that own cancellable provider operations should signal
/// every live execution identified by `call_id`, then let `execute` return
/// the confirmed bounded terminal output. Direct callers may use this
/// compatibility surface; Agen uses [`Tool::cancel_execution`] so providers
/// can bind cancellation to one exact live attempt.
async fn cancel(&self, _call_id: &str) -> Result<(), ToolError> {
Ok(())
}
/// Request cooperative cancellation for one exact started execution.
///
/// The default preserves existing tools by delegating to `cancel(call_id)`.
/// Providers with their own execution registry should override this method
/// and key cancellation by [`ToolExecutionContext::execution_id`].
async fn cancel_execution(&self, ctx: &ToolExecutionContext) -> Result<(), ToolError> {
self.cancel(&ctx.call_id).await
}
}
// =============================================================================
@@ -356,6 +626,9 @@ pub struct ToolCall {
pub struct ToolResult {
/// Corresponding tool call ID
pub tool_use_id: String,
/// Typed terminal state.
#[serde(default, skip_serializing_if = "ToolResultDisposition::is_success")]
pub disposition: ToolResultDisposition,
/// Short summary (always kept in history)
pub summary: String,
/// Detailed output (prunable)
@@ -364,16 +637,29 @@ pub struct ToolResult {
/// Whether this is an error
#[serde(default)]
pub is_error: bool,
/// Durable binary details (prunable with `content`).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<Attachment>,
}
impl ToolResult {
/// Create a success result from a [`ToolOutput`].
pub fn from_output(tool_use_id: impl Into<String>, output: ToolOutput) -> Self {
Self::from_output_with_disposition(tool_use_id, output, ToolResultDisposition::Success)
}
pub fn from_output_with_disposition(
tool_use_id: impl Into<String>,
output: ToolOutput,
disposition: ToolResultDisposition,
) -> Self {
Self {
tool_use_id: tool_use_id.into(),
disposition,
summary: output.summary,
content: output.content,
is_error: false,
is_error: !disposition.is_success(),
attachments: output.attachments,
}
}
@@ -381,9 +667,26 @@ impl ToolResult {
pub fn error(tool_use_id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
tool_use_id: tool_use_id.into(),
disposition: ToolResultDisposition::Error,
summary: message.into(),
content: None,
is_error: true,
attachments: Vec::new(),
}
}
/// Close an execution whose completion and side effects cannot be confirmed.
pub fn outcome_unknown(tool_use_id: impl Into<String>) -> Self {
Self {
tool_use_id: tool_use_id.into(),
disposition: ToolResultDisposition::OutcomeUnknown,
summary: "Tool execution outcome unknown".to_string(),
content: Some(
"Execution was interrupted before completion could be confirmed. Completion and side effects are unknown."
.to_string(),
),
is_error: true,
attachments: Vec::new(),
}
}
}
+210
View File
@@ -0,0 +1,210 @@
mod common;
use agen::interceptor::{
AssistantTurnEndContext, Interceptor, InterceptorCallId, InterceptorInvocation,
InterceptorPhase, InterceptorResult, PendingHistoryAppendsContext, PreLlmRequestContext,
PreRequestAction, PromptAction, PromptSubmitContext, RunExitContext, TurnEndAction,
};
use agen::llm_client::event::{Event, ResponseStatus, StatusEvent};
use agen::{Engine, EngineError, History, HistoryEntry, Item, Role};
use async_trait::async_trait;
use common::MockLlmClient;
use std::sync::{Arc, Mutex};
fn completed_text_events(text: &str) -> Vec<Event> {
vec![
Event::text_block_start(0),
Event::text_delta(0, text),
Event::text_block_stop(0, None),
Event::Status(StatusEvent {
status: ResponseStatus::Completed,
}),
]
}
#[tokio::test]
async fn run_preserves_item_annotations_without_projecting_them() {
let client = MockLlmClient::new(completed_text_events("assistant reply"));
let engine = Engine::<_, agen::state::Mutable, String>::new_annotated(client);
let mut history = History::<String>::new();
let mut next = 0usize;
let mut annotate = |item: &Item| {
next += 1;
let kind = match item {
Item::Message { role, .. } => match role {
Role::User => "user",
Role::Assistant => "assistant",
Role::System => "system",
},
Item::ToolCall { .. } => "tool_call",
Item::ToolResult { .. } => "tool_result",
Item::Reasoning { .. } => "reasoning",
};
Ok(format!("{next}:{kind}"))
};
let output = engine
.run_with_annotation(&mut history, "hello", &mut annotate)
.await;
assert!(matches!(output.result, agen::EngineRunExit::Finished));
assert_eq!(history.len(), 2);
assert_eq!(history.entries()[0].annotation, "1:user");
assert_eq!(history.entries()[1].annotation, "2:assistant");
assert_eq!(history.items_cloned().len(), 2);
}
#[derive(Clone)]
struct AnnotationObservingInterceptor {
observed: Arc<Mutex<Vec<(InterceptorInvocation, Vec<String>)>>>,
}
impl AnnotationObservingInterceptor {
fn record(&self, invocation: &InterceptorInvocation, history: &[HistoryEntry<String>]) {
self.observed.lock().unwrap().push((
invocation.clone(),
history
.iter()
.map(|entry| entry.annotation.clone())
.collect(),
));
}
}
#[async_trait]
impl Interceptor<String> for AnnotationObservingInterceptor {
async fn on_prompt_submit(
&self,
context: PromptSubmitContext<'_, String>,
) -> InterceptorResult<PromptAction> {
self.record(&context.invocation, context.history);
Ok(PromptAction::Continue)
}
async fn pending_history_appends(
&self,
context: PendingHistoryAppendsContext<'_, String>,
) -> InterceptorResult<Vec<Item>> {
self.record(&context.invocation, context.history);
Ok(Vec::new())
}
async fn pre_llm_request(
&self,
context: PreLlmRequestContext<'_, String>,
) -> InterceptorResult<PreRequestAction> {
self.record(&context.invocation, context.history);
Ok(PreRequestAction::Continue)
}
async fn on_assistant_turn_end(
&self,
context: AssistantTurnEndContext<'_, String>,
) -> InterceptorResult<TurnEndAction> {
assert_eq!(context.assistant_entries.len(), 1);
assert_eq!(context.assistant_entries[0].annotation, "2:assistant");
self.record(&context.invocation, context.history);
Ok(TurnEndAction::Finish)
}
async fn on_run_exit(&self, context: RunExitContext<'_, String>) -> InterceptorResult<()> {
self.record(&context.invocation, context.history);
Ok(())
}
}
#[tokio::test]
async fn interceptor_contexts_preserve_annotations_and_typed_lifecycle_identity() {
let client = MockLlmClient::new(completed_text_events("assistant reply"));
let mut engine = Engine::<_, agen::state::Mutable, String>::new_annotated(client);
let observed = Arc::new(Mutex::new(Vec::new()));
engine.set_interceptor(AnnotationObservingInterceptor {
observed: observed.clone(),
});
let mut history = History::<String>::new();
let mut next = 0usize;
let mut annotate = |item: &Item| {
next += 1;
let kind = if item.is_assistant_message() {
"assistant"
} else {
"user"
};
Ok(format!("{next}:{kind}"))
};
let output = engine
.run_with_annotation(&mut history, "hello", &mut annotate)
.await;
assert!(matches!(output.result, agen::EngineRunExit::Finished));
let observed = observed.lock().unwrap();
let phases: Vec<_> = observed
.iter()
.map(|(invocation, _)| invocation.phase)
.collect();
assert_eq!(
phases,
[
InterceptorPhase::PromptSubmit,
InterceptorPhase::PendingHistoryAppends,
InterceptorPhase::PreLlmRequest,
InterceptorPhase::AssistantTurnEnd,
InterceptorPhase::RunExit,
]
);
assert!(
observed
.iter()
.all(|(invocation, _)| invocation.run_id == observed[0].0.run_id)
);
assert_eq!(
observed
.iter()
.map(|(invocation, _)| invocation.counters.invocation.get())
.collect::<Vec<_>>(),
[0, 1, 2, 3, 4]
);
assert_eq!(observed[2].0.call_id, Some(InterceptorCallId::Llm(0)));
assert_eq!(observed[3].0.call_id, Some(InterceptorCallId::Llm(0)));
assert_eq!(observed[1].1, ["1:user"]);
assert_eq!(observed[2].1, ["1:user"]);
assert_eq!(observed[3].1, ["1:user", "2:assistant"]);
assert_eq!(observed[4].1, ["1:user", "2:assistant"]);
}
#[test]
fn append_failure_does_not_make_item_live() {
let client = MockLlmClient::new(vec![]);
let mut engine = Engine::<_, agen::state::Mutable, usize>::new_annotated(client);
let mut history = History::<usize>::new();
let mut fail = |_item: &Item| Err("commit failed".to_string());
let err = engine
.append_history_with(&mut history, [Item::user_message("uncommitted")], &mut fail)
.unwrap_err();
assert!(matches!(err, EngineError::HistoryAppend(message) if message == "commit failed"));
assert!(history.is_empty());
}
#[test]
fn replacement_keeps_items_and_annotations_together() {
let mut history = History::from_entries(vec![
HistoryEntry::new(Item::user_message("old"), "old-ann".to_string()),
HistoryEntry::new(Item::user_message("second"), "second-ann".to_string()),
]);
history.truncate(1);
assert_eq!(history.entries()[0].item.as_text(), Some("old"));
assert_eq!(history.entries()[0].annotation, "old-ann");
let previous = history.replace_entries(vec![HistoryEntry::new(
Item::user_message("restored"),
"restored-ann".to_string(),
)]);
assert_eq!(previous.len(), 1);
assert_eq!(history.entries()[0].item.as_text(), Some("restored"));
assert_eq!(history.entries()[0].annotation, "restored-ann");
}
@@ -8,13 +8,13 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use agen::Engine;
use agen::llm_client::event::{Event, ResponseStatus, StatusEvent as ClientStatusEvent};
use agen::llm_client::retry::RetryPolicy;
use agen::llm_client::{ClientError, LlmClient, Request, ResponseStream};
use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use async_trait::async_trait;
use common::MockLlmClient;
use llm_engine::Engine;
use llm_engine::llm_client::event::{Event, ResponseStatus, StatusEvent as ClientStatusEvent};
use llm_engine::llm_client::retry::RetryPolicy;
use llm_engine::llm_client::{ClientError, LlmClient, Request, ResponseStream};
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
#[derive(Clone)]
struct FailOnceClient {
@@ -58,6 +58,7 @@ async fn test_callback_llm_retry_event() {
max_attempts: 2,
total_timeout: Duration::from_secs(1),
});
let mut history = agen::History::new();
let notices = Arc::new(Mutex::new(Vec::new()));
let sink = notices.clone();
@@ -65,8 +66,11 @@ async fn test_callback_llm_retry_event() {
sink.lock().unwrap().push((llm_call, notice.clone()));
});
let result = engine.run("retry once").await;
assert!(result.is_ok(), "engine should succeed after one retry");
let result = engine.run(&mut history, "retry once").await;
assert!(
matches!(result.result, agen::EngineRunExit::Finished),
"engine should succeed after one retry"
);
let notices = notices.lock().unwrap();
assert_eq!(notices.len(), 1);
@@ -91,6 +95,7 @@ async fn test_callback_text_block_events() {
let client = MockLlmClient::new(events);
let mut engine = Engine::new(client);
let mut history = agen::History::new();
let text_deltas = Arc::new(Mutex::new(Vec::new()));
let text_completes = Arc::new(Mutex::new(Vec::new()));
@@ -108,9 +113,12 @@ async fn test_callback_text_block_events() {
});
});
// Mutable::run consumes self, returns (Locked, EngineResult)
let result = engine.run("Greet me").await;
assert!(result.is_ok(), "Engine should complete");
// Mutable::run consumes self, returns (Locked, EngineRunExit)
let result = engine.run(&mut history, "Greet me").await;
assert!(
matches!(result.result, agen::EngineRunExit::Finished),
"Engine should complete"
);
let deltas = text_deltas.lock().unwrap();
assert_eq!(deltas.len(), 2);
@@ -137,6 +145,7 @@ async fn test_callback_tool_call_complete() {
let client = MockLlmClient::new(events);
let mut engine = Engine::new(client);
let mut history = agen::History::new();
let tool_starts = Arc::new(Mutex::new(Vec::<(String, String)>::new()));
let tool_completes = Arc::new(Mutex::new(Vec::new()));
@@ -154,8 +163,8 @@ async fn test_callback_tool_call_complete() {
});
});
// Mutable::run consumes self, returns (Locked, EngineResult)
let _ = engine.run("Weather please").await;
// Mutable::run consumes self, returns (Locked, EngineRunExit)
let _ = engine.run(&mut history, "Weather please").await;
let starts = tool_starts.lock().unwrap();
assert_eq!(starts.len(), 1);
@@ -183,6 +192,7 @@ async fn test_callback_turn_events() {
let client = MockLlmClient::new(events);
let mut engine = Engine::new(client);
let mut history = agen::History::new();
let turn_starts = Arc::new(Mutex::new(Vec::new()));
let turn_ends = Arc::new(Mutex::new(Vec::new()));
@@ -197,9 +207,9 @@ async fn test_callback_turn_events() {
ends.lock().unwrap().push(turn);
});
// Mutable::run consumes self, returns (Locked, EngineResult)
let result = engine.run("Do something").await;
assert!(result.is_ok());
// Mutable::run consumes self, returns (Locked, EngineRunExit)
let result = engine.run(&mut history, "Do something").await;
assert!(matches!(result.result, agen::EngineRunExit::Finished));
let starts = turn_starts.lock().unwrap();
let ends = turn_ends.lock().unwrap();
@@ -221,7 +231,7 @@ impl Tool for FixedOutputTool {
async fn execute(
&self,
_input_json: &str,
_ctx: llm_engine::tool::ToolExecutionContext,
_ctx: agen::tool::ToolExecutionContext,
) -> Result<ToolOutput, ToolError> {
Ok(self.output.clone())
}
@@ -254,12 +264,14 @@ async fn test_callback_tool_result_events() {
let client = MockLlmClient::new(events);
let mut engine = Engine::new(client);
let mut history = agen::History::new();
engine.register_tool(fixed_tool(
"fixed",
ToolOutput {
summary: "did the thing".into(),
content: Some("full detail body".into()),
attachments: Vec::new(),
},
));
@@ -275,7 +287,7 @@ async fn test_callback_tool_result_events() {
));
});
let _ = engine.run("call it").await;
let _ = engine.run(&mut history, "call it").await;
let observed = captured.lock().unwrap();
assert_eq!(observed.len(), 1);
@@ -296,7 +308,7 @@ impl Tool for ErroringTool {
async fn execute(
&self,
_input_json: &str,
_ctx: llm_engine::tool::ToolExecutionContext,
_ctx: agen::tool::ToolExecutionContext,
) -> Result<ToolOutput, ToolError> {
Err(ToolError::ExecutionFailed(self.message.clone()))
}
@@ -329,6 +341,7 @@ async fn test_callback_tool_result_error_path() {
let client = MockLlmClient::new(events);
let mut engine = Engine::new(client);
let mut history = agen::History::new();
engine.register_tool(erroring_tool("erroring", "boom"));
@@ -344,7 +357,7 @@ async fn test_callback_tool_result_error_path() {
));
});
let _ = engine.run("fail it").await;
let _ = engine.run(&mut history, "fail it").await;
let observed = captured.lock().unwrap();
assert_eq!(observed.len(), 1);
@@ -373,6 +386,7 @@ async fn test_callback_usage_events() {
let client = MockLlmClient::new(events);
let mut engine = Engine::new(client);
let mut history = agen::History::new();
let usage_events = Arc::new(Mutex::new(Vec::new()));
@@ -381,8 +395,8 @@ async fn test_callback_usage_events() {
usages.lock().unwrap().push(event.clone());
});
// Mutable::run consumes self, returns (Locked, EngineResult)
let _ = engine.run("Hello").await;
// Mutable::run consumes self, returns (Locked, EngineRunExit)
let _ = engine.run(&mut history, "Hello").await;
let usages = usage_events.lock().unwrap();
assert_eq!(usages.len(), 1);
@@ -6,11 +6,11 @@ use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use agen::llm_client::event::{BlockType, DeltaContent, Event};
use agen::llm_client::{ClientError, LlmClient, Request};
use agen::timeline::{Handler, TextBlockEvent, TextBlockKind, Timeline};
use async_trait::async_trait;
use futures::Stream;
use llm_engine::llm_client::event::{BlockType, DeltaContent, Event};
use llm_engine::llm_client::{ClientError, LlmClient, Request};
use llm_engine::timeline::{Handler, TextBlockEvent, TextBlockKind, Timeline};
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -19,6 +19,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
pub struct MockLlmClient {
responses: Arc<Vec<Vec<Event>>>,
call_count: Arc<AtomicUsize>,
requests: Arc<Mutex<Vec<Request>>>,
}
impl MockLlmClient {
@@ -30,6 +31,7 @@ impl MockLlmClient {
Self {
responses: Arc::new(responses),
call_count: Arc::new(AtomicUsize::new(0)),
requests: Arc::new(Mutex::new(Vec::new())),
}
}
@@ -41,6 +43,10 @@ impl MockLlmClient {
pub fn event_count(&self) -> usize {
self.responses.iter().map(|v| v.len()).sum()
}
pub fn requests(&self) -> Vec<Request> {
self.requests.lock().unwrap().clone()
}
}
#[async_trait]
@@ -51,8 +57,9 @@ impl LlmClient for MockLlmClient {
async fn stream(
&self,
_request: Request,
request: Request,
) -> Result<Pin<Box<dyn Stream<Item = Result<Event, ClientError>> + Send>>, ClientError> {
self.requests.lock().unwrap().push(request);
let count = self.call_count.fetch_add(1, Ordering::SeqCst);
if count >= self.responses.len() {
return Err(ClientError::Api {
@@ -272,7 +279,7 @@ pub fn assert_timeline_integration(subdir: &str) {
});
for event in &events {
let timeline_event: llm_engine::timeline::event::Event = event.clone().into();
let timeline_event: agen::timeline::event::Event = event.clone().into();
timeline.dispatch(&timeline_event);
}
@@ -9,10 +9,10 @@ use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use agen::Engine;
use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use async_trait::async_trait;
use common::MockLlmClient;
use llm_engine::Engine;
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
/// Fixture directory path
fn fixtures_dir() -> std::path::PathBuf {
@@ -61,7 +61,7 @@ impl Tool for MockWeatherTool {
async fn execute(
&self,
input_json: &str,
_ctx: llm_engine::tool::ToolExecutionContext,
_ctx: agen::tool::ToolExecutionContext,
) -> Result<ToolOutput, ToolError> {
self.call_count.fetch_add(1, Ordering::SeqCst);
@@ -102,7 +102,7 @@ fn test_mock_client_from_fixture() {
/// Creates a client with programmatically constructed events instead of using fixture files.
#[test]
fn test_mock_client_from_events() {
use llm_engine::llm_client::event::Event;
use agen::llm_client::event::Event;
// Specify events directly
let events = vec![
@@ -134,11 +134,15 @@ async fn test_engine_simple_text_response() {
let client = MockLlmClient::from_fixture(&fixture_path).unwrap();
let engine = Engine::new(client);
let mut history = agen::History::new();
// Send a simple message (Mutable::run consumes self, returns tuple)
let result = engine.run("Hello").await;
let result = engine.run(&mut history, "Hello").await;
assert!(result.is_ok(), "Engine should complete successfully");
assert!(
matches!(result.result, agen::EngineRunExit::Finished),
"Engine should complete successfully"
);
}
/// Verify that Engine can correctly process responses containing tool calls
@@ -156,6 +160,7 @@ async fn test_engine_tool_call() {
let client = MockLlmClient::from_fixture(&fixture_path).unwrap();
let mut engine = Engine::new(client);
let mut history = agen::History::new();
// Register tool
let weather_tool = MockWeatherTool::new();
@@ -163,7 +168,9 @@ async fn test_engine_tool_call() {
engine.register_tool(weather_tool.definition());
// Send message (Mutable::run consumes self, returns tuple)
let _result = engine.run("What's the weather in Tokyo?").await;
let _result = engine
.run(&mut history, "What's the weather in Tokyo?")
.await;
// Verify tool was called
// Note: max_turns=1 so no request is sent after tool result
@@ -180,7 +187,7 @@ async fn test_engine_tool_call() {
/// Useful when test independence is needed and external file dependency should be eliminated.
#[tokio::test]
async fn test_engine_with_programmatic_events() {
use llm_engine::llm_client::event::{Event, ResponseStatus, StatusEvent};
use agen::llm_client::event::{Event, ResponseStatus, StatusEvent};
// Construct event sequence programmatically
let events = vec![
@@ -195,11 +202,15 @@ async fn test_engine_with_programmatic_events() {
let client = MockLlmClient::new(events);
let engine = Engine::new(client);
let mut history = agen::History::new();
// Mutable::run consumes self, returns tuple
let result = engine.run("Greet me").await;
let result = engine.run(&mut history, "Greet me").await;
assert!(result.is_ok(), "Engine should complete successfully");
assert!(
matches!(result.result, agen::EngineRunExit::Finished),
"Engine should complete successfully"
);
}
/// Verify that ToolCallCollector correctly collects ToolCall from ToolUse block events
@@ -208,8 +219,8 @@ async fn test_engine_with_programmatic_events() {
/// correctly extracts id, name, and input (JSON).
#[tokio::test]
async fn test_tool_call_collector_integration() {
use llm_engine::llm_client::event::Event;
use llm_engine::timeline::{Timeline, ToolCallCollector};
use agen::llm_client::event::Event;
use agen::timeline::{Timeline, ToolCallCollector};
// Event sequence containing ToolUse block
let events = vec![
@@ -225,7 +236,7 @@ async fn test_tool_call_collector_integration() {
// Dispatch events
for event in &events {
let timeline_event: llm_engine::timeline::event::Event = event.clone().into();
let timeline_event: agen::timeline::event::Event = event.clone().into();
timeline.dispatch(&timeline_event);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More