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,487 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stress one city with mixed Claude stream-json and Codex app-server actors."""
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import signal
|
|
9
|
+
import socket
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
import urllib.parse
|
|
15
|
+
|
|
16
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
ROOT = os.path.dirname(os.path.dirname(HERE))
|
|
18
|
+
GATEWAY = os.path.join(ROOT, 'plugin', 'channel', 'runtime-gateway.js')
|
|
19
|
+
CLIENT = os.path.join(ROOT, 'plugin', 'channel', 'client.js')
|
|
20
|
+
FAKE = os.path.join(ROOT, 'benchmarks', 'latency', 'fake-native-server.mjs')
|
|
21
|
+
CLAUDE_FAKE = os.path.join(ROOT, 'benchmarks', 'latency', 'fake-claude-cli.mjs')
|
|
22
|
+
CHANNEL_DIR = os.path.join(ROOT, 'plugin', 'channel')
|
|
23
|
+
SNAPSHOT_JS = r"""
|
|
24
|
+
const WebSocket = require('ws');
|
|
25
|
+
const ws = new WebSocket(process.argv[1], {origin:'http://127.0.0.1:43111'});
|
|
26
|
+
const timer = setTimeout(() => process.exit(3), 5000);
|
|
27
|
+
ws.on('message', raw => {
|
|
28
|
+
let value; try { value = JSON.parse(String(raw)); } catch { return; }
|
|
29
|
+
if (value.type !== 'activity.state') return;
|
|
30
|
+
clearTimeout(timer); process.stdout.write(JSON.stringify(value)); ws.close();
|
|
31
|
+
setTimeout(() => process.exit(0), 10);
|
|
32
|
+
});
|
|
33
|
+
ws.on('error', () => {});
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_args():
|
|
38
|
+
parser = argparse.ArgumentParser(
|
|
39
|
+
description='Exercise one local city with an even Claude/Codex mix.')
|
|
40
|
+
parser.add_argument('--agents', type=int, default=40,
|
|
41
|
+
help='total actors; must be even (default: 40)')
|
|
42
|
+
parser.add_argument('--rounds', type=int, default=2,
|
|
43
|
+
help='fan-out rounds; round two follows a forced hub restart (default: 2)')
|
|
44
|
+
parser.add_argument('--timeout', type=int, default=20,
|
|
45
|
+
help='seconds allowed per startup or fan-out phase (default: 20)')
|
|
46
|
+
parser.add_argument('--json', action='store_true', help='print only JSON')
|
|
47
|
+
parser.add_argument('--keep', action='store_true', help='keep temporary artifacts')
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
if args.agents < 2 or args.agents % 2:
|
|
50
|
+
parser.error('--agents must be an even integer of at least 2')
|
|
51
|
+
if args.rounds < 1:
|
|
52
|
+
parser.error('--rounds must be at least 1')
|
|
53
|
+
return args
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main():
|
|
57
|
+
args = parse_args()
|
|
58
|
+
report = run(args)
|
|
59
|
+
if args.json:
|
|
60
|
+
print(json.dumps(report, sort_keys=True))
|
|
61
|
+
else:
|
|
62
|
+
print_report(report)
|
|
63
|
+
return 0 if report.get('status') == 'pass' else 1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run(args):
|
|
67
|
+
report = {
|
|
68
|
+
'schema': 'agents-city/mixed-runtime-stress@1',
|
|
69
|
+
'agents': args.agents,
|
|
70
|
+
'mix': {'claude-stream-json': args.agents // 2,
|
|
71
|
+
'codex-app-server-ws': args.agents // 2},
|
|
72
|
+
'rounds': [],
|
|
73
|
+
'liveFeed': {'transport': 'authenticated-read-only-websocket'},
|
|
74
|
+
'status': 'failed',
|
|
75
|
+
}
|
|
76
|
+
base = tempfile.mkdtemp(prefix='agents-city-stress-')
|
|
77
|
+
app = os.path.join(base, 'app')
|
|
78
|
+
city = os.path.join(app, 'stress', 'forty')
|
|
79
|
+
repos = os.path.join(base, 'repos')
|
|
80
|
+
fake_bin = os.path.join(base, 'bin')
|
|
81
|
+
os.makedirs(city)
|
|
82
|
+
os.makedirs(repos)
|
|
83
|
+
os.makedirs(fake_bin)
|
|
84
|
+
fake_claude = os.path.join(fake_bin, 'claude')
|
|
85
|
+
os.symlink(CLAUDE_FAKE, fake_claude)
|
|
86
|
+
actors = mixed_actors(args.agents)
|
|
87
|
+
write_city(city, actors)
|
|
88
|
+
for _, actor in actors:
|
|
89
|
+
os.makedirs(os.path.join(repos, actor))
|
|
90
|
+
port = free_port()
|
|
91
|
+
codex_capture = os.path.join(base, 'codex.jsonl')
|
|
92
|
+
claude_capture = os.path.join(base, 'claude.jsonl')
|
|
93
|
+
claude_behavior = os.path.join(base, 'claude.behavior')
|
|
94
|
+
open(claude_behavior, 'w', encoding='utf-8').write('healthy\n')
|
|
95
|
+
env = dict(
|
|
96
|
+
os.environ,
|
|
97
|
+
AGENTS_CITY_HOME=app,
|
|
98
|
+
AGENTS_CITY_DATA=city,
|
|
99
|
+
AGENTS_CITY_USER='stress',
|
|
100
|
+
CITY_RUNTIME_AUTO='1',
|
|
101
|
+
CITY_BUS_DEBUG='1',
|
|
102
|
+
CITY_CODEX_APP_SERVER_URL=f'ws://127.0.0.1:{port}',
|
|
103
|
+
CITY_CLAUDE_FAKE_CAPTURE=claude_capture,
|
|
104
|
+
CITY_CLAUDE_FAKE_BEHAVIOR=claude_behavior,
|
|
105
|
+
CITY_CLAUDE_STARTUP_TIMEOUT_MS='2000',
|
|
106
|
+
)
|
|
107
|
+
for key in ('CITY_BUS_URL', 'CITY_BUS_TOKEN', 'CITY_DIR'):
|
|
108
|
+
env.pop(key, None)
|
|
109
|
+
runtime_dir = os.path.join(app, '.runtime', 'bus', 'city-forty')
|
|
110
|
+
endpoint_path = os.path.join(runtime_dir, 'endpoint.json')
|
|
111
|
+
metrics_path = os.path.join(runtime_dir, 'runtime-latency.jsonl')
|
|
112
|
+
hub_log = os.path.join(runtime_dir, 'hub.log')
|
|
113
|
+
gateways = []
|
|
114
|
+
gateway_streams = []
|
|
115
|
+
fake = None
|
|
116
|
+
started = time.monotonic()
|
|
117
|
+
try:
|
|
118
|
+
fake = subprocess.Popen(
|
|
119
|
+
['node', FAKE, 'codex', str(port), codex_capture],
|
|
120
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
121
|
+
if not wait_port(port, args.timeout):
|
|
122
|
+
raise RuntimeError('the Codex app-server double did not start')
|
|
123
|
+
|
|
124
|
+
for runtime, actor in actors:
|
|
125
|
+
log = open(os.path.join(base, f'{actor}.log'), 'w+', encoding='utf-8')
|
|
126
|
+
gateway_streams.append(log)
|
|
127
|
+
command = fake_claude if runtime == 'claude' else 'codex'
|
|
128
|
+
gateways.append(subprocess.Popen(
|
|
129
|
+
['node', GATEWAY, '--data', city, '--actor', actor,
|
|
130
|
+
'--cwd', os.path.join(repos, actor), '--command', command,
|
|
131
|
+
'--auto', '1', '--interactive', '0'],
|
|
132
|
+
env=env, stdin=subprocess.DEVNULL,
|
|
133
|
+
stdout=log, stderr=log, text=True))
|
|
134
|
+
|
|
135
|
+
addresses = [f'connected runtime:{actor}' for _, actor in actors]
|
|
136
|
+
if not wait_for(lambda: all(marker in read(hub_log) for marker in addresses),
|
|
137
|
+
args.timeout):
|
|
138
|
+
raise RuntimeError('not all mixed runtimes authenticated on the city hub')
|
|
139
|
+
if any(process.poll() is not None for process in gateways):
|
|
140
|
+
raise RuntimeError('a Codex gateway exited during startup')
|
|
141
|
+
startup_hubs = hub_pids(city)
|
|
142
|
+
if len(startup_hubs) != 1:
|
|
143
|
+
raise RuntimeError(f'concurrent startup created {len(startup_hubs)} hubs: {startup_hubs}')
|
|
144
|
+
report['startupMs'] = round((time.monotonic() - started) * 1000)
|
|
145
|
+
report['startupHubProcesses'] = len(startup_hubs)
|
|
146
|
+
|
|
147
|
+
participants = [actor for _, actor in actors]
|
|
148
|
+
for round_number in range(1, args.rounds + 1):
|
|
149
|
+
if round_number == 2:
|
|
150
|
+
current = hub_pids(city)
|
|
151
|
+
stopped_pid = current[0] if len(current) == 1 else 0
|
|
152
|
+
if not stopped_pid or not stop_pid(stopped_pid):
|
|
153
|
+
raise RuntimeError(f'could not force the single hub outage: {current}')
|
|
154
|
+
# Forty reconnecting clients can elect the replacement before
|
|
155
|
+
# this process observes the brief unlink. That is successful
|
|
156
|
+
# recovery, not a stale endpoint. What must disappear is the
|
|
157
|
+
# stopped PID's ownership; the endpoint may validly already name
|
|
158
|
+
# the one new hub.
|
|
159
|
+
if not wait_for(
|
|
160
|
+
lambda: endpoint_released_or_replaced(endpoint_path, stopped_pid), 4
|
|
161
|
+
):
|
|
162
|
+
raise RuntimeError('the stopped hub still owns the endpoint')
|
|
163
|
+
round_started = time.monotonic()
|
|
164
|
+
thread = open_committee(participants, round_number, env)
|
|
165
|
+
if not thread:
|
|
166
|
+
raise RuntimeError(f'round {round_number} did not open a committee')
|
|
167
|
+
feed = spectator_snapshot(endpoint_path)
|
|
168
|
+
opened_feed = [
|
|
169
|
+
event for event in feed.get('events', [])
|
|
170
|
+
if event.get('thread') == thread
|
|
171
|
+
and event.get('kind') == 'committee.opened'
|
|
172
|
+
]
|
|
173
|
+
invited = '\n'.join(
|
|
174
|
+
str(detail) for event in opened_feed
|
|
175
|
+
for detail in event.get('details', []))
|
|
176
|
+
if len(opened_feed) != 1 or not all(actor in invited for actor in participants):
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
f'round {round_number} spectator feed did not represent all '
|
|
179
|
+
f'{args.agents} selected actors exactly once')
|
|
180
|
+
if not wait_for(
|
|
181
|
+
lambda: round_complete(thread, claude_capture, codex_capture, metrics_path,
|
|
182
|
+
args.agents // 2, args.agents),
|
|
183
|
+
args.timeout,
|
|
184
|
+
):
|
|
185
|
+
raise RuntimeError(
|
|
186
|
+
f'round {round_number} did not reach all {args.agents} native runtimes')
|
|
187
|
+
if not wait_for(lambda: outboxes_empty(runtime_dir, participants), 3):
|
|
188
|
+
raise RuntimeError(f'round {round_number} left a durable outbox undrained')
|
|
189
|
+
fanout_ms = round((time.monotonic() - round_started) * 1000)
|
|
190
|
+
time.sleep(.35)
|
|
191
|
+
claude_count = len(records_for_thread(claude_capture, thread))
|
|
192
|
+
codex_count = len(records_for_thread(codex_capture, thread))
|
|
193
|
+
metrics = [row for row in read_jsonl(metrics_path) if row.get('thread') == thread]
|
|
194
|
+
if claude_count != args.agents // 2 or codex_count != args.agents // 2:
|
|
195
|
+
raise RuntimeError(
|
|
196
|
+
f'round {round_number} duplicated delivery: '
|
|
197
|
+
f'Claude={claude_count}, Codex={codex_count}')
|
|
198
|
+
if len(metrics) != args.agents:
|
|
199
|
+
raise RuntimeError(
|
|
200
|
+
f'round {round_number} recorded {len(metrics)} acceptances, expected {args.agents}')
|
|
201
|
+
latencies = sorted(int(row.get('totalToNativeAcceptMs', 0)) for row in metrics)
|
|
202
|
+
round_report = {
|
|
203
|
+
'round': round_number,
|
|
204
|
+
'thread': thread,
|
|
205
|
+
'fanoutMs': fanout_ms,
|
|
206
|
+
'accepted': len(metrics),
|
|
207
|
+
'claudeDeliveries': claude_count,
|
|
208
|
+
'codexDeliveries': codex_count,
|
|
209
|
+
'p95NativeAcceptMs': percentile(latencies, .95),
|
|
210
|
+
'maxNativeAcceptMs': max(latencies),
|
|
211
|
+
'hubProcesses': len(hub_pids(city)),
|
|
212
|
+
'liveFeedOpenEvents': len(opened_feed),
|
|
213
|
+
'liveFeedParticipants': args.agents,
|
|
214
|
+
}
|
|
215
|
+
if round_report['hubProcesses'] != 1:
|
|
216
|
+
raise RuntimeError(
|
|
217
|
+
f'round {round_number} ended with {round_report["hubProcesses"]} hubs')
|
|
218
|
+
if round_report['p95NativeAcceptMs'] >= 5_000 \
|
|
219
|
+
or round_report['maxNativeAcceptMs'] >= 10_000:
|
|
220
|
+
raise RuntimeError(f'round {round_number} exceeded the local latency budget')
|
|
221
|
+
report['rounds'].append(round_report)
|
|
222
|
+
|
|
223
|
+
all_metrics = read_jsonl(metrics_path)
|
|
224
|
+
expected = args.agents * args.rounds
|
|
225
|
+
if len(all_metrics) != expected:
|
|
226
|
+
raise RuntimeError(f'latency ledger has {len(all_metrics)} rows, expected {expected}')
|
|
227
|
+
transports = {}
|
|
228
|
+
for row in all_metrics:
|
|
229
|
+
transport = row.get('transport', '')
|
|
230
|
+
transports[transport] = transports.get(transport, 0) + 1
|
|
231
|
+
report['transportAcceptances'] = transports
|
|
232
|
+
report['duplicateDeliveries'] = 0
|
|
233
|
+
report['remainingOutboxes'] = count_outboxes(runtime_dir, participants)
|
|
234
|
+
report['terminalInjection'] = os.path.exists(os.path.join(runtime_dir, 'adapters'))
|
|
235
|
+
report['liveFeed'].update({
|
|
236
|
+
'openEvents': sum(row['liveFeedOpenEvents'] for row in report['rounds']),
|
|
237
|
+
'participantsPerRound': [row['liveFeedParticipants'] for row in report['rounds']],
|
|
238
|
+
'survivedHubRestart': args.rounds < 2 or len(report['rounds']) >= 2,
|
|
239
|
+
'duplicates': 0,
|
|
240
|
+
})
|
|
241
|
+
if report['remainingOutboxes'] or report['terminalInjection']:
|
|
242
|
+
raise RuntimeError('stress run used terminal injection or left queued work')
|
|
243
|
+
report['status'] = 'pass'
|
|
244
|
+
except Exception as error:
|
|
245
|
+
report['error'] = str(error)
|
|
246
|
+
finally:
|
|
247
|
+
stop_processes(gateways)
|
|
248
|
+
if fake:
|
|
249
|
+
stop_process(fake)
|
|
250
|
+
for stream in gateway_streams:
|
|
251
|
+
stream.close()
|
|
252
|
+
for pid in hub_pids(city):
|
|
253
|
+
stop_pid(pid)
|
|
254
|
+
remaining = hub_pids(city)
|
|
255
|
+
report['cleanup'] = {
|
|
256
|
+
'remainingHubProcesses': remaining,
|
|
257
|
+
'remainingRuntimeProcesses': sum(
|
|
258
|
+
process.poll() is None for process in gateways),
|
|
259
|
+
}
|
|
260
|
+
if remaining or report['cleanup']['remainingRuntimeProcesses']:
|
|
261
|
+
report['status'] = 'failed'
|
|
262
|
+
report['error'] = 'stress cleanup left an orphan process'
|
|
263
|
+
if args.keep:
|
|
264
|
+
report['artifacts'] = base
|
|
265
|
+
else:
|
|
266
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
267
|
+
return report
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def mixed_actors(total):
|
|
271
|
+
actors = []
|
|
272
|
+
for index in range(total // 2):
|
|
273
|
+
actors.append(('claude', f'claude-{index + 1:02d}'))
|
|
274
|
+
actors.append(('codex', f'codex-{index + 1:02d}'))
|
|
275
|
+
return actors
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def write_city(city, actors):
|
|
279
|
+
open(os.path.join(city, 'city.yml'), 'w', encoding='utf-8').write(
|
|
280
|
+
'id: city_forty\nname: Forty\nslug: forty\nowner: stress\n')
|
|
281
|
+
open(os.path.join(city, 'roads.json'), 'w', encoding='utf-8').write(
|
|
282
|
+
'{"version": 1, "roads": []}\n')
|
|
283
|
+
names = [actor for _, actor in actors]
|
|
284
|
+
card = '---\nuser: stress\nagent: stress/ceo\nrepos: [' + ', '.join(names) + ']\n'
|
|
285
|
+
for runtime, actor in actors:
|
|
286
|
+
card += f'role.{actor}: blank\nruns.{actor}: {runtime}\n'
|
|
287
|
+
open(os.path.join(city, 'stress.md'), 'w', encoding='utf-8').write(card + '---\n')
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def open_committee(participants, round_number, env):
|
|
291
|
+
result = subprocess.run(
|
|
292
|
+
['node', CLIENT, 'committee', 'open', '--input', '-'],
|
|
293
|
+
input=json.dumps({
|
|
294
|
+
'question': f'Mixed-runtime stress round {round_number}.',
|
|
295
|
+
'desiredOutcome': 'Every selected runtime accepts one isolated assignment.',
|
|
296
|
+
'definitionOfDone': [
|
|
297
|
+
f'{len(participants)} native acceptances with no duplicate delivery'],
|
|
298
|
+
'participants': participants,
|
|
299
|
+
}), capture_output=True, text=True,
|
|
300
|
+
env=dict(env, CITY_BUS_ACTOR='seat'), timeout=15)
|
|
301
|
+
if result.returncode != 0:
|
|
302
|
+
raise RuntimeError(result.stderr.strip() or 'committee.open failed')
|
|
303
|
+
return json.loads(result.stdout).get('id', '')
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def spectator_snapshot(endpoint_path):
|
|
307
|
+
endpoint = json.load(open(endpoint_path, encoding='utf-8'))
|
|
308
|
+
parsed = urllib.parse.urlparse(endpoint['url'])
|
|
309
|
+
query = urllib.parse.urlencode({
|
|
310
|
+
'mode': 'spectator', 'token': endpoint['spectatorToken']})
|
|
311
|
+
url = urllib.parse.urlunparse(parsed._replace(query=query))
|
|
312
|
+
result = subprocess.run(
|
|
313
|
+
['node', '--input-type=commonjs', '-e', SNAPSHOT_JS, url],
|
|
314
|
+
cwd=CHANNEL_DIR, capture_output=True, text=True, timeout=7)
|
|
315
|
+
if result.returncode != 0:
|
|
316
|
+
raise RuntimeError(
|
|
317
|
+
f'spectator WebSocket failed: {result.stderr.strip() or result.returncode}')
|
|
318
|
+
try:
|
|
319
|
+
return json.loads(result.stdout)
|
|
320
|
+
except json.JSONDecodeError as error:
|
|
321
|
+
raise RuntimeError(f'spectator WebSocket returned invalid JSON: {error}') from error
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def round_complete(thread, claude_capture, codex_capture, metrics_path, half, total):
|
|
325
|
+
return (
|
|
326
|
+
len(records_for_thread(claude_capture, thread)) == half
|
|
327
|
+
and len(records_for_thread(codex_capture, thread)) == half
|
|
328
|
+
and len([row for row in read_jsonl(metrics_path)
|
|
329
|
+
if row.get('thread') == thread]) == total
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def records_for_thread(path, thread):
|
|
334
|
+
return [row for row in read_jsonl(path) if thread in json.dumps(row)]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def read_jsonl(path):
|
|
338
|
+
if not os.path.exists(path):
|
|
339
|
+
return []
|
|
340
|
+
rows = []
|
|
341
|
+
with open(path, encoding='utf-8') as stream:
|
|
342
|
+
for line in stream:
|
|
343
|
+
try:
|
|
344
|
+
rows.append(json.loads(line))
|
|
345
|
+
except json.JSONDecodeError:
|
|
346
|
+
pass
|
|
347
|
+
return rows
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def read(path):
|
|
351
|
+
try:
|
|
352
|
+
return open(path, encoding='utf-8', errors='replace').read()
|
|
353
|
+
except OSError:
|
|
354
|
+
return ''
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def count_outboxes(runtime_dir, participants):
|
|
358
|
+
root = os.path.join(runtime_dir, 'outbox')
|
|
359
|
+
return sum(
|
|
360
|
+
len(os.listdir(os.path.join(root, actor)))
|
|
361
|
+
for actor in participants
|
|
362
|
+
if os.path.isdir(os.path.join(root, actor))
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def outboxes_empty(runtime_dir, participants):
|
|
367
|
+
return count_outboxes(runtime_dir, participants) == 0
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def endpoint_released_or_replaced(path, stopped_pid):
|
|
371
|
+
if not os.path.exists(path):
|
|
372
|
+
return True
|
|
373
|
+
try:
|
|
374
|
+
endpoint = json.load(open(path, encoding='utf-8'))
|
|
375
|
+
replacement = int(endpoint.get('pid', 0))
|
|
376
|
+
return replacement > 0 and replacement != stopped_pid and alive(replacement)
|
|
377
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError):
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def percentile(values, ratio):
|
|
382
|
+
return values[max(0, math.ceil(len(values) * ratio) - 1)] if values else None
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def free_port():
|
|
386
|
+
with socket.socket() as sock:
|
|
387
|
+
sock.bind(('127.0.0.1', 0))
|
|
388
|
+
return sock.getsockname()[1]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def wait_port(port, timeout):
|
|
392
|
+
return wait_for(lambda: port_open(port), timeout)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def port_open(port):
|
|
396
|
+
try:
|
|
397
|
+
with socket.create_connection(('127.0.0.1', port), timeout=.2):
|
|
398
|
+
return True
|
|
399
|
+
except OSError:
|
|
400
|
+
return False
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def wait_for(condition, timeout):
|
|
404
|
+
deadline = time.monotonic() + timeout
|
|
405
|
+
while time.monotonic() < deadline:
|
|
406
|
+
if condition():
|
|
407
|
+
return True
|
|
408
|
+
time.sleep(.05)
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def hub_pids(city):
|
|
413
|
+
target = os.path.realpath(city)
|
|
414
|
+
result = subprocess.run(
|
|
415
|
+
['ps', '-axo', 'pid=,command='], capture_output=True, text=True)
|
|
416
|
+
marker = 'local-hub.js --data '
|
|
417
|
+
pids = []
|
|
418
|
+
for line in result.stdout.splitlines():
|
|
419
|
+
if marker not in line:
|
|
420
|
+
continue
|
|
421
|
+
raw_pid, command = line.strip().split(None, 1)
|
|
422
|
+
supplied = command.split(marker, 1)[1].strip()
|
|
423
|
+
if os.path.realpath(supplied) == target:
|
|
424
|
+
pids.append(int(raw_pid))
|
|
425
|
+
return sorted(set(pids))
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def stop_pid(pid, timeout=3):
|
|
429
|
+
if not pid:
|
|
430
|
+
return True
|
|
431
|
+
try:
|
|
432
|
+
os.kill(pid, signal.SIGTERM)
|
|
433
|
+
except ProcessLookupError:
|
|
434
|
+
return True
|
|
435
|
+
if wait_for(lambda: not alive(pid), timeout):
|
|
436
|
+
return True
|
|
437
|
+
try:
|
|
438
|
+
os.kill(pid, signal.SIGKILL)
|
|
439
|
+
except ProcessLookupError:
|
|
440
|
+
return True
|
|
441
|
+
return wait_for(lambda: not alive(pid), 1)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def alive(pid):
|
|
445
|
+
try:
|
|
446
|
+
os.kill(pid, 0)
|
|
447
|
+
return True
|
|
448
|
+
except ProcessLookupError:
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def stop_process(process):
|
|
453
|
+
if process.poll() is not None:
|
|
454
|
+
return
|
|
455
|
+
process.terminate()
|
|
456
|
+
try:
|
|
457
|
+
process.wait(timeout=3)
|
|
458
|
+
except subprocess.TimeoutExpired:
|
|
459
|
+
process.kill()
|
|
460
|
+
process.wait(timeout=2)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def stop_processes(processes):
|
|
464
|
+
for process in processes:
|
|
465
|
+
if process.poll() is None:
|
|
466
|
+
process.terminate()
|
|
467
|
+
for process in processes:
|
|
468
|
+
try:
|
|
469
|
+
process.wait(timeout=3)
|
|
470
|
+
except subprocess.TimeoutExpired:
|
|
471
|
+
process.kill()
|
|
472
|
+
process.wait(timeout=2)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def print_report(report):
|
|
476
|
+
print(f"\n mixed runtime stress: {report['agents']} agents")
|
|
477
|
+
print(f" status: {report['status']}")
|
|
478
|
+
for row in report.get('rounds', []):
|
|
479
|
+
print(
|
|
480
|
+
f" round {row['round']}: {row['accepted']} accepted in {row['fanoutMs']}ms "
|
|
481
|
+
f"(p95 {row['p95NativeAcceptMs']}ms, max {row['maxNativeAcceptMs']}ms)")
|
|
482
|
+
if report.get('error'):
|
|
483
|
+
print(f" error: {report['error']}")
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == '__main__':
|
|
487
|
+
sys.exit(main())
|
package/bin/agents
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* The npm front door: `npm install -g agents-city`, then `agents-city`.
|
|
3
|
+
*
|
|
4
|
+
* The same trick Claude Code uses — npm as the installer, the payload being
|
|
5
|
+
* whatever the product actually is. Here the payload is the repo itself: the
|
|
6
|
+
* Python that writes your files, the bash that builds your tmux session, and the
|
|
7
|
+
* prebuilt map. Node is only the dispatcher, and it is guaranteed present because
|
|
8
|
+
* npm just ran.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately not a port to JS: the shared modules (card, parcels, units, roles)
|
|
11
|
+
* are the product's single source of truth and every door reads them — the
|
|
12
|
+
* installed Claude plugin included. Two languages holding one truth is the drift
|
|
13
|
+
* disease this repo already caught once and killed.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
|
|
20
|
+
const RAIZ = path.dirname(__dirname);
|
|
21
|
+
|
|
22
|
+
const ORDENES = {
|
|
23
|
+
hall: { que: ['bin/hall'], di: 'the town hall: manage your city from the browser' },
|
|
24
|
+
seat: { que: ['bin/seat'], di: 'your chair: domain, seat role, repo-agent roles, goal, tmux' },
|
|
25
|
+
cities: { que: ['bin/cities'], di: 'list, create or select your cities' },
|
|
26
|
+
agents: { que: ['bin/agents'], di: 'list agents and manage their workspace mounts' },
|
|
27
|
+
road: { que: ['bin/road'], di: 'connect two cities explicitly' },
|
|
28
|
+
bus: { que: ['bin/bus'], di: 'send information over those roads (seat only)' },
|
|
29
|
+
committee: { que: ['bin/committee'], di: 'chair-mediated work with repo agents' },
|
|
30
|
+
benchmark: { que: ['bin/benchmark'], di: 'offline stress/governance or opt-in live runtime latency' },
|
|
31
|
+
logs: { que: ['bin/logs'], di: 'read or follow visible activity and operational diagnostics' },
|
|
32
|
+
reset: { que: ['bin/reset'], di: 'reset one city to onboarding, recoverably' },
|
|
33
|
+
skills: { que: ['bin/skills'], di: 'recognise the skills installed in each repo' },
|
|
34
|
+
city: { que: ['bin/city'], di: 'draw your city' },
|
|
35
|
+
demo: { que: ['bin/demo'], di: 'Aurora Games — see it working, no account' },
|
|
36
|
+
setup: { que: ['bin/setup.py'], di: 'create or open a personal city' },
|
|
37
|
+
report: { que: ['bin/report.py'], di: 'report growth from your folders' },
|
|
38
|
+
tokens: { que: ['bin/tokens.py'], di: 'report token spend' },
|
|
39
|
+
exit: { que: ['bin/exit'], di: 'close the day — every session and agent, or one city' },
|
|
40
|
+
test: { que: ['bin/test'], di: 'every check in the repo' },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function ayuda() {
|
|
44
|
+
const v = require(path.join(RAIZ, 'package.json')).version;
|
|
45
|
+
console.log(`
|
|
46
|
+
agents-city ${v} — autonomous cities for your coding agents
|
|
47
|
+
|
|
48
|
+
usage: agents-city [command] [args]
|
|
49
|
+
|
|
50
|
+
With no command it opens the town hall.
|
|
51
|
+
`);
|
|
52
|
+
for (const [n, o] of Object.entries(ORDENES)) {
|
|
53
|
+
console.log(` ${n.padEnd(11)}${o.di}`);
|
|
54
|
+
}
|
|
55
|
+
console.log(`
|
|
56
|
+
Needs python3 and tmux (the seat installs tmux itself). On Windows, run it
|
|
57
|
+
inside WSL — the session is made of tmux windows, and there is no native tmux.
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hay(programa) {
|
|
62
|
+
const r = spawnSync(process.platform === 'win32' ? 'where' : 'command',
|
|
63
|
+
process.platform === 'win32' ? [programa] : ['-v', programa],
|
|
64
|
+
{ shell: process.platform !== 'win32', stdio: 'ignore' });
|
|
65
|
+
return r.status === 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function main() {
|
|
69
|
+
const args = process.argv.slice(2);
|
|
70
|
+
const primera = args[0] || 'hall';
|
|
71
|
+
|
|
72
|
+
if (primera === '--help' || primera === '-h' || primera === 'help') { ayuda(); return 0; }
|
|
73
|
+
if (primera === '--version' || primera === '-v') {
|
|
74
|
+
console.log(require(path.join(RAIZ, 'package.json')).version);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (process.platform === 'win32') {
|
|
79
|
+
console.error('\n agents-city runs on tmux and bash, and Windows has neither');
|
|
80
|
+
console.error(' natively. Install it inside WSL and run it from there:\n');
|
|
81
|
+
console.error(' wsl -- npm install -g agents-city\n');
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const orden = ORDENES[primera];
|
|
86
|
+
const resto = orden ? args.slice(1) : args;
|
|
87
|
+
const entrada = orden ? orden.que[0] : ORDENES.hall.que[0];
|
|
88
|
+
if (!orden && args.length && primera.startsWith('-') === false) {
|
|
89
|
+
console.error(`\n No such command: ${primera}\n`);
|
|
90
|
+
ayuda();
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!hay('python3')) {
|
|
95
|
+
console.error('\n agents-city needs python3 on PATH — the files it manages are');
|
|
96
|
+
console.error(' written by Python, the same modules every door shares.');
|
|
97
|
+
console.error(' macOS: xcode-select --install · Debian: apt install python3\n');
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const guion = path.join(RAIZ, entrada);
|
|
102
|
+
if (!fs.existsSync(guion)) {
|
|
103
|
+
console.error(`\n ${entrada} is missing from this install — reinstall:`);
|
|
104
|
+
console.error(' npm install -g agents-city\n');
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
// Unbuffered, so the URL a server prints reaches a pipe as well as a terminal
|
|
108
|
+
// — a CI log or a `| head` otherwise swallows it and the run looks silent.
|
|
109
|
+
const r = spawnSync(guion, orden ? resto : args, {
|
|
110
|
+
stdio: 'inherit', cwd: RAIZ,
|
|
111
|
+
env: { ...process.env, PYTHONUNBUFFERED: '1' },
|
|
112
|
+
});
|
|
113
|
+
return r.status === null ? 1 : r.status;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
process.exit(main());
|
package/bin/benchmark
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Benchmarks are explicit: stress/committee are offline, live uses model accounts.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
case "${1:-}" in
|
|
6
|
+
live) shift; exec python3 "$ROOT/benchmarks/latency/live.py" "$@" ;;
|
|
7
|
+
committee) shift; exec python3 "$ROOT/benchmarks/committee/run.py" "$@" ;;
|
|
8
|
+
stress) shift; exec python3 "$ROOT/benchmarks/stress/run.py" "$@" ;;
|
|
9
|
+
-h|--help|'')
|
|
10
|
+
echo "usage: agents-city benchmark <stress | live | committee> [options]"
|
|
11
|
+
echo
|
|
12
|
+
echo " stress offline 40-agent Claude/Codex fan-out and reconnect stress"
|
|
13
|
+
echo " live real bus -> native model -> committee-position latency (uses accounts/credits)"
|
|
14
|
+
echo " committee offline structural governance benchmark"
|
|
15
|
+
;;
|
|
16
|
+
*) echo "unknown benchmark: $1" >&2; exit 2 ;;
|
|
17
|
+
esac
|
package/bin/bus
ADDED
package/bin/cities
ADDED