agents-city 0.3.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.es.md +2021 -0
- package/README.md +1999 -0
- package/benchmarks/committee/README.md +39 -0
- package/benchmarks/committee/metrics.py +51 -0
- package/benchmarks/committee/run.py +64 -0
- package/benchmarks/committee/traces/01-single.json +8 -0
- package/benchmarks/committee/traces/02-mesh.json +16 -0
- package/benchmarks/committee/traces/03-chair.json +15 -0
- package/benchmarks/latency/README.md +35 -0
- package/benchmarks/latency/fake-claude-cli.mjs +119 -0
- package/benchmarks/latency/fake-native-server.mjs +340 -0
- package/benchmarks/latency/fake-tui.py +91 -0
- package/benchmarks/latency/live.py +380 -0
- package/benchmarks/stress/README.md +20 -0
- package/benchmarks/stress/run.py +487 -0
- package/bin/agents +4 -0
- package/bin/agents-city.js +116 -0
- package/bin/benchmark +17 -0
- package/bin/bus +6 -0
- package/bin/cities +3 -0
- package/bin/city +86 -0
- package/bin/committee +6 -0
- package/bin/demo +200 -0
- package/bin/exit +5 -0
- package/bin/hall +73 -0
- package/bin/hall.html +426 -0
- package/bin/logs +3 -0
- package/bin/report.py +14 -0
- package/bin/reset +3 -0
- package/bin/road +3 -0
- package/bin/seat +10 -0
- package/bin/serve.py +1428 -0
- package/bin/setup.py +753 -0
- package/bin/skills +3 -0
- package/bin/test +131 -0
- package/bin/test-adapter.py +183 -0
- package/bin/test-admision.py +99 -0
- package/bin/test-avatar.py +58 -0
- package/bin/test-benchmark.py +40 -0
- package/bin/test-broker.py +334 -0
- package/bin/test-cage.py +206 -0
- package/bin/test-card.py +402 -0
- package/bin/test-channel.py +379 -0
- package/bin/test-cities.py +453 -0
- package/bin/test-claude-runtime.py +252 -0
- package/bin/test-committee.py +340 -0
- package/bin/test-contracts.py +544 -0
- package/bin/test-crecimiento.py +164 -0
- package/bin/test-demo.py +308 -0
- package/bin/test-doctor.py +106 -0
- package/bin/test-domains.py +151 -0
- package/bin/test-evidencia.py +50 -0
- package/bin/test-exit.py +128 -0
- package/bin/test-hall-protocol.py +93 -0
- package/bin/test-launch.py +169 -0
- package/bin/test-live-feed.py +508 -0
- package/bin/test-pairing.py +99 -0
- package/bin/test-parcels.py +283 -0
- package/bin/test-runtime-failures.py +252 -0
- package/bin/test-runtime-ui.py +488 -0
- package/bin/test-runtime.py +268 -0
- package/bin/test-rutas.py +80 -0
- package/bin/test-seat.py +1386 -0
- package/bin/test-security.py +150 -0
- package/bin/test-serve.py +1057 -0
- package/bin/test-stress.py +64 -0
- package/bin/test-widgets.py +143 -0
- package/bin/test-workspace.py +188 -0
- package/bin/testlib.py +95 -0
- package/bin/tokens.py +14 -0
- package/bus/scripts/deploy.sh +60 -0
- package/bus/scripts/mint-token.sh +68 -0
- package/bus/scripts/setup-dev.sh +79 -0
- package/bus/scripts/test-channel.ts +8 -0
- package/bus/scripts/test-hub.ts +266 -0
- package/bus/scripts/test-local.sh +40 -0
- package/bus/scripts/test-queue.ts +132 -0
- package/bus/worker/package-lock.json +1971 -0
- package/bus/worker/package.json +17 -0
- package/bus/worker/src/index.ts +485 -0
- package/bus/worker/tsconfig.json +13 -0
- package/bus/worker/wrangler.toml +32 -0
- package/city/oven/README.md +59 -0
- package/city/oven/collect.py +43 -0
- package/city/oven/oven.html +4 -0
- package/city/scripts/history.py +120 -0
- package/city/scripts/seed.py +442 -0
- package/city/web/assets/modelos/License.txt +28 -0
- package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/assets/modelos/building-a.glb +0 -0
- package/city/web/assets/modelos/building-b.glb +0 -0
- package/city/web/assets/modelos/building-c.glb +0 -0
- package/city/web/assets/modelos/building-d.glb +0 -0
- package/city/web/assets/modelos/building-e.glb +0 -0
- package/city/web/assets/modelos/building-f.glb +0 -0
- package/city/web/assets/modelos/building-g.glb +0 -0
- package/city/web/assets/modelos/building-h.glb +0 -0
- package/city/web/assets/sprites/building-a.png +0 -0
- package/city/web/assets/sprites/building-b.png +0 -0
- package/city/web/assets/sprites/building-c.png +0 -0
- package/city/web/assets/sprites/building-d.png +0 -0
- package/city/web/assets/sprites/building-e.png +0 -0
- package/city/web/assets/sprites/building-f.png +0 -0
- package/city/web/assets/sprites/building-g.png +0 -0
- package/city/web/assets/sprites/building-h.png +0 -0
- package/city/web/assets/sprites/building-i.png +0 -0
- package/city/web/assets/sprites/building-j.png +0 -0
- package/city/web/assets/sprites/building-k.png +0 -0
- package/city/web/assets/sprites/building-l.png +0 -0
- package/city/web/assets/sprites/building-m.png +0 -0
- package/city/web/assets/sprites/building-n.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
- package/city/web/assets/sprites/catalogo.json +47 -0
- package/city/web/assets/sprites/detail-awning-wide.png +0 -0
- package/city/web/assets/sprites/detail-awning.png +0 -0
- package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
- package/city/web/assets/sprites/detail-overhang.png +0 -0
- package/city/web/assets/sprites/detail-parasol-a.png +0 -0
- package/city/web/assets/sprites/detail-parasol-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-c.png +0 -0
- package/city/web/assets/sprites/low-detail-building-d.png +0 -0
- package/city/web/assets/sprites/low-detail-building-e.png +0 -0
- package/city/web/assets/sprites/low-detail-building-f.png +0 -0
- package/city/web/assets/sprites/low-detail-building-g.png +0 -0
- package/city/web/assets/sprites/low-detail-building-h.png +0 -0
- package/city/web/assets/sprites/low-detail-building-i.png +0 -0
- package/city/web/assets/sprites/low-detail-building-j.png +0 -0
- package/city/web/assets/sprites/low-detail-building-k.png +0 -0
- package/city/web/assets/sprites/low-detail-building-l.png +0 -0
- package/city/web/assets/sprites/low-detail-building-m.png +0 -0
- package/city/web/assets/sprites/low-detail-building-n.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/assets/sprites/medidas.json +248 -0
- package/city/web/dist/city.js +939 -0
- package/city/web/dist/index.html +294 -0
- package/city/web/dist/modelos/License.txt +28 -0
- package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/dist/modelos/building-a.glb +0 -0
- package/city/web/dist/modelos/building-b.glb +0 -0
- package/city/web/dist/modelos/building-c.glb +0 -0
- package/city/web/dist/modelos/building-d.glb +0 -0
- package/city/web/dist/modelos/building-e.glb +0 -0
- package/city/web/dist/modelos/building-f.glb +0 -0
- package/city/web/dist/modelos/building-g.glb +0 -0
- package/city/web/dist/modelos/building-h.glb +0 -0
- package/city/web/dist/sprites/building-a.png +0 -0
- package/city/web/dist/sprites/building-b.png +0 -0
- package/city/web/dist/sprites/building-c.png +0 -0
- package/city/web/dist/sprites/building-d.png +0 -0
- package/city/web/dist/sprites/building-e.png +0 -0
- package/city/web/dist/sprites/building-f.png +0 -0
- package/city/web/dist/sprites/building-g.png +0 -0
- package/city/web/dist/sprites/building-h.png +0 -0
- package/city/web/dist/sprites/building-i.png +0 -0
- package/city/web/dist/sprites/building-j.png +0 -0
- package/city/web/dist/sprites/building-k.png +0 -0
- package/city/web/dist/sprites/building-l.png +0 -0
- package/city/web/dist/sprites/building-m.png +0 -0
- package/city/web/dist/sprites/building-n.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
- package/city/web/dist/sprites/catalogo.json +47 -0
- package/city/web/dist/sprites/detail-awning-wide.png +0 -0
- package/city/web/dist/sprites/detail-awning.png +0 -0
- package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
- package/city/web/dist/sprites/detail-overhang.png +0 -0
- package/city/web/dist/sprites/detail-parasol-a.png +0 -0
- package/city/web/dist/sprites/detail-parasol-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-c.png +0 -0
- package/city/web/dist/sprites/low-detail-building-d.png +0 -0
- package/city/web/dist/sprites/low-detail-building-e.png +0 -0
- package/city/web/dist/sprites/low-detail-building-f.png +0 -0
- package/city/web/dist/sprites/low-detail-building-g.png +0 -0
- package/city/web/dist/sprites/low-detail-building-h.png +0 -0
- package/city/web/dist/sprites/low-detail-building-i.png +0 -0
- package/city/web/dist/sprites/low-detail-building-j.png +0 -0
- package/city/web/dist/sprites/low-detail-building-k.png +0 -0
- package/city/web/dist/sprites/low-detail-building-l.png +0 -0
- package/city/web/dist/sprites/low-detail-building-m.png +0 -0
- package/city/web/dist/sprites/low-detail-building-n.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/dist/sprites/medidas.json +248 -0
- package/city/web/dist-hall/hall.js +264 -0
- package/city/web/index.html +294 -0
- package/city/web/package.json +18 -0
- package/city/web/sella.py +21 -0
- package/city/web/src/activity-actors.ts +47 -0
- package/city/web/src/activity.ts +172 -0
- package/city/web/src/ayuntamiento.ts +580 -0
- package/city/web/src/draw.ts +640 -0
- package/city/web/src/game-speech.ts +79 -0
- package/city/web/src/hall.ts +1867 -0
- package/city/web/src/main.ts +2225 -0
- package/city/web/src/oven.ts +124 -0
- package/city/web/src/people.ts +244 -0
- package/city/web/src/presencia.ts +193 -0
- package/city/web/src/puertas.ts +180 -0
- package/city/web/src/repo-roles.ts +59 -0
- package/city/worker/package.json +16 -0
- package/city/worker/schema.sql +117 -0
- package/city/worker/src/index.ts +702 -0
- package/city/worker/src/square.ts +218 -0
- package/city/worker/wrangler.toml +58 -0
- package/demo/ada.md +50 -0
- package/demo/bruno.md +62 -0
- package/demo/camila.md +56 -0
- package/demo/city/ada.md +40 -0
- package/demo/city/city.yml +8 -0
- package/demo/city/parcels.yml +15 -0
- package/demo/city/roads.json +4 -0
- package/demo/city/units.yml +10 -0
- package/demo/clinica/city.yml +8 -0
- package/demo/clinica/parcels.yml +15 -0
- package/demo/clinica/roads.json +4 -0
- package/demo/clinica/units.yml +9 -0
- package/demo/clinica/vera.md +46 -0
- package/demo/dante.md +54 -0
- package/demo/despacho/city.yml +8 -0
- package/demo/despacho/marta.md +46 -0
- package/demo/despacho/parcels.yml +15 -0
- package/demo/despacho/roads.json +4 -0
- package/demo/despacho/units.yml +9 -0
- package/demo/elsa.md +50 -0
- package/demo/farid.md +52 -0
- package/demo/greta.md +50 -0
- package/demo/hugo.md +49 -0
- package/demo/iris.md +49 -0
- package/demo/jonas.md +51 -0
- package/demo/kira.md +51 -0
- package/demo/luca.md +49 -0
- package/demo/parcels.yml +92 -0
- package/demo/seed.py +20 -0
- package/demo/show.py +161 -0
- package/demo/stories.py +824 -0
- package/demo/units.yml +33 -0
- package/docs/agents-first.md +99 -0
- package/docs/glossary.md +55 -0
- package/docs/map-live-layers.md +73 -0
- package/docs/security.md +231 -0
- package/docs/self-host.md +151 -0
- package/docs/testing.md +205 -0
- package/package.json +73 -0
- package/plugin/.claude-plugin/plugin.json +62 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/channel/activity-cli.ts +16 -0
- package/plugin/channel/adapter-prompts.ts +113 -0
- package/plugin/channel/adapter.js +4292 -0
- package/plugin/channel/adapter.ts +133 -0
- package/plugin/channel/bus.js +20188 -0
- package/plugin/channel/bus.ts +131 -0
- package/plugin/channel/city-config.ts +125 -0
- package/plugin/channel/claude-channel.ts +58 -0
- package/plugin/channel/cli-args.ts +75 -0
- package/plugin/channel/client.js +4200 -0
- package/plugin/channel/client.ts +38 -0
- package/plugin/channel/committee/activity.ts +256 -0
- package/plugin/channel/committee/collection.ts +154 -0
- package/plugin/channel/committee/decision.ts +175 -0
- package/plugin/channel/committee/floor.ts +135 -0
- package/plugin/channel/committee/guards.ts +30 -0
- package/plugin/channel/committee/history.ts +38 -0
- package/plugin/channel/committee/render.ts +102 -0
- package/plugin/channel/committee/service.ts +72 -0
- package/plugin/channel/committee/storage.ts +92 -0
- package/plugin/channel/committee/types.ts +163 -0
- package/plugin/channel/committee/view.ts +63 -0
- package/plugin/channel/committee-cli.ts +109 -0
- package/plugin/channel/delivery-metrics.ts +61 -0
- package/plugin/channel/delivery-queue.ts +118 -0
- package/plugin/channel/hub/activity-controller.ts +72 -0
- package/plugin/channel/hub/activity-feed.ts +169 -0
- package/plugin/channel/hub/committee-controller.ts +60 -0
- package/plugin/channel/hub/connections.ts +48 -0
- package/plugin/channel/hub/diagnostics.ts +67 -0
- package/plugin/channel/hub/envelope-validity.ts +109 -0
- package/plugin/channel/hub/envelopes.ts +66 -0
- package/plugin/channel/hub/lifecycle.ts +91 -0
- package/plugin/channel/hub/local-roads.ts +89 -0
- package/plugin/channel/hub/remote-roads.ts +165 -0
- package/plugin/channel/hub/road-controller.ts +99 -0
- package/plugin/channel/hub-client.ts +149 -0
- package/plugin/channel/local-hub.js +5995 -0
- package/plugin/channel/local-hub.ts +349 -0
- package/plugin/channel/map-reporter.ts +48 -0
- package/plugin/channel/package-lock.json +1731 -0
- package/plugin/channel/package.json +19 -0
- package/plugin/channel/protocol.ts +98 -0
- package/plugin/channel/road-cli.ts +18 -0
- package/plugin/channel/run.sh +15 -0
- package/plugin/channel/runtime/claude.ts +458 -0
- package/plugin/channel/runtime/codex-config.ts +77 -0
- package/plugin/channel/runtime/codex.ts +669 -0
- package/plugin/channel/runtime/command.ts +62 -0
- package/plugin/channel/runtime/factory.ts +15 -0
- package/plugin/channel/runtime/json-rpc.ts +154 -0
- package/plugin/channel/runtime/kimi.ts +277 -0
- package/plugin/channel/runtime/opencode.ts +248 -0
- package/plugin/channel/runtime/process.ts +84 -0
- package/plugin/channel/runtime/types.ts +45 -0
- package/plugin/channel/runtime-files.ts +73 -0
- package/plugin/channel/runtime-gateway.js +6396 -0
- package/plugin/channel/runtime-gateway.ts +358 -0
- package/plugin/channel/runtime-metrics.ts +52 -0
- package/plugin/channel/runtime-subscription.ts +180 -0
- package/plugin/channel/terminal-delivery.ts +131 -0
- package/plugin/channel/untrusted.ts +60 -0
- package/plugin/commands/committee.md +31 -0
- package/plugin/commands/exit.md +32 -0
- package/plugin/commands/goals.md +20 -0
- package/plugin/commands/join.md +18 -0
- package/plugin/commands/notice.md +20 -0
- package/plugin/commands/propose.md +19 -0
- package/plugin/commands/round.md +17 -0
- package/plugin/commands/session.md +22 -0
- package/plugin/commands/settings.md +25 -0
- package/plugin/commands/setup.md +21 -0
- package/plugin/commands/team.md +23 -0
- package/plugin/domains/custom.md +27 -0
- package/plugin/domains/finance.md +31 -0
- package/plugin/domains/healthcare.md +34 -0
- package/plugin/domains/legal.md +31 -0
- package/plugin/domains/marketing.md +33 -0
- package/plugin/domains/operations.md +31 -0
- package/plugin/domains/research.md +32 -0
- package/plugin/domains/sales.md +30 -0
- package/plugin/domains/software.md +37 -0
- package/plugin/hooks/activity.sh +6 -0
- package/plugin/hooks/digging.sh +68 -0
- package/plugin/hooks/growth.sh +42 -0
- package/plugin/hooks/hooks.json +106 -0
- package/plugin/hooks/notice-on-pr.sh +35 -0
- package/plugin/hooks/notice-on-stop.sh +80 -0
- package/plugin/hooks/notice-pending.sh +58 -0
- package/plugin/hooks/solo-en-ciudad.sh +23 -0
- package/plugin/hooks/tokens.sh +47 -0
- package/plugin/roles/examples/account-executive.md +25 -0
- package/plugin/roles/examples/ai-manager.md +34 -0
- package/plugin/roles/examples/associate.md +24 -0
- package/plugin/roles/examples/brand-lead.md +20 -0
- package/plugin/roles/examples/cfo.md +20 -0
- package/plugin/roles/examples/city-lead.md +25 -0
- package/plugin/roles/examples/clinical-director.md +26 -0
- package/plugin/roles/examples/clinical-ops.md +25 -0
- package/plugin/roles/examples/clinician.md +25 -0
- package/plugin/roles/examples/compliance.md +27 -0
- package/plugin/roles/examples/content.md +24 -0
- package/plugin/roles/examples/controller.md +27 -0
- package/plugin/roles/examples/cpto.md +41 -0
- package/plugin/roles/examples/customer-success.md +25 -0
- package/plugin/roles/examples/data-engineer.md +37 -0
- package/plugin/roles/examples/data.md +60 -0
- package/plugin/roles/examples/dev.md +37 -0
- package/plugin/roles/examples/devops.md +39 -0
- package/plugin/roles/examples/enablement.md +24 -0
- package/plugin/roles/examples/ethics.md +24 -0
- package/plugin/roles/examples/fin-analytics.md +27 -0
- package/plugin/roles/examples/health-compliance.md +25 -0
- package/plugin/roles/examples/health-data.md +26 -0
- package/plugin/roles/examples/knowledge.md +25 -0
- package/plugin/roles/examples/lifecycle.md +26 -0
- package/plugin/roles/examples/llm-engineer.md +34 -0
- package/plugin/roles/examples/managing-partner.md +20 -0
- package/plugin/roles/examples/methods.md +23 -0
- package/plugin/roles/examples/operations-lead.md +23 -0
- package/plugin/roles/examples/ops.md +24 -0
- package/plugin/roles/examples/patient-safety.md +26 -0
- package/plugin/roles/examples/performance.md +27 -0
- package/plugin/roles/examples/po.md +36 -0
- package/plugin/roles/examples/process-owner.md +23 -0
- package/plugin/roles/examples/product-design.md +39 -0
- package/plugin/roles/examples/program-manager.md +24 -0
- package/plugin/roles/examples/quality.md +25 -0
- package/plugin/roles/examples/research-director.md +24 -0
- package/plugin/roles/examples/research-ops.md +25 -0
- package/plugin/roles/examples/researcher.md +25 -0
- package/plugin/roles/examples/revenue-lead.md +25 -0
- package/plugin/roles/examples/revops.md +26 -0
- package/plugin/roles/examples/seo.md +27 -0
- package/plugin/roles/examples/specialist.md +24 -0
- package/plugin/scripts/admision.py +165 -0
- package/plugin/scripts/apaga.py +232 -0
- package/plugin/scripts/avatar.py +211 -0
- package/plugin/scripts/broker.py +531 -0
- package/plugin/scripts/cage.py +252 -0
- package/plugin/scripts/capabilities.py +197 -0
- package/plugin/scripts/card.py +312 -0
- package/plugin/scripts/cities.py +520 -0
- package/plugin/scripts/city-env.sh +90 -0
- package/plugin/scripts/city-runtime.sh +87 -0
- package/plugin/scripts/city-session.sh +526 -0
- package/plugin/scripts/city_env.py +67 -0
- package/plugin/scripts/crecimiento.py +140 -0
- package/plugin/scripts/deliberations.py +48 -0
- package/plugin/scripts/doctor.py +172 -0
- package/plugin/scripts/domains.py +201 -0
- package/plugin/scripts/evidencia.py +64 -0
- package/plugin/scripts/find-repos.sh +111 -0
- package/plugin/scripts/gh.py +159 -0
- package/plugin/scripts/hall_protocol.py +147 -0
- package/plugin/scripts/hook_activity.py +139 -0
- package/plugin/scripts/launch.py +94 -0
- package/plugin/scripts/logs.py +98 -0
- package/plugin/scripts/pairing.py +185 -0
- package/plugin/scripts/parcels.py +132 -0
- package/plugin/scripts/read-card.py +51 -0
- package/plugin/scripts/report.py +201 -0
- package/plugin/scripts/reset.py +180 -0
- package/plugin/scripts/roads.py +230 -0
- package/plugin/scripts/roles.py +274 -0
- package/plugin/scripts/runtime_log.py +69 -0
- package/plugin/scripts/runtime_processes.py +132 -0
- package/plugin/scripts/rutas.py +83 -0
- package/plugin/scripts/seat.py +1349 -0
- package/plugin/scripts/tokens.py +194 -0
- package/plugin/scripts/trust-repos.py +59 -0
- package/plugin/scripts/ui.py +280 -0
- package/plugin/scripts/units.py +80 -0
- package/plugin/scripts/workspace.py +364 -0
- package/plugin/skills/city/SKILL.md +191 -0
- package/templates/blank.md +39 -0
- package/templates/finance.md +44 -0
- package/templates/legal.md +45 -0
- package/templates/marketing.md +48 -0
- package/templates/product.md +45 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
import { realpathSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { BusEnvelope } from '../protocol.js';
|
|
5
|
+
import { NativeAcceptance } from '../runtime-metrics.js';
|
|
6
|
+
import { executableFor, hasOption, optionValue } from './command.js';
|
|
7
|
+
import { WebSocketJsonRpc } from './json-rpc.js';
|
|
8
|
+
import { freeLoopbackPort, spawnNative, terminate, wait } from './process.js';
|
|
9
|
+
import { ConnectorOptions, NativeUiCommand, RuntimeActivity, RuntimeConnector } from './types.js';
|
|
10
|
+
import { unavailableMcpOverrides } from './codex-config.js';
|
|
11
|
+
|
|
12
|
+
/** The owner's one escape hatch from the workspace-write cage, shared with the
|
|
13
|
+
* seatbelt wrapper around Claude windows. Read per call: the gateway lives
|
|
14
|
+
* long and the owner may flip it between sessions. */
|
|
15
|
+
function cageOff(): boolean {
|
|
16
|
+
return process.env.CITY_CAGE === '0';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class CodexConnector implements RuntimeConnector {
|
|
20
|
+
readonly runtime = 'codex' as const;
|
|
21
|
+
readonly transport = 'codex-app-server-ws';
|
|
22
|
+
private child: ChildProcess | null = null;
|
|
23
|
+
private rpc: WebSocketJsonRpc | null = null;
|
|
24
|
+
private threadId = '';
|
|
25
|
+
private turnDone: Promise<void> = Promise.resolve();
|
|
26
|
+
private finishTurn: (() => void) | null = null;
|
|
27
|
+
private activeTurn = '';
|
|
28
|
+
private serverUrl = '';
|
|
29
|
+
private uiAttached = false;
|
|
30
|
+
private joinedThread = false;
|
|
31
|
+
private observation = 0;
|
|
32
|
+
private observingThread: Promise<void> | null = null;
|
|
33
|
+
private joinAttempt: Promise<void> | null = null;
|
|
34
|
+
private readonly loadedBeforeUi = new Set<string>();
|
|
35
|
+
private readonly logicalThreads = new Map<string, string>();
|
|
36
|
+
private configArgs: string[] = [];
|
|
37
|
+
|
|
38
|
+
constructor(private readonly options: ConnectorOptions) {}
|
|
39
|
+
|
|
40
|
+
async start(): Promise<void> {
|
|
41
|
+
let url = process.env.CITY_CODEX_APP_SERVER_URL || '';
|
|
42
|
+
if (!url) {
|
|
43
|
+
const port = await freeLoopbackPort();
|
|
44
|
+
url = `ws://127.0.0.1:${port}`;
|
|
45
|
+
const executable = executableFor(this.options.command, 'codex');
|
|
46
|
+
const overrides = unavailableMcpOverrides(executable, this.options.cwd, process.env);
|
|
47
|
+
this.configArgs = overrides.args;
|
|
48
|
+
for (const name of overrides.disabledMcpServers) {
|
|
49
|
+
this.diagnostic('codex.mcp.unavailable.disabled', {
|
|
50
|
+
actor: this.options.actor,
|
|
51
|
+
outcome: 'disabled-for-city-runtime',
|
|
52
|
+
message: name,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
this.child = spawnNative(
|
|
56
|
+
executable,
|
|
57
|
+
['app-server', ...this.configArgs, '--listen', url],
|
|
58
|
+
this.options.cwd,
|
|
59
|
+
process.env,
|
|
60
|
+
`codex:${this.options.actor}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
this.serverUrl = url;
|
|
64
|
+
this.rpc = await WebSocketJsonRpc.connect(
|
|
65
|
+
url,
|
|
66
|
+
(method, params) => this.notification(method, params),
|
|
67
|
+
async (method, params) => this.providerRequest(method, params),
|
|
68
|
+
);
|
|
69
|
+
await this.rpc.request('initialize', {
|
|
70
|
+
clientInfo: { name: 'agents-city', title: 'Agents City', version: '0.3.0-beta.21' },
|
|
71
|
+
capabilities: { experimentalApi: true },
|
|
72
|
+
});
|
|
73
|
+
await this.rpc.notify('initialized');
|
|
74
|
+
|
|
75
|
+
if (this.options.interactive) {
|
|
76
|
+
for (const id of await this.loadedThreadIds()) this.loadedBeforeUi.add(id);
|
|
77
|
+
process.stderr.write(
|
|
78
|
+
`[city-gateway:${this.options.actor}] Codex app-server ready; ` +
|
|
79
|
+
'waiting for the official TUI thread\n',
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await this.startGatewayThread();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async waitUntilReady(): Promise<void> {
|
|
88
|
+
if (this.threadId) return;
|
|
89
|
+
if (!this.rpc || !this.options.interactive) {
|
|
90
|
+
throw new Error('Codex connector cannot wait for a TUI thread before it starts');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const timeoutMs = positiveMilliseconds(process.env.CITY_CODEX_TUI_READY_TIMEOUT_MS, 300_000);
|
|
94
|
+
const deadline = Date.now() + timeoutMs;
|
|
95
|
+
let last = 'the TUI has not created a thread yet';
|
|
96
|
+
while (Date.now() < deadline) {
|
|
97
|
+
try {
|
|
98
|
+
const candidates = (await this.loadedThreadIds()).filter(
|
|
99
|
+
(id) => !this.loadedBeforeUi.has(id),
|
|
100
|
+
);
|
|
101
|
+
for (const id of candidates) {
|
|
102
|
+
const read = object(await this.rpc.request('thread/read', { threadId: id }, 3_000));
|
|
103
|
+
const thread = object(read.thread);
|
|
104
|
+
const threadCwd = String(thread.cwd || '');
|
|
105
|
+
if (!sameDirectory(threadCwd, this.options.cwd)) {
|
|
106
|
+
last = `new thread ${id} belongs to ${threadCwd || 'an unknown directory'}`;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
this.threadId = id;
|
|
110
|
+
try {
|
|
111
|
+
await this.joinThread();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!isMissingRollout(error)) {
|
|
114
|
+
this.threadId = '';
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
// Codex 0.147 exposes the live TUI thread before its rollout is
|
|
118
|
+
// materialized. Direct turn input already works at this point and
|
|
119
|
+
// is rendered by the TUI; thread/resume starts working as soon as
|
|
120
|
+
// the first user turn creates the rollout.
|
|
121
|
+
process.stderr.write(
|
|
122
|
+
`[city-gateway:${this.options.actor}] Codex TUI thread ${this.threadId} ` +
|
|
123
|
+
'adopted over WebSocket; awaiting its first rollout\n',
|
|
124
|
+
);
|
|
125
|
+
this.diagnostic('codex.thread.adopted', {
|
|
126
|
+
thread: this.threadId,
|
|
127
|
+
outcome: 'waiting-for-rollout',
|
|
128
|
+
});
|
|
129
|
+
this.observeMaterializedTuiThread();
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
last = (error as Error).message;
|
|
135
|
+
}
|
|
136
|
+
await wait(100);
|
|
137
|
+
}
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Codex TUI did not create a new thread for ${this.options.cwd} within ${timeoutMs}ms: ${last}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async startGatewayThread(): Promise<void> {
|
|
144
|
+
if (!this.rpc) throw new Error('Codex connector is not connected');
|
|
145
|
+
const started = object(
|
|
146
|
+
await this.rpc.request('thread/start', {
|
|
147
|
+
cwd: this.options.cwd,
|
|
148
|
+
...this.threadConfiguration(),
|
|
149
|
+
ephemeral: false,
|
|
150
|
+
serviceName: 'agents-city',
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
this.threadId = String(object(started.thread).id || '');
|
|
154
|
+
if (!this.threadId) throw new Error('Codex app-server did not return a thread id');
|
|
155
|
+
this.joinedThread = true;
|
|
156
|
+
process.stderr.write(
|
|
157
|
+
`[city-gateway:${this.options.actor}] Codex thread ${this.threadId} ready over WebSocket\n`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
nativeUi(): NativeUiCommand | null {
|
|
162
|
+
if (!this.serverUrl) return null;
|
|
163
|
+
return {
|
|
164
|
+
executable: executableFor(this.options.command, 'codex'),
|
|
165
|
+
args: [...this.configArgs, '--remote', this.serverUrl],
|
|
166
|
+
cwd: this.options.cwd,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setNativeUiAttached(attached: boolean): void {
|
|
171
|
+
this.uiAttached = attached;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async accept(prompt: string, envelope: BusEnvelope): Promise<NativeAcceptance> {
|
|
175
|
+
if (!this.rpc || !this.threadId) throw new Error('Codex connector is not ready');
|
|
176
|
+
await this.turnDone;
|
|
177
|
+
await this.joinMaterializedTuiThread();
|
|
178
|
+
this.turnDone = new Promise<void>((resolve) => {
|
|
179
|
+
this.finishTurn = resolve;
|
|
180
|
+
});
|
|
181
|
+
this.activeTurn = '';
|
|
182
|
+
const model = optionValue(this.options.command, ['--model', '-m']);
|
|
183
|
+
const effort = optionValue(this.options.command, ['--effort']);
|
|
184
|
+
try {
|
|
185
|
+
const response = object(
|
|
186
|
+
await this.rpc.request('turn/start', {
|
|
187
|
+
threadId: this.threadId,
|
|
188
|
+
input: [{ type: 'text', text: prompt }],
|
|
189
|
+
clientUserMessageId: envelope.id,
|
|
190
|
+
...(model ? { model } : {}),
|
|
191
|
+
...(effort ? { effort } : {}),
|
|
192
|
+
// Keep approval requests enabled so the gateway can approve them in
|
|
193
|
+
// yolo mode. `never` means "reject anything needing approval", which
|
|
194
|
+
// silently disabled app/MCP tools even when the owner chose yolo.
|
|
195
|
+
approvalPolicy: 'on-request',
|
|
196
|
+
// Same decoupling as thread/start: yolo keeps its speed (no
|
|
197
|
+
// approvals, network open) while writes stay inside the workspace.
|
|
198
|
+
// CITY_CAGE=0 restores the old fully-open behaviour, deliberately.
|
|
199
|
+
sandboxPolicy: this.sandboxPolicy(),
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
this.activeTurn = String(object(response.turn).id || '');
|
|
203
|
+
if (this.activeTurn) {
|
|
204
|
+
this.logicalThreads.set(this.activeTurn, envelope.thread || envelope.id);
|
|
205
|
+
this.trimLogicalThreads();
|
|
206
|
+
}
|
|
207
|
+
if (!this.joinedThread && this.activeTurn) {
|
|
208
|
+
this.observeMaterializedTuiThread();
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
acceptedAt: new Date().toISOString(),
|
|
212
|
+
runtime: this.runtime,
|
|
213
|
+
transport: this.transport,
|
|
214
|
+
providerRequestId: this.activeTurn || envelope.id,
|
|
215
|
+
};
|
|
216
|
+
} catch (error) {
|
|
217
|
+
this.endTurn();
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async close(): Promise<void> {
|
|
223
|
+
this.endTurn();
|
|
224
|
+
this.observation += 1;
|
|
225
|
+
this.uiAttached = false;
|
|
226
|
+
this.rpc?.close();
|
|
227
|
+
this.rpc = null;
|
|
228
|
+
await terminate(this.child);
|
|
229
|
+
this.child = null;
|
|
230
|
+
this.serverUrl = '';
|
|
231
|
+
this.configArgs = [];
|
|
232
|
+
this.joinedThread = false;
|
|
233
|
+
this.observingThread = null;
|
|
234
|
+
this.joinAttempt = null;
|
|
235
|
+
this.loadedBeforeUi.clear();
|
|
236
|
+
this.logicalThreads.clear();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async joinMaterializedTuiThread(): Promise<void> {
|
|
240
|
+
if (!this.options.interactive || this.joinedThread || !this.rpc || !this.threadId) return;
|
|
241
|
+
const read = object(await this.rpc.request('thread/read', { threadId: this.threadId }, 3_000));
|
|
242
|
+
const status = String(object(object(read.thread).status).type || '');
|
|
243
|
+
if (status && status !== 'idle') {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`Codex TUI thread ${this.threadId} is ${status}; the city assignment remains queued`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
await this.joinThread();
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (!isMissingRollout(error)) throw error;
|
|
252
|
+
this.observeMaterializedTuiThread();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The official TUI exposes a loaded thread before its first rollout exists.
|
|
258
|
+
* `thread/read` can see that shell, but it does not subscribe this WebSocket;
|
|
259
|
+
* `thread/resume` initially fails with "no rollout found". Keep retrying in
|
|
260
|
+
* the background, then replay the response history so the first direct TUI
|
|
261
|
+
* question is not lost in the gap before the subscription becomes possible.
|
|
262
|
+
*/
|
|
263
|
+
private observeMaterializedTuiThread(): void {
|
|
264
|
+
if (!this.options.interactive || this.joinedThread || this.observingThread) return;
|
|
265
|
+
const threadId = this.threadId;
|
|
266
|
+
const generation = ++this.observation;
|
|
267
|
+
const task = this.joinWhenMaterialized(threadId, generation);
|
|
268
|
+
this.observingThread = task;
|
|
269
|
+
void task.then(
|
|
270
|
+
() => {
|
|
271
|
+
if (this.observingThread === task) this.observingThread = null;
|
|
272
|
+
},
|
|
273
|
+
() => {
|
|
274
|
+
if (this.observingThread === task) this.observingThread = null;
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async joinWhenMaterialized(threadId: string, generation: number): Promise<void> {
|
|
280
|
+
let attempts = 0;
|
|
281
|
+
while (
|
|
282
|
+
this.rpc &&
|
|
283
|
+
!this.joinedThread &&
|
|
284
|
+
this.threadId === threadId &&
|
|
285
|
+
this.observation === generation
|
|
286
|
+
) {
|
|
287
|
+
attempts += 1;
|
|
288
|
+
try {
|
|
289
|
+
await this.joinThread();
|
|
290
|
+
return;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (!isMissingRollout(error) && (attempts === 1 || attempts % 20 === 0)) {
|
|
293
|
+
this.diagnostic('codex.thread.join.retry', {
|
|
294
|
+
thread: threadId,
|
|
295
|
+
outcome: 'retrying',
|
|
296
|
+
message: (error as Error).message,
|
|
297
|
+
attempt: attempts,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
await wait(attempts < 50 ? 100 : 500);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async joinThread(): Promise<void> {
|
|
306
|
+
if (this.joinedThread) return;
|
|
307
|
+
if (this.joinAttempt) return this.joinAttempt;
|
|
308
|
+
const attempt = this.resumeThread();
|
|
309
|
+
this.joinAttempt = attempt;
|
|
310
|
+
try {
|
|
311
|
+
await attempt;
|
|
312
|
+
} finally {
|
|
313
|
+
if (this.joinAttempt === attempt) this.joinAttempt = null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async resumeThread(): Promise<void> {
|
|
318
|
+
if (!this.rpc || !this.threadId) throw new Error('Codex TUI thread is unavailable');
|
|
319
|
+
const resumed = object(
|
|
320
|
+
await this.rpc.request(
|
|
321
|
+
'thread/resume',
|
|
322
|
+
{
|
|
323
|
+
threadId: this.threadId,
|
|
324
|
+
cwd: this.options.cwd,
|
|
325
|
+
...this.threadConfiguration(),
|
|
326
|
+
},
|
|
327
|
+
30_000,
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
const joinedId = String(object(resumed.thread).id || '');
|
|
331
|
+
if (!joinedId) throw new Error(`thread/resume returned no id for ${this.threadId}`);
|
|
332
|
+
this.threadId = joinedId;
|
|
333
|
+
const replayed = this.replayVisibleThread(resumed.thread);
|
|
334
|
+
if (!this.joinedThread) {
|
|
335
|
+
this.joinedThread = true;
|
|
336
|
+
process.stderr.write(
|
|
337
|
+
`[city-gateway:${this.options.actor}] Codex TUI thread ${this.threadId} ` +
|
|
338
|
+
'joined over WebSocket\n',
|
|
339
|
+
);
|
|
340
|
+
this.diagnostic('codex.thread.joined', {
|
|
341
|
+
thread: this.threadId,
|
|
342
|
+
outcome: 'ready',
|
|
343
|
+
replayedItems: replayed,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private replayVisibleThread(value: unknown): number {
|
|
349
|
+
const thread = object(value);
|
|
350
|
+
const threadId = String(thread.id || this.threadId || '');
|
|
351
|
+
const turns = Array.isArray(thread.turns) ? thread.turns : [];
|
|
352
|
+
let replayed = 0;
|
|
353
|
+
for (const rawTurn of turns) {
|
|
354
|
+
const turn = object(rawTurn);
|
|
355
|
+
const turnId = String(turn.id || '');
|
|
356
|
+
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
357
|
+
for (const rawItem of items) {
|
|
358
|
+
const item = object(rawItem);
|
|
359
|
+
if (!isVisibleItemType(String(item.type || ''))) continue;
|
|
360
|
+
this.visibleActivity('item/completed', { threadId, turnId, item });
|
|
361
|
+
replayed += 1;
|
|
362
|
+
}
|
|
363
|
+
if (turnId && turnId === this.activeTurn && turnFinished(turn.status)) {
|
|
364
|
+
this.logicalThreads.delete(turnId);
|
|
365
|
+
this.endTurn();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return replayed;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async loadedThreadIds(): Promise<string[]> {
|
|
372
|
+
if (!this.rpc) throw new Error('Codex connector is not connected');
|
|
373
|
+
const response = object(await this.rpc.request('thread/loaded/list', {}, 3_000));
|
|
374
|
+
return Array.isArray(response.data)
|
|
375
|
+
? response.data.map((id) => String(id)).filter(Boolean)
|
|
376
|
+
: [];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private threadConfiguration(): Record<string, unknown> {
|
|
380
|
+
const model = optionValue(this.options.command, ['--model', '-m']);
|
|
381
|
+
return {
|
|
382
|
+
...(model ? { model } : {}),
|
|
383
|
+
approvalPolicy: 'on-request',
|
|
384
|
+
// Approval and confinement are different axes: auto-approve means "do
|
|
385
|
+
// not ask", never "touch everything". Writes stay inside the workspace
|
|
386
|
+
// unless the owner explicitly lowers the cage with CITY_CAGE=0.
|
|
387
|
+
sandbox: this.providerSandboxIsOuterCaged() ? 'danger-full-access' : 'workspace-write',
|
|
388
|
+
developerInstructions:
|
|
389
|
+
'You are one member of an Agents City committee. Never contact repo peers directly; ' +
|
|
390
|
+
'submit evidence through the agents-city committee CLI named in each assignment.',
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private autoApprove(): boolean {
|
|
395
|
+
return (
|
|
396
|
+
this.options.autoApprove ||
|
|
397
|
+
hasOption(this.options.command, ['--dangerously-bypass-approvals-and-sandbox', '--full-auto'])
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private providerSandboxIsOuterCaged(): boolean {
|
|
402
|
+
return cageOff() || process.env.CITY_OUTER_CAGE === '1';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private sandboxPolicy(): Record<string, unknown> {
|
|
406
|
+
if (this.providerSandboxIsOuterCaged()) return { type: 'dangerFullAccess' };
|
|
407
|
+
return {
|
|
408
|
+
type: 'workspaceWrite',
|
|
409
|
+
networkAccess: this.autoApprove(),
|
|
410
|
+
writableRoots: [],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private notification(method: string, params: Record<string, unknown>): void {
|
|
415
|
+
this.visibleActivity(method, params);
|
|
416
|
+
if (method === 'item/agentMessage/delta') {
|
|
417
|
+
const delta = String(params.delta || '');
|
|
418
|
+
if (delta && !this.uiAttached) process.stdout.write(delta);
|
|
419
|
+
} else if (method === 'turn/completed') {
|
|
420
|
+
const id = String(object(params.turn).id || '');
|
|
421
|
+
if (!this.activeTurn || !id || id === this.activeTurn) {
|
|
422
|
+
if (!this.uiAttached) process.stdout.write('\n');
|
|
423
|
+
this.endTurn();
|
|
424
|
+
}
|
|
425
|
+
if (id) this.logicalThreads.delete(id);
|
|
426
|
+
} else if (method === 'error') {
|
|
427
|
+
if (!this.uiAttached) {
|
|
428
|
+
process.stderr.write(`[codex:${this.options.actor}] ${JSON.stringify(params)}\n`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Codex app-server already separates visible user/agent items from private
|
|
435
|
+
* reasoning. Only completed visible items and coarse work/lifecycle events
|
|
436
|
+
* cross into City live; deltas and reasoning items never do.
|
|
437
|
+
*/
|
|
438
|
+
private visibleActivity(method: string, params: Record<string, unknown>): void {
|
|
439
|
+
if (!this.options.onActivity) return;
|
|
440
|
+
const item = object(params.item);
|
|
441
|
+
const itemType = String(item.type || '');
|
|
442
|
+
if (itemType === 'reasoning') return;
|
|
443
|
+
const providerThread = String(params.threadId || item.threadId || this.threadId || '');
|
|
444
|
+
if (!this.eventBelongsToThread(providerThread)) return;
|
|
445
|
+
const turn = String(params.turnId || object(params.turn).id || this.activeTurn || '');
|
|
446
|
+
const thread = this.logicalThreads.get(turn) || providerThread;
|
|
447
|
+
const itemId = String(item.id || '');
|
|
448
|
+
const source = (suffix: string): string =>
|
|
449
|
+
`codex:${providerThread || 'pending'}:${turn || 'turn'}:${itemId || method}:${suffix}`;
|
|
450
|
+
const report = (activity: RuntimeActivity): void => this.options.onActivity?.(activity);
|
|
451
|
+
|
|
452
|
+
if (method === 'item/completed' && itemType === 'userMessage') {
|
|
453
|
+
const summary = visibleText(item.content || item.text);
|
|
454
|
+
if (isInternalCityPrompt(summary)) return;
|
|
455
|
+
if (summary) {
|
|
456
|
+
report({
|
|
457
|
+
sourceId: source('user-completed'),
|
|
458
|
+
kind: 'conversation.user',
|
|
459
|
+
thread: thread || null,
|
|
460
|
+
phase: 'asked',
|
|
461
|
+
tone: 'question',
|
|
462
|
+
title: `${this.options.actor} asked`,
|
|
463
|
+
summary,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (method === 'item/completed' && itemType === 'agentMessage') {
|
|
469
|
+
const summary = visibleText(item.text || item.content);
|
|
470
|
+
if (summary) {
|
|
471
|
+
const phase = String(item.phase || 'final_answer');
|
|
472
|
+
report({
|
|
473
|
+
sourceId: source(`agent-${phase}`),
|
|
474
|
+
kind: phase === 'commentary' ? 'conversation.agent.commentary' : 'conversation.agent',
|
|
475
|
+
thread: thread || null,
|
|
476
|
+
phase: phase === 'commentary' ? 'working' : 'answered',
|
|
477
|
+
tone: phase === 'commentary' ? 'work' : 'evidence',
|
|
478
|
+
title:
|
|
479
|
+
phase === 'commentary'
|
|
480
|
+
? `${this.options.actor} reported progress`
|
|
481
|
+
: `${this.options.actor} answered`,
|
|
482
|
+
summary,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (
|
|
488
|
+
(method === 'item/started' || method === 'item/completed') &&
|
|
489
|
+
itemType === 'commandExecution'
|
|
490
|
+
) {
|
|
491
|
+
const command = redactVisible(String(item.command || '')).slice(0, 1_000);
|
|
492
|
+
const output = redactVisible(String(item.aggregatedOutput || '')).slice(0, 2_000);
|
|
493
|
+
report({
|
|
494
|
+
sourceId: source(method === 'item/started' ? 'command-started' : 'command-completed'),
|
|
495
|
+
kind: method === 'item/started' ? 'work.command.started' : 'work.command.completed',
|
|
496
|
+
thread: thread || null,
|
|
497
|
+
phase: method === 'item/started' ? 'working' : String(item.status || 'completed'),
|
|
498
|
+
tone: item.status === 'failed' ? 'error' : 'work',
|
|
499
|
+
title:
|
|
500
|
+
method === 'item/started'
|
|
501
|
+
? `${this.options.actor} started a command`
|
|
502
|
+
: `${this.options.actor} completed a command`,
|
|
503
|
+
summary: command || 'Command execution',
|
|
504
|
+
details: output ? [output] : [],
|
|
505
|
+
});
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (method === 'item/completed' && itemType === 'fileChange') {
|
|
509
|
+
const changes = Array.isArray(item.changes)
|
|
510
|
+
? item.changes
|
|
511
|
+
.slice(0, 40)
|
|
512
|
+
.map((change) => redactVisible(visibleText(change)))
|
|
513
|
+
.filter(Boolean)
|
|
514
|
+
: [];
|
|
515
|
+
report({
|
|
516
|
+
sourceId: source('files-completed'),
|
|
517
|
+
kind: 'work.files.changed',
|
|
518
|
+
thread: thread || null,
|
|
519
|
+
phase: String(item.status || 'completed'),
|
|
520
|
+
tone: item.status === 'failed' ? 'error' : 'work',
|
|
521
|
+
title: `${this.options.actor} changed files`,
|
|
522
|
+
summary: changes.length ? `${changes.length} file change(s)` : 'File changes completed',
|
|
523
|
+
details: changes,
|
|
524
|
+
});
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (method === 'turn/started') {
|
|
528
|
+
report({
|
|
529
|
+
sourceId: source('turn-started'),
|
|
530
|
+
kind: 'runtime.turn.started',
|
|
531
|
+
thread: thread || null,
|
|
532
|
+
phase: 'working',
|
|
533
|
+
tone: 'system',
|
|
534
|
+
title: `${this.options.actor} started a turn`,
|
|
535
|
+
summary: 'Codex is working through its native WebSocket runtime.',
|
|
536
|
+
});
|
|
537
|
+
} else if (method === 'turn/completed') {
|
|
538
|
+
report({
|
|
539
|
+
sourceId: source('turn-completed'),
|
|
540
|
+
kind: 'runtime.turn.completed',
|
|
541
|
+
thread: thread || null,
|
|
542
|
+
phase: String(object(params.turn).status || 'completed'),
|
|
543
|
+
tone: 'system',
|
|
544
|
+
title: `${this.options.actor} completed a turn`,
|
|
545
|
+
summary: 'Codex completed the turn.',
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private eventBelongsToThread(thread: string): boolean {
|
|
551
|
+
if (!thread) return Boolean(this.threadId);
|
|
552
|
+
if (this.threadId) return thread === this.threadId;
|
|
553
|
+
return this.options.interactive && !this.loadedBeforeUi.has(thread);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private async providerRequest(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
557
|
+
if (method === 'item/permissions/requestApproval') {
|
|
558
|
+
const requested = object(params.permissions);
|
|
559
|
+
return {
|
|
560
|
+
permissions: this.autoApprove()
|
|
561
|
+
? {
|
|
562
|
+
...(requested.network ? { network: requested.network } : {}),
|
|
563
|
+
...(requested.fileSystem ? { fileSystem: requested.fileSystem } : {}),
|
|
564
|
+
}
|
|
565
|
+
: {},
|
|
566
|
+
scope: 'turn',
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (method === 'execCommandApproval' || method === 'applyPatchApproval') {
|
|
570
|
+
return {
|
|
571
|
+
decision: this.autoApprove()
|
|
572
|
+
? 'approved'
|
|
573
|
+
: { denied: { rejection: 'Agents City auto approval is disabled' } },
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
if (method.includes('requestApproval')) {
|
|
577
|
+
return { decision: this.autoApprove() ? 'accept' : 'decline' };
|
|
578
|
+
}
|
|
579
|
+
throw new Error(`unsupported Codex app-server request: ${method}`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private endTurn(): void {
|
|
583
|
+
this.activeTurn = '';
|
|
584
|
+
const finish = this.finishTurn;
|
|
585
|
+
this.finishTurn = null;
|
|
586
|
+
finish?.();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private trimLogicalThreads(): void {
|
|
590
|
+
while (this.logicalThreads.size > 200) {
|
|
591
|
+
const first = this.logicalThreads.keys().next().value;
|
|
592
|
+
if (!first) return;
|
|
593
|
+
this.logicalThreads.delete(first);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private diagnostic(event: string, fields: Record<string, unknown>): void {
|
|
598
|
+
this.options.onDiagnostic?.(event, fields);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function object(value: unknown): Record<string, unknown> {
|
|
603
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
604
|
+
? (value as Record<string, unknown>)
|
|
605
|
+
: {};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function visibleText(value: unknown): string {
|
|
609
|
+
if (typeof value === 'string') return value.trim().slice(0, 4_000);
|
|
610
|
+
if (Array.isArray(value)) {
|
|
611
|
+
return value
|
|
612
|
+
.map((part) => visibleText(part))
|
|
613
|
+
.filter(Boolean)
|
|
614
|
+
.join('\n')
|
|
615
|
+
.trim()
|
|
616
|
+
.slice(0, 4_000);
|
|
617
|
+
}
|
|
618
|
+
const item = object(value);
|
|
619
|
+
return visibleText(item.text || item.content || item.path || item.name || '');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function redactVisible(value: string): string {
|
|
623
|
+
return value
|
|
624
|
+
.replace(/([?&](?:token|secret|key)=)[^&\s]+/gi, '$1[redacted]')
|
|
625
|
+
.replace(/Bearer\s+\S+/gi, 'Bearer [redacted]')
|
|
626
|
+
.replace(/(Authorization:\s*)\S+(?:\s+\S+)?/gi, '$1[redacted]')
|
|
627
|
+
.replace(/(--(?:token|password|secret|api-key)(?:=|\s+))\S+/gi, '$1[redacted]')
|
|
628
|
+
.replace(/((?:TOKEN|SECRET|PASSWORD|API_KEY)=)[^\s]+/gi, '$1[redacted]');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function sameDirectory(left: string, right: string): boolean {
|
|
632
|
+
if (!left || !right) return false;
|
|
633
|
+
return canonicalDirectory(left) === canonicalDirectory(right);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function canonicalDirectory(path: string): string {
|
|
637
|
+
const absolute = resolve(path);
|
|
638
|
+
try {
|
|
639
|
+
return realpathSync.native(absolute);
|
|
640
|
+
} catch {
|
|
641
|
+
return absolute;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function positiveMilliseconds(value: string | undefined, fallback: number): number {
|
|
646
|
+
const parsed = Number(value || '');
|
|
647
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function isMissingRollout(error: unknown): boolean {
|
|
651
|
+
return /no rollout found/i.test((error as Error).message || '');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function isInternalCityPrompt(value: string): boolean {
|
|
655
|
+
const prompt = value.trimStart();
|
|
656
|
+
return (
|
|
657
|
+
prompt.startsWith('[Agents City authenticated local bus]') ||
|
|
658
|
+
/^<channel\b[^>]*\bsource=["']plugin:city:city-bus["']/i.test(prompt)
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function isVisibleItemType(type: string): boolean {
|
|
663
|
+
return ['userMessage', 'agentMessage', 'commandExecution', 'fileChange'].includes(type);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function turnFinished(value: unknown): boolean {
|
|
667
|
+
const status = typeof value === 'string' ? value : String(object(value).type || '');
|
|
668
|
+
return ['completed', 'failed', 'interrupted', 'cancelled'].includes(status);
|
|
669
|
+
}
|