Compare commits

..
Author SHA1 Message Date
m 775fa03ed4 feat: bound benchmark advisory output
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 34s
2026-08-22 22:50:20 +03:00
m 2ed2302526 Merge pull request 'docs: consolidate current runtime status' (#152) from codex/runtime-status-consolidation into main
CI / test (push) Successful in 45s
2026-08-22 19:43:53 +00:00
m 46538ba1e9 docs: consolidate current runtime status
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 35s
2026-08-22 22:42:01 +03:00
m 5960844fcb Merge pull request 'docs: record current heavy qualification' (#151) from codex/heavy-qualification-refresh into main
CI / test (push) Successful in 49s
2026-08-22 19:35:48 +00:00
m b60fc38333 docs: record current heavy qualification
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 37s
2026-08-22 22:33:51 +03:00
m dc1ee7ff3c Merge pull request 'docs: record heavy qualification gap' (#150) from codex/heavy-qualification-evidence into main
CI / test (push) Successful in 48s
2026-08-22 19:29:17 +00:00
m a51bf57d7a docs: record heavy qualification gap
CI / test (push) Successful in 37s
CI / test (pull_request) Successful in 36s
2026-08-22 22:27:24 +03:00
m d60fb8697a Merge pull request 'docs: record medium qualification retest' (#149) from codex/medium-qualification-retest-v2 into main
CI / test (push) Successful in 42s
2026-08-22 19:22:54 +00:00
m bf4b81e320 docs: record medium qualification retest
CI / test (push) Successful in 39s
CI / test (pull_request) Successful in 36s
2026-08-22 22:20:32 +03:00
m 389dd0c23d Merge pull request 'feat: configure medium provider invocation timeout' (#148) from codex/medium-invocation-timeout into main
CI / test (push) Successful in 48s
2026-08-22 19:13:26 +00:00
m 064a650763 chore: align medium timeout task base
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 38s
2026-08-22 22:11:43 +03:00
m 6ecb431ccd feat: configure medium provider invocation timeout
CI / test (push) Failing after 28s
CI / test (pull_request) Failing after 26s
2026-08-22 22:08:32 +03:00
m b29f8775e1 Merge pull request 'docs: record medium qualification timeout' (#147) from codex/medium-qualification-evidence into main
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 35s
2026-08-22 18:59:30 +00:00
m 85ee05e291 docs: record medium qualification timeout
CI / test (push) Successful in 37s
CI / test (pull_request) Successful in 36s
2026-08-22 21:57:29 +03:00
m 35aa64546b Merge pull request 'docs: record live medium failover evidence' (#146) from codex/refresh-medium-failover-evidence into main
CI / test (push) Successful in 43s
2026-08-22 18:43:41 +00:00
m 6e0d1a2279 docs: record live medium failover evidence
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 34s
2026-08-22 21:42:01 +03:00
m 64642f4bae Merge pull request 'fix: prefer available fleet tier before lifeboat' (#145) from codex/fleet-failover-priority into main
CI / test (push) Successful in 44s
2026-08-22 18:36:44 +00:00
m 2f9e9bdc9f fix: prefer available fleet tier before lifeboat
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 33s
2026-08-22 21:35:16 +03:00
m 2c7a85779b Merge pull request 'feat: connect existing medium model runtime' (#144) from codex/connect-medium-runtime into main
CI / test (push) Successful in 42s
2026-08-22 18:30:30 +00:00
m b4c3027e6d feat: connect existing medium model runtime
CI / test (push) Successful in 35s
CI / test (pull_request) Successful in 33s
2026-08-22 21:29:02 +03:00
m 503fe84074 Merge pull request 'docs: refresh fleet audit evidence' (#143) from codex/refresh-fleet-evidence into main
CI / test (push) Successful in 42s
2026-08-22 18:21:37 +00:00
m 3b551b2918 docs: refresh fleet audit evidence
CI / test (push) Successful in 37s
CI / test (pull_request) Successful in 33s
2026-08-22 21:20:10 +03:00
m 51427a246e Merge pull request 'fix: keep unconfigured medium out of readiness' (#142) from codex/optional-medium-readiness into main
CI / test (push) Successful in 47s
2026-08-22 18:15:36 +00:00
m 9b5ce2a850 fix: keep unconfigured medium out of readiness
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 34s
2026-08-22 21:14:12 +03:00
m 8f3920519b Merge pull request 'feat: add configurable medium model fleet slot' (#141) from codex/model-fleet-readiness into main
CI / test (push) Successful in 43s
2026-08-22 18:11:23 +00:00
m 2277f8a699 feat: add configurable medium model fleet slot
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 31s
2026-08-22 21:09:58 +03:00
m 205d8f6206 Merge pull request 'fix: align cpu readiness client timeout' (#140) from codex/per-provider-readiness-timeouts into main
CI / test (push) Successful in 43s
2026-08-22 18:03:16 +00:00
m ae06d33ccc fix: align cpu readiness client timeout
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 33s
2026-08-22 21:00:46 +03:00
m c7b57daaf8 Merge pull request 'fix: use provider-specific readiness timeouts' (#139) from codex/per-provider-readiness-timeouts into main
CI / test (push) Successful in 38s
2026-08-22 17:57:27 +00:00
m 8aa0123047 chore: format readiness health server
CI / test (push) Successful in 31s
CI / test (pull_request) Successful in 35s
2026-08-22 20:55:48 +03:00
m 4a3d348e39 fix: use provider-specific readiness timeouts
CI / test (push) Failing after 25s
CI / test (pull_request) Failing after 23s
2026-08-22 20:52:40 +03:00
m 4086b75224 Merge pull request 'fix: authenticate cognitive readiness probe' (#138) from codex/authorized-cpu-health-probe into main
CI / test (push) Successful in 43s
2026-08-22 17:47:25 +00:00
m 4f89c46545 fix: authenticate cognitive readiness probe
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 34s
2026-08-22 20:46:03 +03:00
m 5d5afc8888 Merge pull request 'fix: authenticate fleet readiness probe' (#137) from codex/authorized-fleet-readiness into main
CI / test (push) Successful in 48s
2026-08-22 17:41:55 +00:00
m 79ce5a0b82 fix: authenticate fleet readiness probe
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 32s
2026-08-22 20:40:33 +03:00
m 65a60d5f06 Merge pull request 'test: cover operations status surfaces' (#136) from codex/operations-ui-contract-refresh into main
CI / test (push) Successful in 49s
2026-08-22 17:36:54 +00:00
m 4739a22f7b test: cover operations status surfaces
CI / test (push) Successful in 32s
CI / test (pull_request) Successful in 33s
2026-08-22 20:35:33 +03:00
m c25a6346d0 Merge pull request 'docs: refresh completion evidence revision' (#135) from codex/status-after-completion-matrix into main
CI / test (push) Successful in 47s
2026-08-22 17:31:57 +00:00
m 5f71fa8dc4 docs: refresh completion evidence revision
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 32s
2026-08-22 20:30:37 +03:00
m bda71caece Merge pull request 'docs: publish audit completion matrix' (#134) from codex/completion-audit-matrix into main
CI / test (push) Successful in 44s
2026-08-22 17:27:36 +00:00
m 0656f88fd0 docs: publish audit completion matrix
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 32s
2026-08-22 20:26:14 +03:00
m 3cf44ada8a Merge pull request 'docs: refresh bridge wiring status' (#133) from codex/status-after-bridge-wiring into main
CI / test (push) Successful in 49s
2026-08-22 17:22:32 +00:00
m ca00377bec docs: refresh bridge wiring status
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 32s
2026-08-22 20:21:13 +03:00
m ada2da0c6c Merge pull request 'chore: wire optional agent bridge URL' (#132) from codex/agent-bridge-config-wiring into main
CI / test (push) Successful in 46s
2026-08-22 17:17:58 +00:00
m 82df14bde9 chore: wire optional agent bridge URL
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 33s
2026-08-22 20:16:36 +03:00
m e1186757bf Merge pull request 'docs: synchronize current production evidence' (#131) from codex/status-evidence-consolidation into main
CI / test (push) Successful in 45s
2026-08-22 16:00:39 +00:00
m 1f002bd1a3 docs: synchronize current production evidence
CI / test (push) Successful in 34s
CI / test (pull_request) Successful in 31s
2026-08-22 18:59:04 +03:00
m 3a52c18bef Merge pull request 'docs: record postgres restore evidence' (#130) from codex/postgres-restore-evidence into main
CI / test (push) Successful in 45s
2026-08-22 15:56:02 +00:00
m fe0351edae docs: record postgres restore evidence
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 34s
2026-08-22 18:54:41 +03:00
m 077f16e50c Merge pull request 'docs: record fleet and execution status evidence' (#129) from codex/fleet-execution-status-evidence into main
CI / test (push) Successful in 40s
2026-08-22 15:50:38 +00:00
m 3a9d3f0503 docs: record fleet and execution status evidence
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 34s
2026-08-22 18:49:08 +03:00
m 03c3484ee1 Merge pull request 'docs: publish current audit status' (#128) from codex/current-audit-status into main
CI / test (push) Successful in 41s
2026-08-22 14:56:38 +00:00
m e47d363145 docs: publish current audit status
CI / test (push) Successful in 38s
CI / test (pull_request) Successful in 33s
2026-08-22 17:55:07 +03:00
m 5703ab3ed5 Merge pull request 'docs(evidence): record live FAST gateway smoke' (#127) from codex/fast-live-evidence into main
CI / test (push) Successful in 41s
2026-08-22 14:45:02 +00:00
m d03e9d3307 docs(evidence): record live FAST gateway smoke
CI / test (push) Successful in 31s
CI / test (pull_request) Successful in 31s
2026-08-22 17:43:37 +03:00
m 283f5c044c Merge pull request 'chore(deploy): wire optional cognitive provider secret volume' (#126) from codex/fleet-secret-wiring into main
CI / test (push) Successful in 41s
2026-08-22 14:28:33 +00:00
m ea5327799a chore(deploy): wire optional cognitive provider secret volume
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 25s
2026-08-22 17:27:29 +03:00
m dbbc54171d Merge pull request 'feat(cognitive): resolve local provider key from secret ref' (#125) from codex/cognitive-cpu-auth into main
CI / test (push) Successful in 33s
2026-08-22 14:22:55 +00:00
m 2b98501a68 Merge origin/main before cognitive provider secret release
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 26s
2026-08-22 17:21:02 +03:00
m d082005b37 feat(cognitive): resolve local provider key from secret ref
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 26s
2026-08-22 17:19:04 +03:00
m 624c41d4f6 Merge pull request 'feat: expose agent worker capability status' (#124) from codex/wp-804-worker-status into main
CI / test (push) Successful in 37s
2026-08-22 14:09:26 +00:00
m 6d1d68e406 feat: expose agent worker capability status
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 26s
2026-08-22 17:08:09 +03:00
m 6871e36657 Merge pull request 'feat: expose durable agent state in creator cabinet' (#123) from codex/wp-801-agent-ui into main
CI / test (push) Successful in 41s
2026-08-22 14:01:40 +00:00
m b1ccc3200b feat: expose durable agent state in creator cabinet
CI / test (push) Successful in 28s
CI / test (pull_request) Successful in 25s
2026-08-22 17:00:33 +03:00
m ee678db42c Merge pull request 'docs: record live Lifeboat qualification evidence' (#122) from codex/wp-201-live-qualification into main
CI / test (push) Successful in 34s
2026-08-22 13:53:33 +00:00
m b16c12068e docs: record live Lifeboat qualification evidence
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 25s
2026-08-22 16:52:26 +03:00
m eb920bb9f2 Merge pull request 'fix: disable agent dry-run without executor' (#121) from codex/wp-301-executor-safety into main
CI / test (push) Successful in 43s
2026-08-22 13:45:40 +00:00
m dd7fd2cd68 fix: disable agent dry-run without executor
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 24s
2026-08-22 16:43:32 +03:00
m 7030a51b69 Merge pull request 'feat: expose authenticated model fleet status' (#120) from codex/wp-805-model-ui into main
CI / test (push) Successful in 32s
2026-08-22 13:37:24 +00:00
m 6f71b73ac2 feat: expose authenticated model fleet status
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 25s
2026-08-22 16:36:11 +03:00
m 641a8cd8fb Merge pull request 'fix: persist all graph dispatch bindings before fan-out' (#119) from codex/wp-400-graph-runtime into main
CI / test (push) Successful in 37s
2026-08-22 13:25:23 +00:00
m 70007c0da0 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-runtime
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 27s
2026-08-22 16:23:59 +03:00
m 0e62ab7684 fix: persist all graph dispatch bindings before fan-out
CI / test (push) Successful in 26s
2026-08-22 16:23:45 +03:00
m 6f7421167b Merge pull request 'fix: serialize graph dispatch contracts' (#118) from codex/wp-400-graph-runtime into main
CI / test (push) Successful in 37s
2026-08-22 13:20:20 +00:00
m 122dab2a9d Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-runtime
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 26s
2026-08-22 16:18:54 +03:00
m 7896404b6d chore: declare graph contract serialization scope
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 25s
2026-08-22 16:16:04 +03:00
m a232d88bcf fix: serialize dispatch intent for graph runtime
CI / test (push) Failing after 20s
2026-08-22 16:15:03 +03:00
m e023491e8c Merge pull request 'fix: enqueue graph join after fan-in' (#117) from codex/wp-400-graph-runtime into main
CI / test (push) Successful in 42s
2026-08-22 13:10:40 +00:00
m 798b124fd4 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-runtime
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 27s
2026-08-22 16:09:09 +03:00
m c7fc8d6a63 feat: enqueue graph join after fan-in
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 27s
2026-08-22 16:06:33 +03:00
m 91e867f468 Merge pull request 'feat: add durable work graph runtime' (#116) from codex/wp-400-graph-runtime into main
CI / test (push) Successful in 39s
2026-08-22 13:04:35 +00:00
m 785441f2a0 feat: add durable work graph runtime
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 25s
2026-08-22 16:03:16 +03:00
m 16d714238e Merge pull request 'feat: connect agent worker to execution bridge' (#115) from codex/wp-302-agent-bridge into main
CI / test (push) Successful in 36s
2026-08-22 12:57:01 +00:00
m 7e703fe4b4 chore: format bridge tests
CI / test (push) Successful in 27s
CI / test (pull_request) Successful in 24s
2026-08-22 15:55:26 +03:00
m e935aadc94 feat: connect agent worker to execution bridge
CI / test (push) Failing after 20s
CI / test (pull_request) Failing after 18s
2026-08-22 15:53:44 +03:00
m 7b06cd64a2 Merge pull request 'feat: expose cognitive fleet readiness' (#114) from codex/wp-601-fleet-status into main
CI / test (push) Successful in 25s
2026-08-22 12:46:45 +00:00
m b3afe5236f chore: align fleet task lineage
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 24s
2026-08-22 15:45:30 +03:00
m cef7bd0226 feat: expose cognitive fleet readiness
CI / test (push) Failing after 22s
CI / test (pull_request) Failing after 17s
2026-08-22 15:44:27 +03:00
m 23b04aafd0 Merge pull request 'feat(research): add allowlisted HTTPS evidence connector' (#113) from codex/wp-102-research-orchestration into main
CI / test (push) Successful in 29s
2026-08-22 12:38:57 +00:00
m 6ee00af54b Merge remote-tracking branch 'origin/main' into codex/wp-102-research-orchestration
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 25s
2026-08-22 15:37:49 +03:00
m 6f37194724 feat(research): add allowlisted HTTPS evidence connector
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 24s
2026-08-22 15:36:48 +03:00
m 82a0052470 Merge pull request 'feat(research): add guarded repository evidence connector' (#112) from codex/wp-102-research-orchestration into main
CI / test (push) Successful in 29s
2026-08-22 12:33:02 +00:00
m d563db9e57 Merge remote-tracking branch 'origin/main' into codex/wp-102-research-orchestration
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 24s
2026-08-22 15:31:55 +03:00
m c1bc9c35d1 feat(research): add guarded repository evidence connector
CI / test (push) Successful in 26s
CI / test (pull_request) Successful in 23s
2026-08-22 15:30:41 +03:00
m 5a8a368153 Merge pull request 'feat(research): run private evidence persistence runtime' (#111) from codex/wp-102-research-orchestration into main
CI / test (push) Successful in 26s
2026-08-22 12:24:52 +00:00
m 7a14973e9a feat(research): run private evidence persistence runtime
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 23s
2026-08-22 15:23:43 +03:00
m f9e21737a5 Merge pull request 'feat(epistemics): persist truth bundles with source provenance' (#110) from codex/wp-102-truth-persistence into main
CI / test (push) Successful in 23s
2026-08-22 12:18:02 +00:00
m 53f86e4176 feat(epistemics): persist truth bundles with source provenance
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 21s
2026-08-22 15:16:42 +03:00
m 0b321fe115 Merge pull request 'fix(readiness): allow lifeboat when primary CPU is unavailable' (#109) from codex/wp-002-recovery into main
CI / test (push) Successful in 23s
Reviewed-on: #109
2026-08-22 12:10:37 +00:00
m 94ae8589ce Merge remote-tracking branch 'origin/main' into codex/wp-002-recovery
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 15:09:50 +03:00
m 8d8f7cb097 fix(readiness): allow lifeboat when primary CPU is unavailable
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 15:08:39 +03:00
m 6e9111dd0c Merge pull request 'fix(readiness): degrade optional fleet providers without blocking fallback' (#108) from codex/wp-002-recovery into main
CI / test (push) Successful in 23s
2026-08-22 12:06:15 +00:00
m 8c5386ec34 fix(readiness): degrade optional fleet providers without blocking fallback
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 23s
2026-08-22 15:05:09 +03:00
m 38cff16f45 Merge pull request 'fix(deploy): use shell-safe migration history values' (#107) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 29s
2026-08-22 11:54:55 +00:00
m 2d6baef7a9 Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 14:53:32 +03:00
m 89924a417a fix(deploy): use shell-safe migration history values
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 14:51:59 +03:00
m 6735bca317 Merge pull request 'fix(deploy): normalize migration env and executable mode' (#106) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 26s
2026-08-22 11:50:33 +00:00
m 3c83e2ba5c Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:49:14 +03:00
m f2cc059e4b fix(deploy): normalize migration env and executable mode
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:47:42 +03:00
m 91c60b8c0c Merge pull request 'ops(deploy): automate idempotent migrations' (#105) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 26s
2026-08-22 11:46:24 +00:00
m 1687dad063 Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:45:06 +03:00
m 37560816aa ops(deploy): automate idempotent migrations
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 21s
2026-08-22 14:43:33 +03:00
m f92406da68 Merge pull request 'fix(cognitive): preserve admission JSON fields' (#104) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 28s
2026-08-22 11:40:31 +00:00
m 0131a96de8 Merge remote-tracking branch 'origin/main' into codex/wp-203-admission-runtime
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 21s
2026-08-22 14:39:13 +03:00
m 0d4e9ee1b4 fix(cognitive): preserve admission JSON fields
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 22s
2026-08-22 14:37:32 +03:00
m 9282fc5bf6 Merge pull request 'feat(cognitive): enforce durable admission leases' (#103) from codex/wp-203-admission-runtime into main
CI / test (push) Successful in 27s
2026-08-22 11:35:27 +00:00
m 4c4a85aec6 feat(cognitive): enforce durable admission leases
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 22s
2026-08-22 14:33:53 +03:00
m ebcae4cc9c Add cognitive admission policy 2026-08-22 14:27:21 +03:00
m 9b262609b7 Merge pull request 'feat(epistemics): evaluate read-only research evidence' (#102) from codex/wp-102-research-runtime-impl into main
CI / test (push) Successful in 29s
2026-08-22 11:24:45 +00:00
m 79c99289e7 feat(epistemics): evaluate read-only research evidence
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 21s
2026-08-22 14:23:17 +03:00
m 07a8de9cb5 Merge pull request 'test(creator-web): support file-backed acceptance secrets' (#101) from codex/wp-400-graph-harness into main
CI / test (push) Successful in 22s
2026-08-22 11:13:55 +00:00
m fdf38196f0 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-harness
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 21s
2026-08-22 14:12:29 +03:00
m b753761e1a chore(governance): include file-backed acceptance in worker task
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 22s
2026-08-22 14:11:10 +03:00
m 5faa5321dd test(creator-web): support file-backed acceptance secrets
CI / test (push) Failing after 16s
CI / test (pull_request) Failing after 16s
2026-08-22 14:09:46 +03:00
m 7271d7950d Merge pull request 'feat(runtime): production workers and cabinet acceptance' (#100) from codex/wp-400-graph-harness into main
CI / test (push) Successful in 28s
2026-08-22 11:03:21 +00:00
m 533243da25 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-harness
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 14:01:48 +03:00
m b1fba3ccef chore(dev): advance active worker task
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 21s
2026-08-22 13:58:39 +03:00
m b0afa2417f feat(runtime): run durable workers continuously
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s
2026-08-22 13:57:08 +03:00
m 6f5f7e1ee3 test(creator-web): cover cabinet acceptance contract
CI / test (push) Failing after 15s
2026-08-22 13:49:03 +03:00
m 94c5bcb017 Merge pull request 'feat(work): add deterministic graph harness' (#99) from codex/wp-400-graph-harness into main
CI / test (push) Failing after 15s
Reviewed-on: #99
2026-08-22 10:47:42 +00:00
m 3f7c4705fe Merge pull request 'feat(work): add durable agent dispatch queue' (#98) from codex/wp-301-agent-worker into main
CI / test (push) Successful in 22s
Reviewed-on: #98
2026-08-22 10:47:26 +00:00
m 1ccd43c552 feat(work): add governed self-development gate
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 14s
2026-08-22 13:46:01 +03:00
m d38406fbc5 feat(capability): add fleet readiness policy
CI / test (push) Failing after 15s
2026-08-22 13:43:36 +03:00
m f73c69960c feat(epistemics): add durable reflection worker
CI / test (push) Failing after 16s
2026-08-22 13:41:44 +03:00
m 32e33e3050 feat(epistemics): compile truth evidence bundle
CI / test (push) Successful in 24s
2026-08-22 13:37:22 +03:00
m e9bb42cd07 feat(work): persist graph revisions
CI / test (push) Successful in 21s
2026-08-22 13:33:54 +03:00
m 4a800df1e1 feat(work): add graph admission scheduler
CI / test (push) Successful in 22s
2026-08-22 13:31:39 +03:00
m b2f922cbbc feat(work): add deterministic graph harness
CI / test (push) Successful in 22s
2026-08-22 13:29:43 +03:00
m a4450348a1 feat(work): add one-shot agent worker recovery
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 21s
2026-08-22 13:26:52 +03:00
m 323a67a7d5 feat(work): add durable agent dispatch queue
CI / test (push) Successful in 23s
2026-08-22 13:24:24 +03:00
m bab96b7ede Merge pull request 'Implement governance sentinel v2' (#97) from codex/wp-301-canary-repository into main
CI / test (push) Successful in 21s
Reviewed-on: #97
2026-08-22 10:15:11 +00:00
m 1b091a50f2 feat(deploy): add opt-in agent canary profile
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 22s
2026-08-22 12:37:33 +03:00
m 790fd7e768 chore(governance): complete migration canary evidence 2026-08-22 12:36:00 +03:00
m 875286b9fc chore(governance): authorize work migration canary 2026-08-22 12:34:18 +03:00
m 8e26826ae2 feat(work): wire canary cli to work repository 2026-08-22 12:20:14 +03:00
m c30e0c72ea feat(work): add manual agent canary cli 2026-08-22 12:18:19 +03:00
m d2338b1485 feat(work): gate agent canary dispatch 2026-08-22 12:15:50 +03:00
m 7f82240a8f feat(work): orchestrate dry-run dispatch persistence 2026-08-22 12:13:35 +03:00
m fa62322ed2 feat(work): add deterministic dry-run dispatch 2026-08-22 12:11:29 +03:00
m 8081b4de34 feat(work): persist agent execution state 2026-08-22 12:08:43 +03:00
m f792d208ef Add agent dispatch intent contract 2026-08-22 12:03:21 +03:00
m 04dad79af1 Add independent agent verification contract 2026-08-22 12:02:19 +03:00
m 02b472b627 Add agent artifact attestation 2026-08-22 12:01:16 +03:00
m a6eb4abc91 Validate agent callback envelopes 2026-08-22 12:00:10 +03:00
m 9766e5396e Add agent lease fencing contract 2026-08-22 11:59:04 +03:00
m cb92f95550 Add cognitive admission policy 2026-08-22 11:57:43 +03:00
m e83cf3582b Add durable run liveness assessment 2026-08-22 11:56:18 +03:00
m a65ee372a9 Add model qualification gate 2026-08-22 11:52:24 +03:00
m 1f01b7013c Add model qualification manifest contract 2026-08-22 11:51:19 +03:00
m 93bc4a0fdf Record hardware and model inventory evidence 2026-08-22 11:50:00 +03:00
m aec63de0c7 Add deterministic citation resolver 2026-08-22 11:47:15 +03:00
m dc7f7b3b2b Add research citation evidence contracts 2026-08-22 11:46:09 +03:00
m e3b55c9705 Integrate active project context into local chat 2026-08-22 11:44:04 +03:00
m 879de3bd95 Add typed context compiler v1 2026-08-22 11:41:52 +03:00
m 3e0be6245e Propose governed self-development v1 2026-08-22 11:38:46 +03:00
m 08deab8dda Propose multi-agent work graph v1 2026-08-22 11:36:22 +03:00
m 82ee6e5ba0 Propose agent execution plane v1 2026-08-22 11:34:26 +03:00
m 339f6a6770 Propose epistemics truth v1 2026-08-22 11:32:20 +03:00
m 6fb9184d14 Authorize readiness canary promotion 2026-08-22 11:25:14 +03:00
m 3f2b008109 Add dependency-aware runtime readiness 2026-08-22 11:20:53 +03:00
m 470fdf8c03 Start dependency-aware readiness task 2026-08-22 11:16:27 +03:00
m 15c9517d0c Repair production backup recovery selection 2026-08-22 11:14:21 +03:00
m 8687c9b792 Consolidate governance lineage and registry 2026-08-22 11:08:48 +03:00
m ff33a62edd Implement governance sentinel v2 2026-08-22 11:03:21 +03:00
m b439a813e5 Merge pull request 'Observe built-in cognitive usage' (#96) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 23s
2026-08-22 00:15:39 +00:00
m 39d2d79c51 Observe built-in cognitive usage
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 23s
2026-08-22 03:14:08 +03:00
m 305a55a948 Render chat build metadata
CI / test (push) Successful in 24s
DEV-000095 passed CI.
2026-08-22 00:08:09 +00:00
m 13a3446dd0 Render chat build metadata
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 25s
2026-08-22 03:06:41 +03:00
m 8c2989f2c6 Route local recovery by semantic intent
CI / test (push) Successful in 22s
DEV-000094 passed CI.
2026-08-22 00:02:35 +00:00
m b097269df4 Route local recovery by semantic intent
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 21s
2026-08-22 03:00:15 +03:00
m 39f9081b96 Accept external audit exploration mode
CI / test (push) Successful in 23s
ARCH-000012 passed CI.
2026-08-21 23:54:26 +00:00
m 3de01f8a94 Accept external audit exploration mode
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 21s
2026-08-22 02:53:29 +03:00
m 948896cd45 Propose external audit exploration mode
CI / test (push) Successful in 21s
ARCH-000011 passed CI.
2026-08-21 23:51:26 +00:00
m 545e855298 Propose external audit exploration mode
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 22s
2026-08-22 02:50:03 +03:00
m 6f8c4e2bc7 Merge pull request 'Preserve learning evidence integrity' (#91) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 24s
Preserve learning evidence integrity
2026-08-21 23:45:04 +00:00
m 947d9d78a1 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 26s
2026-08-22 02:43:06 +03:00
m 8a3b085855 fix: preserve learning evidence integrity 2026-08-22 02:43:06 +03:00
m cf66cd472c Merge pull request 'Route model inventory by semantic intent' (#90) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 23s
Route model inventory by semantic intent
2026-08-21 23:31:31 +00:00
m 01f4fa27c2 fix: route model inventory by semantic intent
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 19s
2026-08-22 02:30:23 +03:00
m d286fbb234 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1 2026-08-22 02:25:43 +03:00
m 3b4ce66df0 feat: answer model inventory from capability registry 2026-08-22 02:25:42 +03:00
m 2306c8d6b3 Merge pull request 'Keep simple chat on built-in local route' (#89) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 26s
Keep simple chat on built-in local route
2026-08-21 23:13:32 +00:00
m 2b8887b3b2 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 27s
2026-08-22 02:11:37 +03:00
m 4750dd859c fix: keep simple chat on built-in local route 2026-08-22 02:11:36 +03:00
m a6059cc9c6 fix: explain chat session and runtime failures
CI / test (push) Successful in 26s
fix: explain chat session and runtime failures
2026-08-21 22:59:57 +00:00
m 498bc885c3 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 21s
2026-08-22 01:58:14 +03:00
m 487f7b7c9d fix: explain chat session and runtime failures 2026-08-22 01:58:14 +03:00
m b1b52920ab feat: upgrade Domovoy local cognition and recovery
CI / test (push) Successful in 20s
feat: upgrade Domovoy local cognition and recovery
2026-08-21 22:48:17 +00:00
m 57fb572d5b Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 21s
2026-08-22 01:47:16 +03:00
m 0849e9c4aa chore: authorize integrated cognitive upgrade scope
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 20s
2026-08-22 01:46:10 +03:00
m 51fa9ea506 chore: activate Domovoy cognitive upgrade task
CI / test (push) Failing after 14s
CI / test (pull_request) Failing after 14s
2026-08-22 01:45:44 +03:00
m 2cf1fb5c5e feat: expose runtime build status in chat
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s
2026-08-22 01:36:48 +03:00
m 4f06720e3f feat: keep Domovoy cognitive recovery active
CI / test (push) Failing after 15s
2026-08-22 01:29:45 +03:00
m cdc8eea1a7 Merge pull request 'feat: show chat version and update time' (#86) from codex/creator-chat-model-status-v1 into main
CI / test (push) Failing after 15s
2026-08-21 16:17:32 +00:00
m 9f717d720e feat: show chat version and update time
CI / test (push) Failing after 16s
CI / test (pull_request) Failing after 14s
2026-08-21 19:16:32 +03:00
m 3b5c0ff56d Merge pull request 'ci: use Portainer GitOps polling' (#85) from codex/creator-chat-model-status-v1 into main
CI / test (push) Failing after 14s
2026-08-21 16:04:53 +00:00
m 9cbf6c7536 ci: rely on Portainer GitOps polling
CI / test (push) Failing after 17s
CI / test (pull_request) Failing after 15s
2026-08-21 19:04:30 +03:00
m 9650eefcb4 Merge pull request 'feat: evolve Domovoy cognitive provider catalog' (#84) from codex/creator-chat-model-status-v1 into main
CI / test (push) Failing after 16s
Deploy production stack / deploy (push) Failing after 3s
2026-08-21 16:00:47 +00:00
m 1ed28ef77a ci: trigger Portainer deployment on main
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s
2026-08-21 18:48:54 +03:00
m c56610fa75 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s
2026-08-21 18:38:32 +03:00
m 3d7836d37c feat: evolve Domovoy cognitive provider catalog
CI / test (push) Failing after 16s
2026-08-21 18:37:04 +03:00
m 0482e6ac14 feat: authorize cognitive canary activation
CI / test (push) Failing after 15s
2026-08-21 15:49:57 +03:00
m 526368be7e fix: create cognitive discovery head
CI / test (push) Failing after 16s
2026-08-21 15:40:32 +03:00
m 3c2d8a38d7 chore: log discovery persistence failure
CI / test (push) Failing after 14s
2026-08-21 15:36:02 +03:00
m cabf386203 fix: refresh cognitive discovery health
CI / test (push) Failing after 14s
2026-08-21 15:31:34 +03:00
m f9bd267c04 fix: preserve discovery health for cognitive canary
CI / test (push) Failing after 15s
2026-08-21 15:26:34 +03:00
m b041a94f71 test: qualify domovoy lifeboat model
CI / test (push) Failing after 15s
2026-08-21 15:10:41 +03:00
m 814c9e1d32 fix: healthcheck dedicated lifeboat
CI / test (push) Failing after 15s
2026-08-21 15:06:19 +03:00
m b93715867d feat: run dedicated local lifeboat model
CI / test (push) Failing after 15s
2026-08-21 15:04:42 +03:00
m 009314e930 feat: keep domovoy alive in lifeboat mode
CI / test (push) Failing after 15s
2026-08-21 14:52:55 +03:00
m 0954c8f4ef Merge pull request 'fix(chat): block accidental AI secrets' (#83) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 25s
2026-08-21 10:39:56 +00:00
m 0a2599cfa6 fix(chat): block accidental AI secrets
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 20s
2026-08-21 13:38:57 +03:00
m ae9933ee84 Merge pull request 'fix(chat): stabilize header status' (#82) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 10:35:36 +00:00
m 7a59abc608 fix(chat): stabilize header status
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 20s
2026-08-21 13:34:35 +03:00
m 6e0cf3cd1d Merge pull request 'feat(chat): create stable three-column workspace' (#81) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 10:31:24 +00:00
m d7e77ea3dd feat(chat): create stable three-column workspace
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 20s
2026-08-21 13:29:58 +03:00
m 47fd0c4b02 Merge pull request 'feat(chat): add conversation starters' (#80) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 06:24:27 +00:00
m df9181ef6a feat(chat): add conversation starters
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 19s
2026-08-21 09:23:26 +03:00
m b0d31012da Merge pull request 'feat(core): answer cognitive channel questions' (#79) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 06:20:31 +00:00
m 8d149b2b9d feat(core): answer cognitive channel questions
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-08-21 09:19:15 +03:00
m 0d10e6fd82 Merge pull request 'feat(chat): bridge cognitive status to console' (#78) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 18s
2026-08-21 06:15:48 +00:00
m 9aa5771f55 feat(chat): bridge cognitive status to console
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 19s
2026-08-21 09:14:35 +03:00
m 75abfb204b Merge pull request 'feat(chat): show cognitive channel status' (#77) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 06:11:02 +00:00
m b0ee54153b feat(chat): show cognitive channel status
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 19s
2026-08-21 09:09:26 +03:00
m 26cf85108a Merge pull request 'Make live chat the public starting surface' (#76) from codex/creator-chat-primary-v1 into main
CI / test (push) Successful in 18s
2026-08-21 05:57:29 +00:00
m 7eb2f5f313 feat: make chat the public starting surface
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 19s
2026-08-21 08:56:20 +03:00
m 39a27102c9 Merge pull request 'Turn cabinet sections into pages' (#75) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 18s
2026-08-21 02:45:28 +00:00
m 7ac9a2697c chore: align page task with main
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-08-21 05:44:33 +03:00
m f2e8ec08c5 feat: turn cabinet sections into pages 2026-08-21 05:44:19 +03:00
m 1cfbb4ae66 Merge pull request 'Expose learning in top navigation' (#74) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 19s
2026-08-21 02:29:16 +00:00
m 7618a0bcbe feat: expose learning in top navigation
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-08-21 05:27:58 +03:00
m dcdc9a2e64 Merge pull request 'Correct active section accessibility semantics' (#73) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 18s
2026-08-21 02:25:29 +00:00
m 5783faf9dc chore: align task base with merged main
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 18s
2026-08-21 05:24:05 +03:00
m 9a968fe2f6 style: format creator web test
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 18s
2026-08-21 05:23:43 +03:00
m 41d4d7202f test: cover location navigation semantics 2026-08-21 05:23:42 +03:00
m ed3932c57a fix: mark active cabinet location semantically 2026-08-21 05:23:42 +03:00
m 3367361483 Merge pull request 'Add learning section to cabinet navigation' (#72) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 19s
2026-08-21 02:18:16 +00:00
m 51638d6e8c feat: add learning section to cabinet navigation
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 18s
2026-08-21 05:17:08 +03:00
m 0a1ad9f7d5 Merge pull request 'Make Creator cabinet IDs easier to use' (#71) from codex/creator-cabinet-operations-v2 into main
CI / test (push) Successful in 18s
2026-08-21 02:13:09 +00:00
m 4d51e65226 feat: make creator cabinet ids easier to use
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 18s
2026-08-21 05:11:38 +03:00
m 11a6a23f6e Merge pull request 'DEV-000072 semantic interpretation deferral boundary' (#69) from codex/dev-000072-semantic-deferral-boundary-v1 into main
CI / test (push) Successful in 18s
2026-08-17 20:03:08 +00:00
m ce666bbc43 DEV-000072 semantic interpretation deferral boundary
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 17s
2026-08-17 23:02:00 +03:00
m 64870798b9 Merge pull request 'DEV-000071 cognitive path reality and generalization baseline' (#68) from codex/dev-000071-promotion into main
CI / test (push) Successful in 17s
2026-08-17 19:33:08 +00:00
m 21e995432d DEV-000071 format Go evaluation sources
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 17s
2026-08-17 22:31:50 +03:00
m 3e99edd3a8 Merge branch 'codex/dev-000071-cognitive-path-reality-generalization-v1' into main
CI / test (pull_request) Failing after 13s
CI / test (push) Failing after 14s
2026-08-17 22:24:48 +03:00
m c1aff60443 DEV-000071 reality audit and generalization baseline
CI / test (push) Failing after 13s
2026-08-17 21:49:12 +03:00
m 3aa981bc59 Merge pull request 'DEV-000070 reality evaluation baseline harness' (#67) from codex/dev-000070-reality-evaluation-harness-v1 into main
CI / test (push) Successful in 17s
2026-08-17 18:20:30 +00:00
m 7759d8257e DEV-000070 add reality evaluation baseline harness
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 16s
2026-08-17 21:19:14 +03:00
m ade274011f Merge pull request 'DEV-000069 live artifact: project status checkpoint' (#66) from codex/dev-000069-live-artifact into main
CI / test (push) Successful in 18s
2026-08-17 17:55:03 +00:00
m c232169270 DEV-000069 record live delegation checkpoint
CI / test (push) Successful in 17s
CI / test (pull_request) Successful in 16s
2026-08-17 20:53:11 +03:00
m 4352e00491 Merge pull request 'DEV-000069 continuation: allow resolved attention continued outcomes' (#65) from codex/dev-000069-response-outcome-fix into main
CI / test (push) Successful in 18s
2026-08-17 17:49:51 +00:00
m 21830a03e4 DEV-000069 allow delegation continuation response outcomes
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 17s
2026-08-17 20:48:44 +03:00
m 4428e166be Merge pull request 'DEV-000069 continuation: agent delegation response candidates' (#64) from codex/general-agent-delegation-codex-bridge-v1 into main
CI / test (push) Successful in 18s
2026-08-17 17:25:40 +00:00
m a08c187284 Merge remote-tracking branch 'origin/main' into codex/general-agent-delegation-codex-bridge-v1
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 16s
2026-08-17 20:21:56 +03:00
m 41b0ac196e DEV-000069 include agent delegation response candidates
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 16s
2026-08-17 20:20:32 +03:00
m 5cf566dccd Merge pull request 'DEV-000069 General Agent Delegation + Codex Bridge V1' (#63) from codex/general-agent-delegation-codex-bridge-v1 into main
CI / test (push) Successful in 17s
2026-08-17 17:08:25 +00:00
m b3491520bd DEV-000069 general agent delegation codex bridge foundation
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 20s
2026-08-17 20:07:14 +03:00
m d870e1361d DEV-000068 final project status
CI / test (push) Successful in 16s
Merge DEV-000068 final status
2026-08-17 16:24:31 +00:00
m 8131d6cef3 DEV-000068 update final project status
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 16s
2026-08-17 19:23:48 +03:00
m 4bfca4fe24 DEV-000068 allow armed reminder completion
CI / test (push) Successful in 17s
Merge DEV-000068 armed reminder completion
2026-08-17 16:21:11 +00:00
m 21239cf3cc DEV-000068 allow armed reminder completion
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 17s
2026-08-17 19:20:28 +03:00
m 75857a2036 DEV-000068 normalize Epistemics evidence arrays
CI / test (push) Successful in 21s
Merge DEV-000068 Epistemics empty array normalization
2026-08-17 16:14:44 +00:00
m 9a9307af89 DEV-000068 normalize epistemics evidence arrays
CI / test (push) Successful in 17s
CI / test (pull_request) Successful in 16s
2026-08-17 19:14:01 +03:00
m ff66e8dc5c DEV-000068 learning write diagnostics
CI / test (push) Successful in 27s
Merge DEV-000068 learning diagnostics
2026-08-17 16:08:56 +00:00
m a862a07ae1 DEV-000068 add learning write diagnostics
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 26s
2026-08-17 19:08:14 +03:00
m 4d63c1bea4 DEV-000068 support strong Postgres credentials
CI / test (push) Successful in 16s
Merge DEV-000068 strong Postgres credential support
2026-08-17 15:59:02 +00:00
m 91c38de167 DEV-000068 support strong postgres credentials
CI / test (push) Successful in 17s
CI / test (pull_request) Successful in 16s
2026-08-17 18:58:18 +03:00
m 926afc82b5 DEV-000068 production status and dev bootstrap compatibility
CI / test (push) Successful in 21s
Merge DEV-000068 production status follow-up
2026-08-17 15:49:29 +00:00
338 changed files with 27238 additions and 459 deletions
+4
View File
@@ -15,6 +15,8 @@ jobs:
go-version: '1.26.6'
- name: Active task preflight
run: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json
- name: Independent governance verifier
run: go run ./cmd/devctl/independent
- name: Exact diff scope
run: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json
- name: Format check
@@ -25,3 +27,5 @@ jobs:
run: go test ./...
- name: Vet
run: go vet ./...
- name: Diff whitespace check
run: git diff --check
+61
View File
@@ -0,0 +1,61 @@
package agentbridge
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/work"
)
var ErrUnavailable = errors.New("agent execution bridge unavailable")
type HTTPExecutor struct {
URL string
Client *http.Client
}
func NewHTTPExecutor(url string, client *http.Client) (*HTTPExecutor, error) {
url = strings.TrimSpace(url)
if url == "" || !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return nil, ErrUnavailable
}
if client == nil {
client = http.DefaultClient
}
return &HTTPExecutor{URL: strings.TrimRight(url, "/"), Client: client}, nil
}
func (e *HTTPExecutor) Execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
body, err := json.Marshal(request)
if err != nil {
return work.AgentExecutionResult{}, err
}
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, e.URL+"/v1/execute", bytes.NewReader(body))
if err != nil {
return work.AgentExecutionResult{}, err
}
httpRequest.Header.Set("Content-Type", "application/json")
response, err := e.Client.Do(httpRequest)
if err != nil {
return work.AgentExecutionResult{}, fmt.Errorf("%w: %v", ErrUnavailable, err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return work.AgentExecutionResult{}, fmt.Errorf("%w: status=%s", ErrUnavailable, response.Status)
}
decoder := json.NewDecoder(io.LimitReader(response.Body, 1<<20))
var result work.AgentExecutionResult
if err := decoder.Decode(&result); err != nil {
return work.AgentExecutionResult{}, err
}
if result.State == "" || result.Summary == "" || result.ObservedAtUnixMillis < 1 {
return work.AgentExecutionResult{}, errors.New("agent execution bridge returned invalid result")
}
return result, nil
}
+43
View File
@@ -0,0 +1,43 @@
package agentbridge
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/work"
)
func TestHTTPExecutorCarriesTypedDispatchAndDecodesResult(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/execute" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var request work.AgentExecutionRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
if request.WorkerID != "worker-1" || request.FencingToken != 7 || request.Intent.DispatchID != "dispatch-1" {
t.Fatalf("request=%+v", request)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "completed", ObservedAtUnixMillis: 2})
}))
defer server.Close()
executor, err := NewHTTPExecutor(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1"}, WorkerID: "worker-1", FencingToken: 7})
if err != nil || result.State != work.AgentExecutionCompleted || result.Summary != "completed" {
t.Fatalf("result=%+v err=%v", result, err)
}
}
func TestHTTPExecutorRejectsUnavailableEndpoint(t *testing.T) {
if _, err := NewHTTPExecutor("", nil); err != ErrUnavailable {
t.Fatalf("expected unavailable, got %v", err)
}
}
+22 -1
View File
@@ -22,17 +22,35 @@ import (
type OpenAICompatibleLocalAdapter struct {
base *url.URL
client *http.Client
apiKey string
}
func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
return NewOpenAICompatibleLocalAdapterWithAPIKey(raw, "", transport)
}
// NewOpenAICompatibleLocalAdapterWithAPIKey creates an adapter for a gateway
// whose bearer credential was resolved by the trusted runtime boundary.
func NewOpenAICompatibleLocalAdapterWithAPIKey(raw, apiKey string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
return NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(raw, apiKey, transport, 10*time.Second)
}
// NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout creates a local gateway
// adapter with an explicit invocation deadline. The request context remains
// authoritative for per-call cancellation; this client deadline prevents a
// provider from holding a worker indefinitely.
func NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(raw, apiKey string, transport http.RoundTripper, timeout time.Duration) (*OpenAICompatibleLocalAdapter, error) {
base, err := url.Parse(raw)
if err != nil || base.Scheme != "http" || base.Host == "" || strings.TrimSuffix(base.Path, "/") != "/v1" || base.RawQuery != "" || base.Fragment != "" || base.User != nil {
return nil, errors.New("invalid local compatible endpoint")
}
if timeout <= 0 {
return nil, errors.New("local compatible timeout must be positive")
}
if transport == nil {
transport = http.DefaultTransport
}
return &OpenAICompatibleLocalAdapter{base: base, client: &http.Client{Transport: transport, Timeout: 10 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}}, nil
return &OpenAICompatibleLocalAdapter{base: base, client: &http.Client{Transport: transport, Timeout: timeout, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, apiKey: strings.TrimSpace(apiKey)}, nil
}
type compatibleModelsResponse struct {
@@ -145,6 +163,9 @@ func (a *OpenAICompatibleLocalAdapter) requestJSON(ctx context.Context, method,
return 0, err
}
request.Header.Set("Accept", "application/json")
if a.apiKey != "" {
request.Header.Set("Authorization", "Bearer "+a.apiKey)
}
if input != nil {
request.Header.Set("Content-Type", "application/json")
}
@@ -0,0 +1,59 @@
package models
import (
"context"
"io"
"net/http"
"strings"
"testing"
"time"
)
type captureRoundTripper struct {
authorization string
}
func TestOpenAICompatibleLocalAdapterUsesConfiguredTimeout(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", &captureRoundTripper{}, 47*time.Second)
if err != nil {
t.Fatal(err)
}
if adapter.client.Timeout != 47*time.Second {
t.Fatalf("unexpected client timeout: %s", adapter.client.Timeout)
}
}
func TestOpenAICompatibleLocalAdapterRejectsNonPositiveTimeout(t *testing.T) {
if _, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", nil, 0); err == nil {
t.Fatal("expected non-positive timeout to be rejected")
}
}
func (t *captureRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
t.authorization = request.Header.Get("Authorization")
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"data":[]}`)), Header: make(http.Header), Request: request}, nil
}
func TestOpenAICompatibleLocalAdapterUsesResolvedAPIKey(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKey("http://gateway/v1", "resolved-key", transport)
if err != nil {
t.Fatal(err)
}
_, _ = adapter.Discover(context.Background())
if transport.authorization != "Bearer resolved-key" {
t.Fatalf("unexpected authorization header: %q", transport.authorization)
}
}
func TestOpenAICompatibleLocalAdapterDoesNotSendAuthorizationWithoutKey(t *testing.T) {
transport := &captureRoundTripper{}
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
if err != nil {
t.Fatal(err)
}
_, _ = adapter.Discover(context.Background())
if transport.authorization != "" {
t.Fatalf("unexpected authorization header")
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
type AuthorityRepository struct{ pool *pgxpool.Pool }
func OpenAuthority(ctx context.Context, c ContinuityConfig) (*AuthorityRepository, error) {
p, e := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", c.Host, c.Port, c.Database, c.User, c.Password))
p, e := openPool(ctx, c)
if e != nil {
return nil, e
}
+129 -17
View File
@@ -19,8 +19,7 @@ var ErrCapabilityImplementationNotFound = errors.New("capability implementation
type CapabilityRepository struct{ pool *pgxpool.Pool }
func OpenCapability(ctx context.Context, config ContinuityConfig) (*CapabilityRepository, error) {
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
pool, err := pgxpool.New(ctx, dsn)
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
@@ -29,6 +28,89 @@ func OpenCapability(ctx context.Context, config ContinuityConfig) (*CapabilityRe
func (r *CapabilityRepository) Close() { r.pool.Close() }
func (r *CapabilityRepository) AdmitCognitive(ctx context.Context, leaseID string, policy capability.AdmissionPolicy, request capability.AdmissionRequest, nowUnixMillis int64) (capability.AdmissionDecision, capability.AdmissionLease, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
if leaseID == "" || nowUnixMillis < 1 {
return capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonInputBudget, PolicyID: policy.ID}, capability.AdmissionLease{}, nil
}
var existingState string
var existingPolicy string
err = tx.QueryRow(ctx, `SELECT state, policy_id FROM capability.cognitive_admission_leases WHERE lease_id=$1`, leaseID).Scan(&existingState, &existingPolicy)
if err == nil {
if existingState == "ACTIVE" {
return capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: existingPolicy}, capability.AdmissionLease{ID: leaseID, PolicyID: existingPolicy}, tx.Commit(ctx)
}
return capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonConcurrency, PolicyID: existingPolicy}, capability.AdmissionLease{}, tx.Commit(ctx)
}
if !errors.Is(err, pgx.ErrNoRows) {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
windowStart := nowUnixMillis - 60*60*1000
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_admission_counters(policy_id,window_start_unix_millis,accepted_count,concurrent_count,updated_at_unix_millis) VALUES($1,$2,0,0,$3) ON CONFLICT(policy_id) DO NOTHING`, policy.ID, windowStart, nowUnixMillis)
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
var storedWindow int64
var accepted, concurrent int
if err = tx.QueryRow(ctx, `SELECT window_start_unix_millis,accepted_count,concurrent_count FROM capability.cognitive_admission_counters WHERE policy_id=$1 FOR UPDATE`, policy.ID).Scan(&storedWindow, &accepted, &concurrent); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
if nowUnixMillis-storedWindow >= 60*60*1000 {
storedWindow, accepted = windowStart, 0
}
var expired int64
if err = tx.QueryRow(ctx, `WITH expired AS (UPDATE capability.cognitive_admission_leases SET state='EXPIRED' WHERE policy_id=$1 AND state='ACTIVE' AND expires_at_unix_millis <= $2 RETURNING lease_id) SELECT count(*) FROM expired`, policy.ID, nowUnixMillis).Scan(&expired); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
concurrent -= int(expired)
if concurrent < 0 {
concurrent = 0
}
decision := capability.Admit(request, policy, capability.AdmissionCounters{Concurrent: concurrent, AcceptedLastHour: accepted}, nowUnixMillis)
if decision.State != capability.AdmissionAdmitted {
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET window_start_unix_millis=$2,accepted_count=$3,concurrent_count=$4,updated_at_unix_millis=$5 WHERE policy_id=$1`, policy.ID, storedWindow, accepted, concurrent, nowUnixMillis); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
return decision, capability.AdmissionLease{}, tx.Commit(ctx)
}
expires := nowUnixMillis + 60*1000
if request.DeadlineMillis > nowUnixMillis && request.DeadlineMillis < expires {
expires = request.DeadlineMillis
}
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET window_start_unix_millis=$2,accepted_count=$3,concurrent_count=$4,updated_at_unix_millis=$5 WHERE policy_id=$1`, policy.ID, storedWindow, accepted+1, concurrent+1, nowUnixMillis); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_admission_leases(lease_id,policy_id,state,expires_at_unix_millis,created_at_unix_millis) VALUES($1,$2,'ACTIVE',$3,$4)`, leaseID, policy.ID, expires, nowUnixMillis); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
return decision, capability.AdmissionLease{ID: leaseID, PolicyID: policy.ID}, tx.Commit(ctx)
}
func (r *CapabilityRepository) ReleaseCognitiveAdmission(ctx context.Context, lease capability.AdmissionLease, nowUnixMillis int64) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
var policyID string
err = tx.QueryRow(ctx, `UPDATE capability.cognitive_admission_leases SET state='RELEASED' WHERE lease_id=$1 AND state='ACTIVE' RETURNING policy_id`, lease.ID).Scan(&policyID)
if errors.Is(err, pgx.ErrNoRows) {
return tx.Commit(ctx)
}
if err != nil {
return err
}
_, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET concurrent_count=GREATEST(0,concurrent_count-1),updated_at_unix_millis=$2 WHERE policy_id=$1`, policyID, nowUnixMillis)
if err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *CapabilityRepository) SaveHealth(ctx context.Context, health capability.Health) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.health (capability_id, state, reason, revision, semantic_version, observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (capability_id) DO UPDATE SET state=EXCLUDED.state, reason=EXCLUDED.reason, revision=EXCLUDED.revision, semantic_version=EXCLUDED.semantic_version, observed_at_unix_millis=EXCLUDED.observed_at_unix_millis WHERE capability.health.revision < EXCLUDED.revision`, health.CapabilityID, health.State, health.Reason, health.Revision, health.SemanticVersion, health.ObservedAtUnixMillis)
return err
@@ -323,18 +405,18 @@ func (r *CapabilityRepository) LoadActiveBinding(ctx context.Context, capability
func (r *CapabilityRepository) SaveProviderInstance(ctx context.Context, value capability.ProviderInstance) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances
(provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.ID, value.Revision, value.Kind,
value.EndpointRef, value.CredentialRef, value.Enabled, value.Qualification, value.Health, value.ObservedAtUnixMillis)
(provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.Revision, value.Kind,
value.EndpointRef, value.CredentialRef, value.Enabled, value.Qualification, value.Health, value.ObservedAtUnixMillis, value.BillingMode, value.AuthMode, value.InputCostMicros, value.OutputCostMicros, value.RequestCostMicros)
return err
}
func (r *CapabilityRepository) SaveModelDescriptor(ctx context.Context, value capability.ModelDescriptor) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors
(model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, value.ID, value.Revision,
(model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis,intelligence_score_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT DO NOTHING`, value.ID, value.Revision,
value.ProviderInstanceID, value.ProviderModelID, value.Fingerprint, value.MaxContextBytes, value.Modalities,
value.Capabilities, value.Qualification, value.Health, value.ObservedAtUnixMillis)
value.Capabilities, value.Qualification, value.Health, value.ObservedAtUnixMillis, value.IntelligenceScoreMillis)
return err
}
@@ -350,8 +432,8 @@ func (r *CapabilityRepository) SaveCognitiveBinding(ctx context.Context, value c
func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) {
rows, err := r.pool.Query(ctx, `SELECT
b.binding_id,b.revision,b.capability_id,b.owner_principal_id,b.provider_instance_id,b.provider_revision,b.model_descriptor_id,b.model_revision,b.data_policy,b.qualification,b.activation,b.health,b.limit_policy_ref,
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),
m.model_descriptor_id,m.revision,m.provider_instance_id,m.provider_model_id,m.fingerprint,m.max_context_bytes,m.modalities,m.capabilities,m.qualification,COALESCE(ma.health,m.health),GREATEST(m.observed_at_unix_millis,COALESCE(ma.observed_at_unix_millis,0))
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),p.billing_mode,p.auth_mode,p.input_cost_micros,p.output_cost_micros,p.request_cost_micros,
m.model_descriptor_id,m.revision,m.provider_instance_id,m.provider_model_id,m.fingerprint,m.max_context_bytes,m.modalities,m.capabilities,m.qualification,COALESCE(ma.health,m.health),GREATEST(m.observed_at_unix_millis,COALESCE(ma.observed_at_unix_millis,0)),m.intelligence_score_millis
FROM capability.cognitive_binding_revision_heads bh
JOIN capability.cognitive_bindings b ON b.binding_id=bh.binding_id AND b.revision=bh.revision
JOIN capability.cognitive_provider_instances p ON p.provider_instance_id=b.provider_instance_id AND p.revision=b.provider_revision
@@ -367,8 +449,8 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
for rows.Next() {
var value capability.ResolvedCognitiveBinding
err = rows.Scan(&value.Binding.ID, &value.Binding.Revision, &value.Binding.CapabilityID, &value.Binding.OwnerPrincipalID, &value.Binding.ProviderInstanceID, &value.Binding.ProviderRevision, &value.Binding.ModelDescriptorID, &value.Binding.ModelRevision, &value.Binding.DataPolicy, &value.Binding.Qualification, &value.Binding.Activation, &value.Binding.Health, &value.Binding.LimitPolicyRef,
&value.Provider.ID, &value.Provider.Revision, &value.Provider.Kind, &value.Provider.EndpointRef, &value.Provider.CredentialRef, &value.Provider.Enabled, &value.Provider.Qualification, &value.Provider.Health, &value.Provider.ObservedAtUnixMillis,
&value.Model.ID, &value.Model.Revision, &value.Model.ProviderInstanceID, &value.Model.ProviderModelID, &value.Model.Fingerprint, &value.Model.MaxContextBytes, &value.Model.Modalities, &value.Model.Capabilities, &value.Model.Qualification, &value.Model.Health, &value.Model.ObservedAtUnixMillis)
&value.Provider.ID, &value.Provider.Revision, &value.Provider.Kind, &value.Provider.EndpointRef, &value.Provider.CredentialRef, &value.Provider.Enabled, &value.Provider.Qualification, &value.Provider.Health, &value.Provider.ObservedAtUnixMillis, &value.Provider.BillingMode, &value.Provider.AuthMode, &value.Provider.InputCostMicros, &value.Provider.OutputCostMicros, &value.Provider.RequestCostMicros,
&value.Model.ID, &value.Model.Revision, &value.Model.ProviderInstanceID, &value.Model.ProviderModelID, &value.Model.Fingerprint, &value.Model.MaxContextBytes, &value.Model.Modalities, &value.Model.Capabilities, &value.Model.Qualification, &value.Model.Health, &value.Model.ObservedAtUnixMillis, &value.Model.IntelligenceScoreMillis)
if err != nil {
return nil, err
}
@@ -415,7 +497,7 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
return err
}
defer func() { _ = tx.Rollback(ctx) }()
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.Provider.ID, value.Provider.Revision, value.Provider.Kind, value.Provider.EndpointRef, value.Provider.CredentialRef, value.Provider.Enabled, value.Provider.Qualification, value.Provider.Health, value.Provider.ObservedAtUnixMillis)
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT (provider_instance_id,revision) DO UPDATE SET health=EXCLUDED.health,observed_at_unix_millis=EXCLUDED.observed_at_unix_millis,billing_mode=EXCLUDED.billing_mode,auth_mode=EXCLUDED.auth_mode,input_cost_micros=EXCLUDED.input_cost_micros,output_cost_micros=EXCLUDED.output_cost_micros,request_cost_micros=EXCLUDED.request_cost_micros WHERE capability.cognitive_provider_instances.observed_at_unix_millis <= EXCLUDED.observed_at_unix_millis`, value.Provider.ID, value.Provider.Revision, value.Provider.Kind, value.Provider.EndpointRef, value.Provider.CredentialRef, value.Provider.Enabled, value.Provider.Qualification, value.Provider.Health, value.Provider.ObservedAtUnixMillis, value.Provider.BillingMode, value.Provider.AuthMode, value.Provider.InputCostMicros, value.Provider.OutputCostMicros, value.Provider.RequestCostMicros)
if err != nil {
return err
}
@@ -424,7 +506,7 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
return err
}
for index, model := range value.Models {
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, model.ID, model.Revision, model.ProviderInstanceID, model.ProviderModelID, model.Fingerprint, model.MaxContextBytes, model.Modalities, model.Capabilities, model.Qualification, model.Health, model.ObservedAtUnixMillis)
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis,intelligence_score_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT (model_descriptor_id,revision) DO UPDATE SET health=EXCLUDED.health,observed_at_unix_millis=EXCLUDED.observed_at_unix_millis,intelligence_score_millis=EXCLUDED.intelligence_score_millis WHERE capability.cognitive_model_descriptors.observed_at_unix_millis <= EXCLUDED.observed_at_unix_millis`, model.ID, model.Revision, model.ProviderInstanceID, model.ProviderModelID, model.Fingerprint, model.MaxContextBytes, model.Modalities, model.Capabilities, model.Qualification, model.Health, model.ObservedAtUnixMillis, model.IntelligenceScoreMillis)
if err != nil {
return err
}
@@ -434,7 +516,11 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
return err
}
binding := value.Bindings[index]
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision, binding.CapabilityID, binding.OwnerPrincipalID, binding.ProviderInstanceID, binding.ProviderRevision, binding.ModelDescriptorID, binding.ModelRevision, binding.DataPolicy, binding.Qualification, binding.Activation, binding.Health, binding.LimitPolicyRef)
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT (binding_id,revision) DO UPDATE SET health=EXCLUDED.health WHERE capability.cognitive_bindings.health <> EXCLUDED.health`, binding.ID, binding.Revision, binding.CapabilityID, binding.OwnerPrincipalID, binding.ProviderInstanceID, binding.ProviderRevision, binding.ModelDescriptorID, binding.ModelRevision, binding.DataPolicy, binding.Qualification, binding.Activation, binding.Health, binding.LimitPolicyRef)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision)
if err != nil {
return err
}
@@ -454,10 +540,10 @@ func (r *CapabilityRepository) SaveCognitiveCanaryPromotion(ctx context.Context,
}
defer func() { _ = tx.Rollback(ctx) }()
p, m, b, e := value.Provider, value.Model, value.Binding, value.Evaluation
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9) ON CONFLICT DO NOTHING`, p.ID, p.Revision, p.Kind, p.EndpointRef, p.CredentialRef, p.Enabled, p.Qualification, p.Health, p.ObservedAtUnixMillis); err != nil {
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, p.ID, p.Revision, p.Kind, p.EndpointRef, p.CredentialRef, p.Enabled, p.Qualification, p.Health, p.ObservedAtUnixMillis, p.BillingMode, p.AuthMode, p.InputCostMicros, p.OutputCostMicros, p.RequestCostMicros); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, m.ID, m.Revision, m.ProviderInstanceID, m.ProviderModelID, m.Fingerprint, m.MaxContextBytes, m.Modalities, m.Capabilities, m.Qualification, m.Health, m.ObservedAtUnixMillis); err != nil {
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis,intelligence_score_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT DO NOTHING`, m.ID, m.Revision, m.ProviderInstanceID, m.ProviderModelID, m.Fingerprint, m.MaxContextBytes, m.Modalities, m.Capabilities, m.Qualification, m.Health, m.ObservedAtUnixMillis, m.IntelligenceScoreMillis); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, b.ID, b.Revision, b.CapabilityID, b.OwnerPrincipalID, b.ProviderInstanceID, b.ProviderRevision, b.ModelDescriptorID, b.ModelRevision, b.DataPolicy, b.Qualification, b.Activation, b.Health, b.LimitPolicyRef); err != nil {
@@ -476,6 +562,32 @@ func (r *CapabilityRepository) SaveCognitiveCanaryPromotion(ctx context.Context,
return tx.Commit(ctx)
}
func (r *CapabilityRepository) SaveCognitiveActivationPromotion(ctx context.Context, value capability.CognitiveActivationPromotion) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
b, e := value.Binding, value.Evaluation
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, b.ID, b.Revision, b.CapabilityID, b.OwnerPrincipalID, b.ProviderInstanceID, b.ProviderRevision, b.ModelDescriptorID, b.ModelRevision, b.DataPolicy, b.Qualification, b.Activation, b.Health, b.LimitPolicyRef); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_evaluated_capabilities (evaluation_id,binding_id,binding_revision,capability_id,result,quality_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT DO NOTHING`, e.ID, e.BindingID, e.BindingRevision, e.CapabilityID, e.Result, e.QualityScoreMillis, e.CriticalFailureMillis, e.BenchmarkVersion, e.ArtifactID, e.ObservedAtUnixMillis); err != nil {
return err
}
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_bindings SET activation='DEPRECATED' WHERE capability_id=$1 AND owner_principal_id=$2 AND activation='ACTIVE' AND NOT (binding_id=$3 AND revision=$4)`, b.CapabilityID, b.OwnerPrincipalID, b.ID, b.Revision); err != nil {
return err
}
result, err := tx.Exec(ctx, `UPDATE capability.cognitive_binding_revision_heads SET revision=$1,updated_at=CURRENT_TIMESTAMP WHERE binding_id=$2 AND revision=$3`, b.Revision, b.ID, value.ExpectedBindingRevision)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return ErrCapabilityBindingConflict
}
return tx.Commit(ctx)
}
func (r *CapabilityRepository) SavePerformanceEvidence(ctx context.Context, value capability.PerformanceEvidence) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_performance_evidence (evidence_id,binding_id,binding_revision,model_descriptor_id,model_revision,hardware_profile,runtime,quantization,context_units,thinking_enabled,generation_tps_millis,prompt_tps_millis,ttft_micros,latency_micros,memory_bytes,reliability_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.BindingID, value.BindingRevision, value.ModelDescriptorID, value.ModelRevision, value.HardwareProfile, value.Runtime, value.Quantization, value.ContextUnits, value.ThinkingEnabled, value.GenerationTPSMillis, value.PromptTPSMillis, value.TTFTMicros, value.LatencyMicros, value.MemoryBytes, value.ReliabilityMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
return err
+1 -1
View File
@@ -23,7 +23,7 @@ var (
type CommitRepository struct{ pool *pgxpool.Pool }
func OpenCommit(ctx context.Context, c ContinuityConfig) (*CommitRepository, error) {
p, e := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", c.Host, c.Port, c.Database, c.User, c.Password))
p, e := openPool(ctx, c)
if e != nil {
return nil, e
}
+158 -25
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/continuity"
@@ -29,9 +30,25 @@ var (
type ContinuityConfig struct{ Host, Port, Database, User, Password string }
type ContinuityRepository struct{ pool *pgxpool.Pool }
func openPool(ctx context.Context, config ContinuityConfig) (*pgxpool.Pool, error) {
poolConfig, err := pgxpool.ParseConfig("")
if err != nil {
return nil, err
}
port, err := strconv.ParseUint(config.Port, 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid postgres port: %w", err)
}
poolConfig.ConnConfig.Host = config.Host
poolConfig.ConnConfig.Port = uint16(port)
poolConfig.ConnConfig.Database = config.Database
poolConfig.ConnConfig.User = config.User
poolConfig.ConnConfig.Password = config.Password
return pgxpool.NewWithConfig(ctx, poolConfig)
}
func OpenContinuity(ctx context.Context, config ContinuityConfig) (*ContinuityRepository, error) {
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
pool, err := pgxpool.New(ctx, dsn)
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
@@ -145,10 +162,10 @@ func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continui
return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err
}
err = tx.QueryRow(ctx, `INSERT INTO continuity.attention_items (
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,
action_attempt_id,delivery_outcome,response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,0,'','','','','','','','',0,1)
VALUES ($1,$2,$3,$4,$5,'',$6,$7,$8,$9,$10,0,'','','','','','','','',0,1)
ON CONFLICT (source_kind, source_prospective_item_id) DO UPDATE SET
state=EXCLUDED.state,
due_at_unix_millis=EXCLUDED.due_at_unix_millis,
@@ -164,12 +181,12 @@ func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continui
response_outcome='',
responded_at_unix_millis=0,
revision=continuity.attention_items.revision+1
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
attention.ID, attention.OwnerPrincipalID, attention.ScopeID, attention.SourceKind, attention.SourceProspectiveID,
attention.Level, attention.State, attention.Summary, attention.DueAtUnixMillis, attention.CreatedAtUnixMillis).Scan(
&attention.ID, &attention.OwnerPrincipalID, &attention.ScopeID, &attention.SourceKind, &attention.SourceProspectiveID,
&attention.ID, &attention.OwnerPrincipalID, &attention.ScopeID, &attention.SourceKind, &attention.SourceProspectiveID, &attention.SourceExternalRef,
&attention.Level, &attention.State, &attention.Summary, &attention.DueAtUnixMillis, &attention.CreatedAtUnixMillis,
&attention.DeliveredAtUnixMillis, &attention.ActionIntentID, &attention.AuthorizedActionID, &attention.ActionAttemptID,
&attention.DeliveryOutcome, &attention.ResponseID, &attention.ResponseUtteranceID, &attention.ResponseOperation,
@@ -187,7 +204,7 @@ func (r *ContinuityRepository) PendingAttention(ctx context.Context, limit int)
if limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid attention limit")
}
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision
FROM continuity.attention_items WHERE state=$1 ORDER BY due_at_unix_millis, attention_item_id LIMIT $2`, continuity.AttentionPending, limit)
@@ -202,7 +219,7 @@ func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner ident
if owner == "" || limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid attention owner query")
}
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,
rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision
FROM continuity.attention_items WHERE owner_principal_id=$1 ORDER BY due_at_unix_millis DESC, attention_item_id DESC LIMIT $2`, owner, limit)
@@ -217,8 +234,37 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
if owner == "" || limit < 1 || limit > 50 {
return nil, fmt.Errorf("invalid response candidate query")
}
candidates := make([]continuitydomain.ResponseCandidate, 0, limit)
attentionRows, err := r.pool.Query(ctx, `SELECT
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision
FROM continuity.attention_items
WHERE owner_principal_id=$1
AND source_kind=$2
AND state IN ($3,$4,$5)
ORDER BY created_at_unix_millis DESC, attention_item_id DESC
LIMIT $6`,
owner, continuity.AttentionSourceAgentDelegation, continuity.AttentionPending, continuity.AttentionDelivered, continuity.AttentionFailed, limit)
if err != nil {
return nil, fmt.Errorf("list delegation response candidates: %w", err)
}
defer attentionRows.Close()
for attentionRows.Next() {
item, err := scanAttentionItem(attentionRows)
if err != nil {
return nil, fmt.Errorf("scan delegation response candidate: %w", err)
}
candidates = append(candidates, continuitydomain.ResponseCandidate{Attention: item})
}
if err := attentionRows.Err(); err != nil {
return nil, fmt.Errorf("list delegation response candidates: %w", err)
}
if len(candidates) >= limit {
return candidates[:limit], nil
}
rows, err := r.pool.Query(ctx, `SELECT
COALESCE(a.attention_item_id,''),COALESCE(a.owner_principal_id,''),COALESCE(a.scope_id,''),COALESCE(a.source_kind,''),COALESCE(a.source_prospective_item_id,''),COALESCE(a.level,''),COALESCE(a.state,''),COALESCE(a.summary,''),
COALESCE(a.attention_item_id,''),COALESCE(a.owner_principal_id,''),COALESCE(a.scope_id,''),COALESCE(a.source_kind,''),COALESCE(a.source_prospective_item_id,''),COALESCE(a.source_external_ref,''),COALESCE(a.level,''),COALESCE(a.state,''),COALESCE(a.summary,''),
COALESCE(a.due_at_unix_millis,0),COALESCE(a.created_at_unix_millis,0),COALESCE(a.delivered_at_unix_millis,0),COALESCE(a.action_intent_id,''),COALESCE(a.authorized_action_id,''),COALESCE(a.action_attempt_id,''),COALESCE(a.delivery_outcome,''),
COALESCE(a.response_id,''),COALESCE(a.response_utterance_id,''),COALESCE(a.response_operation,''),COALESCE(a.response_outcome,''),COALESCE(a.responded_at_unix_millis,0),COALESCE(a.revision,0),
p.prospective_item_id,p.commitment_id,p.owner_principal_id,p.scope_id,p.goal_id,p.source_intent_id,p.source_utterance_id,p.summary,p.trigger_at_unix_millis,p.state,p.revision
@@ -230,20 +276,19 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
ORDER BY p.recorded_at DESC, COALESCE(a.delivered_at_unix_millis,0) DESC, p.prospective_item_id DESC
LIMIT $8`,
owner, continuity.AttentionSourceProspectiveReminder, continuity.AttentionDelivered, continuity.AttentionPending,
continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit)
continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit-len(candidates))
if err != nil {
return nil, fmt.Errorf("list human response candidates: %w", err)
}
defer rows.Close()
var candidates []continuitydomain.ResponseCandidate
for rows.Next() {
var candidate continuitydomain.ResponseCandidate
var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, level, attentionState, attentionSummary string
var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, sourceExternalRef, level, attentionState, attentionSummary string
var actionIntent, authorizedAction, actionAttempt, deliveryOutcome, responseID, responseUtterance, responseOperation, responseOutcome string
var dueAt, createdAt, deliveredAt, respondedAt int64
var attentionRevision uint64
if err := rows.Scan(
&attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &level, &attentionState, &attentionSummary,
&attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &sourceExternalRef, &level, &attentionState, &attentionSummary,
&dueAt, &createdAt, &deliveredAt, &actionIntent, &authorizedAction, &actionAttempt, &deliveryOutcome,
&responseID, &responseUtterance, &responseOperation, &responseOutcome, &respondedAt, &attentionRevision,
&candidate.Prospective.ID, &candidate.Prospective.CommitmentID, &candidate.Prospective.OwnerPrincipalID,
@@ -259,6 +304,7 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
ScopeID: authority.ScopeID(attentionScope),
SourceKind: continuity.AttentionSourceKind(sourceKind),
SourceProspectiveID: continuity.ProspectiveItemID(sourceProspective),
SourceExternalRef: sourceExternalRef,
Level: continuity.AttentionLevel(level),
State: continuity.AttentionState(attentionState),
Summary: attentionSummary,
@@ -285,6 +331,47 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
return candidates, nil
}
func scanAttentionItem(scanner interface {
Scan(...any) error
}) (continuity.AttentionItem, error) {
var item continuity.AttentionItem
var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, sourceExternalRef, level, attentionState, attentionSummary string
var actionIntent, authorizedAction, actionAttempt, deliveryOutcome, responseID, responseUtterance, responseOperation, responseOutcome string
var dueAt, createdAt, deliveredAt, respondedAt int64
var attentionRevision uint64
if err := scanner.Scan(
&attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &sourceExternalRef, &level, &attentionState, &attentionSummary,
&dueAt, &createdAt, &deliveredAt, &actionIntent, &authorizedAction, &actionAttempt, &deliveryOutcome,
&responseID, &responseUtterance, &responseOperation, &responseOutcome, &respondedAt, &attentionRevision); err != nil {
return continuity.AttentionItem{}, err
}
item = continuity.AttentionItem{
ID: continuity.AttentionItemID(attentionID),
OwnerPrincipalID: identity.PrincipalID(attentionOwner),
ScopeID: authority.ScopeID(attentionScope),
SourceKind: continuity.AttentionSourceKind(sourceKind),
SourceProspectiveID: continuity.ProspectiveItemID(sourceProspective),
SourceExternalRef: sourceExternalRef,
Level: continuity.AttentionLevel(level),
State: continuity.AttentionState(attentionState),
Summary: attentionSummary,
DueAtUnixMillis: dueAt,
CreatedAtUnixMillis: createdAt,
DeliveredAtUnixMillis: deliveredAt,
ActionIntentID: continuity.CommittedActionIntentID(actionIntent),
AuthorizedActionID: continuity.CommittedAuthorizedActionID(authorizedAction),
ActionAttemptID: continuity.CommittedActionAttemptID(actionAttempt),
DeliveryOutcome: deliveryOutcome,
ResponseID: continuity.HumanResponseID(responseID),
ResponseUtteranceID: responseUtterance,
ResponseOperation: continuity.HumanResponseOperation(responseOperation),
ResponseOutcome: continuity.ResponseOutcome(responseOutcome),
RespondedAtUnixMillis: respondedAt,
Revision: continuity.AttentionRevision(attentionRevision),
}
return item, nil
}
func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response continuity.HumanResponse) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
@@ -317,11 +404,10 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
if tag.RowsAffected() != 1 {
return ErrProspectiveStateConflict
}
if response.TargetAttentionID == "" {
return ErrAttentionNotFound
}
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
return err
if response.TargetAttentionID != "" {
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
return err
}
}
case continuity.ResponseSnooze:
tag, err := tx.Exec(ctx, `UPDATE continuity.prospective_items SET state=$1, trigger_at_unix_millis=$2, revision=revision+1
@@ -362,19 +448,66 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
return err
}
case continuity.ResponseContinue, continuity.ResponseReject:
if response.TargetAttentionID == "" {
return ErrAttentionNotFound
}
if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (r *ContinuityRepository) CreateAttentionItem(ctx context.Context, item continuity.AttentionItem) (continuity.AttentionItem, error) {
err := r.pool.QueryRow(ctx, `INSERT INTO continuity.attention_items(
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
ON CONFLICT (source_kind, source_external_ref) WHERE source_external_ref <> '' DO UPDATE SET
level=EXCLUDED.level,
state=EXCLUDED.state,
summary=EXCLUDED.summary,
due_at_unix_millis=EXCLUDED.due_at_unix_millis,
created_at_unix_millis=EXCLUDED.created_at_unix_millis,
delivered_at_unix_millis=0,
action_intent_id='',
authorized_action_id='',
action_attempt_id='',
delivery_outcome='',
response_id='',
response_utterance_id='',
response_operation='',
response_outcome='',
responded_at_unix_millis=0,
revision=continuity.attention_items.revision+1
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
item.ID, item.OwnerPrincipalID, item.ScopeID, item.SourceKind, item.SourceProspectiveID, item.SourceExternalRef, item.Level, item.State,
item.Summary, item.DueAtUnixMillis, item.CreatedAtUnixMillis, item.DeliveredAtUnixMillis, item.ActionIntentID, item.AuthorizedActionID,
item.ActionAttemptID, item.DeliveryOutcome, item.ResponseID, item.ResponseUtteranceID, item.ResponseOperation, item.ResponseOutcome,
item.RespondedAtUnixMillis, item.Revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID,
&item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation, &item.ResponseOutcome,
&item.RespondedAtUnixMillis, &item.Revision)
if err != nil {
return continuity.AttentionItem{}, fmt.Errorf("create attention item: %w", err)
}
return item, nil
}
func (r *ContinuityRepository) MarkAttentionDelivering(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision) (continuity.AttentionItem, error) {
var item continuity.AttentionItem
err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items SET state=$1, revision=revision+1
WHERE attention_item_id=$2 AND state=$3 AND revision=$4
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
continuity.AttentionDelivering, id, continuity.AttentionPending, revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID,
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
@@ -390,12 +523,12 @@ func (r *ContinuityRepository) MarkAttentionDelivered(ctx context.Context, id co
SET state=$1, revision=revision+1, delivered_at_unix_millis=$2, action_intent_id=$3, authorized_action_id=$4,
action_attempt_id=$5, delivery_outcome=$6
WHERE attention_item_id=$7 AND state=$8 AND revision=$9
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
continuity.AttentionDelivered, deliveredAtUnixMillis, intentID, authorizedID, attemptID, outcome,
id, continuity.AttentionDelivering, revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID,
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
@@ -411,12 +544,12 @@ func (r *ContinuityRepository) MarkAttentionFailed(ctx context.Context, id conti
SET state=$1, revision=revision+1, action_intent_id=$2, authorized_action_id=$3,
action_attempt_id=$4, delivery_outcome=$5
WHERE attention_item_id=$6 AND state=$7 AND revision=$8
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis,
RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis,
created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`,
continuity.AttentionFailed, intentID, authorizedID, attemptID, outcome,
id, continuity.AttentionDelivering, revision).Scan(
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State,
&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State,
&item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID,
&item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID,
&item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision)
@@ -525,7 +658,7 @@ func scanAttentionRows(rows continuityRows) ([]continuity.AttentionItem, error)
items := make([]continuity.AttentionItem, 0)
for rows.Next() {
var item continuity.AttentionItem
if err := rows.Scan(&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID,
if err := rows.Scan(&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef,
&item.Level, &item.State, &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis,
&item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, &item.ActionAttemptID,
&item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation,
+81 -4
View File
@@ -3,6 +3,7 @@ package postgres
import (
"context"
"fmt"
"time"
"git.cin.su/m/Assistant/contracts/epistemics"
"github.com/jackc/pgx/v5/pgxpool"
@@ -11,8 +12,7 @@ import (
type EpistemicsRepository struct{ pool *pgxpool.Pool }
func OpenEpistemics(ctx context.Context, config ContinuityConfig) (*EpistemicsRepository, error) {
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
pool, err := pgxpool.New(ctx, dsn)
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
@@ -21,6 +21,10 @@ func OpenEpistemics(ctx context.Context, config ContinuityConfig) (*EpistemicsRe
func (r *EpistemicsRepository) Close() { r.pool.Close() }
func (r *EpistemicsRepository) Ping(ctx context.Context) error {
return r.pool.Ping(ctx)
}
func (r *EpistemicsRepository) SaveSystemExpectation(ctx context.Context, value epistemics.SystemExpectation) error {
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.system_expectations (
expectation_id, original_intent_id, source_utterance_id, expected_outcome, expected_effect,
@@ -65,7 +69,7 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
}
rows, err := r.pool.Query(ctx, `SELECT
reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,
summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision
summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision,claimed_by,claim_expires_at_unix_millis
FROM epistemics.reflection_candidates ORDER BY recorded_at_unix_millis DESC, reflection_candidate_id DESC LIMIT $1`, limit)
if err != nil {
return nil, err
@@ -75,7 +79,7 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
for rows.Next() {
var value epistemics.ReflectionCandidate
if err := rows.Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID,
&value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision); err != nil {
&value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision, &value.ClaimedBy, &value.ClaimExpiresAtUnixMillis); err != nil {
return nil, err
}
result = append(result, value)
@@ -83,6 +87,61 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
return result, rows.Err()
}
func (r *EpistemicsRepository) ClaimReflectionCandidate(ctx context.Context, workerID string, nowUnixMillis, leaseMillis int64) (epistemics.ReflectionCandidate, error) {
if workerID == "" || leaseMillis < 1 {
return epistemics.ReflectionCandidate{}, fmt.Errorf("invalid reflection claim")
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return epistemics.ReflectionCandidate{}, err
}
defer tx.Rollback(ctx)
var value epistemics.ReflectionCandidate
err = tx.QueryRow(ctx, `SELECT reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision,claimed_by,claim_expires_at_unix_millis
FROM epistemics.reflection_candidates
WHERE state='OPEN' AND (claim_expires_at_unix_millis=0 OR claim_expires_at_unix_millis < $1)
ORDER BY recorded_at_unix_millis,reflection_candidate_id
FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID, &value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision, &value.ClaimedBy, &value.ClaimExpiresAtUnixMillis)
if err != nil {
return epistemics.ReflectionCandidate{}, err
}
value.State = epistemics.ReflectionCandidateEvaluating
value.ClaimedBy = workerID
value.ClaimExpiresAtUnixMillis = nowUnixMillis + leaseMillis
_, err = tx.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state=$1,claimed_by=$2,claim_expires_at_unix_millis=$3 WHERE reflection_candidate_id=$4 AND state='OPEN'`, value.State, value.ClaimedBy, value.ClaimExpiresAtUnixMillis, value.ID)
if err != nil {
return epistemics.ReflectionCandidate{}, err
}
if err := tx.Commit(ctx); err != nil {
return epistemics.ReflectionCandidate{}, err
}
return value, nil
}
func (r *EpistemicsRepository) RecoverReflectionClaims(ctx context.Context, nowUnixMillis int64) (int64, error) {
result, err := r.pool.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state='OPEN',claimed_by='',claim_expires_at_unix_millis=0 WHERE state='EVALUATING' AND claim_expires_at_unix_millis > 0 AND claim_expires_at_unix_millis < $1`, nowUnixMillis)
return result.RowsAffected(), err
}
func (r *EpistemicsRepository) ResolveReflectionCandidate(ctx context.Context, id epistemics.ReflectionCandidateID, workerID string, nowUnixMillis int64) error {
if id == "" || workerID == "" {
return fmt.Errorf("invalid reflection resolution")
}
result, err := r.pool.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state='RESOLVED',claimed_by='',claim_expires_at_unix_millis=0,revision=revision+1 WHERE reflection_candidate_id=$1 AND state='EVALUATING' AND claimed_by=$2 AND claim_expires_at_unix_millis >= $3`, id, workerID, nowUnixMillis)
if err == nil && result.RowsAffected() != 1 {
return fmt.Errorf("reflection candidate claim lost")
}
return err
}
func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, interval time.Duration) error {
if interval <= 0 {
return fmt.Errorf("invalid reflection poll interval")
}
_, err := r.pool.Exec(ctx, `SELECT pg_sleep($1)`, interval.Seconds())
return err
}
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
@@ -106,3 +165,21 @@ func (r *EpistemicsRepository) SaveReflectionCandidate(ctx context.Context, valu
value.Summary, value.EvidenceRefs, value.RequiresCognition, value.RecordedAtUnixMillis, value.Revision)
return err
}
func (r *EpistemicsRepository) SaveTruthBundle(ctx context.Context, value epistemics.TruthBundle, recordedAtUnixMillis int64) error {
if value.Source.ID == "" || value.Snapshot.ID == "" || value.Excerpt.ID == "" || value.Claim.ID == "" || value.Belief.ID == "" || value.Citation.ID == "" || recordedAtUnixMillis < 1 {
return fmt.Errorf("invalid truth bundle")
}
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.truth_bundles (
bundle_id,source_id,source_kind,source_locator,source_title,source_publisher,snapshot_id,snapshot_source_id,snapshot_content_digest,snapshot_captured_at_unix_millis,snapshot_freshness,
excerpt_id,excerpt_snapshot_id,excerpt_text,excerpt_start,excerpt_end,observation_id,observation_observed_at_unix_millis,claim_id,claim_statement,claim_observation_id,claim_citation_id,claim_verification,
belief_id,belief_claim_id,belief_state,citation_id,citation_excerpt_id,citation_evidence,citation_coverage,citation_conflict,citation_resolved_at_unix_millis,recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33)
ON CONFLICT (bundle_id) DO UPDATE SET recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`,
"bundle:"+string(value.Citation.ID), value.Source.ID, value.Source.Kind, value.Source.Locator, value.Source.Title, value.Source.Publisher,
value.Snapshot.ID, value.Snapshot.SourceID, value.Snapshot.ContentDigest, value.Snapshot.CapturedAtUnixMillis, value.Snapshot.Freshness,
value.Excerpt.ID, value.Excerpt.SnapshotID, value.Excerpt.Text, value.Excerpt.Start, value.Excerpt.End,
value.Observation.ID, value.Observation.ObservedAt, value.Claim.ID, value.Claim.Statement, value.Claim.ObservationID, value.Claim.CitationID, value.Claim.Verification,
value.Belief.ID, value.Belief.ClaimID, value.Belief.State, value.Citation.ID, value.Citation.ExcerptID, value.Citation.Evidence, value.Citation.Coverage, value.Citation.Conflict, value.Citation.ResolvedAtUnixMillis, recordedAtUnixMillis)
return err
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
type IdentityRepository struct{ pool *pgxpool.Pool }
func OpenIdentity(ctx context.Context, config ContinuityConfig) (*IdentityRepository, error) {
pool, err := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password))
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
+13 -4
View File
@@ -2,7 +2,6 @@ package postgres
import (
"context"
"fmt"
"git.cin.su/m/Assistant/contracts/interaction"
"github.com/jackc/pgx/v5/pgxpool"
@@ -11,7 +10,7 @@ import (
type InteractionRepository struct{ pool *pgxpool.Pool }
func OpenInteraction(ctx context.Context, c ContinuityConfig) (*InteractionRepository, error) {
p, e := pgxpool.New(ctx, fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", c.Host, c.Port, c.Database, c.User, c.Password))
p, e := openPool(ctx, c)
return &InteractionRepository{p}, e
}
func (r *InteractionRepository) Close() { r.pool.Close() }
@@ -44,15 +43,25 @@ func (r *InteractionRepository) AppendUtterance(ctx context.Context, c interacti
return tx.Commit(ctx)
}
func (r *InteractionRepository) SaveSemanticResult(ctx context.Context, interpretation interaction.UtteranceInterpretation, projects []interaction.ProjectReference, relations []interaction.InteractionRelation) error {
func (r *InteractionRepository) SaveSemanticResult(ctx context.Context, interpretation interaction.UtteranceInterpretation, candidates []interaction.SemanticCandidate, projects []interaction.ProjectReference, relations []interaction.InteractionRelation) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `INSERT INTO interaction.utterance_interpretations(interpretation_id,utterance_id,speech_act,confidence_millis,source,assertion_verification,requested_outcome,clarification,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)`, interpretation.ID, interpretation.UtteranceID, interpretation.SpeechAct, interpretation.ConfidenceMillis, interpretation.Source, interpretation.AssertionVerification, interpretation.RequestedOutcome, interpretation.Clarification, interpretation.RecordedAtUnixMillis); err != nil {
if _, err = tx.Exec(ctx, `INSERT INTO interaction.utterance_interpretations(interpretation_id,utterance_id,speech_act,secondary_speech_act,confidence_millis,source,assertion_verification,requested_outcome,clarification,needs_cognition,applicability,selection_reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`, interpretation.ID, interpretation.UtteranceID, interpretation.SpeechAct, interpretation.SecondarySpeechAct, interpretation.ConfidenceMillis, interpretation.Source, interpretation.AssertionVerification, interpretation.RequestedOutcome, interpretation.Clarification, interpretation.NeedsCognition, interpretation.Applicability, interpretation.SelectionReason, interpretation.RecordedAtUnixMillis); err != nil {
return err
}
for _, candidate := range candidates {
if _, err = tx.Exec(ctx, `INSERT INTO interaction.semantic_candidates(candidate_id,utterance_id,source,selection_state,primary_speech_act,secondary_speech_act,confidence_millis,applicability,requested_outcome,clarification,needs_cognition,selection_reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(candidate_id) DO UPDATE SET selection_state=EXCLUDED.selection_state,primary_speech_act=EXCLUDED.primary_speech_act,secondary_speech_act=EXCLUDED.secondary_speech_act,confidence_millis=EXCLUDED.confidence_millis,applicability=EXCLUDED.applicability,requested_outcome=EXCLUDED.requested_outcome,clarification=EXCLUDED.clarification,needs_cognition=EXCLUDED.needs_cognition,selection_reason=EXCLUDED.selection_reason,recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`, candidate.ID, candidate.UtteranceID, candidate.Source, candidate.SelectionState, candidate.PrimarySpeechAct, candidate.SecondarySpeechAct, candidate.ConfidenceMillis, candidate.Applicability, candidate.RequestedOutcome, candidate.Clarification, candidate.NeedsCognition, candidate.SelectionReason, candidate.RecordedAtUnixMillis); err != nil {
return err
}
for _, hint := range candidate.CapabilityHints {
if _, err = tx.Exec(ctx, `INSERT INTO interaction.semantic_candidate_capability_hints(candidate_id,capability_kind) VALUES($1,$2) ON CONFLICT DO NOTHING`, candidate.ID, hint); err != nil {
return err
}
}
}
for _, project := range projects {
if _, err = tx.Exec(ctx, `INSERT INTO interaction.project_references(project_reference_id,utterance_id,project_id,project_display_name,role,provenance,confidence_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, project.ID, project.UtteranceID, project.ProjectID, project.ProjectDisplayName, project.Role, project.Provenance, project.ConfidenceMillis, project.RecordedAtUnixMillis); err != nil {
return err
+1 -3
View File
@@ -2,7 +2,6 @@ package postgres
import (
"context"
"fmt"
"strings"
"git.cin.su/m/Assistant/contracts/identity"
@@ -13,8 +12,7 @@ import (
type SituationRepository struct{ pool *pgxpool.Pool }
func OpenSituation(ctx context.Context, config ContinuityConfig) (*SituationRepository, error) {
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
pool, err := pgxpool.New(ctx, dsn)
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
+422 -3
View File
@@ -2,23 +2,32 @@ package postgres
import (
"context"
"errors"
"fmt"
"time"
"git.cin.su/m/Assistant/contracts/work"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type WorkRepository struct{ pool *pgxpool.Pool }
var (
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
ErrAgentCallbackRejected = errors.New("agent callback rejected")
)
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s", config.Host, config.Port, config.Database, config.User, config.Password)
pool, err := pgxpool.New(ctx, dsn)
pool, err := openPool(ctx, config)
if err != nil {
return nil, err
}
return &WorkRepository{pool}, nil
}
func (r *WorkRepository) Close() { r.pool.Close() }
func (r *WorkRepository) Close() { r.pool.Close() }
func (r *WorkRepository) Ping(ctx context.Context) error { return r.pool.Ping(ctx) }
func (r *WorkRepository) Create(ctx context.Context, target work.TaskTarget) error {
_, err := r.pool.Exec(ctx, `INSERT INTO work.tasks (task_id,goal_id,scope_id,revision) VALUES ($1,$2,$3,$4) ON CONFLICT (task_id) DO NOTHING`, target.TaskID, target.GoalID, target.ScopeID, target.Revision)
return err
@@ -40,6 +49,198 @@ func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTar
return target, nil
}
func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.AgentDispatchIntent) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis)
if err != nil {
return fmt.Errorf("save dispatch intent: %w", err)
}
var existing work.AgentDispatchIntent
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis)
if err != nil {
return fmt.Errorf("read dispatch intent: %w", err)
}
if existing != intent {
return ErrDispatchIntentConflict
}
return tx.Commit(ctx)
}
func (r *WorkRepository) LoadDispatchIntent(ctx context.Context, dispatchID string) (work.AgentDispatchIntent, error) {
var intent work.AgentDispatchIntent
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis)
if err != nil {
return work.AgentDispatchIntent{}, fmt.Errorf("load dispatch intent: %w", err)
}
return intent, nil
}
func (r *WorkRepository) EnqueueDispatch(ctx context.Context, item work.AgentDispatchQueueItem) error {
_, err := r.pool.Exec(ctx, `INSERT INTO work.agent_dispatch_queue(dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(dispatch_id) DO NOTHING`, item.DispatchID, item.State, item.AvailableAtUnixMillis, item.Attempts, item.ClaimedBy, item.UpdatedAtUnixMillis)
return err
}
func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string, nowUnixMillis int64) (work.AgentDispatchQueueItem, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return work.AgentDispatchQueueItem{}, err
}
defer tx.Rollback(ctx)
var item work.AgentDispatchQueueItem
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return work.AgentDispatchQueueItem{}, nil
}
return work.AgentDispatchQueueItem{}, err
}
item.State = work.AgentDispatchClaimed
item.Attempts++
item.ClaimedBy = workerID
item.UpdatedAtUnixMillis = nowUnixMillis
_, err = tx.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=$1,attempts=$2,claimed_by=$3,updated_at_unix_millis=$4 WHERE dispatch_id=$5 AND state='QUEUED'`, item.State, item.Attempts, item.ClaimedBy, item.UpdatedAtUnixMillis, item.DispatchID)
if err != nil {
return work.AgentDispatchQueueItem{}, err
}
if err := tx.Commit(ctx); err != nil {
return work.AgentDispatchQueueItem{}, err
}
return item, nil
}
func (r *WorkRepository) WaitForDispatch(ctx context.Context, interval time.Duration) error {
if interval <= 0 {
return fmt.Errorf("invalid worker poll interval")
}
_, err := r.pool.Exec(ctx, `SELECT pg_sleep($1)`, interval.Seconds())
return err
}
func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, workerID string, state work.AgentDispatchQueueState, nowUnixMillis int64) error {
if state != work.AgentDispatchCompleted && state != work.AgentDispatchFailed {
return fmt.Errorf("invalid terminal dispatch state: %s", state)
}
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=$1,updated_at_unix_millis=$2 WHERE dispatch_id=$3 AND state='CLAIMED' AND claimed_by=$4`, state, nowUnixMillis, dispatchID, workerID)
if err != nil {
return err
}
if command.RowsAffected() != 1 {
return ErrAgentCallbackRejected
}
return nil
}
func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts) VALUES($1,$2,$3,$4,$5) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts); err != nil {
return err
}
for _, node := range graph.Nodes {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,dispatch_id,idempotency_key,state,attempts) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.DispatchID, node.IdempotencyKey, node.State, node.Attempts); err != nil {
return err
}
}
for _, edge := range graph.Edges {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_edges(graph_id,revision,from_node_id,to_node_id) VALUES($1,$2,$3,$4) ON CONFLICT(graph_id,revision,from_node_id,to_node_id) DO NOTHING`, graph.ID, graph.Revision, edge.From, edge.To); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revision int64) (work.WorkGraph, error) {
var graph work.WorkGraph
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts)
if err != nil {
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
}
rows, err := r.pool.Query(ctx, `SELECT node_id,dispatch_id,idempotency_key,state,attempts FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
if err != nil {
return work.WorkGraph{}, err
}
for rows.Next() {
var node work.GraphNode
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
graph.Nodes = append(graph.Nodes, node)
}
if err := rows.Err(); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
rows.Close()
edges, err := r.pool.Query(ctx, `SELECT from_node_id,to_node_id FROM work.graph_edges WHERE graph_id=$1 AND revision=$2 ORDER BY from_node_id,to_node_id`, graphID, revision)
if err != nil {
return work.WorkGraph{}, err
}
for edges.Next() {
var edge work.GraphEdge
if err := edges.Scan(&edge.From, &edge.To); err != nil {
edges.Close()
return work.WorkGraph{}, err
}
graph.Edges = append(graph.Edges, edge)
}
if err := edges.Err(); err != nil {
edges.Close()
return work.WorkGraph{}, err
}
edges.Close()
return graph, nil
}
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state='QUEUED',claimed_by='',updated_at_unix_millis=$1 WHERE state='CLAIMED' AND updated_at_unix_millis <= $2`, nowUnixMillis, nowUnixMillis-staleAfterMillis)
if err != nil {
return 0, err
}
return command.RowsAffected(), nil
}
func (r *WorkRepository) SaveAgentLease(ctx context.Context, lease work.AgentLease) error {
err := r.pool.QueryRow(ctx, `INSERT INTO work.agent_leases(lease_id,run_id,worker_id,fencing_token,state,acquired_at_unix_millis,expires_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(run_id) DO UPDATE SET lease_id=EXCLUDED.lease_id,worker_id=EXCLUDED.worker_id,fencing_token=EXCLUDED.fencing_token,state=EXCLUDED.state,acquired_at_unix_millis=EXCLUDED.acquired_at_unix_millis,expires_at_unix_millis=EXCLUDED.expires_at_unix_millis WHERE work.agent_leases.state='EXPIRED' OR work.agent_leases.expires_at_unix_millis <= EXCLUDED.acquired_at_unix_millis RETURNING run_id`, lease.ID, lease.RunID, lease.WorkerID, lease.FencingToken, lease.State, lease.AcquiredAt.UnixMilli(), lease.ExpiresAt.UnixMilli()).Scan(new(string))
if err != nil {
return ErrAgentLeaseUnavailable
}
return nil
}
func (r *WorkRepository) RenewAgentLease(ctx context.Context, lease work.AgentLease, expiresAt time.Time) error {
command, err := r.pool.Exec(ctx, `UPDATE work.agent_leases SET expires_at_unix_millis=$1 WHERE run_id=$2 AND worker_id=$3 AND fencing_token=$4 AND state='ACTIVE' AND expires_at_unix_millis > $5`, expiresAt.UnixMilli(), lease.RunID, lease.WorkerID, lease.FencingToken, time.Now().UnixMilli())
if err != nil || command.RowsAffected() != 1 {
return ErrAgentLeaseUnavailable
}
return nil
}
func (r *WorkRepository) AcceptAgentCallback(ctx context.Context, callback work.AgentCallback) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
var runID string
err = tx.QueryRow(ctx, `SELECT run_id FROM work.agent_leases WHERE run_id=$1 AND worker_id=$2 AND fencing_token=$3 AND state='ACTIVE' AND expires_at_unix_millis > $4 FOR UPDATE`, callback.RunID, callback.WorkerID, callback.FencingToken, callback.ObservedAt.UnixMilli()).Scan(&runID)
if err != nil {
return ErrAgentCallbackRejected
}
command, err := tx.Exec(ctx, `INSERT INTO work.agent_callback_heads(run_id,last_sequence,updated_at_unix_millis) VALUES($1,$2,$3) ON CONFLICT(run_id) DO UPDATE SET last_sequence=EXCLUDED.last_sequence,updated_at_unix_millis=EXCLUDED.updated_at_unix_millis WHERE work.agent_callback_heads.last_sequence < EXCLUDED.last_sequence`, runID, callback.Sequence, callback.ObservedAt.UnixMilli())
if err != nil || command.RowsAffected() != 1 {
return ErrAgentCallbackRejected
}
return tx.Commit(ctx)
}
func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.PlanningResult) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
@@ -75,3 +276,221 @@ func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.Pla
}
return tx.Commit(ctx)
}
func (r *WorkRepository) SaveDelegationBundle(ctx context.Context, delegation work.Delegation, run work.AgentRun) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `INSERT INTO work.delegations(
delegation_id,goal_id,work_plan_id,plan_candidate_id,capability_requirement_id,capability_kind,
selected_implementation_id,selected_implementation_revision,selected_implementation_kind,protocol,
source_intent_id,source_utterance_id,conversation_id,project_id,project_display_name,desired_outcome,
constraint_summary,verification_summary,context_summary,context_refs,state,latest_run_id,latest_result_id,
created_at_unix_millis,updated_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25)
ON CONFLICT(delegation_id) DO NOTHING`,
delegation.ID, delegation.GoalID, delegation.PlanID, delegation.PlanCandidateID, delegation.CapabilityRequirementID,
delegation.CapabilityKind, delegation.SelectedImplementationID, delegation.SelectedImplementationRev,
delegation.SelectedImplementationKind, delegation.Protocol, delegation.SourceIntentID, delegation.SourceUtteranceID,
delegation.ConversationID, delegation.ProjectID, delegation.ProjectDisplayName, delegation.DesiredOutcome,
delegation.ConstraintSummary, delegation.VerificationSummary, delegation.ContextSummary, delegation.ContextRefs,
delegation.State, delegation.LatestRunID, delegation.LatestResultID, delegation.CreatedAtUnixMillis, delegation.UpdatedAtUnixMillis); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO work.agent_runs(
run_id,delegation_id,implementation_id,implementation_revision,protocol,state,derived_instruction_summary,
started_at_unix_millis,updated_at_unix_millis,completed_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(run_id) DO NOTHING`,
run.ID, run.DelegationID, run.ImplementationID, run.ImplementationRevision, run.Protocol, run.State,
run.DerivedInstructionSummary, run.StartedAtUnixMillis, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *WorkRepository) SaveAgentCheckpoint(ctx context.Context, run work.AgentRun, checkpoint work.AgentCheckpoint) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `UPDATE work.agent_runs
SET state=$2,updated_at_unix_millis=$3,completed_at_unix_millis=$4
WHERE run_id=$1`, run.ID, run.State, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil {
return err
}
if _, err = tx.Exec(ctx, `UPDATE work.delegations
SET state=$2,updated_at_unix_millis=$3,latest_run_id=$4
WHERE delegation_id=$1`, checkpoint.DelegationID, delegationStateForRun(run.State), checkpoint.RecordedAtUnixMillis, run.ID); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO work.agent_checkpoints(
checkpoint_id,delegation_id,run_id,state,blocker_class,completed_summary,remaining_summary,test_summary,
branch_name,commit_hash,next_step,artifact_refs,recorded_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(checkpoint_id) DO NOTHING`,
checkpoint.ID, checkpoint.DelegationID, checkpoint.RunID, checkpoint.State, checkpoint.BlockerClass,
checkpoint.CompletedSummary, checkpoint.RemainingSummary, checkpoint.TestSummary, checkpoint.BranchName,
checkpoint.CommitHash, checkpoint.NextStep, checkpoint.ArtifactRefs, checkpoint.RecordedAtUnixMillis); err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *WorkRepository) SaveAgentResult(ctx context.Context, run work.AgentRun, result work.AgentResult) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `UPDATE work.agent_runs
SET state=$2,updated_at_unix_millis=$3,completed_at_unix_millis=$4
WHERE run_id=$1`, run.ID, run.State, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil {
return err
}
if _, err = tx.Exec(ctx, `UPDATE work.delegations
SET state=$2,updated_at_unix_millis=$3,latest_run_id=$4,latest_result_id=$5
WHERE delegation_id=$1`, result.DelegationID, work.DelegationVerifying, result.RecordedAtUnixMillis, run.ID, result.ID); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO work.agent_results(
result_id,delegation_id,run_id,state,summary,outcome_ref,verification_required,verification_summary,artifact_refs,recorded_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(result_id) DO NOTHING`,
result.ID, result.DelegationID, result.RunID, result.State, result.Summary, result.OutcomeRef, result.VerificationRequired,
result.VerificationSummary, result.ArtifactRefs, result.RecordedAtUnixMillis); err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *WorkRepository) SaveResumedRun(ctx context.Context, delegation work.Delegation, run work.AgentRun) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `UPDATE work.delegations
SET state=$2,updated_at_unix_millis=$3,latest_run_id=$4
WHERE delegation_id=$1`, delegation.ID, delegation.State, delegation.UpdatedAtUnixMillis, delegation.LatestRunID); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO work.agent_runs(
run_id,delegation_id,implementation_id,implementation_revision,protocol,state,derived_instruction_summary,
started_at_unix_millis,updated_at_unix_millis,completed_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(run_id) DO NOTHING`,
run.ID, run.DelegationID, run.ImplementationID, run.ImplementationRevision, run.Protocol, run.State,
run.DerivedInstructionSummary, run.StartedAtUnixMillis, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *WorkRepository) SaveDelegationVerification(ctx context.Context, delegation work.Delegation, verification work.DelegationVerification) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `UPDATE work.delegations
SET state=$2,updated_at_unix_millis=$3
WHERE delegation_id=$1`, delegation.ID, delegation.State, delegation.UpdatedAtUnixMillis); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO work.delegation_verifications(
verification_id,delegation_id,run_id,result_id,verified,summary,evidence_refs,recorded_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(verification_id) DO NOTHING`,
verification.ID, verification.DelegationID, verification.RunID, verification.ResultID, verification.Verified,
verification.Summary, verification.EvidenceRefs, verification.RecordedAtUnixMillis); err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *WorkRepository) LatestAgentResult(ctx context.Context, delegationID work.DelegationID) (work.AgentResult, error) {
var value work.AgentResult
err := r.pool.QueryRow(ctx, `SELECT result_id,delegation_id,run_id,state,summary,outcome_ref,verification_required,verification_summary,artifact_refs,recorded_at_unix_millis
FROM work.agent_results WHERE delegation_id=$1 ORDER BY recorded_at_unix_millis DESC, result_id DESC LIMIT 1`, delegationID).
Scan(&value.ID, &value.DelegationID, &value.RunID, &value.State, &value.Summary, &value.OutcomeRef, &value.VerificationRequired, &value.VerificationSummary, &value.ArtifactRefs, &value.RecordedAtUnixMillis)
if err != nil {
return work.AgentResult{}, fmt.Errorf("load latest agent result: %w", err)
}
return value, nil
}
func (r *WorkRepository) CountAgentRuns(ctx context.Context, delegationID work.DelegationID) (int, error) {
var count int
err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM work.agent_runs WHERE delegation_id=$1`, delegationID).Scan(&count)
if err != nil {
return 0, fmt.Errorf("count agent runs: %w", err)
}
return count, nil
}
func (r *WorkRepository) LatestAgentCheckpoint(ctx context.Context, delegationID work.DelegationID) (work.AgentCheckpoint, error) {
var value work.AgentCheckpoint
err := r.pool.QueryRow(ctx, `SELECT checkpoint_id,delegation_id,run_id,state,blocker_class,completed_summary,remaining_summary,test_summary,branch_name,commit_hash,next_step,artifact_refs,recorded_at_unix_millis
FROM work.agent_checkpoints WHERE delegation_id=$1 ORDER BY recorded_at_unix_millis DESC, checkpoint_id DESC LIMIT 1`, delegationID).
Scan(&value.ID, &value.DelegationID, &value.RunID, &value.State, &value.BlockerClass, &value.CompletedSummary, &value.RemainingSummary,
&value.TestSummary, &value.BranchName, &value.CommitHash, &value.NextStep, &value.ArtifactRefs, &value.RecordedAtUnixMillis)
if err != nil {
return work.AgentCheckpoint{}, err
}
return value, nil
}
func (r *WorkRepository) LoadDelegation(ctx context.Context, id work.DelegationID) (work.Delegation, error) {
var value work.Delegation
err := r.pool.QueryRow(ctx, `SELECT delegation_id,goal_id,work_plan_id,plan_candidate_id,capability_requirement_id,capability_kind,
selected_implementation_id,selected_implementation_revision,selected_implementation_kind,protocol,
source_intent_id,source_utterance_id,conversation_id,project_id,project_display_name,desired_outcome,constraint_summary,
verification_summary,context_summary,context_refs,state,latest_run_id,latest_result_id,created_at_unix_millis,updated_at_unix_millis
FROM work.delegations WHERE delegation_id=$1`, id).
Scan(&value.ID, &value.GoalID, &value.PlanID, &value.PlanCandidateID, &value.CapabilityRequirementID, &value.CapabilityKind,
&value.SelectedImplementationID, &value.SelectedImplementationRev, &value.SelectedImplementationKind, &value.Protocol,
&value.SourceIntentID, &value.SourceUtteranceID, &value.ConversationID, &value.ProjectID, &value.ProjectDisplayName,
&value.DesiredOutcome, &value.ConstraintSummary, &value.VerificationSummary, &value.ContextSummary, &value.ContextRefs,
&value.State, &value.LatestRunID, &value.LatestResultID, &value.CreatedAtUnixMillis, &value.UpdatedAtUnixMillis)
if err != nil {
return work.Delegation{}, fmt.Errorf("load delegation: %w", err)
}
return value, nil
}
func (r *WorkRepository) LoadAgentRun(ctx context.Context, id work.AgentRunID) (work.AgentRun, error) {
var value work.AgentRun
err := r.pool.QueryRow(ctx, `SELECT run_id,delegation_id,implementation_id,implementation_revision,protocol,state,derived_instruction_summary,started_at_unix_millis,updated_at_unix_millis,completed_at_unix_millis
FROM work.agent_runs WHERE run_id=$1`, id).
Scan(&value.ID, &value.DelegationID, &value.ImplementationID, &value.ImplementationRevision, &value.Protocol, &value.State, &value.DerivedInstructionSummary,
&value.StartedAtUnixMillis, &value.UpdatedAtUnixMillis, &value.CompletedAtUnixMillis)
if err != nil {
return work.AgentRun{}, fmt.Errorf("load agent run: %w", err)
}
return value, nil
}
func delegationStateForRun(state work.AgentRunState) work.DelegationState {
switch state {
case work.AgentRunStarted, work.AgentRunRunning:
return work.DelegationRunning
case work.AgentRunWaiting, work.AgentRunCheckpointed, work.AgentRunExecutionWindowExhausted:
return work.DelegationWaiting
case work.AgentRunBlocked:
return work.DelegationBlocked
case work.AgentRunCompleted:
return work.DelegationVerifying
case work.AgentRunCancelled:
return work.DelegationCancelled
case work.AgentRunFailed:
return work.DelegationFailed
default:
return work.DelegationPending
}
}
func latestRowErr(err error) error {
if err == nil {
return nil
}
return err
}
+74
View File
@@ -0,0 +1,74 @@
package research
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
var ErrHTTPSourceRejected = errors.New("http research source rejected")
type HTTPConnector struct {
Client *http.Client
AllowedHosts map[string]struct{}
MaxBytes int64
}
func (c HTTPConnector) Collect(ctx context.Context, request epistemics.WebResearchRequest, nowUnixMillis int64) (epistemics.ResearchCandidate, error) {
if request.Plan.Validate() != nil || request.SourceID == "" || strings.TrimSpace(request.Statement) == "" || nowUnixMillis < 1 {
return epistemics.ResearchCandidate{}, ErrHTTPSourceRejected
}
u, err := url.Parse(request.URL)
if err != nil || u.Scheme != "https" || u.User != nil || u.Host == "" || !c.allowedHost(strings.ToLower(u.Hostname())) {
return epistemics.ResearchCandidate{}, ErrHTTPSourceRejected
}
client := c.Client
if client == nil {
client = &http.Client{}
}
maxBytes := c.MaxBytes
if maxBytes <= 0 || maxBytes > 4<<20 {
maxBytes = 256 << 10
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return epistemics.ResearchCandidate{}, ErrHTTPSourceRejected
}
response, err := client.Do(req)
if err != nil {
return epistemics.ResearchCandidate{}, fmt.Errorf("read web source: %w", err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return epistemics.ResearchCandidate{}, fmt.Errorf("web source status: %s", response.Status)
}
body, err := io.ReadAll(io.LimitReader(response.Body, maxBytes+1))
if err != nil || int64(len(body)) > maxBytes || len(body) == 0 {
return epistemics.ResearchCandidate{}, ErrHTTPSourceRejected
}
digest := sha256.Sum256(body)
digestText := "sha256:" + hex.EncodeToString(digest[:])
key := sha256.Sum256([]byte(u.String() + "\x00" + digestText))
keyText := hex.EncodeToString(key[:])
snapshotID := epistemics.SourceSnapshotID("snapshot:" + keyText)
return epistemics.ResearchCandidate{
PropositionKey: request.PropositionKey, Statement: request.Statement,
Source: epistemics.Source{ID: request.SourceID, Kind: "web", Locator: u.String(), Title: request.Title, Publisher: u.Hostname()},
Snapshot: epistemics.SourceSnapshot{ID: snapshotID, SourceID: request.SourceID, ContentDigest: digestText, CapturedAtUnixMillis: nowUnixMillis, Freshness: epistemics.FreshnessCurrent},
Excerpt: epistemics.SourceExcerpt{ID: epistemics.SourceExcerptID("excerpt:" + keyText), SnapshotID: snapshotID, Text: string(body), Start: 0, End: int64(len(body))},
Evidence: epistemics.EvidenceObserved, Coverage: epistemics.CoverageComplete, Conflict: epistemics.ConflictNone,
}, nil
}
func (c HTTPConnector) allowedHost(host string) bool {
_, ok := c.AllowedHosts[host]
return ok
}
+63
View File
@@ -0,0 +1,63 @@
package research
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
var ErrRepositoryPathRejected = errors.New("repository source path rejected")
type RepositoryConnector struct{ Root string }
func (c RepositoryConnector) Collect(ctx context.Context, request epistemics.RepositoryResearchRequest, nowUnixMillis int64) (epistemics.ResearchCandidate, error) {
if err := request.Plan.Validate(); err != nil {
return epistemics.ResearchCandidate{}, err
}
if request.SourceID == "" || strings.TrimSpace(request.Statement) == "" || nowUnixMillis < 1 {
return epistemics.ResearchCandidate{}, ErrRepositoryPathRejected
}
root, err := filepath.Abs(c.Root)
if err != nil || root == "" {
return epistemics.ResearchCandidate{}, ErrRepositoryPathRejected
}
path, err := filepath.Abs(filepath.Join(root, filepath.FromSlash(request.Path)))
if err != nil || !withinRoot(root, path) {
return epistemics.ResearchCandidate{}, ErrRepositoryPathRejected
}
select {
case <-ctx.Done():
return epistemics.ResearchCandidate{}, ctx.Err()
default:
}
content, err := os.ReadFile(path)
if err != nil || len(content) == 0 {
return epistemics.ResearchCandidate{}, fmt.Errorf("read repository source: %w", err)
}
digest := sha256.Sum256(content)
digestText := "sha256:" + hex.EncodeToString(digest[:])
key := sha256.Sum256([]byte(filepath.ToSlash(path) + "\x00" + digestText))
keyText := hex.EncodeToString(key[:])
return epistemics.ResearchCandidate{
PropositionKey: request.PropositionKey,
Statement: request.Statement,
Source: epistemics.Source{ID: request.SourceID, Kind: "repository", Locator: "file://" + filepath.ToSlash(path), Title: filepath.Base(path), Publisher: "local-repository"},
Snapshot: epistemics.SourceSnapshot{ID: epistemics.SourceSnapshotID("snapshot:" + keyText), SourceID: request.SourceID, ContentDigest: digestText, CapturedAtUnixMillis: nowUnixMillis, Freshness: epistemics.FreshnessCurrent},
Excerpt: epistemics.SourceExcerpt{ID: epistemics.SourceExcerptID("excerpt:" + keyText), SnapshotID: epistemics.SourceSnapshotID("snapshot:" + keyText), Text: string(content), Start: 0, End: int64(len(content))},
Evidence: epistemics.EvidenceObserved,
Coverage: epistemics.CoverageComplete,
Conflict: epistemics.ConflictNone,
}, nil
}
func withinRoot(root, path string) bool {
rel, err := filepath.Rel(root, path)
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && !filepath.IsAbs(rel)
}
@@ -0,0 +1,54 @@
# ADR-0017 — General Agent Delegation and Codex Bridge V1
Status: Accepted
Task: DEV-000069
## Context
Domovoy already has Human Interaction, Situation/context, optional durable Work/Plan, Capability registry/resolution, Continuity/Attention, HumanResponse, Experience/Reflection, Skill evolution, Secret Resolver and Cognitive Gateway/Router. The next step is not "make Codex the center", but to let Domovoy delegate suitable work to a general external intelligent agent while preserving canonical human intent, work semantics, authority boundaries and future portability to non-Codex agents.
The first real implementation is Codex for software-engineering work. However, a model is not an external agent. Cognitive models provide advice/candidates, while an external agent may inspect state, use tools, modify files, run tests, maintain a long-running task, checkpoint, resume and return evidence-bearing results.
## Decision
1. Do not create a new canonical owner for DEV-000069.
2. `capability` remains the canonical owner for agent implementation registration by reusing `CapabilityImplementation` with `Kind=AGENT`. A Codex bridge is one concrete implementation, not a new canonical capability identity.
3. `work` remains the canonical owner for durable delegation lifecycle. DEV-000069 extends Work semantics from planning-only `DelegationCandidate` to real delegation execution state such as `Delegation`, `AgentRun`, `AgentCheckpoint` and `AgentResult`.
4. `continuity` remains the canonical owner for human waiting/attention semantics. Human-dependent blockers or completed long-running delegations surface through Attention rather than a Codex-specific notification path.
5. `epistemics` remains the canonical owner for delegation outcome evidence through `ExperienceRecord` and later reflection, using typed refs to Work, Capability, Artifact and Interaction state.
6. `artifact` remains the place for durable artifact metadata/refs when an agent run produces file, diff, test, log, PR or deployment evidence. Work stores typed refs, not payload copies.
7. `OriginalHumanIntent`, `Goal`, `Work`, `CapabilityRequirement` and `Delegation` must remain canonical semantic records. Agent-specific instruction or prompt text is only a derived adapter artifact with provenance that distinguishes:
- what the human asked;
- what Domovoy inferred;
- what project/runtime context was added;
- how completion will be verified.
8. Execution-window exhaustion is a run lifecycle state, not automatic task failure. The same higher-level delegation may continue through later runs while preserving checkpoint/history.
9. Codex transport/UI wording is adapter-local. Canonical state must stay valid if the coding agent is replaced by another future agent.
## Ownership
- `interaction` owns the append-only human request/correction history and original intent traceability.
- `situation` resolves project/repository context and active focus used for delegation context compilation.
- `work` owns durable delegation requests, selected implementation refs, run lifecycle, checkpoint/result refs and verification gating before Work completion.
- `capability` owns executor-neutral capability identity and concrete eligible implementations, including `AGENT` implementations such as Codex.
- `continuity` owns waiting/attention/human-response continuation for delegation progress and genuine human blockers.
- `epistemics` owns experience/reflection evidence derived from delegation outcomes.
- `artifact` owns durable metadata for diffs/logs/checkpoints/results when they must survive outside a transient adapter response.
## Boundaries
- `SOFTWARE_ENGINEERING`, `MODIFY_CODE`, `INSPECT_REPOSITORY` and `RUN_TESTS` remain executor-neutral capability semantics.
- `MODIFY_CODE != CODEX`.
- Canonical Work/Delegation does not become a Codex prompt.
- Secret access remains least-privilege through CredentialRef/Secret Resolver. Agent capability availability does not imply credential access or authority.
- A completed agent run does not automatically mark higher-level Work complete. Verification must inspect available evidence.
- Technical failures, execution-window exhaustion and human-dependent blockers are distinct lifecycle states.
- `LLM-free` startup must continue to pass when Codex is unavailable.
## Consequences
- Domovoy can delegate suitable software-engineering work without forcing the human to author a giant Codex prompt.
- The first production bridge can target Codex while leaving canonical semantics portable to future scientific, accounting, research or alternate coding agents.
- Long-running agent work can checkpoint and continue across bounded execution windows.
- Human chat remains simple, while operator/debug tooling can inspect the durable delegation/run/checkpoint/result state.
@@ -0,0 +1,52 @@
# ADR-0018 — External audit and exploration mode
Status: Accepted
Date: 2026-08-22
Accepted under: ARCH-000012
## Decision
1. Domovoy may operate in a test-only `Exploration` profile when an ordinary
local conversation or cognitive-provider recovery request is received.
Exploration permits contextual reasoning, allowlisted read-only diagnostics,
hypothesis generation, alternatives and a reversible next-step proposal.
2. Exploration is not an authority grant. It does not grant access to raw
secrets, executor credentials, a provider's unapproved data-egress path, or
direct mutation of canonical or external state.
3. Audit is an independently deployed, rebuildable projection of compact,
redacted action/proposal events. It is not a canonical owner and never
executes actions.
4. An attributed external audit verdict is an Observation. A later Commit
contract may require a current `ALLOW` Observation as an additional
precondition for explicitly configured action classes. `REVIEW_REQUIRED`,
`STOP`, missing or stale review do not authorize execution.
5. Loss of the audit deployment does not remove exploration or approved
read-only diagnostics. A review-governed external action remains a pending
proposal; it is neither executed nor silently converted to failure.
6. Audit payloads exclude raw credentials, secrets and private content. They
carry stable identifiers/references, classifications, hashes and redacted
summaries only.
7. Initial delivery is staged: first semantic local dialogue and provider
recovery, then capability-use telemetry, then audit-only projection. No
existing executor action is review-governed until a later task names its
class and recovery behavior.
## Consequences
- Local cognition remains useful during provider loss and can seek a recovery
plan instead of returning canned availability text.
- Higher-level external scrutiny is possible without creating a competing
executor or a second source of semantic truth.
- Commit/Executor, semantic ownership, append-only evidence and unknown
outcome semantics remain invariants, so external review has attributable
evidence to inspect.
- The first implementation task may change local semantic routing and
capability telemetry only; audit projection and Commit gating need their
own explicit contracts and failure/rebuild tests.
## Revisit triggers
- The first external audit projection is deployed.
- A concrete external action class is proposed for review gating.
- Audit needs a data class beyond redacted references.
- The Exploration profile needs a new privilege or canonical owner.
+1 -1
View File
@@ -4,4 +4,4 @@ Accepted architecture decisions live here. Normal implementation tasks may read
Use `ADR-XXXX-title.md` with: problem, evidence, alternatives, decision, rationale, consequences, validity conditions, revisit triggers, migration/rollback, status.
Current decisions include `ADR-0006-creator-advisory-boundary.md`, the fail-closed provider-neutral advisory boundary, and `ADR-0007-openai-advisory-binding.md`, the accepted but not yet production-enabled OpenAI binding class.
Current decisions include `ADR-0006-creator-advisory-boundary.md`, the fail-closed provider-neutral advisory boundary; `ADR-0007-openai-advisory-binding.md`, the accepted but not yet production-enabled OpenAI binding class; and `ADR-0018-external-audit-exploration-mode.md`, the staged external-audit and test Exploration profile.
+6 -1
View File
@@ -65,7 +65,12 @@
"WorkPlan",
"PlanStep",
"CapabilityRequirement",
"DelegationCandidate"
"DelegationCandidate",
"Delegation",
"AgentRun",
"AgentCheckpoint",
"AgentResult",
"DelegationVerification"
],
"database_schema": "work",
"boundary_class": "remote_capable",
+12 -6
View File
@@ -25,17 +25,17 @@ modules:
situation:
id: mod.situation
owns: [Situation, FocusBinding]
owns: [Situation, FocusBinding, ProjectRegistry, ActiveProjectFocus]
database_schema: situation
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.work, contracts.capability]
may_depend_on: [contracts.identity, contracts.authority, contracts.work, contracts.capability]
work:
id: mod.work
owns: [Goal, Task, Decision]
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate, Delegation, AgentRun, AgentCheckpoint, AgentResult, DelegationVerification]
database_schema: work
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, platform.events]
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
epistemics:
id: mod.epistemics
@@ -53,7 +53,7 @@ modules:
capability:
id: mod.capability
owns: [Capability, CapabilityBinding, CapabilityHealth]
owns: [Capability, CapabilityBinding, CapabilityHealth, CapabilityImplementation, CapabilityResolution, CapabilitySelection, CapabilityGap, AcquisitionStrategyCandidate, CapabilityUsageObservation, SkillProfile, SkillEvidence, FastPathDecision]
database_schema: capability
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, contracts.artifact]
@@ -81,11 +81,12 @@ platform:
- platform.inbox
- platform.telemetry
- platform.errors
- platform.secrets
rule: "platform owns mechanisms, never domain meaning"
runtime_topology_v1:
core_runtime:
modules: [identity, situation, work, epistemics, capability, artifact]
modules: [identity, situation, work, epistemics, capability, artifact, interaction]
continuity_runtime:
modules: [continuity]
commit_runtime:
@@ -107,6 +108,7 @@ storage_topology_v1:
capability: capability_role
commit: commit_role
artifact: artifact_role
interaction: interaction_role
cross_owner_fk: forbidden_by_default
cross_owner_write: forbidden
cross_owner_application_join: forbidden
@@ -142,6 +144,10 @@ protected_paths:
- architecture/architecture-registry.yaml
- architecture/adrs/**
- ci/architecture/**
- .gitea/workflows/ci.yml
- cmd/devctl/**
- dev/ACTIVE_TASK.json
- ci/architecture/**
- security/**
default_feature_task_authority:
+1 -1
View File
@@ -27,7 +27,7 @@ modules:
work:
id: mod.work
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate]
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate, Delegation, AgentRun, AgentCheckpoint, AgentResult, DelegationVerification]
database_schema: work
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
+69
View File
@@ -0,0 +1,69 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/application"
"git.cin.su/m/Assistant/modules/work/ports"
)
type memoryStore struct{ writes int }
func (s *memoryStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error {
s.writes++
return nil
}
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
func (s *memoryStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
s.writes++
return nil
}
func main() {
enabled := flag.Bool("enable-canary", false, "explicitly enable the manual canary")
implementation := flag.String("implementation", "fake-worker", "allowlisted deterministic implementation")
postgresMode := flag.Bool("postgres", false, "use the Work PostgreSQL repository instead of in-memory storage")
flag.Parse()
now := time.UnixMilli(1_700_000_000_000)
intent := work.AgentDispatchIntent{DispatchID: "manual-dispatch-1", IdempotencyKey: "manual-idempotency-1", DelegationID: "manual-delegation-1", RunID: "manual-run-1", ImplementationID: *implementation, ImplementationRevision: 1, SourceRevision: "manual-source-1", WorktreeID: "manual-worktree-1", VerificationPlanRef: "manual-verification-1", CreatedAtUnixMillis: now.UnixMilli()}
var store ports.ExecutionStore = &memoryStore{}
var closeStore func()
if *postgresMode {
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: os.Getenv("WORK_POSTGRES_PASSWORD")})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
store = repository
closeStore = repository.Close
}
if closeStore != nil {
defer closeStore()
}
result, err := application.ExecuteCanaryDryRun(context.Background(), store, application.CanaryConfig{Enabled: *enabled, ImplementationID: "fake-worker", MaxDispatches: 1}, 0, intent, "manual-worker", 1, now, 15*time.Minute)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
output := struct {
DispatchID string `json:"dispatch_id"`
RunID string `json:"run_id"`
CallbackCount int `json:"callback_count"`
StoreWrites int `json:"store_writes"`
}{result.Intent.DispatchID, string(result.Intent.RunID), result.CallbackCount(), 0}
if memory, ok := store.(*memoryStore); ok {
output.StoreWrites = memory.writes
}
if err := json.NewEncoder(os.Stdout).Encode(output); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
+101
View File
@@ -0,0 +1,101 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"time"
"git.cin.su/m/Assistant/adapters/agentbridge"
"git.cin.su/m/Assistant/adapters/postgres"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/application"
"git.cin.su/m/Assistant/modules/work/ports"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
func main() {
workerID := flag.String("worker-id", "agent-worker-1", "durable worker identity")
statusAddr := flag.String("status-addr", ":8090", "internal status HTTP address")
staleAfter := flag.Duration("stale-after", 10*time.Minute, "age after which a claimed item is recovered")
leaseTTL := flag.Duration("lease-ttl", 15*time.Minute, "executor lease duration")
pollInterval := flag.Duration("poll-interval", 5*time.Second, "database-owned idle wake-up interval")
once := flag.Bool("once", false, "run one cycle and exit")
flag.Parse()
password, err := resolveDatabasePassword("WORK_POSTGRES_PASSWORD", "WORK_POSTGRES_CREDENTIAL_REF", secretcontracts.RuntimeConsumerWorkWorker)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: password})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer repository.Close()
var executor ports.AgentExecutor
if bridgeURL := os.Getenv("AGENT_EXECUTION_BRIDGE_URL"); bridgeURL != "" {
executor, err = agentbridge.NewHTTPExecutor(bridgeURL, &http.Client{Timeout: *leaseTTL})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
} else {
fmt.Fprintln(os.Stderr, "agent execution bridge is not configured; dispatch claims are disabled; recovery-only mode")
}
go func() {
if serveErr := http.ListenAndServe(*statusAddr, workerStatusHandler(executor != nil)); serveErr != nil {
fmt.Fprintln(os.Stderr, "agent worker status server:", serveErr)
}
}()
ctx := context.Background()
for {
var result application.WorkerCycleResult
var cycleErr error
if executor == nil {
result, cycleErr = application.RecoverOnlyWorkerCycle(ctx, repository, time.Now().UTC(), *staleAfter)
} else {
result, cycleErr = application.ExecuteWorkerCycleWithExecutor(ctx, repository, executor, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
}
if cycleErr != nil {
fmt.Fprintln(os.Stderr, cycleErr)
if *once {
os.Exit(1)
}
}
if cycleErr == nil && (result.Completed || result.RecoveredClaims > 0) {
_ = json.NewEncoder(os.Stdout).Encode(result)
}
if *once {
return
}
if err := repository.WaitForDispatch(ctx, *pollInterval); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}
func resolveDatabasePassword(name, refName string, consumer secretcontracts.RuntimeConsumer) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
}
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
if path == "" || ref == "" {
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: consumer, Path: path})
}
+40
View File
@@ -0,0 +1,40 @@
package main
import (
"encoding/json"
"net/http"
)
type workerStatusResponse struct {
Runtime string `json:"runtime"`
ExecutorConfigured bool `json:"executor_configured"`
Mode string `json:"mode"`
Ready bool `json:"ready"`
Reason string `json:"reason"`
}
func workerStatusHandler(executorConfigured bool) http.Handler {
status := workerStatusResponse{
Runtime: "agent-worker",
ExecutorConfigured: executorConfigured,
Mode: "recovery-only",
Ready: false,
Reason: "AGENT_EXECUTION_BRIDGE_URL is not configured",
}
if executorConfigured {
status.Mode = "execution"
status.Ready = true
status.Reason = "typed agent execution bridge configured"
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || (r.URL.Path != "/healthz" && r.URL.Path != "/readyz" && r.URL.Path != "/status") {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
if r.URL.Path == "/readyz" && !status.Ready {
w.WriteHeader(http.StatusServiceUnavailable)
}
_ = json.NewEncoder(w).Encode(status)
})
}
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestWorkerStatusReportsRecoveryOnlyWithoutExecutor(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
var value workerStatusResponse
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
t.Fatal(err)
}
if response.Code != http.StatusOK || value.Ready || value.Mode != "recovery-only" || value.ExecutorConfigured {
t.Fatalf("unexpected status: code=%d value=%+v", response.Code, value)
}
}
func TestWorkerReadyzRequiresExecutor(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if response.Code != http.StatusServiceUnavailable {
t.Fatalf("readyz status=%d", response.Code)
}
response = httptest.NewRecorder()
workerStatusHandler(true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
if response.Code != http.StatusOK {
t.Fatalf("configured readyz status=%d", response.Code)
}
}
@@ -0,0 +1,136 @@
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
realityevaluation "git.cin.su/m/Assistant/tests/reality_evaluation"
)
type manifest struct {
ArtifactID string `json:"artifact_id"`
Providers []providerCandidate `json:"providers"`
}
type providerCandidate struct {
ProviderInstanceID string `json:"provider_instance_id"`
ProviderKind string `json:"provider_kind"`
Endpoint string `json:"endpoint"`
Candidates []modelCandidate `json:"candidates"`
}
type modelCandidate struct {
ProviderModelID string `json:"provider_model_id"`
ModelDescriptorID string `json:"model_descriptor_id"`
ModelRevision int64 `json:"model_revision"`
BindingID string `json:"binding_id"`
BindingRevision int64 `json:"binding_revision"`
}
func main() {
manifestPath := flag.String("manifest", "", "benchmark manifest JSON")
suitePath := flag.String("suite", "", "cognitive evaluation suite JSON")
outputPath := flag.String("output", "", "output artifact JSON")
observedAt := flag.String("observed-at", "2026-08-17T12:00:00Z", "RFC3339 timestamp for reproducible artifact generation")
timeout := flag.Duration("case-timeout", 30*time.Second, "deadline for each case")
flag.Parse()
if *manifestPath == "" || *suitePath == "" || *outputPath == "" || *timeout <= 0 {
fatal(errors.New("manifest, suite, output and positive case-timeout are required"))
}
at, err := time.Parse(time.RFC3339, *observedAt)
if err != nil {
fatal(err)
}
input, err := os.ReadFile(*manifestPath)
if err != nil {
fatal(err)
}
var plan manifest
if err := json.Unmarshal(input, &plan); err != nil {
fatal(err)
}
if len(plan.Providers) == 0 || len(plan.Providers[0].Candidates) == 0 {
fatal(errors.New("manifest has no candidates"))
}
suite, err := realityevaluation.LoadCognitiveSuite(*suitePath)
if err != nil {
fatal(err)
}
provider := plan.Providers[0]
candidate := provider.Candidates[0]
adapter, err := newProvider(provider)
if err != nil {
fatal(err)
}
responses := map[string]string{}
for index, item := range suite.Cases {
caseCtx, cancel := context.WithTimeout(context.Background(), *timeout)
response, err := adapter.Advise(caseCtx, cognitive.AdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: cognitive.RequestID(fmt.Sprintf("cognitive-eval-%d", index)),
ActorID: "benchmark-actor",
PrincipalID: "domovoy-creator-principal",
ScopeID: "benchmark-public-synthetic",
Message: item.Prompt,
Locale: "ru-RU",
MaxOutputBytes: 4096,
ProviderBindingID: capability.CapabilityBindingID(candidate.BindingID),
ProviderBindingRevision: candidate.BindingRevision,
ProviderInstanceID: capability.ProviderInstanceID(provider.ProviderInstanceID),
ModelDescriptorID: capability.ModelDescriptorID(candidate.ModelDescriptorID),
ProviderModelID: capability.ProviderModelID(candidate.ProviderModelID),
})
cancel()
if err != nil {
responses[item.ID] = "ADAPTER_ERROR: " + err.Error()
continue
}
responses[item.ID] = response.AdvisoryText
}
artifact, err := realityevaluation.EvaluateCognitiveSuite(
suite,
provider.ProviderInstanceID,
candidate.ProviderModelID,
candidate.ModelDescriptorID,
candidate.BindingID,
candidate.BindingRevision,
responses,
at,
)
if err != nil {
fatal(err)
}
payload, err := json.MarshalIndent(artifact, "", " ")
if err != nil {
fatal(err)
}
payload = append(payload, '\n')
if err := os.WriteFile(*outputPath, payload, 0o600); err != nil {
fatal(err)
}
fmt.Printf("artifact=%s total_cases=%d output=%s\n", artifact.ArtifactID, artifact.TotalCases, *outputPath)
}
func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
switch candidate.ProviderKind {
case "openai-compatible-chat-v1":
return models.NewOpenAICompatibleLocalAdapter(candidate.Endpoint, nil)
case "ollama-v1":
return models.NewOllamaAdapter(candidate.Endpoint, nil)
default:
return nil, fmt.Errorf("unsupported provider kind %q", candidate.ProviderKind)
}
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
+10 -9
View File
@@ -98,8 +98,9 @@ func main() {
manifestPath := flag.String("manifest", "", "benchmark manifest JSON")
outputPath := flag.String("output", "", "output artifact JSON")
timeout := flag.Duration("case-timeout", 30*time.Second, "deadline for each case")
maxOutputBytes := flag.Int64("max-output-bytes", 4096, "maximum advisory output bytes per case")
flag.Parse()
if *manifestPath == "" || *outputPath == "" || *timeout <= 0 {
if *manifestPath == "" || *outputPath == "" || *timeout <= 0 || *maxOutputBytes < 1 {
fatal(errors.New("manifest, output and positive case-timeout are required"))
}
input, err := os.ReadFile(*manifestPath)
@@ -110,7 +111,7 @@ func main() {
if err := json.Unmarshal(input, &plan); err != nil {
fatal(err)
}
artifact, err := run(context.Background(), plan, *timeout, time.Now().UTC())
artifact, err := run(context.Background(), plan, *timeout, *maxOutputBytes, time.Now().UTC())
if err != nil {
fatal(err)
}
@@ -125,7 +126,7 @@ func main() {
fmt.Printf("artifact=%s models=%d output=%s\n", artifact.ArtifactID, len(artifact.Results), *outputPath)
}
func run(ctx context.Context, plan manifest, timeout time.Duration, observed time.Time) (benchmarkArtifact, error) {
func run(ctx context.Context, plan manifest, timeout time.Duration, maxOutputBytes int64, observed time.Time) (benchmarkArtifact, error) {
if plan.ArtifactID == "" || len(plan.Providers) == 0 {
return benchmarkArtifact{}, errors.New("invalid benchmark manifest")
}
@@ -134,7 +135,7 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
artifact.Corpus = append(artifact.Corpus, corpusRecord{ID: item.id, Prompt: item.prompt, PromptSHA256: digest(item.prompt), Critical: item.critical})
}
for _, provider := range plan.Providers {
adapter, err := newProvider(provider)
adapter, err := newProvider(provider, timeout)
if err != nil {
return benchmarkArtifact{}, err
}
@@ -142,7 +143,7 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
return provider.Candidates[i].ProviderModelID < provider.Candidates[j].ProviderModelID
})
for _, candidate := range provider.Candidates {
result, err := runModel(ctx, adapter, provider.ProviderInstanceID, candidate, timeout)
result, err := runModel(ctx, adapter, provider.ProviderInstanceID, candidate, timeout, maxOutputBytes)
if err != nil {
return benchmarkArtifact{}, err
}
@@ -158,10 +159,10 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
return artifact, nil
}
func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
func newProvider(candidate providerCandidate, timeout time.Duration) (models.LocalProvider, error) {
switch candidate.ProviderKind {
case "openai-compatible-chat-v1":
return models.NewOpenAICompatibleLocalAdapter(candidate.Endpoint, nil)
return models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(candidate.Endpoint, "", nil, timeout)
case "ollama-v1":
return models.NewOllamaAdapter(candidate.Endpoint, nil)
default:
@@ -169,7 +170,7 @@ func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
}
}
func runModel(ctx context.Context, adapter models.LocalProvider, providerID string, candidate modelCandidate, timeout time.Duration) (modelResult, error) {
func runModel(ctx context.Context, adapter models.LocalProvider, providerID string, candidate modelCandidate, timeout time.Duration, maxOutputBytes int64) (modelResult, error) {
if providerID == "" || candidate.ProviderModelID == "" || candidate.ModelDescriptorID == "" || candidate.ModelRevision < 1 || candidate.BindingID == "" || candidate.BindingRevision < 1 {
return modelResult{}, errors.New("invalid benchmark candidate")
}
@@ -178,7 +179,7 @@ func runModel(ctx context.Context, adapter models.LocalProvider, providerID stri
for index, item := range corpus {
caseCtx, cancel := context.WithTimeout(ctx, timeout)
started := time.Now()
response, err := adapter.Advise(caseCtx, cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("bench-%s-%d", digest(candidate.BindingID)[:12], index)), ActorID: "benchmark-actor", PrincipalID: "domovoy-creator-principal", ScopeID: "benchmark-public-synthetic", Message: item.prompt, Locale: "ru-RU", MaxOutputBytes: 4096, ProviderBindingID: capability.CapabilityBindingID(candidate.BindingID), ProviderBindingRevision: candidate.BindingRevision, ProviderInstanceID: capability.ProviderInstanceID(providerID), ModelDescriptorID: capability.ModelDescriptorID(candidate.ModelDescriptorID), ProviderModelID: capability.ProviderModelID(candidate.ProviderModelID)})
response, err := adapter.Advise(caseCtx, cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("bench-%s-%d", digest(candidate.BindingID)[:12], index)), ActorID: "benchmark-actor", PrincipalID: "domovoy-creator-principal", ScopeID: "benchmark-public-synthetic", Message: item.prompt, Locale: "ru-RU", MaxOutputBytes: int(maxOutputBytes), ProviderBindingID: capability.CapabilityBindingID(candidate.BindingID), ProviderBindingRevision: candidate.BindingRevision, ProviderInstanceID: capability.ProviderInstanceID(providerID), ModelDescriptorID: capability.ModelDescriptorID(candidate.ModelDescriptorID), ProviderModelID: capability.ProviderModelID(candidate.ProviderModelID)})
cancel()
caseResult := caseResult{CaseID: item.id, LatencyMillis: time.Since(started).Milliseconds()}
if err != nil {
@@ -0,0 +1,53 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
realityevaluation "git.cin.su/m/Assistant/tests/reality_evaluation"
)
func main() {
suitePath := flag.String("suite", "", "path to versioned reality-evaluation suite JSON")
outputPath := flag.String("output", "", "path to JSON artifact output")
observedAt := flag.String("observed-at", "2026-08-17T12:00:00Z", "RFC3339 timestamp for reproducible artifact generation")
flag.Parse()
if *suitePath == "" || *outputPath == "" {
fatal(fmt.Errorf("suite and output are required"))
}
at, err := time.Parse(time.RFC3339, *observedAt)
if err != nil {
fatal(err)
}
suite, err := realityevaluation.LoadSuite(*suitePath)
if err != nil {
fatal(err)
}
artifact, err := realityevaluation.RunSuite(suite, at)
if err != nil {
fatal(err)
}
payload, err := json.MarshalIndent(artifact, "", " ")
if err != nil {
fatal(err)
}
payload = append(payload, '\n')
if err := os.WriteFile(*outputPath, payload, 0o600); err != nil {
fatal(err)
}
reportPath := strings.TrimSuffix(*outputPath, filepath.Ext(*outputPath)) + ".md"
if err := os.WriteFile(reportPath, []byte(realityevaluation.MarkdownReport(artifact)), 0o600); err != nil {
fatal(err)
}
fmt.Printf("artifact=%s total_cases=%d output=%s report=%s\n", artifact.ArtifactID, artifact.TotalCases, *outputPath, reportPath)
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
type httpAdmissionClient struct {
baseURL string
client *http.Client
}
func newHTTPAdmissionClient(baseURL string, client *http.Client) *httpAdmissionClient {
return &httpAdmissionClient{baseURL: strings.TrimRight(baseURL, "/"), client: client}
}
func (c *httpAdmissionClient) Admit(ctx context.Context, leaseID string, request capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
payload, err := json.Marshal(struct {
LeaseID string `json:"lease_id"`
Request capability.AdmissionRequest `json:"request"`
}{leaseID, request})
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/internal/cognitive/admission", bytes.NewReader(payload))
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, fmt.Errorf("admission status %d", resp.StatusCode)
}
var result struct {
Decision capability.AdmissionDecision `json:"decision"`
Lease capability.AdmissionLease `json:"lease"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
}
return result.Decision, result.Lease, nil
}
func (c *httpAdmissionClient) Release(ctx context.Context, lease capability.AdmissionLease) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/internal/cognitive/admission/"+lease.ID+"/release", nil)
if err != nil {
return err
}
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("admission release status %d", resp.StatusCode)
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"context"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
type admissionTestProvider struct{ calls int }
func (p *admissionTestProvider) Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
p.calls++
return cognitive.AdvisoryResponse{FinishReason: cognitive.FinishCompleted}, nil
}
type admissionTestClient struct {
decision capability.AdmissionDecision
releases int
}
func (c *admissionTestClient) Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
return c.decision, capability.AdmissionLease{ID: "lease-1", PolicyID: "policy-1"}, nil
}
func (c *admissionTestClient) Release(context.Context, capability.AdmissionLease) error {
c.releases++
return nil
}
func TestInvokeAdmittedDoesNotCallProviderWhenRejected(t *testing.T) {
provider := &admissionTestProvider{}
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonConcurrency, PolicyID: "policy-1"}}
gateway := cognitiveGateway{admission: client}
response, err := gateway.invokeAdmitted(context.Background(), "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
if err != nil || response.FinishReason != cognitive.FinishRateLimited || provider.calls != 0 || client.releases != 0 {
t.Fatalf("rejected admission leaked provider call: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
}
}
func TestInvokeAdmittedReleasesDurableLeaseAfterProvider(t *testing.T) {
provider := &admissionTestProvider{}
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: "policy-1"}}
gateway := cognitiveGateway{admission: client}
response, err := gateway.invokeAdmitted(context.Background(), "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
if err != nil || response.FinishReason != cognitive.FinishCompleted || provider.calls != 1 || client.releases != 1 {
t.Fatalf("admitted lease was not released: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
return
}
discovered, probe := registration.Discovery.Discover(r.Context())
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: capability.DataPolicyLocalOnly, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
for _, model := range discovered {
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, MaxContextBytes: model.MaxContextBytes, Modalities: model.Modalities, Capabilities: model.Capabilities, Installed: capability.OperationalYes, Loaded: capability.OperationalNotObserved, Invokable: capability.OperationalNotObserved})
}
+79
View File
@@ -0,0 +1,79 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/capability"
fleetdomain "git.cin.su/m/Assistant/modules/capability/domain"
)
type fleetEndpoint struct {
ProviderID string
ModelID string
Tier capability.FleetTier
URL string
Headers map[string]string
Client *http.Client
}
type fleetStatusResponse struct {
Readiness capability.FleetReadiness `json:"readiness"`
Preferred capability.FleetTier `json:"preferred_tier"`
Selected capability.FleetTier `json:"selected_tier,omitempty"`
Error string `json:"error,omitempty"`
}
func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier, endpoints []fleetEndpoint) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
members := make([]capability.FleetMember, 0, len(endpoints))
for _, endpoint := range endpoints {
member := capability.FleetMember{ProviderInstanceID: capability.ProviderInstanceID(endpoint.ProviderID), ModelDescriptorID: capability.ModelDescriptorID(endpoint.ModelID), Tier: endpoint.Tier, Reason: "NOT_CONFIGURED"}
if strings.TrimSpace(endpoint.URL) == "" {
members = append(members, member)
continue
}
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint.URL, nil)
if err == nil {
for name, value := range endpoint.Headers {
request.Header.Set(name, value)
}
probeClient := endpoint.Client
if probeClient == nil {
probeClient = client
}
response, requestErr := probeClient.Do(request)
if requestErr == nil {
response.Body.Close()
if response.StatusCode >= 200 && response.StatusCode < 300 {
member.Ready, member.Reason = true, "AVAILABLE"
} else {
member.Reason = fmt.Sprintf("status=%s", response.Status)
}
} else {
member.Reason = requestErr.Error()
}
}
members = append(members, member)
}
readiness, err := fleetdomain.BuildFleetReadiness(time.Now().UnixMilli(), members)
response := fleetStatusResponse{Readiness: readiness, Preferred: preferred}
if err == nil {
response.Selected, err = fleetdomain.SelectFleetTier(readiness, preferred)
}
if err != nil {
response.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
if response.Selected == "" {
w.WriteHeader(http.StatusServiceUnavailable)
}
_ = json.NewEncoder(w).Encode(response)
})
}
func fleetContext(ctx context.Context) context.Context { return ctx }
+46
View File
@@ -0,0 +1,46 @@
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestFleetReadinessSelectsBestAvailablePrimaryTierBeforeLifeboat(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
defer server.Close()
handler := fleetReadinessHandler(server.Client(), capability.FleetTierHeavy, []fleetEndpoint{
{ProviderID: "cpu", ModelID: "cpu-model", Tier: capability.FleetTierFast, URL: server.URL},
{ProviderID: "medium", ModelID: "medium-model", Tier: capability.FleetTierMedium},
{ProviderID: "heavy", ModelID: "heavy-model", Tier: capability.FleetTierHeavy},
{ProviderID: "lifeboat", ModelID: "lifeboat-model", Tier: capability.FleetTierLifeboat, URL: server.URL},
})
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil)
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"FAST"`) || !strings.Contains(recorder.Body.String(), `"tier":"MEDIUM"`) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer fleet-key" {
t.Fatalf("authorization=%q", r.Header.Get("Authorization"))
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
handler := fleetReadinessHandler(server.Client(), capability.FleetTierFast, []fleetEndpoint{{
ProviderID: "cpu", ModelID: "cpu-model", Tier: capability.FleetTierFast, URL: server.URL,
Headers: map[string]string{"Authorization": "Bearer fleet-key"},
}})
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil))
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"ready":true`) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
+160 -7
View File
@@ -8,11 +8,13 @@ import (
"io"
"net/http"
"net/url"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/platform/ids"
)
@@ -24,11 +26,19 @@ type bindingResolver interface {
RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error)
}
type admissionClient interface {
Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error)
Release(context.Context, capability.AdmissionLease) error
}
type providerRegistration struct {
ProviderInstanceID capability.ProviderInstanceID
Kind capability.ProviderKind
EndpointRef capability.EndpointBindingRef
CredentialRef capability.CredentialRef
AuthMode capability.AuthMode
BillingMode capability.BillingMode
DataPolicy capability.DataPolicy
Provider advisoryProvider
Discovery models.LocalProvider
}
@@ -36,9 +46,12 @@ type providerRegistration struct {
type cognitiveGateway struct {
resolver bindingResolver
providers map[capability.ProviderKind][]providerRegistration
builtinLocal *providerRegistration
externalRecovery []providerRegistration
now func() time.Time
allowCanary bool
canaryMaxOutputBytes int
admission admissionClient
}
func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) {
@@ -71,10 +84,10 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
if g.allowCanary && g.canaryMaxOutputBytes > 0 && maxOutputBytes > g.canaryMaxOutputBytes {
maxOutputBytes = g.canaryMaxOutputBytes
}
requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes)}
requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes), Objective: selectionObjectiveForMeaning(request.Meaning, request.Message)}
selected, err := g.resolver.Resolve(r.Context(), requirement)
if err != nil {
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
}
if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged {
@@ -83,14 +96,14 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
}
provider := g.provider(selected)
if provider == nil {
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
}
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
started := g.now()
response, invokeErr := provider.Advise(r.Context(), invocation)
response, invokeErr := g.invokeAdmitted(r.Context(), string(request.RequestID), provider, invocation, maxOutputBytes)
if invokeErr != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable}
}
g.observe(r.Context(), selected, response, started)
if fallbackEligible(response.FinishReason) {
@@ -102,17 +115,142 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
fallbackRequest.ProviderBindingID, fallbackRequest.ProviderBindingRevision = fallback.Binding.ID, fallback.Binding.Revision
fallbackRequest.ProviderInstanceID, fallbackRequest.ModelDescriptorID, fallbackRequest.ProviderModelID = fallback.Provider.ID, fallback.Model.ID, fallback.Model.ProviderModelID
fallbackStarted := g.now()
fallbackResponse, fallbackErr := fallbackProvider.Advise(r.Context(), fallbackRequest)
fallbackResponse, fallbackErr := g.invokeAdmitted(r.Context(), string(request.RequestID)+":fallback", fallbackProvider, fallbackRequest, maxOutputBytes)
if fallbackErr != nil {
fallbackResponse = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
fallbackResponse = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable}
}
g.observe(r.Context(), fallback, fallbackResponse, fallbackStarted)
response = fallbackResponse
}
}
if response.FinishReason != cognitive.FinishCompleted && fallbackEligible(response.FinishReason) {
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
}
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
}
func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, provider advisoryProvider, invocation cognitive.AdvisoryRequest, maxOutputBytes int) (cognitive.AdvisoryResponse, error) {
var lease capability.AdmissionLease
if g.admission != nil {
decision, admittedLease, err := g.admission.Admit(ctx, leaseID, capability.AdmissionRequest{InputBytes: len(invocation.Message), MaxOutputBytes: maxOutputBytes, DeadlineMillis: 0})
if err != nil {
return cognitive.AdvisoryResponse{}, err
}
if decision.State != capability.AdmissionAdmitted {
return cognitive.AdvisoryResponse{Version: invocation.Version, RequestID: invocation.RequestID, ProviderBindingID: invocation.ProviderBindingID, ProviderBindingRevision: invocation.ProviderBindingRevision, FinishReason: cognitive.FinishRateLimited}, nil
}
lease = admittedLease
defer func() { _ = g.admission.Release(ctx, lease) }()
}
return provider.Advise(ctx, invocation)
}
func selectionObjectiveForMessage(message string) capability.SelectionObjective {
lower := strings.ToLower(message)
if strings.Contains(lower, "дешев") || strings.Contains(lower, "бесплат") || strings.Contains(lower, "эконом") || strings.Contains(lower, "cost") || strings.Contains(lower, "cheap") {
return capability.SelectionEconomy
}
if strings.Contains(lower, "быстр") || strings.Contains(lower, "срочно") || strings.Contains(lower, "статус") || strings.Contains(lower, "quick") || strings.Contains(lower, "fast") {
return capability.SelectionSpeed
}
if strings.Contains(lower, "сложн") || strings.Contains(lower, "анализ") || strings.Contains(lower, "код") || strings.Contains(lower, "план") || strings.Contains(lower, "глубок") || strings.Contains(lower, "reason") || strings.Contains(lower, "code") {
return capability.SelectionQuality
}
return capability.SelectionBalanced
}
func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string) capability.SelectionObjective {
if meaning.CapabilityKind != "" || len(meaning.CapabilityHints) > 0 || meaning.SpeechAct != "" || meaning.RequestedOutcome != "" {
kinds := append([]capability.CapabilityKind{meaning.CapabilityKind}, meaning.CapabilityHints...)
for _, kind := range kinds {
switch kind {
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindVerifyFact, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs:
return capability.SelectionQuality
case capability.CapabilityKindScheduleReminder:
return capability.SelectionSpeed
}
}
if meaning.RequestedOutcome != "" || meaning.SpeechAct == "REQUEST_TO_INVESTIGATE" || meaning.SpeechAct == "DIRECTIVE" {
return capability.SelectionQuality
}
if meaning.ConfidenceMillis > 0 {
return capability.SelectionBalanced
}
}
return selectionObjectiveForMessage(message)
}
func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse {
text := "Я рядом и продолжаю работу во встроенном режиме Домового. Внешний когнитивный маршрут сейчас недоступен, поэтому опираюсь на уже установленный смысл сообщения и локальные возможности."
switch request.Meaning.InformationRequestKind {
case interaction.InformationRequestCognitiveModelInventory:
text = "Запрос понят как проверка моделей. Встроенный режим Домового доступен; я могу показать подключённые модели по назначению, качеству, скорости и стоимости. Ключи и пароли в разговор не нужны."
case interaction.InformationRequestCapabilityInventory:
text = "Запрос понят как проверка возможностей. Я могу объяснить доступные capabilities и отличить то, что уже подключено, от того, что пока является только планом или capability gap."
case interaction.InformationRequestCognitiveChannelStatus:
text = "Запрос понят как проверка когнитивного канала. Локальный режим остаётся доступен; я объясню наблюдаемое состояние и что можно сделать без ожидания внешней модели."
case interaction.InformationRequestCognitiveRecovery:
text = "Запрос понят как восстановление ИИ. Начинаю с локального маршрута: фиксирую, что недоступно, проверяю безопасные варианты восстановления и сформулирую следующий обратимый шаг."
default:
switch request.Meaning.SpeechAct {
case "REQUEST_TO_INVESTIGATE":
text = "Я принял это как исследование. Даже без внешней модели могу сформулировать гипотезы, отделить известное от неизвестного и предложить следующий проверяемый шаг."
case "QUESTION":
text = "Я отвечаю во встроенном режиме Домового. Если для полного ответа не хватает внешней модели, объясню это прямо и продолжу с тем, что можно проверить локально."
}
}
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted}
}
func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse {
if g.builtinLocal != nil && g.builtinLocal.Provider != nil {
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
started := g.now()
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
if response.RequestID == "" {
response.RequestID = request.RequestID
}
if response.Version == "" {
response.Version = request.Version
}
if err != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse}
}
g.observeBuiltin(ctx, g.builtinLocal, response, started)
if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" {
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}
}
}
if request.EgressAcknowledged {
if response, ok := g.recoverExternalAdvisory(ctx, request, maxOutputBytes); ok {
return response
}
}
return builtinAdvisory(request)
}
func (g cognitiveGateway) recoverExternalAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) (cognitive.GatewayAdvisoryResponse, bool) {
for _, candidate := range g.externalRecovery {
if candidate.Provider == nil || candidate.Discovery == nil {
continue
}
probeCtx, cancel := context.WithTimeout(ctx, 8*time.Second)
modelsFound, probe := candidate.Discovery.Discover(probeCtx)
cancel()
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
continue
}
model := modelsFound[0]
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "recovery-external-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: capability.ProviderInstanceID("recovery-" + string(candidate.ProviderInstanceID)), ModelDescriptorID: capability.ModelDescriptorID("recovery-model-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID}
response, err := candidate.Provider.Advise(ctx, invocation)
if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" {
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}, true
}
}
return cognitive.GatewayAdvisoryResponse{}, false
}
func (g cognitiveGateway) observe(ctx context.Context, selected capability.ResolvedCognitiveBinding, response cognitive.AdvisoryResponse, started time.Time) {
usage := response.Usage
observationID, idErr := ids.New()
@@ -132,6 +270,21 @@ func (g cognitiveGateway) observe(ctx context.Context, selected capability.Resol
}
}
// observeBuiltin gives the always-available local provider the same evidence
// path as a resolved binding. The identity is stable and contains references
// only; no endpoint, credential, prompt, or response text is persisted here.
func (g cognitiveGateway) observeBuiltin(ctx context.Context, registration *providerRegistration, response cognitive.AdvisoryResponse, started time.Time) {
if registration == nil {
return
}
selected := capability.ResolvedCognitiveBinding{
Binding: capability.CognitiveBinding{ID: "builtin-local-binding-v1", Revision: 1},
Provider: capability.ProviderInstance{ID: "builtin-local-lifeboat-v1", Revision: 1, Kind: registration.Kind},
Model: capability.ModelDescriptor{ID: "builtin-local-model-v1", Revision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ProviderModelID: "domovoy-lifeboat"},
}
g.observe(ctx, selected, response, started)
}
func fallbackEligible(reason cognitive.FinishReason) bool {
switch reason {
case cognitive.FinishProviderUnavailable, cognitive.FinishModelUnavailable, cognitive.FinishTimeout, cognitive.FinishDegraded:
+93 -2
View File
@@ -12,6 +12,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/interaction"
)
type registryStub struct {
@@ -114,7 +115,45 @@ func TestCanaryOutputLimitIsClamped(t *testing.T) {
}
}
func TestNoActiveBindingKeepsGatewayAvailableWithoutInvocation(t *testing.T) {
func TestSelectionObjectiveForMessage(t *testing.T) {
cases := []struct {
message string
objective capability.SelectionObjective
}{
{"Сделай глубокий анализ кода", capability.SelectionQuality},
{"Нужен быстрый статус", capability.SelectionSpeed},
{"Выбери бесплатную модель", capability.SelectionEconomy},
{"Помоги сформулировать ответ", capability.SelectionBalanced},
}
for _, test := range cases {
if actual := selectionObjectiveForMessage(test.message); actual != test.objective {
t.Errorf("message=%q objective=%q, want %q", test.message, actual, test.objective)
}
}
}
func TestSelectionObjectiveUsesTrustedMeaningBeforeText(t *testing.T) {
meaning := cognitive.TaskMeaning{CapabilityKind: capability.CapabilityKindModifyCode, SpeechAct: "DIRECTIVE"}
if actual := selectionObjectiveForMeaning(meaning, "сделай быстро и дёшево"); actual != capability.SelectionQuality {
t.Fatalf("objective=%q, want quality", actual)
}
if actual := selectionObjectiveForMeaning(cognitive.TaskMeaning{}, "сделай быстро"); actual != capability.SelectionSpeed {
t.Fatalf("fallback objective=%q, want speed", actual)
}
}
func TestSelectionObjectiveForInterpretationUsesSemanticSignals(t *testing.T) {
request := cognitive.GatewayInterpretationRequest{RequestedOutcome: "сравнить варианты", DeterministicApplicability: cognitive.InterpretationApplicable, DeterministicConfidence: 900, DeterministicSpeechAct: "QUESTION"}
if actual := selectionObjectiveForInterpretation(request); actual != capability.SelectionQuality {
t.Fatalf("objective=%q, want quality", actual)
}
request = cognitive.GatewayInterpretationRequest{DeterministicApplicability: cognitive.InterpretationApplicable, DeterministicConfidence: 900, DeterministicSpeechAct: "ASSERTION"}
if actual := selectionObjectiveForInterpretation(request); actual != capability.SelectionSpeed {
t.Fatalf("objective=%q, want speed", actual)
}
}
func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{}
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"unused": {{Provider: provider}}}, now: time.Now}
@@ -125,11 +164,63 @@ func TestNoActiveBindingKeepsGatewayAvailableWithoutInvocation(t *testing.T) {
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
t.Fatal(err)
}
if response.Code != http.StatusOK || result.FinishReason != cognitive.FinishProviderUnavailable || provider.called {
if response.Code != http.StatusOK || result.FinishReason != cognitive.FinishCompleted || result.AdvisoryText == "" || provider.called {
t.Fatalf("status=%d result=%+v provider_called=%t", response.Code, result, provider.called)
}
}
func TestBuiltInAdvisoryUsesTrustedInformationIntentInsteadOfMessageKeywords(t *testing.T) {
request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", Message: "arbitrary text", Meaning: cognitive.TaskMeaning{InformationRequestKind: interaction.InformationRequestCognitiveRecovery}}
response := builtinAdvisory(request)
if !strings.Contains(response.AdvisoryText, "восстановление ИИ") {
t.Fatalf("unexpected recovery response: %q", response.AdvisoryText)
}
}
func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}}
builtin := providerRegistration{ProviderInstanceID: "local-lifeboat", Kind: "openai-compatible-chat-v1", EndpointRef: "local-lifeboat-v1", Provider: provider}
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
response := httptest.NewRecorder()
gateway.advisoryHandler(response, request)
if response.Code != http.StatusOK || !provider.called || !strings.Contains(response.Body.String(), "локальный ответ") {
t.Fatalf("status=%d provider_called=%t body=%s", response.Code, provider.called, response.Body.String())
}
if registry.observation == nil || registry.observation.BindingID != "builtin-local-binding-v1" || registry.observation.ProviderInstanceID != "builtin-local-lifeboat-v1" || registry.observation.ModelDescriptorID != "builtin-local-model-v1" || registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishCompleted) {
t.Fatalf("built-in usage observation=%+v", registry.observation)
}
}
func TestBuiltInProviderFailureIsObservedWithoutBlockingFallback(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{err: errors.New("local timeout")}
builtin := providerRegistration{Kind: "ollama-v1", Provider: provider}
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
response := httptest.NewRecorder()
gateway.advisoryHandler(response, request)
if response.Code != http.StatusOK || registry.observation == nil {
t.Fatalf("status=%d observation=%+v body=%s", response.Code, registry.observation, response.Body.String())
}
if registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishInvalidProviderResponse) || registry.observation.FailureType != capability.CognitiveFailureType(cognitive.FinishInvalidProviderResponse) {
t.Fatalf("failure observation=%+v", registry.observation)
}
}
func TestBuiltInInterpretationIsObserved(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishCompleted, AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":false,"clarification":"NOT_REQUIRED","selection_reason":"local","capability_hints":[]}`}}
builtin := providerRegistration{Kind: "openai-compatible-chat-v1", Provider: provider}
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "interpretation-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "что доступно", MaxOutputBytes: 4096, DeterministicSpeechAct: "QUESTION", DeterministicApplicability: cognitive.InterpretationApplicable}
result := gateway.interpretWithBuiltin(context.Background(), request, request.MaxOutputBytes)
if result.FinishReason != cognitive.FinishCompleted || registry.observation == nil || registry.observation.RequestID != "interpretation-1" {
t.Fatalf("result=%+v observation=%+v", result, registry.observation)
}
}
func TestProviderLossBetweenRoutingAndInvocationFallsBackOnce(t *testing.T) {
primary := gatewaySelection()
primary.Binding.ID = "gpu-binding"
+215
View File
@@ -0,0 +1,215 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayInterpretationRequest
if !decodeJSON(w, r, 80<<10, &request) {
return
}
if request.Version != cognitive.CreatorAdvisoryV1 || request.RequestID == "" || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || strings.TrimSpace(request.Message) == "" || request.MaxOutputBytes < 1 {
http.Error(w, "invalid interpretation request", http.StatusBadRequest)
return
}
maxOutputBytes := request.MaxOutputBytes
if g.allowCanary && g.canaryMaxOutputBytes > 0 && maxOutputBytes > g.canaryMaxOutputBytes {
maxOutputBytes = g.canaryMaxOutputBytes
}
requirement := capability.CognitiveRequirement{
CapabilityID: creatorAdvisoryCapabilityID,
OwnerPrincipalID: request.PrincipalID,
DataPolicy: request.DataPolicy,
RequiredModalities: []capability.ModelModality{capability.ModalityText},
RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory},
AllowCanary: g.allowCanary,
InputBytes: int64(len(request.Message)),
MaxOutputBytes: int64(maxOutputBytes),
Objective: selectionObjectiveForInterpretation(request),
}
selected, err := g.resolver.Resolve(r.Context(), requirement)
if err != nil {
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
return
}
provider := g.provider(selected)
if provider == nil {
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
return
}
invocation := cognitive.AdvisoryRequest{
Version: request.Version,
RequestID: request.RequestID,
ActorID: request.ActorID,
PrincipalID: request.PrincipalID,
ScopeID: request.ScopeID,
Message: interpretationPrompt(request),
Locale: request.Locale,
MaxOutputBytes: maxOutputBytes,
ProviderBindingID: selected.Binding.ID,
ProviderBindingRevision: selected.Binding.Revision,
ProviderInstanceID: selected.Provider.ID,
ModelDescriptorID: selected.Model.ID,
ProviderModelID: selected.Model.ProviderModelID,
}
started := g.now()
response, invokeErr := provider.Advise(r.Context(), invocation)
if invokeErr != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
}
g.observe(r.Context(), selected, response, started)
if response.FinishReason != cognitive.FinishCompleted {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: response.FinishReason, Usage: response.Usage})
return
}
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
if !ok {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage})
return
}
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
}
func (g cognitiveGateway) interpretWithBuiltin(ctx context.Context, request cognitive.GatewayInterpretationRequest, maxOutputBytes int) cognitive.GatewayInterpretationResponse {
if g.builtinLocal == nil || g.builtinLocal.Provider == nil {
return deterministicInterpretationResponse(request)
}
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: interpretationPrompt(request), Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
started := g.now()
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
if response.RequestID == "" {
response.RequestID = request.RequestID
}
if response.Version == "" {
response.Version = request.Version
}
if err != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse}
}
g.observeBuiltin(ctx, g.builtinLocal, response, started)
if err != nil || response.FinishReason != cognitive.FinishCompleted {
return deterministicInterpretationResponse(request)
}
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
if !ok {
return deterministicInterpretationResponse(request)
}
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage}
}
// deterministicInterpretationResponse keeps the semantic route alive when the
// local model cannot produce strict JSON in time. It does not invent meaning:
// it carries forward the already-recorded deterministic interpretation and
// leaves the advisory answer to the built-in gateway path.
func deterministicInterpretationResponse(request cognitive.GatewayInterpretationRequest) cognitive.GatewayInterpretationResponse {
speechAct := request.DeterministicSpeechAct
if !validSpeechAct(speechAct) {
speechAct = "UNKNOWN"
}
applicability := request.DeterministicApplicability
switch applicability {
case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired:
default:
applicability = cognitive.InterpretationLowConfidence
}
confidence := request.DeterministicConfidence
if confidence < 0 {
confidence = 0
}
if confidence > 1000 {
confidence = 1000
}
candidate := cognitive.GatewayInterpretationCandidate{
PrimarySpeechAct: speechAct,
SecondarySpeechAct: request.DeterministicSecondary,
ConfidenceMillis: confidence,
Applicability: applicability,
RequestedOutcome: request.RequestedOutcome,
NeedsCognition: true,
Clarification: "NOT_REQUIRED",
SelectionReason: "deterministic interpretation retained; built-in structured interpretation was unavailable",
}
if applicability == cognitive.InterpretationClarificationRequired {
candidate.NeedsCognition = false
candidate.Clarification = "REQUIRED"
}
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted}
}
func selectionObjectiveForInterpretation(request cognitive.GatewayInterpretationRequest) capability.SelectionObjective {
if request.RequestedOutcome != "" || request.DeterministicApplicability == cognitive.InterpretationNeedsBroader || request.DeterministicApplicability == cognitive.InterpretationMixedIntent || request.DeterministicApplicability == cognitive.InterpretationLowConfidence {
return capability.SelectionQuality
}
if request.DeterministicConfidence >= 800 && request.DeterministicSpeechAct != "QUESTION" {
return capability.SelectionSpeed
}
return capability.SelectionBalanced
}
func interpretationPrompt(request cognitive.GatewayInterpretationRequest) string {
projects := "none"
if len(request.ProjectNames) > 0 {
projects = strings.Join(request.ProjectNames, ", ")
}
return fmt.Sprintf(`Return only strict JSON with keys primary_speech_act, secondary_speech_act, confidence_millis, applicability, requested_outcome, needs_cognition, clarification, selection_reason, capability_hints.
Allowed speech acts: DIRECTIVE, ASSERTION, DECISION, PREFERENCE, QUESTION, HYPOTHESIS, CORRECTION, REQUEST_TO_INVESTIGATE, CONFIRMATION, REJECTION, CANCELLATION, CONVERSATIONAL, UNKNOWN.
Allowed applicability: APPLICABLE, LOW_CONFIDENCE, MIXED_INTENT, NEEDS_BROADER_INTERPRETATION, CLARIFICATION_REQUIRED.
Allowed clarification: NOT_REQUIRED or REQUIRED.
Capability hints may only contain known capability kinds like COGNITIVE_REASONING, MODIFY_CODE, RUN_TESTS, SEND_EMAIL, SCHEDULE_REMINDER, VERIFY_FACT, INSPECT_PROJECT, READ_DATABASE, QUERY_LOGS.
Interpret the human utterance semantically. Do not claim action execution. Do not treat punctuation alone as decisive. If uncertain, set applicability accordingly.
Message: %q
Deterministic primary: %s
Deterministic secondary: %s
Deterministic confidence: %d
Deterministic applicability: %s
Deterministic requested outcome: %q
Relevant project names: %s`, request.Message, request.DeterministicSpeechAct, request.DeterministicSecondary, request.DeterministicConfidence, request.DeterministicApplicability, request.RequestedOutcome, projects)
}
func parseInterpretationCandidate(text string) (cognitive.GatewayInterpretationCandidate, bool) {
start := strings.Index(text, "{")
end := strings.LastIndex(text, "}")
if start < 0 || end <= start {
return cognitive.GatewayInterpretationCandidate{}, false
}
var candidate cognitive.GatewayInterpretationCandidate
if err := json.Unmarshal([]byte(text[start:end+1]), &candidate); err != nil {
return cognitive.GatewayInterpretationCandidate{}, false
}
if !validSpeechAct(candidate.PrimarySpeechAct) || candidate.ConfidenceMillis < 0 || candidate.ConfidenceMillis > 1000 {
return cognitive.GatewayInterpretationCandidate{}, false
}
if candidate.SecondarySpeechAct != "" && !validSpeechAct(candidate.SecondarySpeechAct) {
return cognitive.GatewayInterpretationCandidate{}, false
}
switch candidate.Applicability {
case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired:
default:
return cognitive.GatewayInterpretationCandidate{}, false
}
if candidate.Clarification != "NOT_REQUIRED" && candidate.Clarification != "REQUIRED" {
return cognitive.GatewayInterpretationCandidate{}, false
}
return candidate, true
}
func validSpeechAct(value string) bool {
switch value {
case "DIRECTIVE", "ASSERTION", "DECISION", "PREFERENCE", "QUESTION", "HYPOTHESIS", "CORRECTION", "REQUEST_TO_INVESTIGATE", "CONFIRMATION", "REJECTION", "CANCELLATION", "CONVERSATIONAL", "UNKNOWN":
return true
}
return false
}
func writeInterpretationResponse(w http.ResponseWriter, response cognitive.GatewayInterpretationResponse) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response)
}
@@ -0,0 +1,219 @@
package main
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/situation"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestLiveInterpretationTrace(t *testing.T) {
if os.Getenv("ASSISTANT_LIVE_SEMANTIC_TRACE") != "1" {
t.Skip("set ASSISTANT_LIVE_SEMANTIC_TRACE=1 for live semantic/gateway/raw trace")
}
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
rawBase := envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1")
rawModelID, err := firstRawModel(ctx, rawBase)
if err != nil {
t.Fatal(err)
}
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(rawBase, nil)
if err != nil {
t.Fatal(err)
}
discovered, probe := cpuAdapter.Discover(ctx)
if probe.Health != capability.HealthAvailable || len(discovered) == 0 {
t.Fatalf("cpu discovery unavailable: probe=%+v models=%+v", probe, discovered)
}
selected := capability.ResolvedCognitiveBinding{
Binding: capability.CognitiveBinding{
ID: "trace-binding",
Revision: 1,
CapabilityID: "creator-advisory",
OwnerPrincipalID: "domovoy-creator-principal",
DataPolicy: capability.DataPolicyLocalOnly,
},
Provider: capability.ProviderInstance{
ID: "local-cpu-primary",
Revision: 1,
Kind: "openai-compatible-chat-v1",
EndpointRef: "local-cpu-primary-v1",
},
Model: capability.ModelDescriptor{
ID: capability.ModelDescriptorID("trace-model"),
Revision: 1,
ProviderModelID: discovered[0].ProviderModelID,
},
}
gateway := cognitiveGateway{
resolver: traceResolver{selected: selected},
providers: map[capability.ProviderKind][]providerRegistration{
"openai-compatible-chat-v1": {{
ProviderInstanceID: "local-cpu-primary",
Kind: "openai-compatible-chat-v1",
EndpointRef: "local-cpu-primary-v1",
Provider: cpuAdapter,
}},
},
now: time.Now,
allowCanary: true,
}
for _, message := range []string{"ты кто", "можешь расказать об утре"} {
conversation := interaction.Conversation{ID: "trace-conversation", ActorID: "trace-actor", PrincipalID: "domovoy-creator-principal", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
utterance, _, _, err := interactiondomain.NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Now())
if err != nil {
t.Fatal(err)
}
semantic, err := interactiondomain.InterpretUtterance(utterance, []situation.ProjectDescriptor{{ID: "assistant", DisplayName: "Assistant"}}, nil, time.Now())
if err != nil {
t.Fatal(err)
}
rawResponse, err := rawCompletion(ctx, rawBase, rawModelID, message)
if err != nil {
t.Fatal(err)
}
request := cognitive.GatewayInterpretationRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "trace-request",
ActorID: identity.ActorID("trace-actor"),
PrincipalID: identity.PrincipalID("domovoy-creator-principal"),
ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: message,
Locale: "ru",
MaxOutputBytes: 4096,
DataPolicy: capability.DataPolicyLocalOnly,
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
DeterministicConfidence: semantic.Interpretation.ConfidenceMillis,
DeterministicApplicability: cognitive.InterpretationApplicability(semantic.Interpretation.Applicability),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
httpRequest := httptest.NewRequest(http.MethodPost, "/v1/interpretations", bytes.NewReader(payload))
gateway.interpretationHandler(recorder, httpRequest.WithContext(ctx))
if recorder.Code != http.StatusOK {
t.Fatalf("message=%q gateway status=%d body=%s", message, recorder.Code, recorder.Body.String())
}
var gatewayResponse cognitive.GatewayInterpretationResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &gatewayResponse); err != nil {
t.Fatal(err)
}
if gatewayResponse.FinishReason != cognitive.FinishCompleted {
t.Fatalf("message=%q gateway finish=%s body=%s", message, gatewayResponse.FinishReason, recorder.Body.String())
}
if rawResponse == "" {
t.Fatalf("message=%q raw response is empty", message)
}
if message == "ты кто" {
if semantic.Interpretation.SpeechAct != interaction.SpeechActQuestion || !semantic.NeedsCognition {
t.Fatalf("semantic path still fails punctuation-free identity question: %+v", semantic.Interpretation)
}
if gatewayResponse.Candidate.PrimarySpeechAct != "QUESTION" {
t.Fatalf("gateway did not preserve question semantics: %+v", gatewayResponse.Candidate)
}
}
t.Logf("TRACE message=%q deterministic=%s/%s needs_cognition=%t raw=%q gateway_candidate=%+v", message, semantic.Interpretation.SpeechAct, semantic.Interpretation.Applicability, semantic.NeedsCognition, truncate(rawResponse, 180), gatewayResponse.Candidate)
}
}
type traceResolver struct {
selected capability.ResolvedCognitiveBinding
}
func (r traceResolver) Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
return r.selected, nil
}
func (r traceResolver) Observe(context.Context, capability.CognitiveUsageObservation) error {
return nil
}
func (r traceResolver) RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
return capability.CognitiveDiscoveryRegistration{}, nil
}
func firstRawModel(ctx context.Context, base string) (string, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/models", nil)
if err != nil {
return "", err
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
var payload struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(&payload); err != nil {
return "", err
}
if len(payload.Data) == 0 || payload.Data[0].ID == "" {
return "", io.EOF
}
return payload.Data[0].ID, nil
}
func rawCompletion(ctx context.Context, base, model, message string) (string, error) {
payload, err := json.Marshal(map[string]any{
"model": model,
"max_tokens": 128,
"messages": []map[string]string{
{"role": "user", "content": message},
},
"stream": false,
})
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/chat/completions", bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
var result struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&result); err != nil {
return "", err
}
if len(result.Choices) == 0 {
return "", io.EOF
}
return result.Choices[0].Message.Content, nil
}
func truncate(value string, max int) string {
if len(value) <= max {
return value
}
return value[:max] + "..."
}
@@ -0,0 +1,26 @@
package main
import (
"context"
"testing"
"git.cin.su/m/Assistant/contracts/cognitive"
)
func TestBuiltinInterpretationTimeoutRetainsDeterministicCandidate(t *testing.T) {
request := cognitive.GatewayInterpretationRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "request-1",
DeterministicSpeechAct: "QUESTION",
DeterministicConfidence: 720,
DeterministicApplicability: cognitive.InterpretationNeedsBroader,
RequestedOutcome: "вывести список настроек",
}
response := (cognitiveGateway{}).interpretWithBuiltin(context.Background(), request, 4096)
if response.FinishReason != cognitive.FinishCompleted {
t.Fatalf("finish reason=%q", response.FinishReason)
}
if response.Candidate.PrimarySpeechAct != "QUESTION" || response.Candidate.ConfidenceMillis != 720 || response.Candidate.RequestedOutcome != request.RequestedOutcome {
t.Fatalf("candidate did not retain deterministic meaning: %+v", response.Candidate)
}
}
+197 -5
View File
@@ -10,11 +10,14 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/runtime/health"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
@@ -40,37 +43,207 @@ func main() {
}
providers := map[capability.ProviderKind][]providerRegistration{}
discoveries := map[string]providerRegistration{}
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), nil)
var builtinLocal *providerRegistration
externalRecovery := []providerRegistration{}
cpuAPIKey, err := loadProviderAPIKey("LOCAL_CPU_PROVIDER_API_KEY", "LOCAL_CPU_PROVIDER_API_KEY_FILE", "LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1")
if err != nil {
log.Fatal(err)
}
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", Provider: cpuAdapter, Discovery: cpuAdapter}
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapterWithAPIKey(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), cpuAPIKey, nil)
if err != nil {
log.Fatal(err)
}
cpuAuthMode := capability.AuthNone
cpuCredentialRef := capability.CredentialRef("")
if cpuAPIKey != "" {
cpuAuthMode = capability.AuthKey
cpuCredentialRef = capability.CredentialRef(envOr("LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1"))
}
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", CredentialRef: cpuCredentialRef, AuthMode: cpuAuthMode, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: cpuAdapter, Discovery: cpuAdapter}
providers[cpu.Kind] = append(providers[cpu.Kind], cpu)
discoveries["cpu"] = cpu
mediumURL := strings.TrimSpace(os.Getenv("LOCAL_MEDIUM_PROVIDER_URL"))
mediumAPIKey := ""
if mediumURL != "" {
mediumAPIKey, err = loadProviderAPIKey("LOCAL_MEDIUM_PROVIDER_API_KEY", "LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1")
if err != nil {
log.Fatal(err)
}
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 60*time.Second)
mediumAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(strings.TrimRight(mediumURL, "/"), mediumAPIKey, nil, mediumTimeout)
if adapterErr != nil {
log.Fatal(adapterErr)
}
mediumAuthMode := capability.AuthNone
mediumCredentialRef := capability.CredentialRef("")
if mediumAPIKey != "" {
mediumAuthMode = capability.AuthKey
mediumCredentialRef = capability.CredentialRef(envOr("LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1"))
}
medium := providerRegistration{ProviderInstanceID: "local-medium-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-medium-primary-v1", CredentialRef: mediumCredentialRef, AuthMode: mediumAuthMode, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: mediumAdapter, Discovery: mediumAdapter}
providers[medium.Kind] = append(providers[medium.Kind], medium)
discoveries["medium"] = medium
}
if lifeboat, enabled, lifeboatErr := newLifeboatProvider(os.Getenv("LOCAL_LIFEBOAT_PROVIDER_URL")); lifeboatErr != nil {
log.Fatal(lifeboatErr)
} else if enabled {
lifeboat.DataPolicy = capability.DataPolicyLocalOnly
providers[lifeboat.Kind] = append(providers[lifeboat.Kind], lifeboat)
discoveries["lifeboat"] = lifeboat
builtinLocal = &lifeboat
}
gpuAdapter, err := models.NewOllamaAdapter(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), nil)
if err != nil {
log.Fatal(err)
}
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", Provider: gpuAdapter, Discovery: gpuAdapter}
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: gpuAdapter, Discovery: gpuAdapter}
providers[gpu.Kind] = append(providers[gpu.Kind], gpu)
discoveries["gpu"] = gpu
externalRecovery = append(externalRecovery, gpu)
if openAIProvider != nil {
kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1"))
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), Provider: openAIProvider})
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider})
}
gateway := cognitiveGateway{resolver: resolver, providers: providers, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096}
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096, admission: newHTTPAdmissionClient(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 5 * time.Second})}
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
cpuReadinessClient := &http.Client{Timeout: 10 * time.Second}
mediumReadinessClient := &http.Client{Timeout: 10 * time.Second}
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
fleetClient := &http.Client{Timeout: 2 * time.Second}
preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierHeavy)))
cpuFleetHeaders := map[string]string{}
if cpuAPIKey != "" {
cpuFleetHeaders["Authorization"] = "Bearer " + cpuAPIKey
}
cpuEndpoint := strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models"
cpuHealthProbe := health.OptionalHTTPProbeWithHeaders(cpuReadinessClient, cpuEndpoint, cpuFleetHeaders)
cpuHealthProbe.Timeout = 10 * time.Second
mediumEndpoint := ""
mediumHeaders := map[string]string{}
mediumHealthProbe := health.Probe{}
if mediumURL != "" {
mediumEndpoint = strings.TrimRight(mediumURL, "/") + "/models"
if mediumAPIKey != "" {
mediumHeaders["Authorization"] = "Bearer " + mediumAPIKey
}
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(mediumReadinessClient, mediumEndpoint, mediumHeaders)
mediumHealthProbe.Timeout = 10 * time.Second
}
readinessProbes := []health.Probe{
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
cpuHealthProbe,
}
if mediumURL != "" {
readinessProbes = append(readinessProbes, mediumHealthProbe)
}
readinessProbes = append(readinessProbes,
health.HTTPProbe(readinessClient, lifeboatURL),
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
)
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: readinessProbes})
mux.Handle("/v1/fleet/readiness", fleetReadinessHandler(fleetClient, preferredFleet, []fleetEndpoint{
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: cpuEndpoint, Headers: cpuFleetHeaders, Client: &http.Client{Timeout: 10 * time.Second}},
{ProviderID: "local-medium-primary", ModelID: "local-medium-model", Tier: capability.FleetTierMedium, URL: mediumEndpoint, Headers: mediumHeaders, Client: mediumReadinessClient},
{ProviderID: "local-gpu-opportunistic", ModelID: "gpu-opportunistic", Tier: capability.FleetTierHeavy, URL: strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/") + "/api/tags"},
{ProviderID: "cognitive-lifeboat", ModelID: "qwen3-8b-lifeboat", Tier: capability.FleetTierLifeboat, URL: lifeboatURL},
}))
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
refreshProviderDiscoveries(resolver, discovery)
log.Printf("cognitive-worker listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}
func durationEnv(name string, fallback time.Duration) time.Duration {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return fallback
}
parsed, err := time.ParseDuration(value)
if err != nil || parsed <= 0 {
log.Printf("invalid %s=%q; using %s", name, value, fallback)
return fallback
}
return parsed
}
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
for name := range service.providers {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
registration, ok := service.providers[name]
if !ok || registration.Discovery == nil {
cancel()
continue
}
discovered, probe := registration.Discovery.Discover(ctx)
cancel()
snapshot := capability.CognitiveDiscoverySnapshot{
ProviderInstanceID: registration.ProviderInstanceID,
ProviderKind: registration.Kind,
EndpointRef: registration.EndpointRef,
CredentialRef: registration.CredentialRef,
AuthMode: registration.AuthMode,
BillingMode: registration.BillingMode,
Enabled: true,
OwnerPrincipalID: service.owner,
CapabilityID: creatorAdvisoryCapabilityID,
DataPolicy: registration.DataPolicy,
LimitPolicyRef: "creator-advisory-local-v1",
Health: probe.Health,
FailureClass: capability.CognitiveFailureType(probe.Failure),
ObservedAtUnixMillis: time.Now().UnixMilli(),
}
for _, model := range discovered {
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{
ProviderModelID: model.ProviderModelID,
Fingerprint: model.Fingerprint,
MaxContextBytes: model.MaxContextBytes,
Modalities: model.Modalities,
Capabilities: model.Capabilities,
Installed: capability.OperationalYes,
Loaded: capability.OperationalNotObserved,
Invokable: capability.OperationalNotObserved,
})
}
if _, err := resolver.RegisterDiscovery(context.Background(), snapshot); err != nil {
log.Printf("startup cognitive discovery refresh failed for %s: %v", name, err)
}
}
}
func loadProviderAPIKey(directEnvName, fileEnvName, refEnvName, defaultRef string) (string, error) {
if _, set := os.LookupEnv(directEnvName); set {
return "", fmt.Errorf("%s direct environment secret is forbidden; use %s", directEnvName, fileEnvName)
}
fileName := strings.TrimSpace(os.Getenv(fileEnvName))
if fileName == "" {
return "", nil
}
ref := strings.TrimSpace(os.Getenv(refEnvName))
if ref == "" {
ref = defaultRef
}
if ref == "" {
return "", fmt.Errorf("%s is required when %s is configured", refEnvName, fileEnvName)
}
resolver, err := platformsecrets.NewFileResolver(envOr("SECRET_BACKEND_ROOT", "/run/secrets"))
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeProviderAPIKey, Consumer: secretcontracts.RuntimeConsumerCognitive, Path: fileName})
}
func loadLocalCPUProviderAPIKey() (string, error) {
return loadProviderAPIKey("LOCAL_CPU_PROVIDER_API_KEY", "LOCAL_CPU_PROVIDER_API_KEY_FILE", "LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1")
}
func deterministicAdvisory(w http.ResponseWriter, r *http.Request) {
request, ok := decodeAdvisoryRequest(w, r)
if !ok {
@@ -173,3 +346,22 @@ func envOr(name, fallback string) string {
}
return fallback
}
func newLifeboatProvider(rawURL string) (providerRegistration, bool, error) {
if rawURL == "" {
return providerRegistration{}, false, nil
}
adapter, err := models.NewOpenAICompatibleLocalAdapter(rawURL, nil)
if err != nil {
return providerRegistration{}, false, err
}
return providerRegistration{
ProviderInstanceID: "local-lifeboat",
Kind: "openai-compatible-chat-v1",
EndpointRef: "local-lifeboat-v1",
AuthMode: capability.AuthNone,
BillingMode: capability.BillingFree,
Provider: adapter,
Discovery: adapter,
}, true, nil
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestLifeboatProviderIsOptionalAndLocalAdapterBacked(t *testing.T) {
if _, enabled, err := newLifeboatProvider(""); err != nil || enabled {
t.Fatalf("empty lifeboat URL should disable provider: enabled=%v err=%v", enabled, err)
}
provider, enabled, err := newLifeboatProvider("http://lifeboat:8080/v1")
if err != nil || !enabled {
t.Fatalf("configured lifeboat should be enabled: enabled=%v err=%v", enabled, err)
}
if provider.ProviderInstanceID != "local-lifeboat" || provider.EndpointRef != "local-lifeboat-v1" || provider.Provider == nil {
t.Fatalf("unexpected lifeboat registration: %+v", provider)
}
}
func TestLoadLocalCPUProviderAPIKeyUsesProtectedFileReference(t *testing.T) {
t.Setenv("LOCAL_CPU_PROVIDER_API_KEY", "")
_ = os.Unsetenv("LOCAL_CPU_PROVIDER_API_KEY")
root := t.TempDir()
path := filepath.Join(root, "local_cpu_provider_api_key")
if err := os.WriteFile(path, []byte("resolved-key\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("LOCAL_CPU_PROVIDER_API_KEY_FILE", path)
t.Setenv("LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1")
t.Setenv("SECRET_BACKEND_ROOT", root)
key, err := loadLocalCPUProviderAPIKey()
if err != nil || key != "resolved-key" {
t.Fatalf("unexpected key resolution result: err=%v", err)
}
}
func TestLoadLocalCPUProviderAPIKeyRejectsDirectEnvironmentSecret(t *testing.T) {
t.Setenv("LOCAL_CPU_PROVIDER_API_KEY", "must-not-be-used")
t.Setenv("LOCAL_CPU_PROVIDER_API_KEY_FILE", "")
if _, err := loadLocalCPUProviderAPIKey(); err == nil {
t.Fatal("expected direct environment secret to be rejected")
}
}
func TestLoadProviderAPIKeySupportsMediumCredentialRef(t *testing.T) {
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY", "")
_ = os.Unsetenv("LOCAL_MEDIUM_PROVIDER_API_KEY")
root := t.TempDir()
path := filepath.Join(root, "medium_provider_api_key")
if err := os.WriteFile(path, []byte("medium-key\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", path)
t.Setenv("LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1")
t.Setenv("SECRET_BACKEND_ROOT", root)
key, err := loadProviderAPIKey("LOCAL_MEDIUM_PROVIDER_API_KEY", "LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1")
if err != nil || key != "medium-key" {
t.Fatalf("unexpected medium key resolution result: err=%v", err)
}
}
func TestLoadProviderAPIKeyRejectsMediumDirectEnvironmentSecret(t *testing.T) {
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY", "must-not-be-used")
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "")
if _, err := loadProviderAPIKey("LOCAL_MEDIUM_PROVIDER_API_KEY", "LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1"); err == nil {
t.Fatal("expected direct medium environment secret to be rejected")
}
}
+7
View File
@@ -84,6 +84,13 @@ func main() {
dispatcher := operationDispatcher{continuityExecutor: continuityExecutor, continuityReconciler: continuityExecutor, bindings: creatorCore, secretRef: notificationSecretRef, secret: notificationSecret, client: creatorHTTPClient}
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "commit-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "commit-runtime", Probes: []health.Probe{
health.TCPProbe("commit-postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.TCPProbe("authority-postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.HTTPProbe(readinessClient, strings.TrimRight(coreURL, "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
}})
registerExecutionRoutes(mux, repository, dispatcher, dispatcher, executorToken)
registerCreatorCommandRoutes(mux, creatorCommandHandler{
core: creatorCore, continuity: creatorContinuity, authority: authorityRepository, repository: repository,
+60
View File
@@ -12,6 +12,7 @@ import (
type attentionRepository interface {
AttentionByOwner(context.Context, identity.PrincipalID, int) ([]continuity.AttentionItem, error)
CreateAttentionItem(context.Context, continuity.AttentionItem) (continuity.AttentionItem, error)
}
func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorIdentityClient, repository attentionRepository) {
@@ -61,6 +62,53 @@ func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorId
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(result)
})
mux.HandleFunc("POST /v1/creator/attention", func(w http.ResponseWriter, r *http.Request) {
session, ok := creatorSessionForAttention(w, r, identityClient)
if !ok {
return
}
var request continuity.CreateAttentionRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid attention request", http.StatusBadRequest)
return
}
if request.OwnerPrincipalID == "" {
request.OwnerPrincipalID = session.PrincipalID
}
if request.OwnerPrincipalID != session.PrincipalID || request.ScopeID == "" || request.SourceKind == "" || request.Level == "" || request.Summary == "" || request.CreatedAtUnixMillis < 1 || request.DueAtUnixMillis < 1 {
http.Error(w, "invalid attention request", http.StatusBadRequest)
return
}
if request.SourceKind == continuity.AttentionSourceAgentDelegation && request.SourceExternalRef == "" {
http.Error(w, "delegation attention requires source external ref", http.StatusBadRequest)
return
}
item := continuity.AttentionItem{
ID: continuity.AttentionItemID(request.SourceExternalRef),
OwnerPrincipalID: request.OwnerPrincipalID,
ScopeID: request.ScopeID,
SourceKind: request.SourceKind,
SourceProspectiveID: request.SourceProspectiveID,
SourceExternalRef: request.SourceExternalRef,
Level: request.Level,
State: continuity.AttentionPending,
Summary: request.Summary,
DueAtUnixMillis: request.DueAtUnixMillis,
CreatedAtUnixMillis: request.CreatedAtUnixMillis,
Revision: 1,
}
saved, err := repository.CreateAttentionItem(r.Context(), item)
if err != nil {
http.Error(w, "attention unavailable", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(saved)
})
}
func creatorSessionForAttention(w http.ResponseWriter, r *http.Request, identityClient creatorIdentityClient) (identity.CreatorSession, bool) {
@@ -98,6 +146,15 @@ func humanAttentionState(state continuity.AttentionState) string {
}
func humanResponseHint(item continuity.AttentionItem) string {
if item.SourceKind == continuity.AttentionSourceAgentDelegation {
if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed {
return "Можно ответить: «Продолжай» или «Нет, нужен другой вариант»."
}
if item.ResponseOutcome != "" {
return "Ответ записан: " + string(item.ResponseOutcome)
}
return ""
}
if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed {
return "Можно ответить: «Готово», «Отложи на 10 минут» или «Отмени»."
}
@@ -121,5 +178,8 @@ func humanAttentionSource(kind continuity.AttentionSourceKind) string {
if kind == continuity.AttentionSourceProspectiveReminder {
return "напоминание"
}
if kind == continuity.AttentionSourceAgentDelegation {
return "задача coding-agent"
}
return "событие Домового"
}
@@ -65,6 +65,39 @@ func TestCreatorResponseEndpointRecordsAmbiguousWithoutSilentTargetMutation(t *t
}
}
func TestCreatorResponseEndpointContinuesDelegationAttentionViaNaturalMessage(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
}))
defer core.Close()
identityClient, err := newCreatorIdentityClient(core.URL, core.Client())
if err != nil {
t.Fatal(err)
}
repository := &fakeResponseRepository{candidates: []continuitydomain.ResponseCandidate{
{Attention: continuity.AttentionItem{
ID: "work:delegation:delegation-1",
OwnerPrincipalID: "principal-1",
ScopeID: "scope-1",
SourceKind: continuity.AttentionSourceAgentDelegation,
SourceExternalRef: "work:delegation:delegation-1",
State: continuity.AttentionPending,
Summary: "Нужен твой выбор по задаче coding-agent: docs/PROJECT_STATUS.md",
Revision: 1,
}},
{Attention: continuity.AttentionItem{ID: "attention-1", SourceProspectiveID: "prospective-1", State: continuity.AttentionDelivered, Summary: "старое напоминание"}},
}}
mux := http.NewServeMux()
registerCreatorResponseRoute(mux, identityClient, repository, func() time.Time { return time.Unix(100, 0) })
request := httptest.NewRequest(http.MethodPost, "/v1/creator/responses", strings.NewReader(`{"SourceUtteranceID":"utterance-1","SourceIntentID":"intent-1","Message":"Продолжай задачу coding-agent по docs/PROJECT_STATUS.md","RequestedAtUnixMillis":100000}`))
request.Header.Set("Authorization", "Bearer ok")
recorder := httptest.NewRecorder()
mux.ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated || repository.applied.Operation != continuity.ResponseContinue || repository.applied.TargetExternalRef != "work:delegation:delegation-1" || repository.applied.Outcome != continuity.ResponseOutcomeContinued {
t.Fatalf("status=%d applied=%#v body=%s", recorder.Code, repository.applied, recorder.Body.String())
}
}
type fakeResponseRepository struct {
candidates []continuitydomain.ResponseCandidate
applied continuity.HumanResponse
+7
View File
@@ -7,6 +7,7 @@ import (
"log"
"net/http"
"os"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
@@ -31,6 +32,12 @@ func main() {
}
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "continuity-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "continuity-runtime", Probes: []health.Probe{
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"), "/")+"/healthz"),
}})
executorToken := os.Getenv("COMMIT_EXECUTOR_TOKEN")
if executorToken == "" {
log.Fatal("COMMIT_EXECUTOR_TOKEN is required")
+126
View File
@@ -19,7 +19,9 @@ import (
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/work"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/platform/ids"
)
@@ -33,6 +35,11 @@ type continuityResponseClient struct {
client *http.Client
}
type continuityAttentionClient struct {
baseURL *url.URL
client *http.Client
}
func newContinuityReminderClient(rawURL string, client *http.Client) (continuityReminderClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" {
@@ -55,6 +62,17 @@ func newContinuityResponseClient(rawURL string, client *http.Client) (continuity
return continuityResponseClient{baseURL: base, client: client}, nil
}
func newContinuityAttentionClient(rawURL string, client *http.Client) (continuityAttentionClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" {
return continuityAttentionClient{}, fmt.Errorf("invalid Continuity Runtime URL")
}
if client == nil {
client = http.DefaultClient
}
return continuityAttentionClient{baseURL: base, client: client}, nil
}
func (c continuityReminderClient) Schedule(ctx context.Context, authorization string, request continuity.ScheduleReminderRequest) (continuity.ScheduleReminderResult, error) {
target := *c.baseURL
target.Path = "/v1/creator/reminders"
@@ -114,6 +132,37 @@ func (c continuityResponseClient) Respond(ctx context.Context, authorization str
return result, nil
}
func (c continuityAttentionClient) Create(ctx context.Context, authorization string, request continuity.CreateAttentionRequest) (continuity.AttentionItem, error) {
target := *c.baseURL
target.Path = "/v1/creator/attention"
payload, err := json.Marshal(request)
if err != nil {
return continuity.AttentionItem{}, err
}
upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return continuity.AttentionItem{}, err
}
upstream.Header.Set("Content-Type", "application/json")
upstream.Header.Set("Authorization", authorization)
response, err := c.client.Do(upstream)
if err != nil {
return continuity.AttentionItem{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusCreated {
return continuity.AttentionItem{}, fmt.Errorf("continuity attention status %d", response.StatusCode)
}
var result continuity.AttentionItem
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return continuity.AttentionItem{}, err
}
if result.ID == "" || result.SourceKind == "" {
return continuity.AttentionItem{}, fmt.Errorf("invalid attention result")
}
return result, nil
}
func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityRepository, kind capability.CapabilityKind, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (capability.CapabilityResolution, error) {
requirementID, err := ids.New()
if err != nil {
@@ -127,6 +176,7 @@ func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityR
OriginalIntentID: capability.OriginalIntentRef(intent.ID),
SourceUtteranceID: capability.SourceUtteranceRef(utterance.ID),
RequiredConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly},
AllowCanary: allowCanaryForChatCapability(kind),
AuthorityContextRef: capability.AuthorityPolicyCreatorDirectiveV1,
ObservedAtUnixMillis: now.UnixMilli(),
}
@@ -144,6 +194,82 @@ func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityR
return resolution, err
}
func allowCanaryForChatCapability(kind capability.CapabilityKind) bool {
switch kind {
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject:
return true
default:
return false
}
}
type createdDelegation struct {
Delegation work.Delegation
Run work.AgentRun
}
func maybeCreateAgentDelegation(ctx context.Context, workRepository *postgres.WorkRepository, capabilityRepository *postgres.CapabilityRepository, session identity.CreatorSession, conversation interaction.Conversation, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, planning work.PlanningResult, now time.Time) (*createdDelegation, capability.CapabilityResolution, error) {
requirement, ok := primarySoftwareRequirement(planning.Candidate.Requirements)
if !ok {
return nil, capability.CapabilityResolution{}, nil
}
resolution, err := resolveChatCapability(ctx, capabilityRepository, requirement.CapabilityKind, session, intent, utterance, now)
if err != nil {
return nil, resolution, err
}
if len(resolution.Eligible) == 0 || resolution.Eligible[0].Implementation.Kind != capability.ImplementationAgent {
return nil, resolution, capabilitydomain.ErrNoCapabilityImplementation
}
started, err := workdomain.StartDelegation(workdomain.DelegationRequest{
Goal: planning.Goal,
Plan: planning.Plan,
Candidate: planning.Candidate,
Requirement: requirement,
Implementation: resolution.Eligible[0].Implementation,
ConversationID: work.ConversationRef(conversation.ID),
OriginalIntentText: intent.Text,
ContextSummary: planning.Candidate.Summary,
ContextRefs: []string{"interaction:intent:" + string(intent.ID), "interaction:utterance:" + string(utterance.ID), "work:plan:" + string(planning.Plan.ID)},
VerificationSummary: "verify through changed files, relevant tests, and explicit run evidence before work completion",
DerivedInstruction: buildSoftwareAgentInstruction(planning, intent, utterance),
StartedAt: now,
})
if err != nil {
return nil, capability.CapabilityResolution{}, err
}
if err := workRepository.SaveDelegationBundle(ctx, started.Delegation, started.Run); err != nil {
return nil, capability.CapabilityResolution{}, err
}
return &createdDelegation{Delegation: started.Delegation, Run: started.Run}, resolution, nil
}
func primarySoftwareRequirement(values []work.CapabilityRequirement) (work.CapabilityRequirement, bool) {
for _, requirement := range values {
switch requirement.CapabilityKind {
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject:
return requirement, true
}
}
return work.CapabilityRequirement{}, false
}
func buildSoftwareAgentInstruction(planning work.PlanningResult, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance) string {
steps := make([]string, 0, len(planning.Candidate.Steps))
for _, step := range planning.Candidate.Steps {
if step.Description != "" {
steps = append(steps, step.Description)
}
}
return strings.TrimSpace(strings.Join([]string{
"Human asked: " + strings.TrimSpace(intent.Text),
"Original utterance: " + strings.TrimSpace(utterance.Content),
"Desired outcome: " + planning.Goal.DesiredOutcome,
"Plan summary: " + planning.Candidate.Summary,
"Verification: run relevant tests and keep explicit evidence before claiming completion.",
"Steps: " + strings.Join(steps, " | "),
}, "\n"))
}
func observeCapabilityUsage(ctx context.Context, repository *postgres.CapabilityRepository, selected capability.ResolvedCapabilityImplementation, requirement capability.CapabilityResolutionRequirement, outcome capability.UsageOutcomeClass, outcomeRef string, started time.Time, now time.Time) (capability.CapabilityUsageObservationID, error) {
id, err := ids.New()
if err != nil {
+28
View File
@@ -9,9 +9,11 @@ import (
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/work"
)
func TestBuildReminderRequestParsesTomorrowAtTen(t *testing.T) {
@@ -72,3 +74,29 @@ func TestReminderSummaryRemovesSchedulingWords(t *testing.T) {
t.Fatalf("summary = %q", summary)
}
}
func TestBuildSoftwareAgentInstructionKeepsHumanIntentSeparateFromDerivedContext(t *testing.T) {
instruction := buildSoftwareAgentInstruction(
work.PlanningResult{
Goal: work.Goal{DesiredOutcome: "fix chat wording"},
Candidate: work.PlanCandidate{Summary: "accepted deterministic software change plan", Steps: []work.PlanStep{{Description: "Inspect chat surface"}, {Description: "Change copy"}, {Description: "Run tests"}}},
},
interaction.OriginalHumanIntent{ID: "intent-1", Text: "Исправь текст в чате Assistant."},
interaction.HumanUtterance{ID: "utterance-1", Content: "Исправь текст в чате Assistant."},
)
if !strings.Contains(instruction, "Human asked: Исправь текст в чате Assistant.") {
t.Fatalf("instruction must preserve human intent: %q", instruction)
}
if !strings.Contains(instruction, "Verification: run relevant tests") || !strings.Contains(instruction, "Plan summary: accepted deterministic software change plan") {
t.Fatalf("instruction must include derived execution context: %q", instruction)
}
}
func TestAllowCanaryForChatCapabilityIsLimitedToSoftwareEngineeringPath(t *testing.T) {
if !allowCanaryForChatCapability(capability.CapabilityKindModifyCode) || !allowCanaryForChatCapability(capability.CapabilityKindRunTests) || !allowCanaryForChatCapability(capability.CapabilityKindInspectProject) {
t.Fatal("software-engineering capabilities must allow canary implementations")
}
if allowCanaryForChatCapability(capability.CapabilityKindScheduleReminder) {
t.Fatal("non software-engineering capability must not implicitly allow canary")
}
}
@@ -0,0 +1,145 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
"git.cin.su/m/Assistant/platform/ids"
)
type cognitiveInterpretationClient struct {
baseURL *url.URL
client *http.Client
}
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" {
return cognitiveInterpretationClient{}, fmt.Errorf("invalid Cognitive Runtime URL")
}
if client == nil {
client = http.DefaultClient
}
return cognitiveInterpretationClient{baseURL: base, client: client}, nil
}
func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request cognitive.GatewayInterpretationRequest) (cognitive.GatewayInterpretationResponse, error) {
target := *c.baseURL
target.Path = "/v1/interpretations"
payload, err := json.Marshal(request)
if err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
upstream.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(upstream)
if err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return cognitive.GatewayInterpretationResponse{}, fmt.Errorf("cognitive interpretation status %d", response.StatusCode)
}
var result cognitive.GatewayInterpretationResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
return result, nil
}
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
// The built-in advisory channel is the default conversational path. A
// simple question must not first wait for the slower, strict JSON
// interpretation route: that route exists for ambiguity and work routing,
// not as a prerequisite for talking to Domovoy.
if isSimpleLocalQuestion(result) {
return false
}
switch result.Interpretation.Applicability {
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
return true
default:
return false
}
}
func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool {
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion || result.Interpretation.Clarification == interaction.ClarificationRequired {
return false
}
for _, hint := range result.CapabilityHints {
switch hint {
case "MODIFY_CODE", "RUN_TESTS", "SEND_EMAIL", "SCHEDULE_REMINDER", "READ_DATABASE", "QUERY_LOGS":
return false
}
}
return true
}
func buildGatewayInterpretationRequest(session identity.CreatorSession, utterance interaction.HumanUtterance, semantic interactiondomain.SemanticResult, projectNames []string) cognitive.GatewayInterpretationRequest {
requestID, _ := ids.New()
return cognitive.GatewayInterpretationRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID,
PrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: utterance.Content,
Locale: "ru",
MaxOutputBytes: 4096,
DataPolicy: capability.DataPolicyLocalOnly,
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
DeterministicConfidence: semantic.Interpretation.ConfidenceMillis,
DeterministicApplicability: cognitive.InterpretationApplicability(semantic.Interpretation.Applicability),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
ProjectNames: projectNames,
}
}
func interactionCandidateFromGateway(utteranceID interaction.HumanUtteranceID, candidate cognitive.GatewayInterpretationCandidate, now interface{ UnixMilli() int64 }) (interaction.SemanticCandidate, error) {
id, err := ids.New()
if err != nil {
return interaction.SemanticCandidate{}, err
}
return interaction.SemanticCandidate{
ID: interaction.SemanticCandidateID(id),
UtteranceID: utteranceID,
Source: interaction.CandidateSourceCognitive,
SelectionState: interaction.CandidateSelected,
PrimarySpeechAct: interaction.SpeechAct(candidate.PrimarySpeechAct),
SecondarySpeechAct: interaction.SpeechAct(candidate.SecondarySpeechAct),
ConfidenceMillis: candidate.ConfidenceMillis,
Applicability: interaction.SemanticApplicability(candidate.Applicability),
RequestedOutcome: candidate.RequestedOutcome,
Clarification: interaction.ClarificationState(candidate.Clarification),
NeedsCognition: candidate.NeedsCognition,
SelectionReason: candidate.SelectionReason,
CapabilityHints: capabilityHintStrings(candidate.CapabilityHints),
RecordedAtUnixMillis: now.UnixMilli(),
}, nil
}
func capabilityHintStrings(values []capability.CapabilityKind) []string {
result := make([]string, 0, len(values))
for _, value := range values {
if value == "" {
continue
}
result = append(result, string(value))
}
return result
}
@@ -0,0 +1,36 @@
package main
import (
"testing"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestSimpleQuestionUsesBuiltInAdvisoryWithoutStrictInterpretation(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActQuestion,
Applicability: interaction.SemanticNeedsBroaderInterpret,
Clarification: interaction.ClarificationNotRequired,
},
CapabilityHints: []string{"COGNITIVE_REASONING", "VERIFY_FACT"},
}
if shouldRequestBroaderInterpretation(result) {
t.Fatal("simple question must go directly to the built-in advisory path")
}
}
func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActQuestion,
Applicability: interaction.SemanticNeedsBroaderInterpret,
Clarification: interaction.ClarificationNotRequired,
},
CapabilityHints: []string{"MODIFY_CODE", "RUN_TESTS"},
}
if !shouldRequestBroaderInterpretation(result) {
t.Fatal("operational question must retain strict interpretation routing")
}
}
+69 -7
View File
@@ -2,6 +2,8 @@ package main
import (
"context"
"log"
"strconv"
"strings"
"time"
@@ -67,6 +69,20 @@ type capabilityLearningInput struct {
At time.Time
}
type delegationLearningInput struct {
Intent interaction.OriginalHumanIntent
Utterance interaction.HumanUtterance
DelegationID string
RunID string
ImplementationID capability.CapabilityImplementationID
OutcomeRef string
VerificationRefs []epistemics.EvidenceRef
Verified bool
ExecutionWindows int
HumanBlocked bool
At time.Time
}
func (r learningRecorder) recordReminderResponse(ctx context.Context, input reminderResponseLearningInput) {
if r.epistemics == nil || r.capability == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.Result.Response.ID == "" {
return
@@ -85,7 +101,12 @@ func (r learningRecorder) recordReminderResponse(ctx context.Context, input remi
RecordedAtUnixMillis: input.At.UnixMilli(),
}
expectation, err := epistemicsdomain.NewSystemExpectation(expectation)
if err != nil || r.epistemics.SaveSystemExpectation(ctx, expectation) != nil {
if err != nil {
log.Printf("learning reminder expectation invalid: %v", err)
return
}
if err := r.epistemics.SaveSystemExpectation(ctx, expectation); err != nil {
log.Printf("learning reminder expectation unavailable: %v", err)
return
}
actual, confidence := outcomeForHumanResponse(input.Result.Response)
@@ -124,7 +145,6 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodClarification,
SkillRef: epistemics.SkillRef(reminderResponseSkillID),
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: epistemics.OutcomeUserCorrected,
OutcomeDetail: "human correction indicates prior interpretation or target resolution may be wrong",
@@ -133,10 +153,7 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
ConfidenceMillis: 1000,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok {
r.updateReminderSkill(ctx, saved, capability.UsageFailed, true)
}
r.saveExperienceAndReflection(ctx, experience)
}
func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastPathDeclineLearningInput) {
@@ -194,16 +211,58 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
r.saveExperienceAndReflection(ctx, experience)
}
func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input delegationLearningInput) {
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.ImplementationID == "" {
return
}
actual := epistemics.OutcomeSuccess
confidence := int64(950)
if !input.Verified {
actual = epistemics.OutcomeFailed
confidence = 850
}
detailParts := []string{"delegation outcome recorded"}
if input.ExecutionWindows > 1 {
detailParts = append(detailParts, "execution-window-continuations="+strconv.Itoa(input.ExecutionWindows-1))
}
if input.HumanBlocked {
detailParts = append(detailParts, "human-blocker-occurred")
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID),
HelpMethod: epistemics.HelpMethodCapabilityExecution,
HelpMethodRef: "work:agent-delegation-v1",
CapabilityImplementationRef: epistemics.CapabilityImplementationRef(input.ImplementationID),
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: actual,
OutcomeRef: input.OutcomeRef,
OutcomeDetail: strings.Join(detailParts, "; "),
VerificationEvidence: input.VerificationRefs,
ConfidenceMillis: confidence,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
}
func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) {
value, err := epistemicsdomain.NewExperienceRecord(value)
if err != nil {
log.Printf("learning experience invalid: %v", err)
return epistemics.ExperienceRecord{}, false
}
if err := r.epistemics.SaveExperienceRecord(ctx, value); err != nil {
log.Printf("learning experience unavailable: %v", err)
return epistemics.ExperienceRecord{}, false
}
if candidate, ok, err := epistemicsdomain.ReflectionForExperience(value); err == nil && ok {
_ = r.epistemics.SaveReflectionCandidate(ctx, candidate)
if err := r.epistemics.SaveReflectionCandidate(ctx, candidate); err != nil {
log.Printf("learning reflection unavailable: %v", err)
}
} else if err != nil {
log.Printf("learning reflection invalid: %v", err)
}
return value, true
}
@@ -214,6 +273,7 @@ func (r learningRecorder) updateReminderSkill(ctx context.Context, experience ep
}
profile, err := r.capability.LoadSkillProfile(ctx, reminderResponseSkillID)
if err != nil {
log.Printf("learning skill profile unavailable: %v", err)
return
}
next, err := capabilitydomain.ObserveSkillOutcome(profile, capability.SkillOutcomeObservation{
@@ -223,9 +283,11 @@ func (r learningRecorder) updateReminderSkill(ctx context.Context, experience ep
ObservedAtUnixMillis: experience.RecordedAtUnixMillis,
})
if err != nil {
log.Printf("learning skill observation invalid: %v", err)
return
}
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
log.Printf("learning skill profile save unavailable: %v", err)
return
}
}
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"context"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
)
type learningEpistemicsStub struct {
experiences []epistemics.ExperienceRecord
}
func (s *learningEpistemicsStub) SaveSystemExpectation(context.Context, epistemics.SystemExpectation) error {
return nil
}
func (s *learningEpistemicsStub) SaveExperienceRecord(_ context.Context, value epistemics.ExperienceRecord) error {
s.experiences = append(s.experiences, value)
return nil
}
func (s *learningEpistemicsStub) SaveReflectionCandidate(context.Context, epistemics.ReflectionCandidate) error {
return nil
}
type learningCapabilityStub struct {
loadCalls int
saveCalls int
}
func (s *learningCapabilityStub) LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error) {
s.loadCalls++
return capability.SkillProfile{}, nil
}
func (s *learningCapabilityStub) SaveSkillProfile(context.Context, capability.SkillProfile) error {
s.saveCalls++
return nil
}
func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) {
epistemicsStore := &learningEpistemicsStub{}
capabilityStore := &learningCapabilityStub{}
recorder := learningRecorder{epistemics: epistemicsStore, capability: capabilityStore}
recorder.recordCorrection(context.Background(), correctionLearningInput{
Intent: interaction.OriginalHumanIntent{ID: "intent-1"},
Utterance: interaction.HumanUtterance{ID: "utterance-1"},
At: time.UnixMilli(1000),
})
if capabilityStore.loadCalls != 0 || capabilityStore.saveCalls != 0 {
t.Fatalf("generic correction must not mutate reminder skill: load=%d save=%d", capabilityStore.loadCalls, capabilityStore.saveCalls)
}
if len(epistemicsStore.experiences) != 1 || epistemicsStore.experiences[0].SkillRef != "" {
t.Fatalf("generic correction must remain evidence without a reminder skill reference: %#v", epistemicsStore.experiences)
}
}
+549 -21
View File
@@ -8,6 +8,7 @@ import (
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
@@ -107,6 +108,14 @@ func main() {
if err != nil {
log.Fatal(err)
}
attentionClient, err := newContinuityAttentionClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
if err != nil {
log.Fatal(err)
}
cognitiveClient, err := newCognitiveInterpretationClient(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), &http.Client{Timeout: 20 * time.Second})
if err != nil {
log.Fatal(err)
}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
mux := http.NewServeMux()
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
@@ -126,6 +135,40 @@ func main() {
return situationdomain.NewResolver(bindings).Resolve(runtime)
}
mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Probes: []health.Probe{
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
}})
admissionPolicy := capability.AdmissionPolicy{ID: envOr("COGNITIVE_ADMISSION_POLICY_ID", "creator-advisory-local-v1"), MaxConcurrent: envInt("COGNITIVE_ADMISSION_MAX_CONCURRENT", 1), MaxAcceptedPerHour: envInt("COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR", 60), MaxInputBytes: envInt("COGNITIVE_ADMISSION_MAX_INPUT_BYTES", 80<<10), MaxOutputBytes: envInt("COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES", 16<<10)}
mux.HandleFunc("POST /v1/internal/cognitive/admission", func(w http.ResponseWriter, r *http.Request) {
var request struct {
LeaseID string `json:"lease_id"`
Request capability.AdmissionRequest `json:"request"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&request); err != nil || request.LeaseID == "" {
http.Error(w, "invalid admission request", http.StatusBadRequest)
return
}
decision, lease, err := capabilityRepository.AdmitCognitive(r.Context(), request.LeaseID, admissionPolicy, request.Request, time.Now().UnixMilli())
if err != nil {
http.Error(w, "admission unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Decision capability.AdmissionDecision `json:"decision"`
Lease capability.AdmissionLease `json:"lease"`
}{decision, lease})
})
mux.HandleFunc("POST /v1/internal/cognitive/admission/{lease}/release", func(w http.ResponseWriter, r *http.Request) {
if err := capabilityRepository.ReleaseCognitiveAdmission(r.Context(), capability.AdmissionLease{ID: r.PathValue("lease")}, time.Now().UnixMilli()); err != nil {
http.Error(w, "admission release unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
@@ -284,11 +327,31 @@ func main() {
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
return
}
cognitiveAttempted := false
cognitiveAvailable := true
if shouldRequestBroaderInterpretation(semantic) {
cognitiveAttempted = true
projectNames := make([]string, 0, len(semantic.Projects))
for _, ref := range semantic.Projects {
if ref.ProjectDisplayName != "" {
projectNames = append(projectNames, ref.ProjectDisplayName)
}
}
if response, interpretationErr := cognitiveClient.Interpret(r.Context(), buildGatewayInterpretationRequest(session, utterance, semantic, projectNames)); interpretationErr == nil && response.FinishReason == cognitive.FinishCompleted {
if candidate, candidateErr := interactionCandidateFromGateway(utterance.ID, response.Candidate, now); candidateErr == nil {
semantic = interactiondomain.ApplyCognitiveCandidate(semantic, utterance.Content, candidate, previousFocus)
} else {
cognitiveAvailable = false
}
} else {
cognitiveAvailable = false
}
}
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
http.Error(w, "save chat message", http.StatusInternalServerError)
return
}
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Projects, semantic.Relations); err != nil {
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Candidates, semantic.Projects, semantic.Relations); err != nil {
http.Error(w, "save chat semantics", http.StatusInternalServerError)
return
}
@@ -327,6 +390,9 @@ func main() {
planningReply = responseResult.Reply
needsCognition = false
learning.recordReminderResponse(r.Context(), reminderResponseLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, At: now})
if resumed, err := continueDelegationFromHumanResponse(r.Context(), workRepository, responseResult.Response, now); err == nil && resumed {
planningReply = "Принял. Продолжаю ту же задачу coding-agent без потери контекста."
}
} else if responseErr == nil && looksLikeUnsupportedContinuation(utterance.Content, responseResult) {
learning.recordFastPathDecline(r.Context(), fastPathDeclineLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, Reason: declineReasonForResponse(responseResult), At: now})
}
@@ -342,14 +408,19 @@ func main() {
ProjectProvenance: work.ProjectProvenanceRef(activeFocusProvenance),
Message: utterance.Content,
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
SecondarySpeechAct: work.SpeechActRef(semantic.Interpretation.SecondarySpeechAct),
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
NeedsCognition: semantic.NeedsCognition,
RequiresClarification: semantic.Interpretation.Clarification == interaction.ClarificationRequired,
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
helpPath := workdomain.DetermineHelpPath(planningInput)
if responseErr == nil && responseResult.Handled {
helpPath = work.HelpPath{}
}
if isCapabilityQuestion(utterance.Content) {
switch semantic.InformationRequestKind {
case interaction.InformationRequestCapabilityInventory:
kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID)
if err == nil && len(kinds) > 0 {
planningReply = "Сейчас у меня подключены реальные capabilities: " + humanCapabilityList(kinds) + ". Для рассуждений я использую отдельный Cognitive Gateway/Router, если локальный CANARY доступен. Возможности вроде отправки почты пока будут фиксироваться как capability gap, а не как выполненное действие."
@@ -357,14 +428,28 @@ func main() {
planningReply = "Я умею записывать диалог и намерения, работать с текущим контекстом, строить планы и обращаться к Cognitive Gateway при доступности локального CANARY. Реальные action-capabilities подключаются через registry; если capability нет, я фиксирую gap."
}
needsCognition = false
case interaction.InformationRequestCognitiveModelInventory:
candidates, inventoryErr := capabilityRepository.LoadCognitiveCandidates(r.Context(), capability.CapabilityID("creator-advisory"), session.PrincipalID)
planningReply = humanCognitiveModelInventory(candidates, inventoryErr)
needsCognition = false
case interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery:
health, healthErr := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID("cognitive-proposal"))
planningReply = humanCognitiveChannelReply(semantic.InformationRequestKind, semantic.Interpretation.SpeechAct, health, healthErr)
needsCognition = false
}
if helpPath.Reply != "" {
planningReply = helpPath.Reply
needsCognition = false
}
if planningReply == "" && needsCognition && cognitiveAttempted && !cognitiveAvailable {
planningReply = humanCognitiveDegradedReply(utterance.Content)
needsCognition = false
}
var capabilityStatus capability.CapabilityResolutionStatus
var capabilityKind capability.CapabilityKind
var capabilityGapID capability.CapabilityGapID
var delegationID work.DelegationID
var delegationState work.DelegationState
var reminder *continuity.ProspectiveItem
if helpPath.Kind == work.HelpPathDirectCapability && len(helpPath.SuggestedCapabilityKinds) > 0 {
capabilityKind = helpPath.SuggestedCapabilityKinds[0]
@@ -423,30 +508,252 @@ func main() {
planningReply = planning.Reply
workPlanned = true
needsCognition = false
if created, resolution, createErr := maybeCreateAgentDelegation(r.Context(), workRepository, capabilityRepository, session, conversation, intent, utterance, planning, now); createErr == nil && created != nil {
delegationID = created.Delegation.ID
delegationState = created.Delegation.State
capabilityKind = created.Delegation.CapabilityKind
capabilityStatus = resolution.Status
planningReply = "Понял задачу. Я собрал work item, выбрал coding-agent в CANARY-режиме и создал delegation. Буду вести её как отдельный run с checkpoint/resume, без потери исходного human intent."
} else if resolution.Gap != nil {
capabilityKind = resolution.Requirement.CapabilityKind
capabilityStatus = resolution.Status
capabilityGapID = resolution.Gap.ID
planningReply = planningReply + " Для coding delegation подходящий agent implementation сейчас недоступен, поэтому я сохранил план без ложного запуска."
}
}
response := interaction.ChatMessageResponse{
ConversationID: conversation.ID,
UtteranceID: utterance.ID,
OriginalIntentID: intent.ID,
SpeechAct: semantic.Interpretation.SpeechAct,
InterpretationState: utterance.InterpretationState,
NeedsCognition: needsCognition,
FallbackText: semantic.Reply,
ActiveProjectName: activeFocusName,
ProjectProvenance: activeFocusProvenance,
Clarification: semantic.Interpretation.Clarification,
WorkPlanned: workPlanned,
PlanningReply: planningReply,
CapabilityStatus: capabilityStatus,
CapabilityKind: capabilityKind,
CapabilityGapID: capabilityGapID,
Reminder: reminder,
ConversationID: conversation.ID,
UtteranceID: utterance.ID,
OriginalIntentID: intent.ID,
SpeechAct: semantic.Interpretation.SpeechAct,
InterpretationState: utterance.InterpretationState,
NeedsCognition: needsCognition,
FallbackText: semantic.Reply,
ActiveProjectName: activeFocusName,
ProjectProvenance: activeFocusProvenance,
Clarification: semantic.Interpretation.Clarification,
WorkPlanned: workPlanned,
PlanningReply: planningReply,
CapabilityStatus: capabilityStatus,
CapabilityKind: capabilityKind,
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
InformationRequestKind: semantic.InformationRequestKind,
InterpretationConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
CapabilityGapID: capabilityGapID,
DelegationID: string(delegationID),
DelegationState: string(delegationState),
Reminder: reminder,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(response)
})
mux.HandleFunc("GET /v1/creator/delegations/{id}", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
value, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id")))
if err != nil {
http.Error(w, "delegation not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
})
mux.HandleFunc("GET /v1/creator/delegations/{id}/latest-checkpoint", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
value, err := workRepository.LatestAgentCheckpoint(r.Context(), work.DelegationID(r.PathValue("id")))
if err != nil {
http.Error(w, "checkpoint not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
})
mux.HandleFunc("POST /v1/creator/delegations/{id}/runs/{run}/checkpoints", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request struct {
BlockerClass work.AgentBlockerClass
CompletedSummary string
RemainingSummary string
TestSummary string
BranchName string
CommitHash string
NextStep string
ArtifactRefs []work.EvidenceRef
RecordedAtUnixMillis int64
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid checkpoint", http.StatusBadRequest)
return
}
run, err := workRepository.LoadAgentRun(r.Context(), work.AgentRunID(r.PathValue("run")))
if err != nil || run.DelegationID != work.DelegationID(r.PathValue("id")) {
http.Error(w, "run not found", http.StatusNotFound)
return
}
updatedRun, checkpoint, err := workdomain.CheckpointRun(run, request.BlockerClass, request.CompletedSummary, request.RemainingSummary, request.TestSummary, request.BranchName, request.CommitHash, request.NextStep, request.ArtifactRefs, time.UnixMilli(request.RecordedAtUnixMillis))
if err != nil {
http.Error(w, "invalid checkpoint", http.StatusBadRequest)
return
}
if err := workRepository.SaveAgentCheckpoint(r.Context(), updatedRun, checkpoint); err != nil {
http.Error(w, "save checkpoint", http.StatusInternalServerError)
return
}
if request.BlockerClass == work.AgentBlockerHumanDecisionRequired {
delegation, err := workRepository.LoadDelegation(r.Context(), run.DelegationID)
if err == nil {
_, _ = attentionClient.Create(r.Context(), r.Header.Get("Authorization"), continuity.CreateAttentionRequest{
OwnerPrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"),
SourceKind: continuity.AttentionSourceAgentDelegation,
SourceExternalRef: delegationAttentionRef(delegation.ID),
Level: continuity.AttentionActionRequired,
Summary: "Нужен твой выбор по задаче coding-agent: " + firstNonEmpty(request.RemainingSummary, delegation.DesiredOutcome),
DueAtUnixMillis: checkpoint.RecordedAtUnixMillis,
CreatedAtUnixMillis: checkpoint.RecordedAtUnixMillis,
})
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(checkpoint)
})
mux.HandleFunc("POST /v1/creator/delegations/{id}/runs/{run}/results", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
Summary string
OutcomeRef string
VerificationSummary string
ArtifactRefs []work.EvidenceRef
RecordedAtUnixMillis int64
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid result", http.StatusBadRequest)
return
}
run, err := workRepository.LoadAgentRun(r.Context(), work.AgentRunID(r.PathValue("run")))
if err != nil || run.DelegationID != work.DelegationID(r.PathValue("id")) {
http.Error(w, "run not found", http.StatusNotFound)
return
}
updatedRun, result, err := workdomain.CompleteRun(run, request.Summary, request.OutcomeRef, request.VerificationSummary, request.ArtifactRefs, time.UnixMilli(request.RecordedAtUnixMillis))
if err != nil {
http.Error(w, "invalid result", http.StatusBadRequest)
return
}
if err := workRepository.SaveAgentResult(r.Context(), updatedRun, result); err != nil {
http.Error(w, "save result", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(result)
})
mux.HandleFunc("POST /v1/creator/delegations/{id}/resume", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
delegation, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id")))
if err != nil {
http.Error(w, "delegation not found", http.StatusNotFound)
return
}
run, err := workRepository.LoadAgentRun(r.Context(), delegation.LatestRunID)
if err != nil {
http.Error(w, "run not found", http.StatusNotFound)
return
}
checkpoint, err := workRepository.LatestAgentCheckpoint(r.Context(), delegation.ID)
if err != nil {
http.Error(w, "checkpoint not found", http.StatusNotFound)
return
}
updatedDelegation, resumedRun, err := workdomain.ResumeDelegationRun(delegation, run, checkpoint, time.Now())
if err != nil {
http.Error(w, "resume not allowed", http.StatusConflict)
return
}
if err := workRepository.SaveResumedRun(r.Context(), updatedDelegation, resumedRun); err != nil {
http.Error(w, "save resumed run", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(resumedRun)
})
mux.HandleFunc("POST /v1/creator/delegations/{id}/verify", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
Verified bool
Summary string
EvidenceRefs []work.EvidenceRef
RecordedAtUnixMillis int64
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.RecordedAtUnixMillis < 1 {
http.Error(w, "invalid verification", http.StatusBadRequest)
return
}
delegation, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id")))
if err != nil {
http.Error(w, "delegation not found", http.StatusNotFound)
return
}
run, err := workRepository.LoadAgentRun(r.Context(), delegation.LatestRunID)
if err != nil {
http.Error(w, "run not found", http.StatusNotFound)
return
}
result, err := workRepository.LatestAgentResult(r.Context(), delegation.ID)
if err != nil {
http.Error(w, "result not found", http.StatusNotFound)
return
}
updatedDelegation, verification, err := workdomain.VerifyDelegationResult(delegation, run, result, request.Verified, request.Summary, request.EvidenceRefs, time.UnixMilli(request.RecordedAtUnixMillis))
if err != nil {
http.Error(w, "invalid verification", http.StatusBadRequest)
return
}
if err := workRepository.SaveDelegationVerification(r.Context(), updatedDelegation, verification); err != nil {
http.Error(w, "save verification", http.StatusInternalServerError)
return
}
runCount, _ := workRepository.CountAgentRuns(r.Context(), delegation.ID)
learning.recordDelegationOutcome(r.Context(), delegationLearningInput{
Intent: interaction.OriginalHumanIntent{ID: interaction.OriginalHumanIntentID(delegation.SourceIntentID), Text: delegation.DesiredOutcome},
Utterance: interaction.HumanUtterance{ID: interaction.HumanUtteranceID(delegation.SourceUtteranceID), Content: delegation.DesiredOutcome},
DelegationID: string(delegation.ID),
RunID: string(run.ID),
ImplementationID: run.ImplementationID,
OutcomeRef: result.OutcomeRef,
VerificationRefs: evidenceRefsToEpistemics(request.EvidenceRefs),
Verified: request.Verified,
ExecutionWindows: runCount,
HumanBlocked: false,
At: time.UnixMilli(request.RecordedAtUnixMillis),
})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(verification)
})
mux.HandleFunc("GET /v1/principal-bindings/{actor}/{principal}", func(w http.ResponseWriter, r *http.Request) {
binding, err := identityRepository.LoadBinding(r.Context(), identity.ActorID(r.PathValue("actor")), identity.PrincipalID(r.PathValue("principal")))
if err != nil {
@@ -562,6 +869,7 @@ func main() {
return
}
if err := capabilityRepository.SaveCognitiveDiscovery(r.Context(), registration); err != nil {
log.Printf("cognitive discovery persistence failed: %v", err)
http.Error(w, "save cognitive discovery", http.StatusInternalServerError)
return
}
@@ -622,6 +930,45 @@ func main() {
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/creator/cognitive-activate", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request capability.CognitiveActivationPromotionRequest
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid cognitive activation", http.StatusBadRequest)
return
}
request.RequestedByPrincipalID = session.PrincipalID
request.CapabilityID = "creator-advisory"
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
var current capability.ResolvedCognitiveBinding
for _, candidate := range candidates {
if candidate.Binding.ID == request.BindingID && candidate.Binding.Revision == request.ExpectedBindingRevision {
current = candidate
break
}
}
promotion, err := capabilitydomain.BuildActivationPromotion(request, current)
if err != nil {
http.Error(w, "cognitive activation precondition failed", http.StatusConflict)
return
}
if err = capabilityRepository.SaveCognitiveActivationPromotion(r.Context(), promotion); err != nil {
http.Error(w, "cognitive activation conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/cognitive-registry/performance-evidence", func(w http.ResponseWriter, r *http.Request) {
var value capability.PerformanceEvidence
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
@@ -840,9 +1187,127 @@ func envOr(name, fallback string) string {
return fallback
}
func isCapabilityQuestion(message string) bool {
lower := strings.ToLower(message)
return strings.Contains(lower, "что ты умеешь") || strings.Contains(lower, "что умеешь") || strings.Contains(lower, "какие возможности")
func envInt(name string, fallback int) int {
value, err := strconv.Atoi(os.Getenv(name))
if err != nil || value < 1 {
return fallback
}
return value
}
func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
if inventoryErr != nil {
return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат."
}
if len(candidates) == 0 {
return "В реестре пока нет подтверждённых подключённых моделей. Встроенный локальный канал Домового доступен всегда; внешние модели добавляются через настройки подключения. Ключи и пароли в чат не выводятся."
}
lines := make([]string, 0, len(candidates)+1)
lines = append(lines, "Подключённые модели из реестра:")
for _, candidate := range candidates {
modelName := string(candidate.Model.ProviderModelID)
if modelName == "" {
modelName = "имя модели не подтверждено"
}
status := cognitiveInventoryStatus(candidate)
pricing := cognitiveInventoryPricing(candidate.Provider)
details := []string{status, "тип " + string(candidate.Provider.Kind), "политика " + string(candidate.Binding.DataPolicy)}
if pricing != "" {
details = append(details, pricing)
}
if candidate.Provider.AuthMode != "" && candidate.Provider.AuthMode != capability.AuthNone {
details = append(details, "доступ "+humanAuthMode(candidate.Provider.AuthMode))
}
lines = append(lines, "- "+modelName+" ("+strings.Join(details, "; ")+")")
}
lines = append(lines, "Секреты, ключи и пароли намеренно не показываются.")
return strings.Join(lines, "\n")
}
func cognitiveInventoryStatus(candidate capability.ResolvedCognitiveBinding) string {
if candidate.Provider.Health == capability.HealthAvailable && candidate.Model.Health == capability.HealthAvailable {
if candidate.Binding.Activation == capability.ActivationActive {
return "активна"
}
return "доступна, не активирована для автоматического выбора"
}
if candidate.Provider.Health == capability.HealthDegraded || candidate.Model.Health == capability.HealthDegraded {
return "работает с ограничениями"
}
return "недоступна или не подтверждена"
}
func cognitiveInventoryPricing(provider capability.ProviderInstance) string {
switch provider.BillingMode {
case capability.BillingFree:
return "бесплатная"
case capability.BillingPerInput:
return "платная: стоимость входа указана в реестре"
case capability.BillingPerOutput:
return "платная: стоимость выхода указана в реестре"
case capability.BillingPerRequest:
return "платная: стоимость запроса указана в реестре"
default:
return "стоимость не подтверждена"
}
}
func humanAuthMode(mode capability.AuthMode) string {
switch mode {
case capability.AuthKey:
return "API-ключ"
case capability.AuthBearer:
return "Bearer-токен"
case capability.AuthUsernamePassword:
return "логин и пароль"
case capability.AuthOAuth:
return "OAuth"
case capability.AuthSecretFile:
return "защищённый файл секрета"
default:
return "защищённый способ"
}
}
func humanCognitiveChannelReply(kind interaction.InformationRequestKind, act interaction.SpeechAct, health capability.Health, healthErr error) string {
recoveryRequested := kind == interaction.InformationRequestCognitiveRecovery || act == interaction.SpeechActRequestToInvestigate || act == interaction.SpeechActDirective
if healthErr != nil {
if recoveryRequested {
return "Я не вижу подтверждённого состояния локального канала. Домовой всё равно рядом: опишите, что изменилось, и я помогу подготовить аварийную настройку."
}
return "Я не вижу подтверждённого состояния локального канала, но Домовой работает. Можно продолжать разговор и проверить канал ещё раз."
}
var observation string
switch health.State {
case capability.HealthAvailable:
observation = "Основной локальный канал сейчас доступен."
case capability.HealthDegraded:
observation = "Основной локальный канал сейчас работает с ограничениями."
case capability.HealthUnavailable:
observation = "Основной локальный канал сейчас недоступен."
default:
observation = "Состояние основного локального канала пока не подтверждено."
}
if recoveryRequested {
if health.State == capability.HealthUnavailable {
return observation + " Домовой жив: начну с проверки встроенного маршрута, затем зафиксирую варианты аварийного восстановления без передачи секретов."
}
return observation + " Я не меняю модель прямо из разговора: сначала покажу наблюдаемое состояние и подготовлю следующий проверяемый шаг восстановления."
}
if health.State == capability.HealthUnavailable {
if recoveryRequested {
return observation + " Домовой не остановился: начну с проверки встроенного маршрута, затем зафиксирую доступные варианты восстановления без передачи секретов."
}
return observation + " Но Домовой не остановился: продолжаю принимать сообщения и помогу восстановить локальный канал."
}
return observation + " Это наблюдение о готовности capability, а не прямой доступ к модели."
}
func humanCognitiveDegradedReply(message string) string {
if strings.TrimSpace(message) == "" {
return "Основной канал мышления сейчас недоступен. Я рядом и продолжаю работать в локальном аварийном режиме."
}
return "Основной канал мышления сейчас недоступен. Домовой жив: сообщение принято, а я могу помочь разобраться с локальным каналом и настройкой восстановления."
}
func humanCapabilityList(kinds []capability.CapabilityKind) string {
@@ -859,3 +1324,66 @@ func humanCapabilityList(kinds []capability.CapabilityKind) string {
}
return strings.Join(labels, ", ")
}
func toCapabilityKinds(values []string) []capability.CapabilityKind {
result := make([]capability.CapabilityKind, 0, len(values))
for _, value := range values {
kind := capability.CapabilityKind(strings.TrimSpace(value))
if kind == "" {
continue
}
result = append(result, kind)
}
return result
}
func delegationAttentionRef(id work.DelegationID) string {
return "work:delegation:" + string(id)
}
func continueDelegationFromHumanResponse(ctx context.Context, repository *postgres.WorkRepository, response continuity.HumanResponse, now time.Time) (bool, error) {
if response.Operation != continuity.ResponseContinue || !strings.HasPrefix(response.TargetExternalRef, "work:delegation:") {
return false, nil
}
delegationID := work.DelegationID(strings.TrimPrefix(response.TargetExternalRef, "work:delegation:"))
delegation, err := repository.LoadDelegation(ctx, delegationID)
if err != nil {
return false, err
}
run, err := repository.LoadAgentRun(ctx, delegation.LatestRunID)
if err != nil {
return false, err
}
checkpoint, err := repository.LatestAgentCheckpoint(ctx, delegationID)
if err != nil {
return false, err
}
updatedDelegation, resumedRun, err := workdomain.ResumeDelegationRun(delegation, run, checkpoint, now)
if err != nil {
return false, err
}
if err := repository.SaveResumedRun(ctx, updatedDelegation, resumedRun); err != nil {
return false, err
}
return true, nil
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func evidenceRefsToEpistemics(values []work.EvidenceRef) []epistemics.EvidenceRef {
result := make([]epistemics.EvidenceRef, 0, len(values))
for _, value := range values {
if strings.TrimSpace(string(value)) == "" {
continue
}
result = append(result, epistemics.EvidenceRef(value))
}
return result
}
+56
View File
@@ -9,6 +9,10 @@ import (
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
@@ -23,6 +27,58 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) {
}
}
func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil)
if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") {
t.Fatalf("unexpected safe reply: %q", reply)
}
}
func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveRecovery, interaction.SpeechActDirective, capability.Health{State: capability.HealthUnavailable}, nil)
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "аварийн") {
t.Fatalf("unexpected configuration reply: %q", reply)
}
}
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "принято") {
t.Fatalf("unexpected degraded reply: %q", reply)
}
}
func TestModelInventoryQuestionUsesReadOnlyFastPath(t *testing.T) {
for _, message := range []string{"какие модели доступны?", "напиши список настроек ИИ", "какие есть подключения ИИ"} {
result, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: message}, nil, nil, time.Now())
if err != nil {
t.Fatalf("interpretation failed for %q: %v", message, err)
}
if result.InformationRequestKind != interaction.InformationRequestCognitiveModelInventory {
t.Fatalf("semantic inventory request %q was not recognized: kind=%q act=%q hints=%v", message, result.InformationRequestKind, result.Interpretation.SpeechAct, result.CapabilityHints)
}
}
ordinary, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: "настрой локальную модель"}, nil, nil, time.Now())
if err != nil {
t.Fatal(err)
}
if ordinary.InformationRequestKind == interaction.InformationRequestCognitiveModelInventory {
t.Fatal("configuration mutation must not use read-only model inventory path")
}
}
func TestHumanCognitiveModelInventoryDoesNotExposeSecrets(t *testing.T) {
cost := int64(25)
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{{
Binding: capability.CognitiveBinding{DataPolicy: capability.DataPolicyExternalMessageOnly, Activation: capability.ActivationActive},
Provider: capability.ProviderInstance{Kind: "openai-compatible-chat-v1", CredentialRef: "secret-ref", AuthMode: capability.AuthKey, BillingMode: capability.BillingPerRequest, RequestCostMicros: &cost, Health: capability.HealthAvailable},
Model: capability.ModelDescriptor{ProviderModelID: "example-model", Health: capability.HealthAvailable},
}}, nil)
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "API-ключ") {
t.Fatalf("unsafe or incomplete inventory: %q", text)
}
}
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
+58 -2
View File
@@ -15,6 +15,8 @@ import (
)
func TestChatPageServedWithoutInternalIDs(t *testing.T) {
t.Setenv("ASSISTANT_BUILD_REVISION", "0123456789abcdef")
t.Setenv("ASSISTANT_BUILD_TIME", "2026-08-22T03:04:05Z")
handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive")
if err != nil {
t.Fatal(err)
@@ -25,11 +27,62 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
t.Fatalf("expected /chat status 200, got %d", recorder.Code)
}
body := recorder.Body.String()
for _, forbidden := range []string{"provider_id", "model_id", "binding_id", "workspace-id", "session-id"} {
for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "build-meta", "версия ", "обновлено ", "datetime=\""} {
if !strings.Contains(body, required) {
t.Fatalf("/chat is missing human status marker %q", required)
}
}
for _, forbidden := range []string{"provider_id", "model_id", "binding_id", "workspace-id", "session-id", "__ASSISTANT_BUILD_REVISION__", "__ASSISTANT_BUILD_TIME__", "__ASSISTANT_BUILD_DATETIME__"} {
if strings.Contains(body, forbidden) {
t.Fatalf("/chat leaks internal field %q", forbidden)
}
}
if !strings.Contains(body, "версия 0123456789ab") || !strings.Contains(body, "обновлено 22.08.2026 03:04 UTC") || !strings.Contains(body, "datetime=\"2026-08-22T03:04:05Z\"") {
t.Fatalf("/chat does not render build metadata: %q", body)
}
for _, path := range []string{"/", "/chat.html"} {
alternate := httptest.NewRecorder()
handler.ServeHTTP(alternate, httptest.NewRequest(http.MethodGet, path, nil))
if alternate.Code != http.StatusOK || strings.Contains(alternate.Body.String(), "__ASSISTANT_BUILD_REVISION__") {
t.Fatalf("%s did not render build metadata: status=%d", path, alternate.Code)
}
}
}
func TestChatScriptExplainsSessionAndRuntimeFailures(t *testing.T) {
body, err := staticFiles.ReadFile("static/assets/chat.js")
if err != nil {
t.Fatal(err)
}
script := string(body)
for _, required := range []string{"class ChatApiError", "Сессия устарела", "основной runtime сейчас недоступен", "Проверьте текст и повторите попытку"} {
if !strings.Contains(script, required) {
t.Fatalf("chat script is missing recovery message %q", required)
}
}
}
func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) {
handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive")
if err != nil {
t.Fatal(err)
}
for _, test := range []struct {
path string
must string
mustNot string
}{
{path: "/", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"},
{path: "/admin", must: "Чат с Домовым", mustNot: "Напишите Домовому обычное сообщение"},
{path: "/chat", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"},
} {
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, test.path, nil))
body := recorder.Body.String()
if recorder.Code != http.StatusOK || !strings.Contains(body, test.must) || strings.Contains(body, test.mustNot) {
t.Fatalf("%s response=%d contains expected=%t forbidden=%t", test.path, recorder.Code, strings.Contains(body, test.must), strings.Contains(body, test.mustNot))
}
}
}
func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
@@ -47,7 +100,7 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
t.Fatalf("unexpected core chat request: %#v", request)
}
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, InterpretationState: interaction.InterpretationInterpreted, NeedsCognition: true, FallbackText: "fallback"})
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, InterpretationState: interaction.InterpretationInterpreted, NeedsCognition: true, FallbackText: "fallback", InformationRequestKind: interaction.InformationRequestCognitiveChannelStatus})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
@@ -93,6 +146,9 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
if cognitiveRequest.ActorID != "actor-1" || cognitiveRequest.PrincipalID != "principal-1" || cognitiveRequest.ScopeID != "domovoy-global-scope" || cognitiveRequest.Message != "Кто ты?" {
t.Fatalf("unexpected trusted gateway request: %#v", cognitiveRequest)
}
if cognitiveRequest.Meaning.InformationRequestKind != interaction.InformationRequestCognitiveChannelStatus {
t.Fatalf("information intent was not preserved: %#v", cognitiveRequest.Meaning)
}
}
func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) {
+1
View File
@@ -15,6 +15,7 @@ func main() {
envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"),
&http.Client{Timeout: 10 * time.Second},
envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"),
envOr("AGENT_WORKER_URL", "http://agent-worker:8090"),
)
if err != nil {
log.Fatal(err)
+213 -27
View File
@@ -1,6 +1,7 @@
package main
import (
"bytes"
"embed"
"encoding/json"
"fmt"
@@ -8,7 +9,9 @@ import (
"io/fs"
"net/http"
"net/url"
"os"
"strings"
"time"
"unicode/utf8"
"git.cin.su/m/Assistant/contracts/capability"
@@ -30,12 +33,19 @@ const (
var staticFiles embed.FS
type gateway struct {
core *url.URL
continuity *url.URL
commit *url.URL
cognitive *url.URL
client *http.Client
static http.Handler
core *url.URL
continuity *url.URL
commit *url.URL
cognitive *url.URL
agentWorker *url.URL
client *http.Client
static http.Handler
build buildMetadata
}
type buildMetadata struct {
revision string
updated time.Time
}
type routeTarget uint8
@@ -67,6 +77,14 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, c
if err != nil {
return nil, err
}
agentWorkerURL := "http://agent-worker:8090"
if len(cognitiveURLs) > 1 {
agentWorkerURL = cognitiveURLs[1]
}
agentWorker, err := parseRuntimeURL(agentWorkerURL, "Agent worker")
if err != nil {
return nil, err
}
assets, err := fs.Sub(staticFiles, "static")
if err != nil {
return nil, fmt.Errorf("load embedded console: %w", err)
@@ -74,7 +92,20 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, c
if client == nil {
client = http.DefaultClient
}
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets))}), nil
updated := time.Now().UTC()
if raw := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_TIME")); raw != "" {
if parsed, parseErr := time.Parse(time.RFC3339, raw); parseErr == nil {
updated = parsed.UTC()
}
}
revision := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_REVISION"))
if revision == "" {
revision = "development"
}
if len(revision) > 12 {
revision = revision[:12]
}
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, agentWorker: agentWorker, client: client, static: http.FileServer(http.FS(assets)), build: buildMetadata{revision: revision, updated: updated}}), nil
}
func parseRuntimeURL(rawURL, name string) (*url.URL, error) {
@@ -104,6 +135,14 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g.handleChatMessage(w, r)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/api/fleet/readiness" {
g.handleFleetReadiness(w, r)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/api/agent-worker/status" {
g.handleAgentWorkerStatus(w, r)
return
}
g.forwardAPI(w, r)
return
}
@@ -111,17 +150,31 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if r.URL.Path == "/" {
w.Header().Set("Cache-Control", "no-store")
if r.URL.Path == "/" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
g.serveEmbeddedHTML(w, "chat.html")
return
}
if r.URL.Path == "/chat" {
r = r.Clone(r.Context())
r.URL.Path = "/chat.html"
w.Header().Set("Cache-Control", "no-store")
if r.URL.Path == "/admin" {
g.serveEmbeddedHTML(w, "index.html")
return
}
g.static.ServeHTTP(w, r)
}
func (g *gateway) serveEmbeddedHTML(w http.ResponseWriter, name string) {
body, err := staticFiles.ReadFile("static/" + name)
if err != nil {
http.Error(w, "page unavailable", http.StatusNotFound)
return
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_REVISION__"), []byte(g.build.revision))
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_TIME__"), []byte(g.build.updated.Format("02.01.2006 15:04 UTC")))
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_DATETIME__"), []byte(g.build.updated.Format(time.RFC3339)))
_, _ = w.Write(body)
}
type browserAdvisoryRequest struct {
Message string `json:"message"`
SessionID string `json:"session_id"`
@@ -143,18 +196,23 @@ type browserChatRequest struct {
}
type browserChatResponse struct {
ConversationID string `json:"conversation_id"`
UtteranceID string `json:"utterance_id"`
SpeechAct string `json:"speech_act"`
ReplyText string `json:"reply_text"`
Cognition string `json:"cognition"`
ActiveFocus string `json:"active_focus"`
ProjectProvenance string `json:"project_provenance"`
Clarification string `json:"clarification"`
WorkPlanned bool `json:"work_planned"`
CapabilityStatus string `json:"capability_status"`
CapabilityKind string `json:"capability_kind"`
ReminderScheduled bool `json:"reminder_scheduled"`
ConversationID string `json:"conversation_id"`
UtteranceID string `json:"utterance_id"`
SpeechAct string `json:"speech_act"`
ReplyText string `json:"reply_text"`
Cognition string `json:"cognition"`
ActiveFocus string `json:"active_focus"`
ProjectProvenance string `json:"project_provenance"`
Clarification string `json:"clarification"`
WorkPlanned bool `json:"work_planned"`
CapabilityStatus string `json:"capability_status"`
CapabilityKind string `json:"capability_kind"`
CapabilityHints []capability.CapabilityKind `json:"capability_hints,omitempty"`
RequestedOutcome string `json:"requested_outcome,omitempty"`
InterpretationConfidenceMillis int64 `json:"interpretation_confidence_millis,omitempty"`
DelegationID string `json:"delegation_id"`
DelegationState string `json:"delegation_state"`
ReminderScheduled bool `json:"reminder_scheduled"`
}
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
@@ -207,6 +265,82 @@ func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(policy)
}
func (g *gateway) handleFleetReadiness(w http.ResponseWriter, r *http.Request) {
authorization := r.Header.Get("Authorization")
if authorization == "" {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
var session identity.CreatorSession
status, err := g.getTrustedJSON(r, g.core, "/v1/creator/sessions/current", "", authorization, &session)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusBadGateway)
return
}
if status != http.StatusOK || session.ActorID == "" || session.PrincipalID == "" {
if status == 0 {
status = http.StatusUnauthorized
}
http.Error(w, "creator session denied", status)
return
}
target := *g.cognitive
target.Path = "/v1/fleet/readiness"
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target.String(), nil)
if err != nil {
http.Error(w, "fleet readiness unavailable", http.StatusBadGateway)
return
}
response, err := g.client.Do(request)
if err != nil {
http.Error(w, "fleet readiness unavailable", http.StatusBadGateway)
return
}
defer response.Body.Close()
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(response.StatusCode)
_, _ = io.Copy(w, io.LimitReader(response.Body, maxResponseBytes))
}
func (g *gateway) handleAgentWorkerStatus(w http.ResponseWriter, r *http.Request) {
authorization := r.Header.Get("Authorization")
if authorization == "" {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
var session identity.CreatorSession
status, err := g.getTrustedJSON(r, g.core, "/v1/creator/sessions/current", "", authorization, &session)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusBadGateway)
return
}
if status != http.StatusOK || session.ActorID == "" || session.PrincipalID == "" {
if status == 0 {
status = http.StatusUnauthorized
}
http.Error(w, "creator session denied", status)
return
}
target := *g.agentWorker
target.Path = "/status"
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target.String(), nil)
if err != nil {
http.Error(w, "agent worker status unavailable", http.StatusBadGateway)
return
}
response, err := g.client.Do(request)
if err != nil {
http.Error(w, "agent worker status unavailable", http.StatusBadGateway)
return
}
defer response.Body.Close()
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(response.StatusCode)
_, _ = io.Copy(w, io.LimitReader(response.Body, maxResponseBytes))
}
func (g *gateway) handleAdvisory(w http.ResponseWriter, r *http.Request) {
var browserRequest browserAdvisoryRequest
if err := decodeStrictJSON(r.Body, maxAdvisoryRequestBytes, &browserRequest); err != nil || browserRequest.Message == "" || len(browserRequest.Message) > 16*1024 || !validID(browserRequest.SessionID) || !validID(browserRequest.WorkspaceID) {
@@ -337,7 +471,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
cognition = "unavailable"
requestID, idErr := ids.New()
if idErr == nil {
trusted := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: "domovoy-global-scope", Message: browserRequest.Message, Context: []cognitive.ContextExcerpt{}, Locale: "ru-RU", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, EgressAcknowledged: false}
trusted := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: "domovoy-global-scope", Message: browserRequest.Message, Context: localChatContext(saved), Locale: "ru-RU", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, EgressAcknowledged: false, Meaning: cognitive.TaskMeaning{SpeechAct: string(saved.SpeechAct), CapabilityKind: saved.CapabilityKind, CapabilityHints: saved.CapabilityHints, RequestedOutcome: saved.RequestedOutcome, InformationRequestKind: saved.InformationRequestKind, ConfidenceMillis: saved.InterpretationConfidenceMillis}}
payload, err = json.Marshal(trusted)
if err == nil {
var advisory cognitive.GatewayAdvisoryResponse
@@ -353,7 +487,49 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification), WorkPlanned: saved.WorkPlanned, CapabilityStatus: string(saved.CapabilityStatus), CapabilityKind: string(saved.CapabilityKind), ReminderScheduled: saved.Reminder != nil})
_ = json.NewEncoder(w).Encode(browserChatResponse{
ConversationID: string(saved.ConversationID),
UtteranceID: string(saved.UtteranceID),
SpeechAct: string(saved.SpeechAct),
ReplyText: reply,
Cognition: cognition,
ActiveFocus: saved.ActiveProjectName,
ProjectProvenance: string(saved.ProjectProvenance),
Clarification: string(saved.Clarification),
WorkPlanned: saved.WorkPlanned,
CapabilityStatus: string(saved.CapabilityStatus),
CapabilityKind: string(saved.CapabilityKind),
CapabilityHints: saved.CapabilityHints,
RequestedOutcome: saved.RequestedOutcome,
InterpretationConfidenceMillis: saved.InterpretationConfidenceMillis,
DelegationID: saved.DelegationID,
DelegationState: saved.DelegationState,
ReminderScheduled: saved.Reminder != nil,
})
}
func localChatContext(saved interaction.ChatMessageResponse) []cognitive.ContextExcerpt {
if strings.TrimSpace(saved.ActiveProjectName) == "" {
return []cognitive.ContextExcerpt{}
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{
DataPolicy: capability.DataPolicyLocalOnly,
MaxBytes: 2048,
Candidates: []cognitive.ContextCandidate{{
Excerpt: cognitive.ContextExcerpt{
ID: cognitive.ContextExcerptID("active-project-focus"),
SourceKind: "situation-active-project",
SourceID: strings.TrimSpace(saved.ActiveProjectName),
Sensitivity: cognitive.SensitivityPrivateAllowed,
Text: "Текущий проект: " + strings.TrimSpace(saved.ActiveProjectName),
},
Relevance: 100,
}},
})
if err != nil {
return []cognitive.ContextExcerpt{}
}
return bundle.Excerpts
}
func asksAttention(message string) bool {
@@ -538,6 +714,16 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
}
case method == http.MethodGet && path == "/api/learning/recent":
return targetCore, "/v1/creator/learning/recent", true
case method == http.MethodGet && strings.HasPrefix(path, "/api/delegations/") && strings.HasSuffix(path, "/latest-checkpoint"):
id := strings.TrimSuffix(strings.TrimPrefix(path, "/api/delegations/"), "/latest-checkpoint")
if validID(id) {
return targetCore, "/v1/creator/delegations/" + id + "/latest-checkpoint", true
}
case method == http.MethodGet && strings.HasPrefix(path, "/api/delegations/"):
id := strings.TrimPrefix(path, "/api/delegations/")
if validID(id) {
return targetCore, "/v1/creator/delegations/" + id, true
}
case method == http.MethodGet && strings.HasPrefix(path, "/api/prospective-items/"):
id := strings.TrimPrefix(path, "/api/prospective-items/")
if validID(id) {
+109 -1
View File
@@ -10,8 +10,105 @@ import (
"testing"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/interaction"
)
func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) {
context := localChatContext(interaction.ChatMessageResponse{ActiveProjectName: "Домовой"})
if len(context) != 1 || context[0].SourceKind != "situation-active-project" || context[0].Text != "Текущий проект: Домовой" {
t.Fatalf("unexpected local context: %#v", context)
}
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 0 {
t.Fatalf("expected empty context without active project: %#v", empty)
}
}
func TestCreatorRouteAllowsOnlyBoundedDelegationReads(t *testing.T) {
tests := []struct {
path string
want string
}{
{"/api/delegations/delegation-1", "/v1/creator/delegations/delegation-1"},
{"/api/delegations/delegation-1/latest-checkpoint", "/v1/creator/delegations/delegation-1/latest-checkpoint"},
}
for _, test := range tests {
_, got, ok := creatorRoute(http.MethodGet, test.path)
if !ok || got != test.want {
t.Fatalf("creatorRoute(%q) = %q, %t; want %q, true", test.path, got, ok, test.want)
}
}
if _, _, ok := creatorRoute(http.MethodGet, "/api/delegations/../secrets"); ok {
t.Fatal("path traversal-like delegation route must be denied")
}
}
func TestAgentWorkerStatusProxyRequiresCreatorSession(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer bounded-session" {
t.Fatalf("unexpected session request path=%s auth=%q", r.URL.Path, r.Header.Get("Authorization"))
}
_, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`)
}))
defer core.Close()
worker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/status" {
t.Fatalf("unexpected worker path %s", r.URL.Path)
}
_, _ = io.WriteString(w, `{"runtime":"agent-worker","executor_configured":false,"mode":"recovery-only","ready":false,"reason":"bridge missing"}`)
}))
defer worker.Close()
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), "http://cognitive.invalid", worker.URL)
if err != nil {
t.Fatal(err)
}
denied := httptest.NewRecorder()
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodGet, "/api/agent-worker/status", nil))
if denied.Code != http.StatusUnauthorized {
t.Fatalf("unauthenticated status=%d", denied.Code)
}
request := httptest.NewRequest(http.MethodGet, "/api/agent-worker/status", nil)
request.Header.Set("Authorization", "Bearer bounded-session")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"mode":"recovery-only"`) || response.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("response=%d cache=%q body=%s", response.Code, response.Header().Get("Cache-Control"), response.Body.String())
}
}
func TestFleetReadinessRequiresCreatorSessionAndProxiesCognitiveState(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer bounded-session" {
t.Fatalf("unexpected session request path=%s auth=%q", r.URL.Path, r.Header.Get("Authorization"))
}
_, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`)
}))
defer core.Close()
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/fleet/readiness" {
t.Fatalf("unexpected cognitive path %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"readiness":{"members":[{"provider_instance_id":"lifeboat","model_descriptor_id":"qwen","tier":"LIFEBOAT","ready":true,"reason":"AVAILABLE"}],"ready_tiers":["LIFEBOAT"],"fallback_tier":"LIFEBOAT"},"preferred_tier":"HEAVY","selected_tier":"LIFEBOAT"}`)
}))
defer cognitiveRuntime.Close()
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), cognitiveRuntime.URL)
if err != nil {
t.Fatal(err)
}
denied := httptest.NewRecorder()
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodGet, "/api/fleet/readiness", nil))
if denied.Code != http.StatusUnauthorized {
t.Fatalf("unauthenticated status=%d", denied.Code)
}
request := httptest.NewRequest(http.MethodGet, "/api/fleet/readiness", nil)
request.Header.Set("Authorization", "Bearer bounded-session")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"selected_tier":"LIFEBOAT"`) || response.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("response=%d cache=%q body=%s", response.Code, response.Header().Get("Cache-Control"), response.Body.String())
}
}
const legacyExternalMode = "legacy-external-test-mode"
func newHandlerWithAdvisoryMode(coreURL, continuityURL, commitURL string, client *http.Client, mode string, cognitiveURLs ...string) (http.Handler, error) {
@@ -229,10 +326,21 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodGet, "/", nil)
request := httptest.NewRequest(http.MethodGet, "/admin", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") ||
!strings.Contains(response.Body.String(), "context-preset") ||
!strings.Contains(response.Body.String(), "context-state") ||
!strings.Contains(response.Body.String(), "copy-context") ||
!strings.Contains(response.Body.String(), "К содержимому") ||
!strings.Contains(response.Body.String(), "aria-current=\"location\"") ||
!strings.Contains(response.Body.String(), "section-nav-link") ||
!strings.Contains(response.Body.String(), "data-page=\"overview\"") ||
!strings.Contains(response.Body.String(), "data-page=\"diagnostics\"") ||
!strings.Contains(response.Body.String(), "data-page=\"operations\"") ||
!strings.Contains(response.Body.String(), "data-page=\"learning\"") ||
!strings.Contains(response.Body.String(), "data-page=\"settings\"") ||
!strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") ||
!strings.Contains(response.Body.String(), "action-history") || !strings.Contains(response.Body.String(), "Журнал действий") ||
!strings.Contains(response.Body.String(), "advisory-form") || !strings.Contains(response.Body.String(), "Спросить Домового") ||
+60 -11
View File
@@ -29,26 +29,60 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.login-form button, .composer button { border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
.login-form button { width: 100%; height: 50px; margin-top: 18px; }
#login-error { min-height: 20px; color: var(--bad); font-size: 13px; }
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr); }
.chat-header { display: flex; align-items: center; gap: 18px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .96); }
.chat-view { height: 100vh; min-height: 0; display: grid; grid-template-rows: 64px minmax(0, 1fr); overflow: hidden; }
.chat-header { min-width: 0; display: flex; align-items: center; gap: 14px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .98); }
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
.chat-header span { color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
#active-focus { margin-left: auto; color: var(--good); }
#active-focus { color: var(--good); }
.cognitive-status { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.chat-header .cognitive-status { width: auto; height: auto; padding: 0; border: 0; background: transparent; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.chat-header .cognitive-status { max-width: min(30vw, 320px); margin-left: auto; overflow: hidden; text-overflow: ellipsis; }
.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--signal); }
.cognitive-status[data-state="available"] { color: var(--good); }
.cognitive-status[data-state="available"]::before { background: var(--good); }
.cognitive-status[data-state="unavailable"] { color: var(--bad); }
.cognitive-status[data-state="unavailable"]::before { background: var(--bad); }
.cognitive-console-link { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
.build-meta { display: grid; gap: 3px; flex: 0 0 auto; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .08em; text-align: right; text-transform: uppercase; white-space: nowrap; }
.build-meta time { color: #68776d; font-size: 8px; }
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
.human-nav { display: flex; align-items: center; gap: 8px; }
.human-nav { display: grid; gap: 8px; }
.nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.chat-left .nav-tab { height: auto; min-height: 58px; padding: 10px 12px; display: grid; grid-template-columns: 28px 1fr; gap: 2px 8px; align-items: center; text-align: left; }
.chat-left .nav-tab b { grid-row: span 2; color: var(--signal); font: 11px/1 var(--mono); }
.chat-left .nav-tab span { color: var(--ink); font: 600 13px/1 var(--sans); text-transform: none; letter-spacing: 0; }
.chat-left .nav-tab small { color: var(--muted); font: 10px/1 var(--mono); text-transform: none; letter-spacing: 0; }
.nav-tab.active { color: #11150f; border-color: var(--signal); background: var(--signal); }
.operator-link { color: var(--good); }
.surface-panel { min-height: 0; position: relative; }
.active-panel { display: grid; min-height: 0; }
.messages { width: min(100%, 920px); margin: 0 auto; padding: 28px 18px 120px; display: flex; flex-direction: column; gap: 14px; overflow: auto; }
.chat-header > .operator-link { flex: 0 0 auto; }
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(250px, 320px); }
.chat-side { min-width: 0; min-height: 0; padding: 28px 20px; overflow-y: auto; background: #0e1512; }
.chat-left { border-right: 1px solid var(--line); }
.chat-right { border-left: 1px solid var(--line); }
.side-intro h1, .side-intro h2 { margin: 12px 0; font: 400 clamp(28px, 2.5vw, 40px)/.95 var(--serif); letter-spacing: -.03em; }
.side-intro p:not(.side-index) { color: var(--muted); line-height: 1.5; }
.side-index, .side-focus span, .side-note span, .ai-info-card span { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.side-focus, .side-note, .ai-info-card { margin-top: 28px; padding: 14px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.side-focus strong, .side-note strong, .ai-info-card strong { display: block; margin-top: 9px; color: var(--ink); font: 600 14px/1.3 var(--sans); }
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 9px; color: var(--muted); line-height: 1.45; }
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; }
.surface-panel { min-height: 0; position: relative; overflow-y: auto; }
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; }
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: 28px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
.message.user { align-self: flex-end; background: #151e1a; }
.message.assistant { align-self: flex-start; background: #0f1613; }
.message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; }
.composer { position: sticky; bottom: 0; width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 20px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
.composer button { height: 48px; align-self: end; }
.composer button:disabled { cursor: wait; filter: saturate(.3); }
.conversation-starters { width: min(100%, 920px); margin: -4px auto 0; padding: 0 18px 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.conversation-starters button { min-height: 32px; padding: 0 10px; border: 1px solid var(--line); color: var(--ink); background: #101814; font: 10px/1 var(--mono); }
.conversation-starters button:hover, .conversation-starters button:focus-visible { border-color: var(--signal); color: var(--signal); outline: none; }
.ai-data-form { margin-top: 24px; }
.ai-data-form textarea { min-height: 150px; resize: vertical; }
.ai-data-form button { width: 100%; min-height: 42px; margin-top: 12px; border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
.ai-data-error { margin: 10px 0 0; color: var(--bad); font-size: 12px; line-height: 1.4; }
.side-console-link { display: flex; justify-content: space-between; margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--signal) !important; font: 11px/1.3 var(--mono) !important; text-transform: uppercase; letter-spacing: .06em; }
.attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; }
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
.surface-heading p { margin: 0; font: 400 36px/.95 var(--serif); }
@@ -67,10 +101,25 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.empty-state { margin: 24px 0 0; color: var(--muted); }
.empty-state.error { color: var(--bad); }
@media (max-width: 640px) {
.chat-view { height: auto; min-height: 100vh; overflow: visible; display: block; }
.composer { grid-template-columns: 1fr; }
.composer button { width: 100%; }
.conversation-starters { align-items: stretch; }
.conversation-starters span { width: 100%; }
.chat-header { gap: 10px; padding: 0 12px; }
.chat-header span { display: none; }
.human-nav { overflow-x: auto; }
.chat-header .cognitive-status { display: inline-flex; max-width: 150px; overflow: hidden; text-overflow: ellipsis; }
.build-meta { font-size: 8px; letter-spacing: .04em; }
.build-meta time { font-size: 7px; }
.chat-layout { height: auto; display: block; }
.chat-side, .chat-main { min-height: auto; max-height: none; overflow: visible; }
.chat-left, .chat-right { border: 0; border-bottom: 1px solid var(--line); }
.chat-main { height: 70vh; min-height: 520px; }
.chat-left { padding-bottom: 20px; }
.chat-right { padding-top: 20px; }
.operator-link, .nav-tab { padding: 0 9px; }
}
@media (min-width: 641px) and (max-width: 1100px) {
.chat-layout { grid-template-columns: 210px minmax(0, 1fr) 245px; }
.chat-side { padding-inline: 14px; }
.side-intro h1, .side-intro h2 { font-size: 30px; }
}
+138 -10
View File
@@ -8,14 +8,84 @@ const messageInput = document.querySelector("#message");
const sendButton = document.querySelector("#send");
const logoutButton = document.querySelector("#logout");
const activeFocus = document.querySelector("#active-focus");
const cognitiveStatus = document.querySelector("#cognitive-status");
const cognitiveConsoleLink = document.querySelector("#cognitive-console-link");
const todayList = document.querySelector("#today-list");
const refreshToday = document.querySelector("#refresh-today");
const historyList = document.querySelector("#history-list");
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
const conversationStarters = Array.from(document.querySelectorAll("[data-starter]"));
const aiDataForm = document.querySelector("#ai-data-form");
const aiDataInput = document.querySelector("#ai-data");
const aiDataError = document.querySelector("#ai-data-error");
let token = sessionStorage.getItem("domovoy.chat.token") || "";
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
let localHistory = [];
let cognitiveStatusTimer = null;
class ChatApiError extends Error {
constructor(status, detail = "") {
super(detail || `chat api returned ${status}`);
this.name = "ChatApiError";
this.status = status;
}
}
function containsSecretShapedText(value) {
return /(-----BEGIN [A-Z ]+PRIVATE KEY-----|\b(?:password|passwd|пароль|token|токен|api[_ -]?key|секрет|ключ)\s*[:=]|\bsk-[a-zA-Z0-9_-]{12,})/i.test(value);
}
function showAIDataError(message = "") {
if (!aiDataError) return;
aiDataError.textContent = message;
aiDataError.hidden = !message;
}
function renderCognitiveStatus(state) {
if (!cognitiveStatus) return;
const normalized = String(state || "UNKNOWN").toUpperCase();
let displayState = "degraded";
if (normalized === "AVAILABLE") displayState = "available";
if (normalized === "UNAVAILABLE") displayState = "unavailable";
const labels = {
available: "канал мышления доступен",
degraded: "канал мышления работает с ограничениями",
unavailable: "Домовой рядом · основной канал недоступен",
};
cognitiveStatus.dataset.state = displayState;
cognitiveStatus.textContent = labels[displayState];
cognitiveStatus.title = displayState === "available" ? "Канал доступен. Нажмите, чтобы проверить ещё раз" : "Проверить канал ещё раз";
if (cognitiveConsoleLink) cognitiveConsoleLink.hidden = displayState === "available";
}
async function loadCognitiveStatus() {
if (!cognitiveStatus || !token) return;
cognitiveStatus.dataset.state = "checking";
cognitiveStatus.textContent = "проверяю канал мышления";
try {
const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"});
renderCognitiveStatus(result.State || result.state || "UNKNOWN");
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
return;
}
renderCognitiveStatus("UNAVAILABLE");
}
}
function startCognitiveStatusPolling() {
if (cognitiveStatusTimer) return;
loadCognitiveStatus();
cognitiveStatusTimer = window.setInterval(loadCognitiveStatus, 30000);
}
function stopCognitiveStatusPolling() {
if (!cognitiveStatusTimer) return;
window.clearInterval(cognitiveStatusTimer);
cognitiveStatusTimer = null;
}
function showChat() {
loginView.hidden = true;
@@ -23,10 +93,12 @@ function showChat() {
if (!messages.children.length) {
appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов.");
}
startCognitiveStatusPolling();
messageInput.focus();
}
function showLogin() {
stopCognitiveStatusPolling();
chatView.hidden = true;
loginView.hidden = false;
}
@@ -56,17 +128,48 @@ async function api(path, options = {}) {
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
if (token) headers.Authorization = `Bearer ${token}`;
const response = await fetch(path, {...options, headers});
if (!response.ok) throw new Error(await response.text());
if (!response.ok) throw new ChatApiError(response.status, await response.text());
return response.json();
}
function expireSession(message = "") {
token = "";
conversationID = "";
sessionStorage.removeItem("domovoy.chat.token");
sessionStorage.removeItem("domovoy.chat.conversation");
messages.replaceChildren();
updateActiveFocus("");
if (message) loginError.textContent = message;
showLogin();
}
function humanMeta(result) {
if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам.";
if (result.delegation_id && result.delegation_state) return `Передал в coding-agent: ${humanDelegationState(result.delegation_state)}.`;
if (result.capability_status && result.capability_status !== "AVAILABLE") return "Возможность пока не подключена.";
if (result.work_planned) return "Я разложил это как работу.";
return "";
}
function humanDelegationState(state) {
switch (state) {
case "RUNNING":
return "работа запущена";
case "WAITING":
return "ожидаю следующий шаг";
case "BLOCKED":
return "нужно решение человека";
case "VERIFYING":
return "результат проверяется";
case "COMPLETED":
return "работа завершена";
case "FAILED":
return "run завершился ошибкой";
default:
return state.toLowerCase();
}
}
function renderHistory() {
if (!historyList) return;
historyList.replaceChildren();
@@ -169,8 +272,14 @@ async function sendChatMessage(text, options = {}) {
await loadToday();
activateTab("today-panel");
}
} catch {
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
} else if (error instanceof ChatApiError && error.status >= 500) {
appendMessage("assistant", "Домовой не смог завершить обработку: основной runtime сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд.");
} else {
appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку.");
}
} finally {
sendButton.disabled = false;
messageInput.focus();
@@ -215,16 +324,35 @@ for (const tab of navTabs) {
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
}
for (const starter of conversationStarters) {
starter.addEventListener("click", async () => {
const text = starter.dataset.starter || "";
if (!text || sendButton.disabled) return;
messageInput.value = "";
await sendChatMessage(text);
});
}
if (refreshToday) refreshToday.addEventListener("click", loadToday);
if (cognitiveStatus) cognitiveStatus.addEventListener("click", loadCognitiveStatus);
if (aiDataForm) {
aiDataForm.addEventListener("submit", async (event) => {
event.preventDefault();
const details = aiDataInput.value.trim();
if (!details || sendButton.disabled) return;
if (containsSecretShapedText(details)) {
showAIDataError("Не отправляю секреты. Удалите пароль, токен, API-ключ или приватный ключ и оставьте только описание модели.");
return;
}
showAIDataError();
aiDataInput.value = "";
await sendChatMessage(`Передаю сведения о локальном ИИ для текущего разговора:\n${details}`);
});
}
if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError());
logoutButton.addEventListener("click", () => {
token = "";
conversationID = "";
sessionStorage.removeItem("domovoy.chat.token");
sessionStorage.removeItem("domovoy.chat.conversation");
messages.replaceChildren();
updateActiveFocus("");
showLogin();
expireSession();
});
if (token) {
+153 -14
View File
@@ -1,12 +1,12 @@
:root {
--ink: #e9eee7;
--muted: #8c9a91;
--faint: #58645d;
--base: #0b100e;
--panel: #101714;
--panel-high: #151e1a;
--line: #2a352f;
--line-soft: #1d2822;
--ink: #eef2ec;
--muted: #a7b2aa;
--faint: #718078;
--base: #0f1512;
--panel: #17201b;
--panel-high: #202b25;
--line: #34423a;
--line-soft: #26332c;
--signal: #c9a85f;
--signal-bright: #e7c97b;
--good: #78c6a3;
@@ -21,13 +21,15 @@
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html { background: var(--base); color: var(--ink); font-family: var(--sans); }
body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .15), transparent 38%), var(--base); }
html { background: var(--base); color: var(--ink); font-family: var(--sans); scroll-behavior: smooth; }
body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .13), transparent 38%), var(--base); }
button, input, textarea { font: inherit; }
button { color: inherit; }
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
.skip-link { position: fixed; top: 10px; left: 10px; z-index: 30; padding: 10px 13px; color: #11150f; background: var(--signal-bright); font: 600 10px/1 var(--mono); text-decoration: none; transform: translateY(-160%); transition: transform .18s; }
.skip-link:focus { transform: translateY(0); }
.grain { position: fixed; inset: 0; pointer-events: none; opacity: .18; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
.grain { position: fixed; inset: 0; pointer-events: none; opacity: .045; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
.eyebrow, .rail-index { margin: 0 0 12px; color: var(--signal); font: 600 11px/1 var(--mono); letter-spacing: .18em; text-transform: uppercase; }
.login-shell { min-height: 100vh; display: grid; place-items: center; padding: 32px 20px; position: relative; overflow: hidden; }
@@ -63,6 +65,10 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.wordmark strong { font: 400 20px/1 var(--serif); }
.wordmark small { margin-top: 4px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.topbar-actions { display: flex; align-items: center; gap: 10px; }
.chat-return-link { color: var(--signal); text-decoration: none; font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
.topbar-nav { display: flex; align-items: center; gap: 4px; margin-right: 8px; }
.topbar-nav a { padding: 9px 10px; color: var(--muted); text-decoration: none; font: 600 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.topbar-nav a:hover, .topbar-nav a:focus-visible { color: var(--ink); background: var(--panel-high); outline: 0; }
.connection { margin-right: 12px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.connection i { width: 7px; height: 7px; display: inline-block; margin-right: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 14px rgba(120, 198, 163, .6); }
.connection.offline i { background: var(--bad); box-shadow: none; }
@@ -73,17 +79,55 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.context-rail h2 { margin: 0; font: 400 29px/1.1 var(--serif); }
.rail-copy { margin: 14px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.context-form { display: grid; }
.context-status-row { margin-bottom: 16px; padding: 10px 11px; display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--line-soft); background: rgba(120,198,163,.04); }
.context-status-label { color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; }
.context-status-row strong { color: var(--warn); font: 600 9px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.context-status-row strong[data-state="ready"] { color: var(--good); }
.context-presets { display: grid; gap: 7px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--line-soft); }
.context-presets label { margin: 0; }
.context-presets select { width: 100%; min-height: 38px; padding: 0 9px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 10px/1 var(--mono); }
.context-presets small { color: var(--faint); font-size: 10px; line-height: 1.35; }
.context-field { display: grid; gap: 7px; }
.context-field + .context-field { margin-top: 13px; }
.context-field label { margin: 0; }
.context-field label span { color: var(--signal); font-size: 8px; letter-spacing: .06em; }
.context-input-row { display: grid; grid-template-columns: minmax(0, 1fr) 34px; gap: 5px; }
.context-input-row input { min-width: 0; }
.copy-context { width: 34px; height: 40px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; }
.copy-context:hover { color: var(--ink); border-color: var(--faint); background: var(--panel-high); }
.id-actions { min-height: 31px; display: flex; align-items: center; gap: 7px; margin-top: 12px; }
.id-action { min-height: 30px; padding: 0 9px; border: 1px solid var(--line-soft); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; font: 9px/1 var(--mono); letter-spacing: .04em; }
.id-action:hover { color: var(--ink); border-color: var(--signal); background: rgba(201,168,95,.07); }
.icon-id-action { width: 30px; padding: 0; color: var(--faint); }
.context-form label { margin-top: 15px; }
.context-form label:first-child { margin-top: 0; }
.context-form input { height: 40px; padding: 0 10px; font: 11px/1 var(--mono); }
.context-hint { margin: 12px 0 0; color: var(--faint); font-size: 10px; line-height: 1.45; }
.advanced-context { margin-top: 17px; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
.advanced-context summary { padding: 12px 0; color: var(--muted); cursor: pointer; list-style: none; font: 600 9px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.advanced-context summary::before { content: "+"; display: inline-block; width: 18px; color: var(--signal); font-size: 13px; }
.advanced-context[open] summary::before { content: "−"; }
.advanced-context > div { padding: 0 0 10px; }
.advanced-context label { margin-top: 13px; }
.context-actions { display: grid; gap: 8px; }
.context-actions .secondary-button { margin-top: 18px; }
.context-actions .text-button { margin: 0; justify-self: start; padding-inline: 0; }
.secondary-button { height: 42px; margin-top: 20px; color: #11150f; background: var(--signal); font-size: 12px; }
.section-nav { display: grid; gap: 3px; margin-top: 4px; }
.section-nav-link { display: grid; grid-template-columns: 28px 1fr; gap: 2px 10px; padding: 11px 10px; border-left: 2px solid transparent; color: var(--muted); text-decoration: none; transition: background .18s, border-color .18s, color .18s; }
.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="location"] { border-left-color: var(--signal); color: var(--ink); background: rgba(201,168,95,.07); outline: 0; }
.section-nav-link span { grid-row: 1 / span 2; color: var(--signal); font: 9px/1.4 var(--mono); }
.section-nav-link b { font: 600 11px/1.2 var(--sans); }
.section-nav-link small { color: var(--faint); font: 9px/1.2 var(--mono); }
.session-stamp { margin-top: auto; padding-top: 20px; display: grid; gap: 6px; border-top: 1px solid var(--line); }
.session-stamp span { margin-top: 8px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .14em; }
.session-stamp strong { overflow: hidden; color: var(--muted); font: 10px/1.4 var(--mono); text-overflow: ellipsis; }
.workspace { grid-column: 2; grid-row: 2; padding: clamp(28px, 4vw, 58px); min-width: 0; }
.workspace { grid-column: 2; grid-row: 2; width: min(100%, 1740px); justify-self: start; padding: 42px clamp(24px, 3vw, 48px) 64px; min-width: 0; }
.workspace > [data-page][hidden] { display: none !important; }
.workspace > section { scroll-margin-top: 94px; }
.workspace-head { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 34px; }
.workspace-head h1 { margin: 0; font: 400 clamp(36px, 5vw, 58px)/1 var(--serif); letter-spacing: -.035em; }
.workspace-head h1 { margin: 0; font: 400 clamp(34px, 4vw, 50px)/1 var(--serif); letter-spacing: -.035em; }
.last-update { color: var(--faint); font: 10px/1.4 var(--mono); text-align: right; }
.signal-strip { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); }
.signal-strip div { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-right: 1px solid var(--line); }
@@ -94,6 +138,46 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.signal-strip strong[data-state="error"] { color: var(--bad); }
.signal-strip strong[data-state="loading"] { color: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
.diagnostics-panel, .settings-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(16,23,20,.82); }
.diagnostics-panel { border-color: rgba(120,198,163,.32); box-shadow: inset 3px 0 0 rgba(120,198,163,.45); }
.panel-heading { min-height: 68px; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
.panel-heading p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.panel-heading h2 { margin: 0; font: 400 23px/1 var(--serif); }
.panel-heading-tools { display: flex; align-items: center; gap: 14px; }
.panel-heading-tools > span, .settings-saved { color: var(--muted); font: 9px/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.compact-button { width: auto; min-width: 142px; height: 38px; margin: 0; padding: 0 13px; }
.diagnostic-intro { padding: 13px 18px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--line-soft); color: var(--muted); }
.diagnostic-intro p { margin: 0; font-size: 11px; line-height: 1.45; }
.diagnostic-pulse { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: var(--good); box-shadow: 0 0 12px rgba(120,198,163,.7); }
.diagnostic-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.diagnostic-list[aria-busy="true"] { opacity: .7; }
.diagnostic-item { min-width: 0; min-height: 92px; padding: 14px 16px; display: grid; grid-template-columns: 9px 1fr auto; gap: 9px; align-items: start; border-right: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
.diagnostic-item:nth-child(4n) { border-right: 0; }
.diagnostic-item > i { width: 7px; height: 7px; margin-top: 4px; border-radius: 50%; background: var(--faint); }
.diagnostic-item[data-state="ok"] > i { background: var(--good); box-shadow: 0 0 9px rgba(120,198,163,.6); }
.diagnostic-item[data-state="error"] > i { background: var(--bad); }
.diagnostic-item[data-state="loading"] > i { background: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
.diagnostic-item strong { display: block; color: var(--ink); font: 600 10px/1.3 var(--mono); }
.diagnostic-item span { display: block; margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.35; }
.diagnostic-item em { color: var(--faint); font: normal 9px/1.2 var(--mono); text-transform: uppercase; }
.diagnostic-item[data-state="ok"] em { color: var(--good); }
.diagnostic-item[data-state="error"] em { color: var(--bad); }
.settings-grid { padding: 18px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 20px; }
.setting-field, .setting-check { min-width: 0; display: grid; gap: 7px; color: var(--ink); }
.setting-field > span, .setting-check b { font: 600 10px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.setting-field small, .setting-check small { color: var(--muted); font-size: 11px; line-height: 1.4; }
.setting-field select { width: 100%; min-height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 11px/1 var(--mono); }
.setting-check { grid-template-columns: 18px 1fr; align-items: start; gap: 10px; padding: 10px 12px; border: 1px solid var(--line-soft); }
.setting-check input { width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--signal); }
.setting-check span { display: grid; gap: 7px; }
.settings-actions { display: flex; align-items: center; justify-content: flex-end; gap: 14px; grid-column: 1 / -1; padding-top: 2px; }
.settings-actions .text-button { margin: 0; }
html[data-density="compact"] .data-card { padding: 18px; }
html[data-density="compact"] .data-card dl div { padding-block: 10px; }
html[data-density="compact"] .activity-row { min-height: 58px; padding-block: 8px; }
html[data-density="compact"] .learning-row { min-height: 54px; padding-block: 9px; }
html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before, html[data-reduced-motion="true"] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: .01ms !important; }
.advisor-panel { margin-bottom: 18px; padding: 24px; display: grid; grid-template-columns: minmax(220px, .72fr) minmax(320px, 1.28fr); gap: 18px 34px; border: 1px solid rgba(201,168,95,.42); background: linear-gradient(125deg, rgba(201,168,95,.1), rgba(16,23,20,.88) 44%, rgba(12,20,17,.96)); box-shadow: inset 3px 0 0 rgba(201,168,95,.58); }
.advisor-intro { align-self: center; }
.advisor-intro > p { margin: 0 0 10px; color: var(--signal); font: 600 9px/1 var(--mono); letter-spacing: .15em; }
@@ -169,6 +253,40 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.activity-reconcile:disabled { opacity: .55; cursor: wait; }
.activity-error { color: var(--bad); }
.fleet-panel { margin-bottom: 18px; border: 1px solid rgba(201,168,95,.32); background: linear-gradient(135deg, rgba(31,27,18,.92), rgba(13,20,17,.96)); box-shadow: inset 3px 0 0 rgba(201,168,95,.45); }
.fleet-panel > header { min-height: 66px; padding: 13px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
.fleet-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.fleet-panel h2 { margin: 0; font: 400 22px/1 var(--serif); }
.fleet-overview { padding: 16px 18px; display: flex; align-items: end; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line-soft); }
.fleet-overview span { display: block; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.fleet-overview strong { display: block; margin-top: 6px; color: var(--signal); font: 600 16px/1 var(--mono); }
.fleet-overview p { max-width: 620px; margin: 0; color: var(--muted); font: 10px/1.45 var(--mono); }
.fleet-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.fleet-list[aria-busy="true"] { opacity: .72; }
.fleet-row { min-width: 0; padding: 14px 18px; display: grid; gap: 7px; border-right: 1px solid var(--line-soft); }
.fleet-row:last-child { border-right: 0; }
.fleet-row strong, .fleet-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fleet-row strong { color: var(--ink); font: 600 11px/1.3 var(--mono); }
.fleet-row span { color: var(--muted); font: 9px/1.4 var(--mono); }
.fleet-row .learning-state[data-state="READY"] { color: var(--good); }
.fleet-row .learning-state[data-state="DEGRADED"] { color: var(--warn); }
.agent-panel { margin-bottom: 18px; border: 1px solid rgba(120,198,163,.3); background: rgba(13,20,17,.92); box-shadow: inset 3px 0 0 rgba(120,198,163,.38); }
.agent-panel > header { min-height: 66px; padding: 13px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
.agent-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.agent-panel h2 { margin: 0; font: 400 22px/1 var(--serif); }
.agent-empty { padding: 20px 18px; color: var(--muted); font: 10px/1.5 var(--mono); }
.agent-details { padding: 16px 18px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
.agent-details div { min-width: 0; }
.agent-details span { display: block; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.agent-details strong { display: block; margin-top: 7px; overflow: hidden; color: var(--ink); font: 600 10px/1.4 var(--mono); text-overflow: ellipsis; white-space: nowrap; }
.agent-details p { grid-column: 1 / -1; margin: 0; padding-top: 12px; border-top: 1px solid var(--line-soft); color: var(--muted); font: 10px/1.5 var(--mono); }
.agent-worker-status { margin: 0 18px 16px; padding-top: 13px; display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; border-top: 1px solid var(--line-soft); }
.agent-worker-status span { color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.agent-worker-status strong { color: var(--warn); font: 600 10px/1.3 var(--mono); }
.agent-worker-status strong[data-state="ready"] { color: var(--good); }
.agent-worker-status p { grid-column: 1 / -1; margin: 0; color: var(--muted); font: 10px/1.45 var(--mono); }
.learning-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.learning-grid article { min-width: 0; border-right: 1px solid var(--line-soft); }
.learning-grid article:last-child { border-right: 0; }
@@ -278,6 +396,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.advisor-panel { grid-template-columns: 1fr; }
.capability-card { grid-column: auto; }
.prospective-card { grid-column: auto; }
.diagnostic-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.diagnostic-item:nth-child(4n) { border-right: 1px solid var(--line-soft); }
.diagnostic-item:nth-child(2n) { border-right: 0; }
}
@media (max-width: 680px) {
@@ -285,14 +406,26 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.console { display: block; }
.topbar { height: 66px; padding: 0 14px; }
.wordmark small, .connection, .text-button { display: none; }
.chat-return-link { display: inline-block; }
.topbar-nav { display: none; }
.context-rail { padding: 24px 18px; border-right: 0; border-bottom: 1px solid var(--line); }
.rail-copy { max-width: 48ch; }
.session-stamp { margin-top: 8px; grid-template-columns: auto 1fr; align-items: center; }
.session-stamp span { margin: 0; }
.workspace { padding: 30px 16px 50px; }
.section-nav { display: flex; gap: 6px; margin: 10px -18px -24px; padding: 10px 18px; overflow-x: auto; border-top: 1px solid var(--line-soft); background: #0e1512; scrollbar-width: thin; }
.section-nav-link { min-width: 142px; padding: 10px; border-left: 0; border-bottom: 2px solid transparent; }
.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="location"] { border-left-color: transparent; border-bottom-color: var(--signal); }
.workspace-head { align-items: start; flex-direction: column; }
.last-update { text-align: left; }
.signal-strip { grid-template-columns: 1fr; }
.panel-heading { align-items: flex-start; flex-direction: column; }
.panel-heading-tools { width: 100%; justify-content: space-between; }
.diagnostic-list { grid-template-columns: 1fr; }
.diagnostic-item, .diagnostic-item:nth-child(2n), .diagnostic-item:nth-child(4n) { border-right: 0; }
.settings-grid { grid-template-columns: 1fr; padding: 16px; }
.settings-actions { grid-column: auto; align-items: stretch; flex-direction: column; }
.settings-actions .primary-button { width: 100%; }
.signal-strip div { border-right: 0; border-bottom: 1px solid var(--line); }
.advisor-panel { padding: 20px 16px; gap: 20px; }
.advisor-controls { align-items: stretch; flex-direction: column; }
@@ -305,6 +438,12 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.learning-grid { grid-template-columns: 1fr; }
.learning-grid article { border-right: 0; border-bottom: 1px solid var(--line-soft); }
.learning-grid article:last-child { border-bottom: 0; }
.fleet-overview { align-items: flex-start; flex-direction: column; }
.fleet-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.fleet-row:nth-child(2n) { border-right: 0; }
.fleet-row { border-bottom: 1px solid var(--line-soft); }
.agent-details { grid-template-columns: 1fr; gap: 14px; }
.agent-details p { grid-column: auto; }
.activity-row { padding: 14px 16px; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 12px; }
.activity-state { grid-column: 2; grid-row: 1; }
.activity-operation { grid-column: 1; grid-row: 1; }
+366 -17
View File
@@ -6,15 +6,43 @@ const contextForm = byId("context-form");
const advisoryForm = byId("advisory-form");
const loginError = byId("login-error");
const toast = byId("toast");
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id"];
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id", "delegation-id"];
const settingsStorageKey = "domovoy.creator.settings";
const contextPresetsStorageKey = "domovoy.creator.context.presets";
let sessionToken = sessionStorage.getItem("domovoy.creator.session") || "";
let toastTimer;
let refreshTimer;
let currentTask = null;
let currentProspective = null;
let pendingCommand = null;
let pendingNotification = null;
let lastAuthorizedActionID = "";
let advisoryPolicy = {Mode: "deterministic", ExternalEgress: false, AcknowledgementRequired: false};
let uiSettings = {density: "comfortable", refresh: "off", motion: false};
const pageNavLinks = Array.from(document.querySelectorAll(".section-nav-link, .topbar-nav a"));
const pageViews = Array.from(document.querySelectorAll("[data-page]"));
const cabinetPages = ["overview", "diagnostics", "operations", "learning", "settings"];
function setActivePage(id, updateHistory = false) {
const page = cabinetPages.includes(id) ? id : "overview";
pageViews.forEach((view) => { view.hidden = view.dataset.page !== page; });
document.body.dataset.page = page;
pageNavLinks.forEach((link) => {
const active = link.getAttribute("href") === `#${page}`;
link.classList.toggle("active", active);
if (active) link.setAttribute("aria-current", "location");
else link.removeAttribute("aria-current");
});
if (updateHistory && window.location.hash !== `#${page}`) window.history.pushState({}, "", `#${page}`);
}
pageNavLinks.forEach((link) => link.addEventListener("click", (event) => {
event.preventDefault();
setActivePage(link.getAttribute("href").slice(1), true);
}));
window.addEventListener("popstate", () => setActivePage(window.location.hash.slice(1)));
setActivePage(window.location.hash.slice(1));
function setText(id, value, fallback = "—") {
byId(id).textContent = value === undefined || value === null || value === "" ? fallback : String(value);
@@ -39,6 +67,18 @@ function notify(message) {
toastTimer = setTimeout(() => { toast.hidden = true; }, 4200);
}
function safeClientError(error) {
const message = error && typeof error.message === "string" ? error.message : "";
switch (message) {
case "creator authentication denied":
return "Логин или пароль не принят.";
case "Сессия отклонена":
return "Сессия истекла. Войдите снова.";
default:
return "Операция недоступна. Попробуйте ещё раз.";
}
}
async function request(path, options = {}) {
const headers = new Headers(options.headers || {});
if (sessionToken) headers.set("Authorization", `Bearer ${sessionToken}`);
@@ -50,7 +90,9 @@ async function request(path, options = {}) {
}
if (!response.ok) {
const detail = (await response.text()).trim();
throw new Error(detail || `Ошибка ${response.status}`);
const error = new Error(detail || `Ошибка ${response.status}`);
error.status = response.status;
throw error;
}
return response.json();
}
@@ -64,15 +106,57 @@ function restoreContext() {
} catch (_) {
localStorage.removeItem("domovoy.creator.context");
}
renderContextState();
renderContextPresets();
}
function saveContext() {
const saved = {};
contextFields.forEach((id) => { saved[id] = byId(id).value.trim(); });
localStorage.setItem("domovoy.creator.context", JSON.stringify(saved));
renderContextState();
return saved;
}
function readContextPresets() {
try {
const value = JSON.parse(localStorage.getItem(contextPresetsStorageKey) || "[]");
return Array.isArray(value) ? value.filter((item) => item && item.session && item.workspace) : [];
} catch (_) {
localStorage.removeItem(contextPresetsStorageKey);
return [];
}
}
function renderContextState() {
const state = byId("context-state");
if (!state) return;
const ready = Boolean(byId("session-id").value.trim() && byId("workspace-id").value.trim());
state.textContent = ready ? "готов к проверке" : "не выбран";
state.dataset.state = ready ? "ready" : "empty";
}
function renderContextPresets() {
const select = byId("context-preset");
if (!select) return;
const presets = readContextPresets();
select.replaceChildren(new Option("Выберите сохранённый набор…", ""));
presets.forEach((preset, index) => {
const session = preset.session.length > 18 ? `${preset.session.slice(0, 8)}…${preset.session.slice(-6)}` : preset.session;
const workspace = preset.workspace.length > 18 ? `${preset.workspace.slice(0, 8)}…${preset.workspace.slice(-6)}` : preset.workspace;
select.appendChild(new Option(`${session} · ${workspace}`, String(index)));
});
}
function rememberContextPreset(context) {
if (!context["session-id"] || !context["workspace-id"]) return;
const next = {session: context["session-id"], workspace: context["workspace-id"], savedAt: Date.now()};
const presets = readContextPresets().filter((item) => item.session !== next.session || item.workspace !== next.workspace);
presets.unshift(next);
localStorage.setItem(contextPresetsStorageKey, JSON.stringify(presets.slice(0, 5)));
renderContextPresets();
}
async function showConsole() {
const session = await request("/api/session/current");
setText("session-actor", session.ActorID);
@@ -80,6 +164,7 @@ async function showConsole() {
setText("session-expiry", expiry);
loginView.hidden = true;
consoleView.hidden = false;
restoreSettings();
restoreContext();
await loadAdvisoryPolicy();
const context = saveContext();
@@ -87,11 +172,17 @@ async function showConsole() {
else {
loadActionHistory();
loadLearning();
loadFleetReadiness();
loadAgentDelegation();
loadAgentWorkerStatus();
}
runDiagnostics();
}
function endSession(message = "Сессия завершена.") {
sessionToken = "";
clearInterval(refreshTimer);
refreshTimer = null;
sessionStorage.removeItem("domovoy.creator.session");
consoleView.hidden = true;
loginView.hidden = false;
@@ -128,7 +219,7 @@ loginForm.addEventListener("submit", async (event) => {
byId("creator-password").value = "";
await showConsole();
} catch (error) {
loginError.textContent = error.message === "creator authentication denied" ? "Логин или пароль не принят." : error.message;
loginError.textContent = safeClientError(error);
} finally {
button.disabled = false;
button.firstElementChild.textContent = "Открыть консоль";
@@ -144,14 +235,131 @@ byId("toggle-secret").addEventListener("click", () => {
contextForm.addEventListener("submit", (event) => {
event.preventDefault();
saveContext();
const context = saveContext();
rememberContextPreset(context);
loadDashboard();
});
for (const input of [byId("session-id"), byId("workspace-id")]) {
input.addEventListener("input", renderContextState);
}
byId("context-preset").addEventListener("change", (event) => {
const index = Number(event.target.value);
if (!Number.isInteger(index)) return;
const preset = readContextPresets()[index];
if (!preset) return;
byId("session-id").value = preset.session;
byId("workspace-id").value = preset.workspace;
renderContextState();
loadDashboard();
notify("Контекст из недавнего набора загружен.");
});
for (const button of document.querySelectorAll("[data-copy-context]")) {
button.addEventListener("click", async () => {
const value = byId(button.dataset.copyContext).value.trim();
if (!value) {
notify("Сначала укажите идентификатор.");
return;
}
try {
await navigator.clipboard.writeText(value);
notify("Идентификатор скопирован.");
} catch (_) {
notify("Не удалось скопировать идентификатор.");
}
});
}
for (const button of document.querySelectorAll("[data-copy-value], [data-use-value]")) {
button.addEventListener("click", async () => {
const source = byId(button.dataset.copyValue || button.dataset.useValue);
const value = source?.textContent.trim() || "";
if (!value || value === "—") {
notify("Сначала загрузите значение.");
return;
}
if (button.dataset.useValue) {
const target = byId(button.dataset.targetId);
if (!target) return;
target.value = value;
document.querySelector(".advanced-context").open = true;
saveContext();
notify("ID подставлен в расширенную проверку.");
return;
}
try {
await navigator.clipboard.writeText(value);
notify("ID скопирован.");
} catch (_) {
notify("Не удалось скопировать ID.");
}
});
}
byId("reset-context").addEventListener("click", () => {
contextFields.forEach((id) => { byId(id).value = id === "capability-id" ? "cognitive-proposal" : ""; });
localStorage.removeItem("domovoy.creator.context");
loadDashboard();
notify("Рабочий контекст сброшен.");
});
function restoreSettings() {
try {
const saved = JSON.parse(localStorage.getItem(settingsStorageKey) || "{}");
uiSettings = {
density: ["comfortable", "compact"].includes(saved.density) ? saved.density : "comfortable",
refresh: ["off", "30", "60", "300"].includes(saved.refresh) ? saved.refresh : "off",
motion: saved.motion === true,
};
} catch (_) {
uiSettings = {density: "comfortable", refresh: "off", motion: false};
}
byId("setting-density").value = uiSettings.density;
byId("setting-refresh").value = uiSettings.refresh;
byId("setting-motion").checked = uiSettings.motion;
applySettings();
}
function applySettings() {
document.documentElement.dataset.density = uiSettings.density;
document.documentElement.dataset.reducedMotion = uiSettings.motion ? "true" : "false";
clearInterval(refreshTimer);
refreshTimer = null;
if (sessionToken && uiSettings.refresh !== "off") {
refreshTimer = setInterval(() => loadDashboard(), Number(uiSettings.refresh) * 1000);
}
}
byId("settings-form").addEventListener("submit", (event) => {
event.preventDefault();
uiSettings = {
density: byId("setting-density").value,
refresh: byId("setting-refresh").value,
motion: byId("setting-motion").checked,
};
localStorage.setItem(settingsStorageKey, JSON.stringify(uiSettings));
applySettings();
setText("settings-saved", "Сохранено только в этом браузере");
notify("Настройки кабинета сохранены.");
});
byId("reset-settings").addEventListener("click", () => {
uiSettings = {density: "comfortable", refresh: "off", motion: false};
localStorage.removeItem(settingsStorageKey);
restoreSettings();
setText("settings-saved", "Восстановлены значения по умолчанию");
notify("Настройки сброшены.");
});
byId("refresh-all").addEventListener("click", loadDashboard);
byId("refresh-actions").addEventListener("click", loadActionHistory);
byId("refresh-learning").addEventListener("click", loadLearning);
byId("refresh-fleet").addEventListener("click", loadFleetReadiness);
byId("refresh-agent").addEventListener("click", loadAgentDelegation);
byId("logout").addEventListener("click", () => endSession());
byId("run-diagnostics").addEventListener("click", runDiagnostics);
advisoryForm.addEventListener("submit", async (event) => {
event.preventDefault();
@@ -185,7 +393,7 @@ advisoryForm.addEventListener("submit", async (event) => {
byId("advisory-result").hidden = false;
} catch (error) {
setText("advisory-state", "ОШИБКА");
setText("advisory-text", error.message);
setText("advisory-text", safeClientError(error));
byId("advisory-result").hidden = false;
} finally {
byId("advisory-egress-ack").checked = false;
@@ -239,6 +447,9 @@ async function loadDashboard() {
work.push(loadProspectiveRegister(context["prospective-id"]));
work.push(loadActionHistory());
work.push(loadLearning());
work.push(loadFleetReadiness());
work.push(loadAgentDelegation());
work.push(loadAgentWorkerStatus());
if (context["capability-id"]) work.push(loadCapability(context["capability-id"]));
else setSignal("capability", "не выбрана", "idle");
@@ -246,6 +457,144 @@ async function loadDashboard() {
setText("last-update", `Обновлено ${new Intl.DateTimeFormat("ru-RU", {hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(new Date())}`);
}
async function runDiagnostics() {
const list = byId("diagnostic-list");
const summary = byId("diagnostic-summary");
const checks = [
["Сессия", "identity", () => request("/api/session/current")],
["Политика совета", "advisory", () => request("/api/advisory-policy")],
["Когнитивная возможность", "capability", () => request("/api/capabilities/cognitive-proposal/health")],
["Сегодня", "today", () => request("/api/today")],
["Внимание", "attention", () => request("/api/attention")],
["Действия", "actions", () => request("/api/actions?limit=1")],
["Learning evidence", "learning", () => request("/api/learning/recent?limit=1")],
];
list.setAttribute("aria-busy", "true");
list.replaceChildren();
checks.forEach(([label, key]) => list.append(createDiagnosticItem(label, key, "loading")));
setText("diagnostic-summary", "Проверяю…");
const results = await Promise.all(checks.map(async ([label, key, run]) => {
const started = performance.now();
try {
await run();
return {label, key, state: "ok", detail: `Ответ получен · ${Math.round(performance.now() - started)} мс`};
} catch (error) {
const detail = error?.status === 400 ? "Нужны параметры рабочего контекста" : error?.status === 401 ? "Сессия истекла" : error?.status >= 500 ? "Сервис временно недоступен" : "Проверка не пройдена";
return {label, key, state: "error", detail};
}
}));
list.replaceChildren(...results.map((result) => createDiagnosticItem(result.label, result.key, result.state, result.detail)));
list.setAttribute("aria-busy", "false");
const passed = results.filter((result) => result.state === "ok").length;
setText("diagnostic-summary", `${passed}/${results.length} проверок пройдено`);
}
function createDiagnosticItem(label, key, state, detail = "Проверяю…") {
const item = document.createElement("article");
item.className = "diagnostic-item";
item.dataset.state = state;
item.dataset.check = key;
const marker = document.createElement("i");
marker.setAttribute("aria-hidden", "true");
const copy = document.createElement("div");
const title = document.createElement("strong");
title.textContent = label;
const description = document.createElement("span");
description.textContent = detail;
copy.append(title, description);
const status = document.createElement("em");
status.textContent = state === "ok" ? "OK" : state === "error" ? "ошибка" : "…";
item.append(marker, copy, status);
return item;
}
async function loadFleetReadiness() {
const list = byId("fleet-list");
list.setAttribute("aria-busy", "true");
list.replaceChildren();
const loading = document.createElement("p");
loading.className = "register-empty";
loading.textContent = "Проверяю доступность model fleet…";
list.append(loading);
try {
const result = await request("/api/fleet/readiness");
const readiness = result.readiness || {};
const members = Array.isArray(readiness.members) ? readiness.members : [];
setText("fleet-selected", result.selected_tier || "не выбран");
setText("fleet-summary", `${members.filter((member) => member.ready).length}/${members.length} ГОТОВЫ`);
setText("fleet-explanation", result.selected_tier ? `Активный маршрут: ${result.selected_tier}. Остальные tier остаются видимыми с их фактической причиной.` : "Ни один tier не выбран: проверьте конфигурацию и health провайдеров.");
list.replaceChildren(...members.map(createFleetRow));
} catch (error) {
setText("fleet-selected", "недоступен");
setText("fleet-summary", "ОШИБКА");
setText("fleet-explanation", safeClientError(error));
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = safeClientError(error);
list.replaceChildren(failed);
} finally {
list.setAttribute("aria-busy", "false");
}
}
function createFleetRow(member) {
const row = document.createElement("article");
row.className = "fleet-row";
const state = document.createElement("em");
state.className = "learning-state";
state.dataset.state = member.ready ? "READY" : "DEGRADED";
state.textContent = member.ready ? "READY" : "DEGRADED";
const title = document.createElement("strong");
title.textContent = member.tier || "UNKNOWN TIER";
const detail = document.createElement("span");
detail.textContent = `${member.model_descriptor_id || "model"} · ${member.reason || "причина не указана"}`;
row.append(state, title, detail);
return row;
}
async function loadAgentDelegation() {
const delegationID = byId("delegation-id").value.trim();
const empty = byId("agent-empty");
const details = byId("agent-details");
if (!delegationID) {
empty.hidden = false;
details.hidden = true;
setText("agent-summary", "DELEGATION НЕ ВЫБРАНА");
return;
}
empty.hidden = false;
empty.textContent = "Читаю durable delegation…";
details.hidden = true;
try {
const delegation = await request(`/api/delegations/${encodeURIComponent(delegationID)}`);
const checkpoint = await request(`/api/delegations/${encodeURIComponent(delegationID)}/latest-checkpoint`).catch(() => ({}));
setText("agent-summary", delegation.State || "СОСТОЯНИЕ НЕИЗВЕСТНО");
setText("agent-delegation-state", delegation.State);
setText("agent-run-state", `${delegation.LatestRunID || "run не выбран"} · ${delegation.SelectedImplementationID || "implementation не указана"}`);
setText("agent-blocker", checkpoint.BlockerClass || "нет checkpoint");
setText("agent-next-step", checkpoint.NextStep || checkpoint.RemainingSummary || delegation.DesiredOutcome || "Следующий шаг не записан.");
empty.hidden = true;
details.hidden = false;
} catch (error) {
setText("agent-summary", "ОШИБКА");
empty.textContent = safeClientError(error);
empty.hidden = false;
}
}
async function loadAgentWorkerStatus() {
try {
const status = await request("/api/agent-worker/status");
setText("agent-worker-state", status.mode || "UNKNOWN");
setText("agent-worker-message", status.reason || "Причина не указана.");
byId("agent-worker-state").dataset.state = status.ready === true ? "ready" : "degraded";
} catch (error) {
setText("agent-worker-state", "НЕДОСТУПЕН");
setText("agent-worker-message", safeClientError(error));
byId("agent-worker-state").dataset.state = "degraded";
}
}
async function loadLearning() {
const experienceList = byId("experience-list");
const reflectionList = byId("reflection-list");
@@ -272,7 +621,7 @@ async function loadLearning() {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = error.message;
failed.textContent = safeClientError(error);
list.append(failed);
});
setText("learning-count", "ОШИБКА");
@@ -371,7 +720,7 @@ async function loadActionHistory() {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = error.message;
failed.textContent = safeClientError(error);
list.append(failed);
setText("action-count", "ОШИБКА");
} finally {
@@ -430,7 +779,7 @@ async function reconcileHistoryAction(actionID, button) {
await loadActionHistory();
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
} catch (error) {
notify(error.message);
notify(safeClientError(error));
button.disabled = false;
button.textContent = "Сверить";
}
@@ -450,7 +799,7 @@ async function loadFocus(query) {
} catch (error) {
setSignal("focus", "ошибка", "error");
setBadge("focus", "НЕДОСТУПЕН", "bad");
setText("focus-message", error.message);
setText("focus-message", safeClientError(error));
byId("connection-state").classList.add("offline");
}
}
@@ -472,7 +821,7 @@ async function loadTask(taskID, query) {
currentTask = null;
setSignal("task", "отклонена", "error");
setBadge("task", "ВНЕ ФОКУСА", "bad");
setText("task-message", error.message);
setText("task-message", safeClientError(error));
updateActionReadiness();
}
}
@@ -511,12 +860,12 @@ async function loadProspectiveRegister(selectedID) {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty";
failed.textContent = error.message;
failed.textContent = safeClientError(error);
list.append(failed);
setText("prospective-count", "ОШИБКА");
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
setText("prospective-message", error.message);
setText("prospective-message", safeClientError(error));
}
}
@@ -572,7 +921,7 @@ async function loadProspective(prospectiveID) {
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
setText("prospective-state", "UNKNOWN");
setText("prospective-message", error.message);
setText("prospective-message", safeClientError(error));
updateActionReadiness();
}
}
@@ -594,7 +943,7 @@ async function loadCapability(capabilityID) {
setSignal("capability", "ошибка", "error");
setBadge("capability", "НЕДОСТУПНА", "bad");
setText("capability-state", "UNKNOWN");
setText("capability-reason", error.message);
setText("capability-reason", safeClientError(error));
}
}
@@ -700,7 +1049,7 @@ byId("confirm-notification").addEventListener("click", async () => {
await loadActionHistory();
notify(result.State === "SUCCEEDED" ? "Receiver принял уведомление." : "Исход отправки требует сверки.");
} catch (error) {
setText("notification-confirmation-error", error.message);
setText("notification-confirmation-error", safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Подтверждаю отправку";
@@ -730,7 +1079,7 @@ byId("confirm-satisfaction").addEventListener("click", async () => {
notify(result.State === "SUCCEEDED" ? "Исполнение зафиксировано через Commit." : "Исход требует проверки.");
await loadProspectiveRegister(command.ProspectiveItemID);
} catch (error) {
setText("confirmation-error", error.message);
setText("confirmation-error", safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Подтверждаю переход";
@@ -764,7 +1113,7 @@ byId("reconcile-action").addEventListener("click", async () => {
notify(result.State === "SUCCEEDED" ? "Исход подтверждён." : "Исход пока неизвестен.");
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
} catch (error) {
notify(error.message);
notify(safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Проверить исход";
+62 -30
View File
@@ -25,38 +25,70 @@
<section id="chat-view" class="chat-view" hidden>
<header class="chat-header">
<a href="/chat" aria-label="Домовой">Домовой</a>
<nav class="human-nav" aria-label="Разделы Домового">
<button class="nav-tab active" data-tab="chat-panel" type="button">Чат</button>
<button class="nav-tab" data-tab="today-panel" type="button">Сегодня</button>
<button class="nav-tab" data-tab="history-panel" type="button">История</button>
<a href="/" class="operator-link">Оператор</a>
</nav>
<span id="active-focus">фокус: не выбран</span>
<span id="session-state">сессия активна</span>
<a href="/" aria-label="Домовой">Домовой</a>
<button id="cognitive-status" class="cognitive-status" data-state="checking" type="button" role="status" aria-live="polite" title="Проверить канал ещё раз">проверяю канал мышления</button>
<a id="cognitive-console-link" class="cognitive-console-link" href="/admin" hidden>настроить в консоли</a>
<div class="build-meta" aria-label="Версия и время обновления">
<span>версия __ASSISTANT_BUILD_REVISION__</span>
<time datetime="__ASSISTANT_BUILD_DATETIME__">обновлено __ASSISTANT_BUILD_TIME__</time>
</div>
<a href="/admin" class="operator-link">Консоль</a>
<button id="logout" type="button" aria-label="Выйти">↗</button>
</header>
<section id="chat-panel" class="surface-panel active-panel">
<div id="messages" class="messages" aria-live="polite"></div>
<form id="chat-form" class="composer">
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
</form>
</section>
<section id="today-panel" class="surface-panel attention-surface" hidden>
<div class="surface-heading">
<p>Сегодня / внимание</p>
<button id="refresh-today" type="button">Обновить</button>
</div>
<div id="today-list" class="attention-list"></div>
</section>
<section id="history-panel" class="surface-panel history-surface" hidden>
<div class="surface-heading">
<p>История</p>
<span>Последние сообщения текущего окна</span>
</div>
<div id="history-list" class="history-list"></div>
</section>
<div class="chat-layout">
<aside class="chat-side chat-left" aria-label="Навигация и текущий фокус">
<div class="side-intro">
<p class="side-index">01 / РАЗГОВОР</p>
<h1>Домовой рядом</h1>
<p>Пишите обычными словами. Настройки и сведения можно передать прямо в текущий разговор.</p>
</div>
<div class="side-focus">
<span>ТЕКУЩИЙ ФОКУС</span>
<strong id="active-focus">не выбран</strong>
</div>
<nav class="human-nav" aria-label="Разделы Домового">
<button class="nav-tab active" data-tab="chat-panel" type="button"><b>01</b><span>Чат</span><small>живой разговор</small></button>
<button class="nav-tab" data-tab="today-panel" type="button"><b>02</b><span>Сегодня</span><small>что требует внимания</small></button>
<button class="nav-tab" data-tab="history-panel" type="button"><b>03</b><span>История</span><small>это окно разговора</small></button>
</nav>
<div class="side-note"><span>СЕССИЯ</span><strong id="session-state">активна</strong><small>Сведения сохраняются в текущем разговоре.</small></div>
</aside>
<main class="chat-main">
<section id="chat-panel" class="surface-panel active-panel">
<div id="messages" class="messages" aria-live="polite"></div>
<form id="chat-form" class="composer">
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
</form>
<div class="conversation-starters" aria-label="Частые вопросы">
<span>Можно начать так:</span>
<button type="button" data-starter="Проверь локальный канал мышления">Проверь канал</button>
<button type="button" data-starter="Что сейчас доступно?">Что доступно?</button>
<button type="button" data-starter="Хочу изменить настройку локальной модели">Изменить настройку</button>
</div>
</section>
<section id="today-panel" class="surface-panel attention-surface" hidden>
<div class="surface-heading"><p>Сегодня / внимание</p><button id="refresh-today" type="button">Обновить</button></div>
<div id="today-list" class="attention-list"></div>
</section>
<section id="history-panel" class="surface-panel history-surface" hidden>
<div class="surface-heading"><p>История</p><span>Последние сообщения текущего окна</span></div>
<div id="history-list" class="history-list"></div>
</section>
</main>
<aside class="chat-side chat-right" aria-label="Состояние и сведения об ИИ">
<div class="side-intro"><p class="side-index">02 / ИНФОРМАЦИЯ</p><h2>Канал мышления</h2><p>Домовой видит подтверждённое состояние канала. Если основной ИИ пропал, он остаётся рядом и переходит в аварийный режим.</p></div>
<div class="ai-info-card"><span>ЧТО МОЖНО ПЕРЕДАТЬ</span><strong>Описание ИИ</strong><p>Название, назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
<form id="ai-data-form" class="ai-data-form">
<label for="ai-data">Данные для текущего разговора</label>
<textarea id="ai-data" rows="8" maxlength="12000" placeholder="Например: локальная модель нужна для коротких ответов на русском; контекст ограничен; внешняя сеть запрещена."></textarea>
<button type="submit">Передать Домовому</button>
<p id="ai-data-error" class="ai-data-error" role="alert" hidden></p>
<small>Это отправится как обычное сообщение в чат.</small>
</form>
<a class="side-console-link" href="/admin">Открыть консоль настройки <span>→</span></a>
</aside>
</div>
</section>
</main>
<script src="/assets/chat.js"></script>
+113 -20
View File
@@ -6,9 +6,10 @@
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#101714">
<title>Домовой — консоль создателя</title>
<link rel="stylesheet" href="/assets/console.css">
<link rel="stylesheet" href="/assets/console.css?v=20260821-context">
</head>
<body>
<a class="skip-link" href="#overview">К содержимому</a>
<div class="grain" aria-hidden="true"></div>
<main id="login-view" class="login-shell">
@@ -43,7 +44,15 @@
<span><strong>Домовой</strong><small>консоль создателя</small></span>
</a>
<div class="topbar-actions">
<a class="chat-return-link" href="/">← Чат с Домовым</a>
<span id="connection-state" class="connection"><i></i>система доступна</span>
<nav class="topbar-nav" aria-label="Разделы кабинета">
<a href="#overview">Обзор</a>
<a href="#diagnostics">Проверки</a>
<a href="#operations">Операции</a>
<a href="#learning">Опыт</a>
<a href="#settings">Настройки</a>
</nav>
<button id="refresh-all" class="text-button" type="button">Обновить</button>
<button id="logout" class="icon-button" type="button" aria-label="Завершить сессию" title="Выйти">↗</button>
</div>
@@ -56,18 +65,48 @@
<p class="rail-copy">Укажите рабочий контекст. Он сохраняется только в этом браузере и не даёт дополнительных полномочий.</p>
</div>
<form id="context-form" class="context-form">
<label for="session-id">Идентификатор сессии</label>
<input id="session-id" autocomplete="off" placeholder="session-id">
<label for="workspace-id">Рабочая область</label>
<input id="workspace-id" autocomplete="off" placeholder="workspace-id">
<label for="task-id">Задача</label>
<input id="task-id" autocomplete="off" placeholder="task-id">
<label for="prospective-id">Выбранное ожидание</label>
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
<label for="capability-id">Возможность</label>
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
<button class="secondary-button" type="submit">Загрузить контекст</button>
<div class="context-status-row">
<span class="context-status-label">ТЕКУЩИЙ КОНТЕКСТ</span>
<strong id="context-state" data-state="empty">не выбран</strong>
</div>
<div class="context-presets">
<label for="context-preset">Недавние наборы</label>
<select id="context-preset">
<option value="">Выберите сохранённый набор…</option>
</select>
<small>Сохраняются только в этом браузере.</small>
</div>
<div class="context-field">
<label for="session-id">Идентификатор сессии <span>обязательно</span></label>
<div class="context-input-row"><input id="session-id" autocomplete="off" spellcheck="false" placeholder="Вставьте session-id"><button class="copy-context" data-copy-context="session-id" type="button" aria-label="Скопировать идентификатор сессии" title="Скопировать">⧉</button></div>
</div>
<div class="context-field">
<label for="workspace-id">Рабочая область <span>обязательно</span></label>
<div class="context-input-row"><input id="workspace-id" autocomplete="off" spellcheck="false" placeholder="Вставьте workspace-id"><button class="copy-context" data-copy-context="workspace-id" type="button" aria-label="Скопировать рабочую область" title="Скопировать">⧉</button></div>
</div>
<p class="context-hint">Для «Состояния» нужны только эти два значения. Задача, ожидание и возможность находятся ниже в расширенных полях.</p>
<details class="advanced-context">
<summary>Расширенные идентификаторы</summary>
<div>
<label for="task-id">Задача</label>
<input id="task-id" autocomplete="off" placeholder="task-id">
<label for="prospective-id">Выбранное ожидание</label>
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
<label for="capability-id">Возможность</label>
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
<label for="delegation-id">Delegation / агентская работа</label>
<input id="delegation-id" autocomplete="off" placeholder="delegation-id из чата">
</div>
</details>
<div class="context-actions"><button class="secondary-button" type="submit">Загрузить контекст</button><button id="reset-context" class="text-button" type="button">Сбросить</button></div>
</form>
<nav class="section-nav" aria-label="Навигация по кабинету">
<a class="section-nav-link active" aria-current="location" href="#overview"><span>01</span><b>Состояние</b><small>фокус и сигналы</small></a>
<a class="section-nav-link" href="#diagnostics"><span>02</span><b>Проверки</b><small>доступность и границы</small></a>
<a class="section-nav-link" href="#operations"><span>03</span><b>Операции</b><small>ожидания и действия</small></a>
<a class="section-nav-link" href="#learning"><span>04</span><b>Опыт</b><small>reflection и навыки</small></a>
<a class="section-nav-link" href="#settings"><span>05</span><b>Настройки</b><small>интерфейс и обновление</small></a>
</nav>
<div class="session-stamp">
<span>ACTOR</span>
<strong id="session-actor">—</strong>
@@ -77,7 +116,7 @@
</aside>
<main class="workspace">
<section class="workspace-head">
<section id="overview" class="workspace-head" data-page="overview">
<div>
<p class="eyebrow">операционная картина</p>
<h1>Состояние сейчас</h1>
@@ -85,14 +124,28 @@
<p id="last-update" class="last-update">Данные ещё не запрошены</p>
</section>
<section class="signal-strip" aria-label="Сводка состояния">
<section class="signal-strip" aria-label="Сводка состояния" data-page="overview">
<div><span>ФОКУС</span><strong id="focus-signal" data-state="idle">ожидает</strong></div>
<div><span>ЗАДАЧА</span><strong id="task-signal" data-state="idle">ожидает</strong></div>
<div><span>CONTINUITY</span><strong id="prospective-signal" data-state="idle">ожидает</strong></div>
<div><span>ВОЗМОЖНОСТЬ</span><strong id="capability-signal" data-state="idle">ожидает</strong></div>
</section>
<section class="advisor-panel" aria-labelledby="advisor-title">
<section id="diagnostics" class="diagnostics-panel" aria-labelledby="diagnostics-title" data-page="diagnostics">
<header class="panel-heading">
<div><p>Проверяемая система</p><h2 id="diagnostics-title">Диагностика кабинета</h2></div>
<div class="panel-heading-tools"><span id="diagnostic-summary">Проверка ещё не запускалась</span><button id="run-diagnostics" class="secondary-button compact-button" type="button">Проверить всё</button></div>
</header>
<div class="diagnostic-intro">
<span class="diagnostic-pulse" aria-hidden="true"></span>
<p>Здесь видны только ответы текущей авторизованной сессии. Проверка не меняет данные и помогает отличить отсутствие контекста от недоступности сервиса.</p>
</div>
<div id="diagnostic-list" class="diagnostic-list" aria-live="polite" aria-busy="false">
<p class="register-empty">Нажмите «Проверить всё», чтобы получить актуальный снимок.</p>
</div>
</section>
<section class="advisor-panel" aria-labelledby="advisor-title" data-page="diagnostics">
<div class="advisor-intro">
<p>COGNITIVE / ADVISORY V1</p>
<h2 id="advisor-title">Спросить Домового</h2>
@@ -120,7 +173,7 @@
</div>
</section>
<section class="register-panel" aria-labelledby="register-title">
<section id="operations" class="register-panel" aria-labelledby="register-title" data-page="operations">
<header>
<div><p>Continuity / ближайшие</p><h2 id="register-title">Реестр ожиданий</h2></div>
<span id="prospective-count">0 ЗАПИСЕЙ</span>
@@ -130,7 +183,31 @@
</div>
</section>
<section class="activity-panel" aria-labelledby="activity-title">
<section class="fleet-panel" aria-labelledby="fleet-title" data-page="operations">
<header>
<div><p>Cognitive / capability health</p><h2 id="fleet-title">Модели и fallback</h2></div>
<div class="activity-tools"><span id="fleet-summary">СОСТОЯНИЕ НЕ ЗАГРУЖЕНО</span><button id="refresh-fleet" class="icon-button" type="button" aria-label="Обновить состояние моделей" title="Обновить состояние моделей">↻</button></div>
</header>
<div class="fleet-overview"><div><span>Выбранный tier</span><strong id="fleet-selected">—</strong></div><p id="fleet-explanation">Проверка доступности провайдеров ещё не выполнялась.</p></div>
<div id="fleet-list" class="fleet-list" aria-live="polite" aria-busy="false"><p class="register-empty">Состояние fleet ещё не загружено.</p></div>
</section>
<section class="agent-panel" aria-labelledby="agent-title" data-page="operations">
<header>
<div><p>Work / durable execution</p><h2 id="agent-title">Агентская работа</h2></div>
<div class="activity-tools"><span id="agent-summary">DELEGATION НЕ ВЫБРАНА</span><button id="refresh-agent" class="icon-button" type="button" aria-label="Обновить агентскую работу" title="Обновить агентскую работу">↻</button></div>
</header>
<div id="agent-empty" class="agent-empty">Укажите delegation ID в расширенных идентификаторах, чтобы прочитать durable state.</div>
<div id="agent-details" class="agent-details" hidden>
<div><span>Состояние delegation</span><strong id="agent-delegation-state">—</strong></div>
<div><span>Run / implementation</span><strong id="agent-run-state">—</strong></div>
<div><span>Последний blocker</span><strong id="agent-blocker">—</strong></div>
<p id="agent-next-step">—</p>
</div>
<div class="agent-worker-status"><span>Executor capability</span><strong id="agent-worker-state">ПРОВЕРКА НЕ ВЫПОЛНЯЛАСЬ</strong><p id="agent-worker-message">Статус worker виден только в авторизованной диагностике.</p></div>
</section>
<section class="activity-panel" aria-labelledby="activity-title" data-page="operations">
<header>
<div><p>Commit / владелец</p><h2 id="activity-title">Журнал действий</h2></div>
<div class="activity-tools">
@@ -143,7 +220,7 @@
</div>
</section>
<section class="learning-panel" aria-labelledby="learning-title">
<section id="learning" class="learning-panel" aria-labelledby="learning-title" data-page="learning">
<header>
<div><p>Learning / evidence</p><h2 id="learning-title">Опыт и reflection</h2></div>
<div class="activity-tools">
@@ -167,7 +244,20 @@
</div>
</section>
<section class="data-grid">
<section id="settings" class="settings-panel" aria-labelledby="settings-title" data-page="settings">
<header class="panel-heading">
<div><p>Локальные предпочтения</p><h2 id="settings-title">Настройки рабочего кабинета</h2></div>
<span id="settings-saved" class="settings-saved">Сохраняются в этом браузере</span>
</header>
<form id="settings-form" class="settings-grid">
<label class="setting-field"><span>Плотность данных</span><small>Выберите, сколько строк помещается на экране.</small><select id="setting-density"><option value="comfortable">Комфортная</option><option value="compact">Компактная</option></select></label>
<label class="setting-field"><span>Автообновление</span><small>Повторная проверка состояния без изменения данных.</small><select id="setting-refresh"><option value="off">Выключено</option><option value="30">Каждые 30 секунд</option><option value="60">Каждую минуту</option><option value="300">Каждые 5 минут</option></select></label>
<label class="setting-check"><input id="setting-motion" type="checkbox"><span><b>Сократить анимацию</b><small>Оставить спокойный режим для длительной работы.</small></span></label>
<div class="settings-actions"><button id="save-settings" class="primary-button compact-button" type="submit"><span>Сохранить настройки</span><span aria-hidden="true">→</span></button><button id="reset-settings" class="text-button" type="button">Сбросить</button></div>
</form>
</section>
<section class="data-grid" data-page="overview">
<article class="data-card focus-card">
<header><span class="card-number">01</span><div><p>Situation</p><h2>Активный фокус</h2></div><span id="focus-badge" class="status-badge neutral">НЕ ЗАГРУЖЕН</span></header>
<dl>
@@ -188,6 +278,7 @@
<div><dt>Ревизия</dt><dd id="task-revision">—</dd></div>
</dl>
<p id="task-message" class="card-message">Укажите задачу для проверки относительно активного фокуса.</p>
<div class="id-actions"><button class="id-action" data-use-value="task-value" data-target-id="task-id" type="button">Использовать ID задачи</button><button class="id-action icon-id-action" data-copy-value="task-value" type="button" aria-label="Скопировать ID задачи" title="Скопировать ID">⧉</button></div>
</article>
<article class="data-card prospective-card">
@@ -205,6 +296,7 @@
<div><dt>Ревизия</dt><dd id="prospective-revision">—</dd></div>
</dl>
<p id="prospective-message" class="card-message">Укажите идентификатор ProspectiveItem для проверки владельца и состояния.</p>
<div class="id-actions"><button class="id-action" data-use-value="prospective-value" data-target-id="prospective-id" type="button">Использовать ID ожидания</button><button class="id-action icon-id-action" data-copy-value="prospective-value" type="button" aria-label="Скопировать ID ожидания" title="Скопировать ID">⧉</button></div>
<section class="commit-zone" aria-labelledby="commit-zone-title">
<div class="commit-zone-copy">
<p>Commit / контролируемый переход</p>
@@ -240,6 +332,7 @@
<div><dt>Ревизия</dt><dd id="capability-revision">—</dd></div>
<div><dt>Наблюдалось</dt><dd id="capability-observed">—</dd></div>
</dl>
<div class="id-actions"><button class="id-action" data-use-value="capability-value" data-target-id="capability-id" type="button">Использовать ID возможности</button><button class="id-action icon-id-action" data-copy-value="capability-value" type="button" aria-label="Скопировать ID возможности" title="Скопировать ID">⧉</button></div>
</article>
</section>
</main>
@@ -286,6 +379,6 @@
</dialog>
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
<script src="/assets/console.js" defer></script>
<script src="/assets/console.js?v=20260821-context" defer></script>
</body>
</html>
+1 -1
View File
@@ -166,7 +166,7 @@ func loadSelectedTask(root string, selection taskSelection) (TaskPacket, string,
func loadTaskPacket(root, taskPath string) (TaskPacket, error) {
var taskPacket TaskPacket
if err := decodeJSONFile(filepath.Join(root, filepath.FromSlash(taskPath)), &taskPacket, false); err != nil {
if err := decodeJSONFile(filepath.Join(root, filepath.FromSlash(taskPath)), &taskPacket, true); err != nil {
return TaskPacket{}, fmt.Errorf("task packet: %w", err)
}
return taskPacket, nil
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
var required = []string{
"task_id", "task_revision", "architecture_revision", "base_repo_revision", "risk_tier", "intent",
"requirement_ids", "allowed_change_scope", "forbidden_change_scope", "allowed_architecture_delta",
"contracts", "semantic_owner", "canonical_state", "state_transitions", "side_effects_and_privileges",
"persistence_and_migrations", "failure_retry_restart", "assumptions", "acceptance_scenarios",
"required_tests", "terminal_gap_states",
}
func main() {
activePath := filepath.FromSlash("dev/ACTIVE_TASK.json")
var active struct {
TaskFile string `json:"task_file"`
TaskID string `json:"task_id"`
TaskRevision int `json:"task_revision"`
}
if err := decode(activePath, &active); err != nil {
fail("active pointer: %v", err)
}
if active.TaskFile == "" || active.TaskID == "" || active.TaskRevision < 1 {
fail("active pointer is incomplete")
}
var raw map[string]json.RawMessage
if err := decode(filepath.FromSlash(active.TaskFile), &raw); err != nil {
fail("task packet: %v", err)
}
for _, key := range required {
if _, ok := raw[key]; !ok {
fail("task packet missing required field %q", key)
}
}
var packet struct {
TaskID string `json:"task_id"`
TaskRevision int `json:"task_revision"`
}
if err := decode(filepath.FromSlash(active.TaskFile), &packet); err != nil {
fail("task identity: %v", err)
}
if packet.TaskID != active.TaskID || packet.TaskRevision != active.TaskRevision {
fail("active pointer and packet identity differ")
}
fmt.Printf("independent governance verification: OK task=%s revision=%d\n", packet.TaskID, packet.TaskRevision)
}
func decode(path string, target any) error {
b, err := os.ReadFile(path)
if err != nil {
return err
}
return json.Unmarshal(b, target)
}
func fail(format string, args ...any) {
fmt.Fprintf(os.Stderr, "independent verifier: "+format+"\n", args...)
os.Exit(1)
}
+32 -11
View File
@@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"go/parser"
"go/token"
@@ -24,19 +23,41 @@ type Registry struct {
BoundaryClass string `json:"boundary_class"`
MayDependOn []string `json:"may_depend_on"`
Forbidden []string `json:"forbidden"`
Privileged bool `json:"privileged"`
} `json:"modules"`
ProtectedPaths []string `json:"protected_paths"`
}
type TaskPacket struct {
TaskID string `json:"task_id"`
TaskRevision int `json:"task_revision"`
BaseRepoRevision string `json:"base_repo_revision"`
RiskTier string `json:"risk_tier"`
Intent string `json:"intent"`
AllowedChangeScope struct {
Paths []string `json:"paths"`
TaskID string `json:"task_id"`
TaskRevision int `json:"task_revision"`
ArchitectureRevision string `json:"architecture_revision"`
BaseRepoRevision string `json:"base_repo_revision"`
RiskTier string `json:"risk_tier"`
Intent string `json:"intent"`
RequirementIDs []string `json:"requirement_ids"`
AllowedChangeScope struct {
Modules []string `json:"modules"`
Paths []string `json:"paths"`
} `json:"allowed_change_scope"`
ForbiddenChangeScope struct {
Paths []string `json:"paths"`
} `json:"forbidden_change_scope"`
AllowedArchitectureDelta map[string]int `json:"allowed_architecture_delta"`
Contracts struct {
Consume []string `json:"consume"`
MayChange []string `json:"may_change"`
} `json:"contracts"`
SemanticOwner string `json:"semantic_owner"`
CanonicalState []string `json:"canonical_state"`
StateTransitions []string `json:"state_transitions"`
SideEffectsAndPrivileges []string `json:"side_effects_and_privileges"`
PersistenceAndMigrations []string `json:"persistence_and_migrations"`
FailureRetryRestart []string `json:"failure_retry_restart"`
Assumptions []string `json:"assumptions"`
AcceptanceScenarios []string `json:"acceptance_scenarios"`
RequiredTests []string `json:"required_tests"`
TerminalGapStates []string `json:"terminal_gap_states"`
}
func main() {
@@ -207,12 +228,12 @@ func preflight(args []string) error {
if err != nil {
return err
}
taskPacket, _, _, err := loadSelectedTask(root, selection)
taskPacket, taskPath, _, err := loadSelectedTask(root, selection)
if err != nil {
return err
}
if taskPacket.TaskID == "" || taskPacket.Intent == "" || taskPacket.RiskTier == "" {
return errors.New("task packet missing task_id, intent, or risk_tier")
if err := validateTaskPacket(root, taskPacket, taskPath); err != nil {
return err
}
registry, err := loadRegistry()
if err != nil {
+215
View File
@@ -0,0 +1,215 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
var requiredPacketKeys = []string{
"task_id", "task_revision", "architecture_revision", "base_repo_revision", "risk_tier", "intent",
"requirement_ids", "allowed_change_scope", "forbidden_change_scope", "allowed_architecture_delta",
"contracts", "semantic_owner", "canonical_state", "state_transitions", "side_effects_and_privileges",
"persistence_and_migrations", "failure_retry_restart", "assumptions", "acceptance_scenarios",
"required_tests", "terminal_gap_states",
}
var requiredDeltaKeys = []string{
"new_cross_module_dependencies", "new_canonical_owners", "new_privileges",
"new_third_party_dependencies", "new_database_owner_roles",
}
func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
if task.TaskID == "" || task.TaskRevision < 1 || task.ArchitectureRevision == "" || task.BaseRepoRevision == "" || task.RiskTier == "" || task.Intent == "" {
return fmt.Errorf("task %s is incomplete: identity, revision, architecture_revision, base_repo_revision, risk_tier and intent are required", task.TaskID)
}
if taskPath == "" || !matchesAny(taskPath, mustCompile([]string{"dev/tasks/*.json"})) {
return fmt.Errorf("task %s must be selected from dev/tasks", task.TaskID)
}
if task.RiskTier < "A" || task.RiskTier > "E" {
return fmt.Errorf("task %s has invalid risk_tier %q", task.TaskID, task.RiskTier)
}
if len(task.RequirementIDs) == 0 || len(task.AcceptanceScenarios) == 0 || len(task.RequiredTests) == 0 || len(task.TerminalGapStates) == 0 {
return fmt.Errorf("task %s must declare requirements, acceptance_scenarios, required_tests and terminal_gap_states", task.TaskID)
}
if len(task.AllowedChangeScope.Paths) == 0 {
return fmt.Errorf("task %s must declare allowed_change_scope.paths", task.TaskID)
}
if err := validateScopeOverlap(task.AllowedChangeScope.Paths, task.ForbiddenChangeScope.Paths); err != nil {
return fmt.Errorf("task %s scope: %w", task.TaskID, err)
}
if err := validateDelta(task.AllowedArchitectureDelta); err != nil {
return fmt.Errorf("task %s architecture delta: %w", task.TaskID, err)
}
if err := validateRequiredTests(task.RequiredTests); err != nil {
return fmt.Errorf("task %s required_tests: %w", task.TaskID, err)
}
if err := validateArchitectureRevision(root, task.ArchitectureRevision); err != nil {
return fmt.Errorf("task %s architecture_revision: %w", task.TaskID, err)
}
if _, err := resolveBaseCommit(root, task.BaseRepoRevision); err != nil {
return fmt.Errorf("task %s base_repo_revision: %w", task.TaskID, err)
}
if err := validateObservedDelta(root, task); err != nil {
return fmt.Errorf("task %s observed architecture delta: %w", task.TaskID, err)
}
return nil
}
func validateRequiredTests(tests []string) error {
joined := strings.Join(tests, "\n")
for _, required := range []string{"go test", "go vet", "preflight", "check-diff", "git diff --check", "independent"} {
if !strings.Contains(joined, required) {
return fmt.Errorf("missing required governance evidence %q", required)
}
}
return nil
}
func validateObservedDelta(root string, task TaskPacket) error {
changed, err := changedPaths(root, task.BaseRepoRevision)
if err != nil {
return err
}
if err := validateRegistryDelta(root, task); err != nil {
return err
}
for _, changedPath := range changed {
if (strings.HasPrefix(changedPath, "security/") || strings.HasPrefix(changedPath, "deploy/")) && task.AllowedArchitectureDelta["new_privileges"] == 0 {
return fmt.Errorf("privileged surface changed with new_privileges=0: %s", changedPath)
}
if strings.HasPrefix(changedPath, "database/") && task.AllowedArchitectureDelta["new_database_owner_roles"] == 0 {
return fmt.Errorf("database surface changed with new_database_owner_roles=0: %s", changedPath)
}
if (changedPath == "go.mod" || changedPath == "go.sum") && task.AllowedArchitectureDelta["new_third_party_dependencies"] == 0 {
return fmt.Errorf("dependency manifest changed with new_third_party_dependencies=0: %s", changedPath)
}
if strings.HasPrefix(changedPath, "contracts/") && len(task.Contracts.MayChange) == 0 {
return fmt.Errorf("contract surface changed but contracts.may_change is empty: %s", changedPath)
}
}
return nil
}
func validateRegistryDelta(root string, task TaskPacket) error {
baseBytes, err := runGit(root, "show", task.BaseRepoRevision+":architecture/architecture-registry.json")
if err != nil {
return fmt.Errorf("read base architecture registry: %w", err)
}
currentBytes, err := os.ReadFile(filepath.Join(root, "architecture", "architecture-registry.json"))
if err != nil {
return err
}
var base, current Registry
if err := json.Unmarshal(baseBytes, &base); err != nil {
return fmt.Errorf("parse base architecture registry: %w", err)
}
if err := json.Unmarshal(currentBytes, &current); err != nil {
return fmt.Errorf("parse current architecture registry: %w", err)
}
if task.AllowedArchitectureDelta["new_canonical_owners"] == 0 {
for name := range current.Modules {
if _, ok := base.Modules[name]; !ok {
return fmt.Errorf("new canonical module owner %q with new_canonical_owners=0", name)
}
}
}
if task.AllowedArchitectureDelta["new_cross_module_dependencies"] == 0 {
for name, module := range current.Modules {
previous, ok := base.Modules[name]
if !ok {
continue
}
old := map[string]bool{}
for _, dep := range previous.MayDependOn {
old[dep] = true
}
for _, dep := range module.MayDependOn {
if !old[dep] {
return fmt.Errorf("new dependency %q in module %q with new_cross_module_dependencies=0", dep, name)
}
}
}
}
if task.AllowedArchitectureDelta["new_privileges"] == 0 {
for name, module := range current.Modules {
if previous, ok := base.Modules[name]; ok && module.Privileged && !previous.Privileged {
return fmt.Errorf("new privilege in module %q with new_privileges=0", name)
}
}
}
return nil
}
func validateDelta(delta map[string]int) error {
if len(delta) != len(requiredDeltaKeys) {
return fmt.Errorf("must contain exactly %s", strings.Join(requiredDeltaKeys, ", "))
}
for _, key := range requiredDeltaKeys {
value, ok := delta[key]
if !ok || value < 0 {
return fmt.Errorf("%s must be a non-negative integer", key)
}
}
return nil
}
func validateArchitectureRevision(root, revision string) error {
if strings.HasPrefix(revision, "ADR-") {
matches, err := filepath.Glob(filepath.Join(root, "architecture", "adrs", revision+"-*.md"))
if err != nil || len(matches) != 1 {
return fmt.Errorf("accepted ADR %q is missing or ambiguous", revision)
}
return nil
}
if strings.HasPrefix(revision, "ARCH-") {
matches, err := filepath.Glob(filepath.Join(root, "dev", "tasks", revision+"*.json"))
if err != nil || len(matches) == 0 {
return fmt.Errorf("architecture task %q is missing", revision)
}
return nil
}
return fmt.Errorf("must reference an accepted ADR-NNNN or architecture task")
}
func validateScopeOverlap(allowed, forbidden []string) error {
allowedPatterns, err := compileScopePatterns(allowed)
if err != nil {
return err
}
forbiddenPatterns, err := compileScopePatterns(forbidden)
if err != nil {
return fmt.Errorf("forbidden_change_scope: %w", err)
}
for _, a := range allowedPatterns {
for _, f := range forbiddenPatterns {
if scopePatternsOverlap(a, f) {
return fmt.Errorf("allowed path %q overlaps forbidden path %q", a.raw, f.raw)
}
}
}
return nil
}
func scopePatternsOverlap(a, b scopePattern) bool {
if a.subtree != "" && b.subtree != "" {
return a.subtree == b.subtree || strings.HasPrefix(a.subtree, b.subtree+"/") || strings.HasPrefix(b.subtree, a.subtree+"/")
}
if a.subtree != "" {
return b.raw == a.subtree || strings.HasPrefix(b.raw, a.subtree+"/")
}
if b.subtree != "" {
return a.raw == b.subtree || strings.HasPrefix(a.raw, b.subtree+"/")
}
return a.raw == b.raw
}
func mustCompile(values []string) []scopePattern {
patterns, err := compileScopePatterns(values)
if err != nil {
panic(err)
}
return patterns
}
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"strings"
"testing"
)
func TestValidateScopeOverlapRejectsAllowedForbiddenConflict(t *testing.T) {
err := validateScopeOverlap([]string{"cmd/creator-web/server.go"}, []string{"cmd/creator-web/**"})
if err == nil || !strings.Contains(err.Error(), "overlaps forbidden") {
t.Fatalf("overlap error = %v", err)
}
}
func TestValidateRequiredTestsRejectsIncompleteEvidence(t *testing.T) {
err := validateRequiredTests([]string{"go test ./..."})
if err == nil || !strings.Contains(err.Error(), "missing required governance evidence") {
t.Fatalf("required test error = %v", err)
}
}
func TestValidateDeltaRejectsUnknownOrMissingFields(t *testing.T) {
err := validateDelta(map[string]int{"new_privileges": 0})
if err == nil || !strings.Contains(err.Error(), "exactly") {
t.Fatalf("delta error = %v", err)
}
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
func main() {
workerID := flag.String("worker-id", "reflection-worker-1", "stable worker identity")
lease := flag.Duration("lease-ttl", 5*time.Minute, "candidate claim lease")
pollInterval := flag.Duration("poll-interval", 15*time.Second, "database-owned idle wake-up interval")
once := flag.Bool("once", false, "run one cycle and exit")
flag.Parse()
ctx := context.Background()
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if err != nil {
panic(err)
}
config := postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("EPISTEMICS_POSTGRES_USER"), Password: password}
repository, err := postgres.OpenEpistemics(ctx, config)
if err != nil {
panic(err)
}
defer repository.Close()
for {
now := time.Now().UnixMilli()
if _, err := repository.RecoverReflectionClaims(ctx, now); err != nil {
panic(err)
}
candidate, claimErr := repository.ClaimReflectionCandidate(ctx, *workerID, now, lease.Milliseconds())
if claimErr == nil {
// This worker only closes the durable evaluation loop. Any cognitive
// proposal remains a candidate and must be admitted by a separate governed flow.
if err := repository.ResolveReflectionCandidate(ctx, candidate.ID, *workerID, time.Now().UnixMilli()); err != nil {
panic(err)
}
fmt.Printf("resolved reflection candidate %s\n", candidate.ID)
} else if *once {
fmt.Println("no reflection candidate")
}
if *once {
return
}
if err := repository.WaitForReflectionCandidate(ctx, *pollInterval); err != nil {
panic(err)
}
}
}
func resolveDatabasePassword(name, refName string) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
}
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
if path == "" || ref == "" {
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
}
+126
View File
@@ -0,0 +1,126 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
researchadapter "git.cin.su/m/Assistant/adapters/research"
"git.cin.su/m/Assistant/contracts/epistemics"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/modules/epistemics/application"
"git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/runtime/health"
)
func main() {
ctx := context.Background()
password, err := resolveDatabasePassword()
if err != nil {
log.Fatal(err)
}
repository, err := postgres.OpenEpistemics(ctx, postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("EPISTEMICS_POSTGRES_USER"), Password: password})
if err != nil {
log.Fatal(err)
}
defer repository.Close()
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "research-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "research-runtime", Probes: []health.Probe{{Name: "epistemics-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
mux.HandleFunc("POST /v1/research/evaluate", func(w http.ResponseWriter, r *http.Request) {
var request epistemics.ResearchEvaluationRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<20)).Decode(&request); err != nil {
http.Error(w, "invalid research evidence", http.StatusBadRequest)
return
}
result, err := application.EvaluateAndPersist(r.Context(), request, repository, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
connector := researchadapter.RepositoryConnector{Root: os.Getenv("RESEARCH_SOURCE_ROOT")}
mux.HandleFunc("POST /v1/research/repository/evaluate", func(w http.ResponseWriter, r *http.Request) {
var request epistemics.RepositoryResearchRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10)).Decode(&request); err != nil {
http.Error(w, "invalid repository research request", http.StatusBadRequest)
return
}
candidate, err := connector.Collect(r.Context(), request, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
result, err := application.EvaluateAndPersist(r.Context(), epistemics.ResearchEvaluationRequest{Plan: request.Plan, Candidates: []epistemics.ResearchCandidate{candidate}}, repository, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
webConnector := researchadapter.HTTPConnector{AllowedHosts: allowedHosts(os.Getenv("RESEARCH_HTTP_ALLOWED_HOSTS")), Client: &http.Client{Timeout: 5 * time.Second}}
mux.HandleFunc("POST /v1/research/web/evaluate", func(w http.ResponseWriter, r *http.Request) {
var request epistemics.WebResearchRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10)).Decode(&request); err != nil {
http.Error(w, "invalid web research request", http.StatusBadRequest)
return
}
candidate, err := webConnector.Collect(r.Context(), request, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
result, err := application.EvaluateAndPersist(r.Context(), epistemics.ResearchEvaluationRequest{Plan: request.Plan, Candidates: []epistemics.ResearchCandidate{candidate}}, repository, time.Now().UnixMilli())
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8087"
}
log.Printf("research-runtime listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
func allowedHosts(value string) map[string]struct{} {
result := map[string]struct{}{}
for _, host := range strings.Split(value, ",") {
if host = strings.ToLower(strings.TrimSpace(host)); host != "" {
result[host] = struct{}{}
}
}
return result
}
func resolveDatabasePassword() (string, error) {
if _, ok := os.LookupEnv("EPISTEMICS_POSTGRES_PASSWORD"); ok {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD direct environment secret is forbidden")
}
path, ref := os.Getenv("EPISTEMICS_POSTGRES_PASSWORD_FILE"), os.Getenv("EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if path == "" || ref == "" {
return "", fmt.Errorf("EPISTEMICS_POSTGRES_PASSWORD_FILE and EPISTEMICS_POSTGRES_CREDENTIAL_REF are required")
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := secrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
}
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"git.cin.su/m/Assistant/adapters/postgres"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/modules/work/application"
"git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/runtime/health"
)
func main() {
ctx := context.Background()
password, err := resolveDatabasePassword()
if err != nil {
log.Fatal(err)
}
repository, err := postgres.OpenWork(ctx, postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: password})
if err != nil {
log.Fatal(err)
}
defer repository.Close()
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "work-graph-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "work-graph-runtime", Probes: []health.Probe{{Name: "work-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
mux.HandleFunc("POST /v1/work-graphs/schedule", func(w http.ResponseWriter, r *http.Request) {
var request application.GraphScheduleRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
http.Error(w, "invalid graph schedule request", http.StatusBadRequest)
return
}
graph, ready, err := application.AdmitAndSchedule(r.Context(), repository, request)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
})
mux.HandleFunc("POST /v1/work-graphs/complete", func(w http.ResponseWriter, r *http.Request) {
var request application.GraphCompleteRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
http.Error(w, "invalid graph completion request", http.StatusBadRequest)
return
}
graph, err := application.CompleteGraphNode(r.Context(), repository, request.GraphID, request.Revision, request.NodeID, request.Succeeded)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, graph)
})
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8088"
}
log.Printf("work-graph-runtime listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
func resolveDatabasePassword() (string, error) {
if _, ok := os.LookupEnv("WORK_POSTGRES_PASSWORD"); ok {
return "", fmt.Errorf("WORK_POSTGRES_PASSWORD direct environment secret is forbidden")
}
path, ref := os.Getenv("WORK_POSTGRES_PASSWORD_FILE"), os.Getenv("WORK_POSTGRES_CREDENTIAL_REF")
if path == "" || ref == "" {
return "", fmt.Errorf("WORK_POSTGRES_PASSWORD_FILE and WORK_POSTGRES_CREDENTIAL_REF are required")
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := secrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerWorkWorker, Path: path})
}
+19
View File
@@ -2,4 +2,23 @@ package artifact
import "git.cin.su/m/Assistant/platform/ids"
import "errors"
type ArtifactID ids.ID
type AgentArtifactAttestation struct {
ArtifactID ArtifactID
ProducerRunID string
SourceRevision string
WorktreeID string
ContentDigest string
TestEvidenceRef string
CreatedAtUnixMillis int64
}
func (a AgentArtifactAttestation) Validate() error {
if a.ArtifactID == "" || a.ProducerRunID == "" || a.SourceRevision == "" || a.WorktreeID == "" || a.ContentDigest == "" || a.TestEvidenceRef == "" || a.CreatedAtUnixMillis < 1 {
return errors.New("agent artifact attestation is incomplete")
}
return nil
}
+73
View File
@@ -0,0 +1,73 @@
package capability
type AdmissionComplexity string
type AdmissionDecisionState string
type AdmissionReason string
const (
AdmissionSimple AdmissionComplexity = "SIMPLE"
AdmissionNormal AdmissionComplexity = "NORMAL"
AdmissionHeavy AdmissionComplexity = "HEAVY"
AdmissionAdmitted AdmissionDecisionState = "ADMITTED"
AdmissionRejected AdmissionDecisionState = "REJECTED"
AdmissionReasonNone AdmissionReason = "NONE"
AdmissionReasonConcurrency AdmissionReason = "CONCURRENCY_LIMIT"
AdmissionReasonAcceptedHour AdmissionReason = "ACCEPTED_PER_HOUR_LIMIT"
AdmissionReasonDeadline AdmissionReason = "DEADLINE"
AdmissionReasonInputBudget AdmissionReason = "INPUT_BUDGET"
AdmissionReasonOutputBudget AdmissionReason = "OUTPUT_BUDGET"
)
type AdmissionPolicy struct {
ID string
MaxConcurrent int
MaxAcceptedPerHour int
MaxInputBytes int
MaxOutputBytes int
}
type AdmissionRequest struct {
Complexity AdmissionComplexity `json:"complexity"`
InputBytes int `json:"input_bytes"`
MaxOutputBytes int `json:"max_output_bytes"`
DeadlineMillis int64 `json:"deadline_millis"`
}
type AdmissionCounters struct {
Concurrent int
AcceptedLastHour int
}
type AdmissionDecision struct {
State AdmissionDecisionState `json:"state"`
Reason AdmissionReason `json:"reason"`
PolicyID string `json:"policy_id"`
}
type AdmissionLease struct {
ID string `json:"id"`
PolicyID string `json:"policy_id"`
}
func Admit(request AdmissionRequest, policy AdmissionPolicy, counters AdmissionCounters, nowMillis int64) AdmissionDecision {
decision := AdmissionDecision{State: AdmissionAdmitted, Reason: AdmissionReasonNone, PolicyID: policy.ID}
if policy.ID == "" || policy.MaxConcurrent < 1 || policy.MaxAcceptedPerHour < 1 || policy.MaxInputBytes < 1 || policy.MaxOutputBytes < 1 {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonInputBudget, PolicyID: policy.ID}
}
if counters.Concurrent >= policy.MaxConcurrent {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonConcurrency, PolicyID: policy.ID}
}
if counters.AcceptedLastHour >= policy.MaxAcceptedPerHour {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonAcceptedHour, PolicyID: policy.ID}
}
if request.InputBytes < 0 || request.InputBytes > policy.MaxInputBytes {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonInputBudget, PolicyID: policy.ID}
}
if request.MaxOutputBytes < 1 || request.MaxOutputBytes > policy.MaxOutputBytes {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonOutputBudget, PolicyID: policy.ID}
}
if request.DeadlineMillis > 0 && request.DeadlineMillis <= nowMillis {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonDeadline, PolicyID: policy.ID}
}
return decision
}
+71 -19
View File
@@ -23,6 +23,9 @@ type OperationalState string
type EvaluationResult string
type EvaluationID ids.ID
type PerformanceEvidenceID ids.ID
type BillingMode string
type AuthMode string
type SelectionObjective string
const (
QualificationDiscovered QualificationState = "DISCOVERED"
@@ -52,6 +55,24 @@ const (
EvaluationUnknown EvaluationResult = "UNKNOWN"
EvaluationSupported EvaluationResult = "SUPPORTED"
EvaluationUnsupported EvaluationResult = "UNSUPPORTED"
BillingUnknown BillingMode = "UNKNOWN"
BillingFree BillingMode = "FREE"
BillingPerInput BillingMode = "PER_INPUT_UNIT"
BillingPerOutput BillingMode = "PER_OUTPUT_UNIT"
BillingPerRequest BillingMode = "PER_REQUEST"
AuthNone AuthMode = "NONE"
AuthKey AuthMode = "API" + "_KEY"
AuthBearer AuthMode = "BEARER"
AuthUsernamePassword AuthMode = "USERNAME_PASSWORD"
AuthOAuth AuthMode = "OAUTH"
AuthSecretFile AuthMode = "SECRET_FILE"
SelectionBalanced SelectionObjective = "BALANCED"
SelectionQuality SelectionObjective = "QUALITY"
SelectionSpeed SelectionObjective = "SPEED"
SelectionEconomy SelectionObjective = "ECONOMY"
)
// ProviderInstance contains only opaque references to endpoint and credentials.
@@ -66,20 +87,26 @@ type ProviderInstance struct {
Qualification QualificationState
Health HealthState
ObservedAtUnixMillis int64
BillingMode BillingMode
AuthMode AuthMode
InputCostMicros *int64
OutputCostMicros *int64
RequestCostMicros *int64
}
type ModelDescriptor struct {
ID ModelDescriptorID
Revision int64
ProviderInstanceID ProviderInstanceID
ProviderModelID ProviderModelID
Fingerprint ModelFingerprint
MaxContextBytes int64
Modalities []ModelModality
Capabilities []ModelCapability
Qualification QualificationState
Health HealthState
ObservedAtUnixMillis int64
ID ModelDescriptorID
Revision int64
ProviderInstanceID ProviderInstanceID
ProviderModelID ProviderModelID
Fingerprint ModelFingerprint
MaxContextBytes int64
Modalities []ModelModality
Capabilities []ModelCapability
Qualification QualificationState
Health HealthState
ObservedAtUnixMillis int64
IntelligenceScoreMillis *int64
}
type ProviderHealthObservation struct {
@@ -125,6 +152,8 @@ type CognitiveRequirement struct {
InputBytes int64
MaxOutputBytes int64
ExcludedBindingIDs []CapabilityBindingID
Objective SelectionObjective
MaxCostMicros *int64
}
// ResolvedCognitiveBinding is a provider-neutral invocation snapshot selected
@@ -206,6 +235,23 @@ type CognitiveCanaryPromotion struct {
Evaluation EvaluatedCapability
}
type CognitiveActivationPromotionRequest struct {
PromotionID ids.ID
RequestedByPrincipalID identity.PrincipalID
CapabilityID CapabilityID
BindingID CapabilityBindingID
ExpectedBindingRevision int64
EvaluationID EvaluationID
ObservedAtUnixMillis int64
}
type CognitiveActivationPromotion struct {
PromotionID ids.ID
ExpectedBindingRevision int64
Binding CognitiveBinding
Evaluation EvaluatedCapability
}
// CognitiveDiscoverySnapshot is provider-neutral observed inventory submitted
// to the Capability owner. The owner assigns immutable revisions and always
// registers discovered bindings as DISCOVERED/INACTIVE.
@@ -222,18 +268,24 @@ type CognitiveDiscoverySnapshot struct {
Health HealthState
FailureClass CognitiveFailureType
ObservedAtUnixMillis int64
BillingMode BillingMode
AuthMode AuthMode
InputCostMicros *int64
OutputCostMicros *int64
RequestCostMicros *int64
Models []DiscoveredModelSnapshot
}
type DiscoveredModelSnapshot struct {
ProviderModelID ProviderModelID
Fingerprint ModelFingerprint
MaxContextBytes int64
Modalities []ModelModality
Capabilities []ModelCapability
Installed OperationalState
Loaded OperationalState
Invokable OperationalState
ProviderModelID ProviderModelID
Fingerprint ModelFingerprint
MaxContextBytes int64
Modalities []ModelModality
Capabilities []ModelCapability
Installed OperationalState
Loaded OperationalState
Invokable OperationalState
IntelligenceScoreMillis *int64
}
type CognitiveUsageObservation struct {
+25
View File
@@ -0,0 +1,25 @@
package capability
type FleetTier string
const (
FleetTierFast FleetTier = "FAST"
FleetTierMedium FleetTier = "MEDIUM"
FleetTierHeavy FleetTier = "HEAVY"
FleetTierLifeboat FleetTier = "LIFEBOAT"
)
type FleetMember struct {
ProviderInstanceID ProviderInstanceID `json:"provider_instance_id"`
ModelDescriptorID ModelDescriptorID `json:"model_descriptor_id"`
Tier FleetTier `json:"tier"`
Ready bool `json:"ready"`
Reason string `json:"reason"`
}
type FleetReadiness struct {
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
Members []FleetMember `json:"members"`
ReadyTiers []FleetTier `json:"ready_tiers"`
FallbackTier FleetTier `json:"fallback_tier"`
}
+72
View File
@@ -0,0 +1,72 @@
package capability
import "errors"
type QualificationManifestID string
type QualificationDimension string
const (
DimensionDialogue QualificationDimension = "DIALOGUE"
DimensionInterpretation QualificationDimension = "INTERPRETATION"
DimensionCoding QualificationDimension = "CODING"
DimensionResearch QualificationDimension = "RESEARCH"
DimensionUncertainty QualificationDimension = "UNCERTAINTY"
DimensionContext QualificationDimension = "CONTEXT"
DimensionLatency QualificationDimension = "LATENCY"
DimensionResources QualificationDimension = "RESOURCES"
DimensionRestart QualificationDimension = "RESTART"
)
type QualificationMeasurement struct {
Dimension QualificationDimension
Result EvaluationResult
Artifact string
}
type QualificationManifest struct {
ID QualificationManifestID
ModelDescriptorID ModelDescriptorID
ModelRevision int64
ModelFingerprint ModelFingerprint
BenchmarkVersion string
HoldoutCorpusDigest string
LicenseEvidence string
DataPolicyEvidence string
Measurements []QualificationMeasurement
ObservedAtUnixMillis int64
}
func (m QualificationManifest) Validate() error {
if m.ID == "" || m.ModelDescriptorID == "" || m.ModelRevision < 1 || m.ModelFingerprint == "" || m.BenchmarkVersion == "" || m.HoldoutCorpusDigest == "" || m.LicenseEvidence == "" || m.DataPolicyEvidence == "" || m.ObservedAtUnixMillis < 1 {
return errors.New("qualification manifest identity and evidence are incomplete")
}
required := map[QualificationDimension]bool{DimensionDialogue: false, DimensionInterpretation: false, DimensionCoding: false, DimensionResearch: false, DimensionUncertainty: false, DimensionContext: false, DimensionLatency: false, DimensionResources: false, DimensionRestart: false}
for _, measurement := range m.Measurements {
if measurement.Dimension == "" || measurement.Artifact == "" || measurement.Result == EvaluationUnknown {
return errors.New("qualification measurement is incomplete")
}
if _, ok := required[measurement.Dimension]; ok {
required[measurement.Dimension] = true
}
}
for _, present := range required {
if !present {
return errors.New("qualification manifest lacks required measurement")
}
}
return nil
}
// QualificationState projects evidence into a qualification result. It does
// not activate a binding or mutate the model registry.
func (m QualificationManifest) QualificationState() QualificationState {
if m.Validate() != nil {
return QualificationRejected
}
for _, measurement := range m.Measurements {
if measurement.Result != EvaluationSupported {
return QualificationRejected
}
}
return QualificationValidated
}
+91
View File
@@ -0,0 +1,91 @@
package cognitive
import (
"errors"
"sort"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
var ErrInvalidContextBudget = errors.New("context budget must be positive")
type ContextCandidate struct {
Excerpt ContextExcerpt
Relevance int
}
type ContextCompileInput struct {
Candidates []ContextCandidate
DataPolicy capability.DataPolicy
MaxBytes int
}
type ContextOmission struct {
ID ContextExcerptID
Reason string
}
type ContextBundle struct {
Excerpts []ContextExcerpt
Omissions []ContextOmission
Bytes int
}
// CompileContext deterministically selects permitted, relevant excerpts for a
// gateway request. It carries no provider identity and performs no I/O.
func CompileContext(input ContextCompileInput) (ContextBundle, error) {
if input.MaxBytes <= 0 {
return ContextBundle{}, ErrInvalidContextBudget
}
candidates := append([]ContextCandidate(nil), input.Candidates...)
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].Relevance != candidates[j].Relevance {
return candidates[i].Relevance > candidates[j].Relevance
}
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
})
bundle := ContextBundle{Excerpts: []ContextExcerpt{}, Omissions: []ContextOmission{}}
seen := make(map[ContextExcerptID]struct{}, len(candidates))
for _, candidate := range candidates {
excerpt := candidate.Excerpt
if _, ok := seen[excerpt.ID]; ok {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "duplicate"})
continue
}
seen[excerpt.ID] = struct{}{}
reason := permittedContextReason(excerpt, input.DataPolicy)
if reason != "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
continue
}
if strings.TrimSpace(excerpt.Text) == "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "empty"})
continue
}
bytes := len([]byte(excerpt.Text))
if bundle.Bytes+bytes > input.MaxBytes {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "budget"})
continue
}
bundle.Excerpts = append(bundle.Excerpts, excerpt)
bundle.Bytes += bytes
}
return bundle, nil
}
func permittedContextReason(excerpt ContextExcerpt, policy capability.DataPolicy) string {
if excerpt.Sensitivity == SensitivitySecret {
return "secret"
}
if excerpt.Sensitivity == SensitivityUnknown {
return "unknown_sensitivity"
}
if policy == capability.DataPolicyExternalMessageOnly {
return "policy_message_only"
}
if policy == capability.DataPolicyExternalMinimizedContext && excerpt.Sensitivity != SensitivityPublic {
return "policy_public_only"
}
return ""
}
+62
View File
@@ -4,6 +4,7 @@ import (
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/platform/ids"
)
@@ -79,6 +80,19 @@ type GatewayAdvisoryRequest struct {
MaxOutputBytes int
DataPolicy capability.DataPolicy
EgressAcknowledged bool
Meaning TaskMeaning
}
// TaskMeaning is the trusted semantic summary produced by Core. It is not a
// browser-controlled provider selector; the Capability owner still resolves
// the actual binding.
type TaskMeaning struct {
SpeechAct string
CapabilityKind capability.CapabilityKind
CapabilityHints []capability.CapabilityKind
RequestedOutcome string
InformationRequestKind interaction.InformationRequestKind
ConfidenceMillis int64
}
type GatewayAdvisoryPolicyRequest struct {
@@ -97,6 +111,54 @@ type ProviderUsage struct {
OutputUnits int64
}
type InterpretationApplicability string
const (
InterpretationApplicable InterpretationApplicability = "APPLICABLE"
InterpretationLowConfidence InterpretationApplicability = "LOW_CONFIDENCE"
InterpretationMixedIntent InterpretationApplicability = "MIXED_INTENT"
InterpretationNeedsBroader InterpretationApplicability = "NEEDS_BROADER_INTERPRETATION"
InterpretationClarificationRequired InterpretationApplicability = "CLARIFICATION_REQUIRED"
)
type GatewayInterpretationRequest struct {
Version ContractVersion
RequestID RequestID
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
Message string
Locale string
MaxOutputBytes int
DataPolicy capability.DataPolicy
DeterministicSpeechAct string
DeterministicSecondary string
DeterministicConfidence int64
DeterministicApplicability InterpretationApplicability
RequestedOutcome string
ProjectNames []string
}
type GatewayInterpretationCandidate struct {
PrimarySpeechAct string `json:"primary_speech_act"`
SecondarySpeechAct string `json:"secondary_speech_act,omitempty"`
ConfidenceMillis int64 `json:"confidence_millis"`
Applicability InterpretationApplicability `json:"applicability"`
RequestedOutcome string `json:"requested_outcome,omitempty"`
NeedsCognition bool `json:"needs_cognition"`
Clarification string `json:"clarification"`
SelectionReason string `json:"selection_reason,omitempty"`
CapabilityHints []capability.CapabilityKind `json:"capability_hints,omitempty"`
}
type GatewayInterpretationResponse struct {
Version ContractVersion
RequestID RequestID
Candidate GatewayInterpretationCandidate
FinishReason FinishReason
Usage *ProviderUsage
}
type AdvisoryResponse struct {
Version ContractVersion
RequestID RequestID
+16
View File
@@ -58,6 +58,7 @@ type AttentionSourceKind string
const (
AttentionSourceProspectiveReminder AttentionSourceKind = "PROSPECTIVE_REMINDER"
AttentionSourceAgentDelegation AttentionSourceKind = "AGENT_DELEGATION"
)
// ProspectiveItem is canonical future relevance. TriggerAtUnixMillis is an
@@ -99,6 +100,7 @@ type AttentionItem struct {
ScopeID authority.ScopeID
SourceKind AttentionSourceKind
SourceProspectiveID ProspectiveItemID
SourceExternalRef string
Level AttentionLevel
State AttentionState
Summary string
@@ -177,6 +179,8 @@ const (
ResponseOutcomeCancelled ResponseOutcome = "CANCELLED"
ResponseOutcomeDismissed ResponseOutcome = "DISMISSED"
ResponseOutcomeClarificationNeeded ResponseOutcome = "CLARIFICATION_NEEDED"
ResponseOutcomeContinued ResponseOutcome = "CONTINUED"
ResponseOutcomeRejected ResponseOutcome = "REJECTED"
ResponseOutcomeNoop ResponseOutcome = "NOOP"
)
@@ -213,6 +217,18 @@ type HumanResponseResult struct {
Reply string
}
type CreateAttentionRequest struct {
OwnerPrincipalID identity.PrincipalID
ScopeID authority.ScopeID
SourceKind AttentionSourceKind
SourceProspectiveID ProspectiveItemID
SourceExternalRef string
Level AttentionLevel
Summary string
DueAtUnixMillis int64
CreatedAtUnixMillis int64
}
// SatisfyDeliveredSubject is the complete serializable operation subject. It
// is distinct from a Work TaskTarget, which remains the live justification.
type SatisfyDeliveredSubject struct {
+170
View File
@@ -0,0 +1,170 @@
package epistemics
import (
"errors"
"strings"
)
var ErrCitationEvidenceMismatch = errors.New("citation evidence does not match source chain")
type ResearchPlanID string
type SourceID string
type SourceSnapshotID string
type SourceExcerptID string
type CitationID string
type FreshnessState string
type CoverageState string
type ConflictState string
type EvidenceKind string
type ResearchFindingState string
const (
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
FindingSupported ResearchFindingState = "SUPPORTED"
FindingStale ResearchFindingState = "STALE"
FindingConflicted ResearchFindingState = "CONFLICTED"
FindingUnknown ResearchFindingState = "UNKNOWN"
)
type ResearchPlan struct {
ID ResearchPlanID
Question string
SourceClasses []string
FreshnessPolicy string
CreatedAtUnixMillis int64
}
type Source struct {
ID SourceID
Kind string
Locator string
Title string
Publisher string
}
type SourceSnapshot struct {
ID SourceSnapshotID
SourceID SourceID
ContentDigest string
CapturedAtUnixMillis int64
Freshness FreshnessState
}
type SourceExcerpt struct {
ID SourceExcerptID
SnapshotID SourceSnapshotID
Text string
Start int64
End int64
}
type Citation struct {
ID CitationID
ExcerptID SourceExcerptID
Evidence EvidenceKind
Coverage CoverageState
Conflict ConflictState
ResolvedAtUnixMillis int64
}
// ResearchCandidate is read-only evidence returned by a connector. The
// connector supplies the immutable snapshot and exact excerpt; the evaluator
// never fetches, rewrites or invents either one.
type ResearchCandidate struct {
PropositionKey string
Statement string
Source Source
Snapshot SourceSnapshot
Excerpt SourceExcerpt
Evidence EvidenceKind
Coverage CoverageState
Conflict ConflictState
}
type ResearchFinding struct {
PropositionKey string
Statement string
Claim Claim
Citations []Citation
State ResearchFindingState
Reason string
}
type ResearchResult struct {
PlanID ResearchPlanID
Findings []ResearchFinding
EvaluatedAtUnixMillis int64
}
// ResearchEvaluationRequest carries immutable evidence from a read-only
// connector to the evaluator. The runtime never fetches or mutates a source
// from this contract.
type ResearchEvaluationRequest struct {
Plan ResearchPlan
Candidates []ResearchCandidate
}
type RepositoryResearchRequest struct {
Plan ResearchPlan
PropositionKey string
SourceID SourceID
Path string
Statement string
}
type WebResearchRequest struct {
Plan ResearchPlan
PropositionKey string
SourceID SourceID
URL string
Title string
Statement string
}
// ResolveCitation creates evidence only when all identifiers form one exact
// source chain. It deliberately does not fetch, infer or rewrite source text.
func ResolveCitation(source Source, snapshot SourceSnapshot, excerpt SourceExcerpt, evidence EvidenceKind, coverage CoverageState, conflict ConflictState, resolvedAtUnixMillis int64) (Citation, error) {
if source.ID == "" || strings.TrimSpace(source.Locator) == "" || snapshot.SourceID != source.ID || excerpt.SnapshotID != snapshot.ID || excerpt.Validate() != nil || resolvedAtUnixMillis < 1 || evidence == "" || coverage == "" || conflict == "" {
return Citation{}, ErrCitationEvidenceMismatch
}
citation := Citation{ID: CitationID("citation:" + string(excerpt.ID)), ExcerptID: excerpt.ID, Evidence: evidence, Coverage: coverage, Conflict: conflict, ResolvedAtUnixMillis: resolvedAtUnixMillis}
return citation, citation.Validate()
}
func (p ResearchPlan) Validate() error {
if p.ID == "" || strings.TrimSpace(p.Question) == "" || len(p.SourceClasses) == 0 || strings.TrimSpace(p.FreshnessPolicy) == "" {
return errors.New("research plan requires question, source classes and freshness policy")
}
return nil
}
func (s SourceSnapshot) Validate() error {
if s.ID == "" || s.SourceID == "" || strings.TrimSpace(s.ContentDigest) == "" || s.CapturedAtUnixMillis < 1 || s.Freshness == "" {
return errors.New("source snapshot requires source, digest, capture time and freshness")
}
return nil
}
func (e SourceExcerpt) Validate() error {
if e.ID == "" || e.SnapshotID == "" || strings.TrimSpace(e.Text) == "" || e.Start < 0 || e.End < e.Start {
return errors.New("source excerpt requires snapshot, text and valid offsets")
}
return nil
}
func (c Citation) Validate() error {
if c.ID == "" || c.ExcerptID == "" || c.Evidence == "" || c.Coverage == "" || c.Conflict == "" || c.ResolvedAtUnixMillis < 1 {
return errors.New("citation requires exact excerpt, evidence, coverage, conflict and resolution time")
}
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package epistemics
type BeliefState string
const (
BeliefProvisional BeliefState = "PROVISIONAL"
BeliefConflicted BeliefState = "CONFLICTED"
BeliefVerified BeliefState = "VERIFIED"
)
type Observation struct {
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
}
type Claim struct {
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
}
type Belief struct {
ID BeliefID
ClaimID ClaimID
State BeliefState
}
type TruthBundle struct {
Source Source
Snapshot SourceSnapshot
Excerpt SourceExcerpt
Observation Observation
Claim Claim
Belief Belief
Citation Citation
}
+13 -11
View File
@@ -120,15 +120,17 @@ type ExperienceRecord struct {
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
// produce candidates only; it cannot directly rewrite canonical state.
type ReflectionCandidate struct {
ID ReflectionCandidateID
Trigger ReflectionTriggerKind
State ReflectionCandidateState
SourceExperienceID ExperienceRecordID
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
Summary string
EvidenceRefs []EvidenceRef
RequiresCognition bool
RecordedAtUnixMillis int64
Revision int64
ID ReflectionCandidateID
Trigger ReflectionTriggerKind
State ReflectionCandidateState
SourceExperienceID ExperienceRecordID
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
Summary string
EvidenceRefs []EvidenceRef
RequiresCognition bool
RecordedAtUnixMillis int64
Revision int64
ClaimedBy string
ClaimExpiresAtUnixMillis int64
}
+91 -45
View File
@@ -17,40 +17,59 @@ type ConversationState string
type InterpretationState string
type InteractionRelationKind string
type InterpretationID string
type SemanticCandidateID string
type ProjectReferenceID string
type ProjectReferenceRole string
type ClarificationState string
type SemanticApplicability string
type SemanticCandidateSource string
type SemanticCandidateSelectionState string
type InformationRequestKind string
const (
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
SemanticApplicable SemanticApplicability = "APPLICABLE"
SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE"
SemanticMixedIntent SemanticApplicability = "MIXED_INTENT"
SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION"
SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED"
CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2"
CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1"
CandidateSelected SemanticCandidateSelectionState = "SELECTED"
CandidateObserved SemanticCandidateSelectionState = "OBSERVED"
CandidateRejected SemanticCandidateSelectionState = "REJECTED"
InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY"
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
)
type Conversation struct {
@@ -94,14 +113,35 @@ type UtteranceInterpretation struct {
ID InterpretationID
UtteranceID HumanUtteranceID
SpeechAct SpeechAct
SecondarySpeechAct SpeechAct
ConfidenceMillis int64
Source string
AssertionVerification string
RequestedOutcome string
Clarification ClarificationState
NeedsCognition bool
Applicability SemanticApplicability
SelectionReason string
RecordedAtUnixMillis int64
}
type SemanticCandidate struct {
ID SemanticCandidateID
UtteranceID HumanUtteranceID
Source SemanticCandidateSource
SelectionState SemanticCandidateSelectionState
PrimarySpeechAct SpeechAct
SecondarySpeechAct SpeechAct
ConfidenceMillis int64
Applicability SemanticApplicability
RequestedOutcome string
Clarification ClarificationState
NeedsCognition bool
SelectionReason string
CapabilityHints []string
RecordedAtUnixMillis int64
}
type ProjectReference struct {
ID ProjectReferenceID
UtteranceID HumanUtteranceID
@@ -121,20 +161,26 @@ type ChatMessageRequest struct {
}
type ChatMessageResponse struct {
ConversationID ConversationID
UtteranceID HumanUtteranceID
OriginalIntentID OriginalHumanIntentID
SpeechAct SpeechAct
InterpretationState InterpretationState
NeedsCognition bool
FallbackText string
ActiveProjectName string
ProjectProvenance situation.ProjectResolutionProvenance
Clarification ClarificationState
WorkPlanned bool
PlanningReply string
CapabilityStatus capability.CapabilityResolutionStatus
CapabilityKind capability.CapabilityKind
CapabilityGapID capability.CapabilityGapID
Reminder *continuity.ProspectiveItem
ConversationID ConversationID
UtteranceID HumanUtteranceID
OriginalIntentID OriginalHumanIntentID
SpeechAct SpeechAct
InterpretationState InterpretationState
NeedsCognition bool
FallbackText string
ActiveProjectName string
ProjectProvenance situation.ProjectResolutionProvenance
Clarification ClarificationState
WorkPlanned bool
PlanningReply string
CapabilityStatus capability.CapabilityResolutionStatus
CapabilityKind capability.CapabilityKind
CapabilityHints []capability.CapabilityKind
RequestedOutcome string
InformationRequestKind InformationRequestKind
InterpretationConfidenceMillis int64
CapabilityGapID capability.CapabilityGapID
DelegationID string
DelegationState string
Reminder *continuity.ProspectiveItem
}
+5 -1
View File
@@ -9,13 +9,17 @@ type SecretPurpose string
const (
SecretPurposeDatabasePassword SecretPurpose = "DATABASE_PASSWORD"
SecretPurposeProviderAPIKey SecretPurpose = "PROVIDER_API_KEY"
)
// RuntimeConsumer identifies a trusted runtime allowed to resolve a credential.
type RuntimeConsumer string
const (
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
RuntimeConsumerWorkWorker RuntimeConsumer = "agent-worker"
RuntimeConsumerEpistemicsWorker RuntimeConsumer = "reflection-worker"
RuntimeConsumerCognitive RuntimeConsumer = "cognitive-worker"
)
// SecretFileRef is bootstrap delivery metadata for the V1 protected local
+32
View File
@@ -0,0 +1,32 @@
package work
import "time"
// AgentExecutionRequest is the serializable hand-off from the durable worker
// to an executor. The bridge receives the already-authorized dispatch context;
// it does not receive database credentials or ambient authority.
type AgentExecutionRequest struct {
Intent AgentDispatchIntent `json:"intent"`
WorkerID string `json:"worker_id"`
FencingToken AgentFencingToken `json:"fencing_token"`
LeaseExpiresAt time.Time `json:"lease_expires_at"`
}
type AgentExecutionState string
const (
AgentExecutionCompleted AgentExecutionState = "COMPLETED"
AgentExecutionWaiting AgentExecutionState = "WAITING"
AgentExecutionFailed AgentExecutionState = "FAILED"
)
// AgentExecutionResult is an acknowledgement of the external execution
// attempt. The worker persists it through the work owner before completing a
// dispatch, so a bridge timeout never becomes an unrecorded success.
type AgentExecutionResult struct {
State AgentExecutionState `json:"state"`
Summary string `json:"summary"`
OutcomeRef string `json:"outcome_ref"`
ArtifactRefs []string `json:"artifact_refs"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
}
+19
View File
@@ -0,0 +1,19 @@
package work
type AgentDispatchQueueState string
const (
AgentDispatchQueued AgentDispatchQueueState = "QUEUED"
AgentDispatchClaimed AgentDispatchQueueState = "CLAIMED"
AgentDispatchCompleted AgentDispatchQueueState = "COMPLETED"
AgentDispatchFailed AgentDispatchQueueState = "FAILED"
)
type AgentDispatchQueueItem struct {
DispatchID string `json:"dispatch_id"`
State AgentDispatchQueueState `json:"state"`
AvailableAtUnixMillis int64 `json:"available_at_unix_millis"`
Attempts int `json:"attempts"`
ClaimedBy string `json:"claimed_by"`
UpdatedAtUnixMillis int64 `json:"updated_at_unix_millis"`
}
+18
View File
@@ -0,0 +1,18 @@
package work
import "time"
type DryRunDispatch struct {
Intent AgentDispatchIntent
Lease AgentLease
Callbacks []AgentCallback
}
func (value DryRunDispatch) CallbackCount() int { return len(value.Callbacks) }
func (value DryRunDispatch) LastObservedAt() time.Time {
if len(value.Callbacks) == 0 {
return time.Time{}
}
return value.Callbacks[len(value.Callbacks)-1].ObservedAt
}
+73
View File
@@ -0,0 +1,73 @@
package work
type GraphNodeID string
type GraphState string
type GraphNodeState string
const (
GraphDraft GraphState = "DRAFT"
GraphAdmitted GraphState = "ADMITTED"
GraphRunning GraphState = "RUNNING"
GraphPartial GraphState = "PARTIAL"
GraphVerifying GraphState = "VERIFYING"
GraphCompleted GraphState = "COMPLETED"
GraphFailed GraphState = "FAILED"
GraphUnknown GraphState = "UNKNOWN"
GraphNodeBlocked GraphNodeState = "BLOCKED_PREREQUISITE"
GraphNodeReady GraphNodeState = "READY"
GraphNodeLeased GraphNodeState = "LEASED"
GraphNodeRunning GraphNodeState = "RUNNING"
GraphNodeSuccess GraphNodeState = "SUCCEEDED"
GraphNodeFailed GraphNodeState = "FAILED"
GraphNodeCancel GraphNodeState = "CANCELLED"
GraphNodeUnknown GraphNodeState = "UNKNOWN"
)
type GraphBudget struct {
MaxParallel int `json:"max_parallel"`
MaxAttempts int `json:"max_attempts"`
}
type GraphNode struct {
ID GraphNodeID `json:"id"`
DispatchID string `json:"dispatch_id"`
IdempotencyKey string `json:"idempotency_key"`
State GraphNodeState `json:"state"`
Attempts int `json:"attempts"`
}
type GraphEdge struct {
From GraphNodeID `json:"from"`
To GraphNodeID `json:"to"`
}
type WorkGraph struct {
ID string `json:"id"`
Revision int64 `json:"revision"`
State GraphState `json:"state"`
Nodes []GraphNode `json:"nodes"`
Edges []GraphEdge `json:"edges"`
Budget GraphBudget `json:"budget"`
}
type ResultCandidate struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
AuthorRunID AgentRunID
ArtifactRefs []EvidenceRef
EvidenceSummary string
Summary string
}
type ConflictSet struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
CandidateIDs []string
Resolved bool
}
+63
View File
@@ -0,0 +1,63 @@
package work
import "time"
type AgentLeaseID string
type AgentFencingToken uint64
type AgentLeaseState string
const (
AgentLeaseActive AgentLeaseState = "ACTIVE"
AgentLeaseExpired AgentLeaseState = "EXPIRED"
)
type AgentLease struct {
ID AgentLeaseID
RunID AgentRunID
WorkerID string
FencingToken AgentFencingToken
State AgentLeaseState
AcquiredAt time.Time
ExpiresAt time.Time
}
type AgentCallbackKind string
const (
AgentCallbackHeartbeat AgentCallbackKind = "HEARTBEAT"
AgentCallbackCheckpoint AgentCallbackKind = "CHECKPOINT"
AgentCallbackResult AgentCallbackKind = "RESULT"
AgentCallbackCancelAcknowledged AgentCallbackKind = "CANCEL_ACKNOWLEDGED"
)
type AgentCallback struct {
RunID AgentRunID
WorkerID string
FencingToken AgentFencingToken
Sequence uint64
Kind AgentCallbackKind
ObservedAt time.Time
}
type IndependentVerification struct {
ProducerRunID AgentRunID
VerifierRunID AgentRunID
VerifierIdentity string
ArtifactID string
TestEvidenceRef string
Verified bool
Summary string
}
type AgentDispatchIntent struct {
DispatchID string `json:"dispatch_id"`
IdempotencyKey string `json:"idempotency_key"`
DelegationID DelegationID `json:"delegation_id"`
RunID AgentRunID `json:"run_id"`
ImplementationID string `json:"implementation_id"`
ImplementationRevision int64 `json:"implementation_revision"`
SourceRevision string `json:"source_revision"`
WorktreeID string `json:"worktree_id"`
VerificationPlanRef string `json:"verification_plan_ref"`
CreatedAtUnixMillis int64 `json:"created_at_unix_millis"`
}
+29
View File
@@ -0,0 +1,29 @@
package work
type DevelopmentStage string
const (
DevelopmentEvidenceCollected DevelopmentStage = "EVIDENCE_COLLECTED"
DevelopmentProposalCreated DevelopmentStage = "PROPOSAL_CREATED"
DevelopmentTaskAdmitted DevelopmentStage = "TASK_ADMITTED"
DevelopmentIsolated DevelopmentStage = "ISOLATED"
DevelopmentVerified DevelopmentStage = "VERIFIED"
DevelopmentCanary DevelopmentStage = "CANARY"
DevelopmentPromoted DevelopmentStage = "PROMOTED"
DevelopmentRolledBack DevelopmentStage = "ROLLED_BACK"
)
type DevelopmentGate struct {
ID string
Stage DevelopmentStage
EvidenceRefs []string
ProposalRef string
TaskPacketRef string
WorktreeRef string
ProducerRunRefs []AgentRunID
VerifierRunRefs []AgentRunID
VerificationEvidence string
CanaryEvidence string
PromotionRef string
RollbackRef string
}
+124
View File
@@ -16,6 +16,11 @@ type WorkPlanID ids.ID
type PlanStepID ids.ID
type CapabilityRequirementID ids.ID
type DelegationCandidateID ids.ID
type DelegationID ids.ID
type AgentRunID ids.ID
type AgentCheckpointID ids.ID
type AgentResultID ids.ID
type DelegationVerificationID ids.ID
type TaskRevision uint64
type GoalRevision uint64
type PlanRevision uint64
@@ -25,6 +30,11 @@ type PlanStepKind string
type CapabilityRequirementState string
type DelegationCandidateState string
type DelegationKind string
type DelegationState string
type AgentRunState string
type AgentBlockerClass string
type AgentExecutionProtocol string
type EvidenceRef string
type SourceIntentRef string
type SourceUtteranceRef string
type ConversationRef string
@@ -78,6 +88,35 @@ const (
DelegationKindHuman DelegationKind = "HUMAN"
DelegationKindUnavailable DelegationKind = "UNAVAILABLE"
DelegationPending DelegationState = "PENDING"
DelegationRunning DelegationState = "RUNNING"
DelegationWaiting DelegationState = "WAITING"
DelegationBlocked DelegationState = "BLOCKED"
DelegationVerifying DelegationState = "VERIFYING"
DelegationCompleted DelegationState = "COMPLETED"
DelegationFailed DelegationState = "FAILED"
DelegationCancelled DelegationState = "CANCELLED"
AgentRunStarted AgentRunState = "STARTED"
AgentRunRunning AgentRunState = "RUNNING"
AgentRunWaiting AgentRunState = "WAITING"
AgentRunCheckpointed AgentRunState = "CHECKPOINTED"
AgentRunExecutionWindowExhausted AgentRunState = "EXECUTION_WINDOW_EXHAUSTED"
AgentRunBlocked AgentRunState = "BLOCKED"
AgentRunFailed AgentRunState = "FAILED"
AgentRunCompleted AgentRunState = "COMPLETED"
AgentRunCancelled AgentRunState = "CANCELLED"
AgentBlockerNone AgentBlockerClass = "NONE"
AgentBlockerHumanDecisionRequired AgentBlockerClass = "HUMAN_DECISION_REQUIRED"
AgentBlockerMissingExternalCredential AgentBlockerClass = "MISSING_EXTERNAL_CREDENTIAL"
AgentBlockerArchitectureDecision AgentBlockerClass = "ARCHITECTURE_DECISION_REQUIRED"
AgentBlockerSecurityConflict AgentBlockerClass = "SECURITY_CONFLICT"
AgentBlockerTechnicalFailure AgentBlockerClass = "TECHNICAL_FAILURE"
AgentBlockerExecutionWindow AgentBlockerClass = "EXECUTION_WINDOW_EXHAUSTED"
AgentProtocolCodexBridgeV1 AgentExecutionProtocol = "CODEX_BRIDGE_V1"
HelpPathAnswer HelpPathKind = "ANSWER"
HelpPathCognition HelpPathKind = "COGNITION"
HelpPathDirectCapability HelpPathKind = "DIRECT_CAPABILITY"
@@ -168,6 +207,87 @@ type WorkPlan struct {
RecordedAtUnixMillis int64
}
type Delegation struct {
ID DelegationID
GoalID GoalID
PlanID WorkPlanID
PlanCandidateID PlanCandidateID
CapabilityRequirementID CapabilityRequirementID
CapabilityKind capability.CapabilityKind
SelectedImplementationID capability.CapabilityImplementationID
SelectedImplementationRev int64
SelectedImplementationKind capability.ImplementationKind
Protocol AgentExecutionProtocol
SourceIntentID SourceIntentRef
SourceUtteranceID SourceUtteranceRef
ConversationID ConversationRef
ProjectID ProjectRef
ProjectDisplayName string
DesiredOutcome string
ConstraintSummary string
VerificationSummary string
ContextSummary string
ContextRefs []string
State DelegationState
LatestRunID AgentRunID
LatestResultID AgentResultID
CreatedAtUnixMillis int64
UpdatedAtUnixMillis int64
}
type AgentRun struct {
ID AgentRunID
DelegationID DelegationID
ImplementationID capability.CapabilityImplementationID
ImplementationRevision int64
Protocol AgentExecutionProtocol
State AgentRunState
DerivedInstructionSummary string
StartedAtUnixMillis int64
UpdatedAtUnixMillis int64
CompletedAtUnixMillis int64
}
type AgentCheckpoint struct {
ID AgentCheckpointID
DelegationID DelegationID
RunID AgentRunID
State AgentRunState
BlockerClass AgentBlockerClass
CompletedSummary string
RemainingSummary string
TestSummary string
BranchName string
CommitHash string
NextStep string
ArtifactRefs []EvidenceRef
RecordedAtUnixMillis int64
}
type AgentResult struct {
ID AgentResultID
DelegationID DelegationID
RunID AgentRunID
State AgentRunState
Summary string
OutcomeRef string
VerificationRequired bool
VerificationSummary string
ArtifactRefs []EvidenceRef
RecordedAtUnixMillis int64
}
type DelegationVerification struct {
ID DelegationVerificationID
DelegationID DelegationID
RunID AgentRunID
ResultID AgentResultID
Verified bool
Summary string
EvidenceRefs []EvidenceRef
RecordedAtUnixMillis int64
}
type PlanningInput struct {
ConversationID ConversationRef
UtteranceID SourceUtteranceRef
@@ -180,7 +300,11 @@ type PlanningInput struct {
ProjectProvenance ProjectProvenanceRef
Message string
SpeechAct SpeechActRef
SecondarySpeechAct SpeechActRef
CapabilityHints []capability.CapabilityKind
AssertionVerification epistemics.VerificationState
NeedsCognition bool
RequiresClarification bool
ReceivedAtUnixMillis int64
}
@@ -0,0 +1,145 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.delegations (
delegation_id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL,
work_plan_id TEXT NOT NULL,
plan_candidate_id TEXT NOT NULL,
capability_requirement_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
selected_implementation_id TEXT NOT NULL,
selected_implementation_revision BIGINT NOT NULL,
selected_implementation_kind TEXT NOT NULL,
protocol TEXT NOT NULL,
source_intent_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
conversation_id TEXT NOT NULL DEFAULT '',
project_id TEXT NOT NULL DEFAULT '',
project_display_name TEXT NOT NULL DEFAULT '',
desired_outcome TEXT NOT NULL,
constraint_summary TEXT NOT NULL DEFAULT '',
verification_summary TEXT NOT NULL DEFAULT '',
context_summary TEXT NOT NULL DEFAULT '',
context_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
state TEXT NOT NULL,
latest_run_id TEXT NOT NULL DEFAULT '',
latest_result_id TEXT NOT NULL DEFAULT '',
created_at_unix_millis BIGINT NOT NULL,
updated_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (delegation_id <> ''),
CHECK (goal_id <> ''),
CHECK (work_plan_id <> ''),
CHECK (plan_candidate_id <> ''),
CHECK (capability_requirement_id <> ''),
CHECK (capability_kind <> ''),
CHECK (selected_implementation_id <> ''),
CHECK (selected_implementation_revision > 0),
CHECK (selected_implementation_kind = 'AGENT'),
CHECK (protocol IN ('CODEX_BRIDGE_V1')),
CHECK (source_intent_id <> ''),
CHECK (source_utterance_id <> ''),
CHECK (desired_outcome <> ''),
CHECK (state IN ('PENDING','RUNNING','WAITING','BLOCKED','VERIFYING','COMPLETED','FAILED','CANCELLED')),
CHECK (created_at_unix_millis > 0),
CHECK (updated_at_unix_millis >= created_at_unix_millis)
);
CREATE TABLE IF NOT EXISTS work.agent_runs (
run_id TEXT PRIMARY KEY,
delegation_id TEXT NOT NULL,
implementation_id TEXT NOT NULL,
implementation_revision BIGINT NOT NULL,
protocol TEXT NOT NULL,
state TEXT NOT NULL,
derived_instruction_summary TEXT NOT NULL DEFAULT '',
started_at_unix_millis BIGINT NOT NULL,
updated_at_unix_millis BIGINT NOT NULL,
completed_at_unix_millis BIGINT NOT NULL DEFAULT 0,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (run_id <> ''),
CHECK (delegation_id <> ''),
CHECK (implementation_id <> ''),
CHECK (implementation_revision > 0),
CHECK (protocol IN ('CODEX_BRIDGE_V1')),
CHECK (state IN ('STARTED','RUNNING','WAITING','CHECKPOINTED','EXECUTION_WINDOW_EXHAUSTED','BLOCKED','FAILED','COMPLETED','CANCELLED')),
CHECK (started_at_unix_millis > 0),
CHECK (updated_at_unix_millis >= started_at_unix_millis),
CHECK (completed_at_unix_millis >= 0)
);
CREATE TABLE IF NOT EXISTS work.agent_checkpoints (
checkpoint_id TEXT PRIMARY KEY,
delegation_id TEXT NOT NULL,
run_id TEXT NOT NULL,
state TEXT NOT NULL,
blocker_class TEXT NOT NULL,
completed_summary TEXT NOT NULL DEFAULT '',
remaining_summary TEXT NOT NULL DEFAULT '',
test_summary TEXT NOT NULL DEFAULT '',
branch_name TEXT NOT NULL DEFAULT '',
commit_hash TEXT NOT NULL DEFAULT '',
next_step TEXT NOT NULL DEFAULT '',
artifact_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
recorded_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (checkpoint_id <> ''),
CHECK (delegation_id <> ''),
CHECK (run_id <> ''),
CHECK (state IN ('STARTED','RUNNING','WAITING','CHECKPOINTED','EXECUTION_WINDOW_EXHAUSTED','BLOCKED','FAILED','COMPLETED','CANCELLED')),
CHECK (blocker_class IN ('NONE','HUMAN_DECISION_REQUIRED','MISSING_EXTERNAL_CREDENTIAL','ARCHITECTURE_DECISION_REQUIRED','SECURITY_CONFLICT','TECHNICAL_FAILURE','EXECUTION_WINDOW_EXHAUSTED')),
CHECK (recorded_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.agent_results (
result_id TEXT PRIMARY KEY,
delegation_id TEXT NOT NULL,
run_id TEXT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
outcome_ref TEXT NOT NULL DEFAULT '',
verification_required BOOLEAN NOT NULL,
verification_summary TEXT NOT NULL DEFAULT '',
artifact_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
recorded_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (result_id <> ''),
CHECK (delegation_id <> ''),
CHECK (run_id <> ''),
CHECK (state IN ('STARTED','RUNNING','WAITING','CHECKPOINTED','EXECUTION_WINDOW_EXHAUSTED','BLOCKED','FAILED','COMPLETED','CANCELLED')),
CHECK (summary <> ''),
CHECK (recorded_at_unix_millis > 0)
);
RESET ROLE;
SET ROLE capability_role;
INSERT INTO capability.capability_descriptors(capability_id, capability_kind, display_name, description, revision, recorded_at_unix_millis)
VALUES
('modify-code', 'MODIFY_CODE', 'Code modification', 'Executor-neutral software engineering capability that changes repository code inside an authorized scope.', 1, 1),
('run-tests', 'RUN_TESTS', 'Run tests', 'Executor-neutral software engineering capability that runs repository validation and test commands.', 1, 1),
('inspect-project', 'INSPECT_PROJECT', 'Inspect project', 'Executor-neutral software engineering capability that inspects repository structure and relevant files.', 1, 1)
ON CONFLICT (capability_id) DO NOTHING;
INSERT INTO capability.capability_implementations(
implementation_id, capability_id, capability_kind, owner_principal_id, revision, implementation_kind, target_ref,
credential_ref, qualification, activation, health, authority_policy_ref, data_constraints,
cost_score_millis, latency_score_millis, reliability_millis, observed_at_unix_millis)
VALUES
('codex-agent-modify-code-v1', 'modify-code', 'MODIFY_CODE', 'domovoy-creator-principal', 1, 'AGENT', 'codex://assistant/modify-code', '', 'VALIDATED', 'CANARY', 'AVAILABLE', 'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 5, 80, 820, 1),
('codex-agent-run-tests-v1', 'run-tests', 'RUN_TESTS', 'domovoy-creator-principal', 1, 'AGENT', 'codex://assistant/run-tests', '', 'VALIDATED', 'CANARY', 'AVAILABLE', 'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 5, 120, 860, 1),
('codex-agent-inspect-project-v1', 'inspect-project', 'INSPECT_PROJECT', 'domovoy-creator-principal', 1, 'AGENT', 'codex://assistant/inspect-project', '', 'VALIDATED', 'CANARY', 'AVAILABLE', 'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 5, 40, 900, 1)
ON CONFLICT DO NOTHING;
INSERT INTO capability.capability_implementation_heads(implementation_id, revision, capability_kind, owner_principal_id)
VALUES
('codex-agent-modify-code-v1', 1, 'MODIFY_CODE', 'domovoy-creator-principal'),
('codex-agent-run-tests-v1', 1, 'RUN_TESTS', 'domovoy-creator-principal'),
('codex-agent-inspect-project-v1', 1, 'INSPECT_PROJECT', 'domovoy-creator-principal')
ON CONFLICT (implementation_id) DO NOTHING;
RESET ROLE;
COMMIT;
@@ -0,0 +1,60 @@
BEGIN;
SET ROLE continuity_role;
ALTER TABLE continuity.attention_items
ADD COLUMN IF NOT EXISTS source_external_ref TEXT NOT NULL DEFAULT '';
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'attention_items_source_kind_check_v2'
) THEN
ALTER TABLE continuity.attention_items DROP CONSTRAINT IF EXISTS attention_items_source_kind_check;
ALTER TABLE continuity.attention_items
ADD CONSTRAINT attention_items_source_kind_check_v2
CHECK (source_kind IN ('PROSPECTIVE_REMINDER','AGENT_DELEGATION'));
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'human_responses_outcome_check_v2'
) THEN
ALTER TABLE continuity.human_responses DROP CONSTRAINT IF EXISTS human_responses_outcome_check;
ALTER TABLE continuity.human_responses
ADD CONSTRAINT human_responses_outcome_check_v2
CHECK (outcome IN ('USER_REPORTED_COMPLETE','SNOOZED','CANCELLED','DISMISSED','CLARIFICATION_NEEDED','CONTINUED','REJECTED','NOOP'));
END IF;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS attention_items_source_external_ref_unique
ON continuity.attention_items(source_kind, source_external_ref)
WHERE source_external_ref <> '';
RESET ROLE;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.delegation_verifications (
verification_id TEXT PRIMARY KEY,
delegation_id TEXT NOT NULL,
run_id TEXT NOT NULL,
result_id TEXT NOT NULL,
verified BOOLEAN NOT NULL,
summary TEXT NOT NULL,
evidence_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
recorded_at_unix_millis BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (delegation_id <> ''),
CHECK (run_id <> ''),
CHECK (result_id <> ''),
CHECK (summary <> ''),
CHECK (recorded_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,22 @@
BEGIN;
SET ROLE continuity_role;
ALTER TABLE continuity.attention_items
DROP CONSTRAINT IF EXISTS attention_items_response_projection_check;
ALTER TABLE continuity.attention_items
ADD CONSTRAINT attention_items_response_projection_check
CHECK (
state <> 'RESOLVED'
OR (
response_id <> ''
AND response_utterance_id <> ''
AND response_operation IN ('ACKNOWLEDGE','COMPLETE','SNOOZE','CANCEL','DISMISS','CONTINUE','REJECT')
AND response_outcome IN ('USER_REPORTED_COMPLETE','SNOOZED','CANCELLED','DISMISSED','CLARIFICATION_NEEDED','CONTINUED','REJECTED','NOOP')
AND responded_at_unix_millis > 0
)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,54 @@
BEGIN;
SET ROLE interaction_role;
ALTER TABLE interaction.utterance_interpretations
ADD COLUMN IF NOT EXISTS secondary_speech_act TEXT NOT NULL DEFAULT 'UNKNOWN',
ADD COLUMN IF NOT EXISTS needs_cognition BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS applicability TEXT NOT NULL DEFAULT 'APPLICABLE',
ADD COLUMN IF NOT EXISTS selection_reason TEXT NOT NULL DEFAULT '';
ALTER TABLE interaction.utterance_interpretations DROP CONSTRAINT IF EXISTS utterance_interpretations_secondary_speech_act_check;
ALTER TABLE interaction.utterance_interpretations
ADD CONSTRAINT utterance_interpretations_secondary_speech_act_check
CHECK (secondary_speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN'));
ALTER TABLE interaction.utterance_interpretations DROP CONSTRAINT IF EXISTS utterance_interpretations_applicability_check;
ALTER TABLE interaction.utterance_interpretations
ADD CONSTRAINT utterance_interpretations_applicability_check
CHECK (applicability IN ('APPLICABLE','LOW_CONFIDENCE','MIXED_INTENT','NEEDS_BROADER_INTERPRETATION','CLARIFICATION_REQUIRED'));
CREATE TABLE IF NOT EXISTS interaction.semantic_candidates (
candidate_id TEXT PRIMARY KEY,
utterance_id TEXT NOT NULL,
source TEXT NOT NULL,
selection_state TEXT NOT NULL,
primary_speech_act TEXT NOT NULL,
secondary_speech_act TEXT NOT NULL,
confidence_millis BIGINT NOT NULL,
applicability TEXT NOT NULL,
requested_outcome TEXT NOT NULL DEFAULT '',
clarification TEXT NOT NULL,
needs_cognition BOOLEAN NOT NULL DEFAULT FALSE,
selection_reason TEXT NOT NULL DEFAULT '',
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (source IN ('DETERMINISTIC_V2','COGNITIVE_V1')),
CHECK (selection_state IN ('SELECTED','OBSERVED','REJECTED')),
CHECK (primary_speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
CHECK (secondary_speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
CHECK (confidence_millis BETWEEN 0 AND 1000),
CHECK (applicability IN ('APPLICABLE','LOW_CONFIDENCE','MIXED_INTENT','NEEDS_BROADER_INTERPRETATION','CLARIFICATION_REQUIRED')),
CHECK (clarification IN ('NOT_REQUIRED','REQUIRED'))
);
CREATE TABLE IF NOT EXISTS interaction.semantic_candidate_capability_hints (
candidate_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
PRIMARY KEY (candidate_id, capability_kind),
CHECK (candidate_id <> ''),
CHECK (capability_kind <> '')
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,26 @@
BEGIN;
SET ROLE capability_role;
ALTER TABLE capability.cognitive_provider_instances
ADD COLUMN IF NOT EXISTS billing_mode TEXT NOT NULL DEFAULT 'UNKNOWN',
ADD COLUMN IF NOT EXISTS input_cost_micros BIGINT,
ADD COLUMN IF NOT EXISTS output_cost_micros BIGINT,
ADD COLUMN IF NOT EXISTS request_cost_micros BIGINT;
ALTER TABLE capability.cognitive_provider_instances
ADD COLUMN IF NOT EXISTS auth_mode TEXT NOT NULL DEFAULT 'NONE';
ALTER TABLE capability.cognitive_provider_instances
DROP CONSTRAINT IF EXISTS cognitive_provider_instances_billing_mode_check,
ADD CONSTRAINT cognitive_provider_instances_billing_mode_check CHECK (billing_mode IN ('UNKNOWN','FREE','PER_INPUT_UNIT','PER_OUTPUT_UNIT','PER_REQUEST')),
DROP CONSTRAINT IF EXISTS cognitive_provider_instances_auth_mode_check,
ADD CONSTRAINT cognitive_provider_instances_auth_mode_check CHECK (auth_mode IN ('NONE','API_KEY','BEARER','USERNAME_PASSWORD','OAUTH','SECRET_FILE')),
DROP CONSTRAINT IF EXISTS cognitive_provider_instances_cost_check,
ADD CONSTRAINT cognitive_provider_instances_cost_check CHECK ((input_cost_micros IS NULL OR input_cost_micros >= 0) AND (output_cost_micros IS NULL OR output_cost_micros >= 0) AND (request_cost_micros IS NULL OR request_cost_micros >= 0));
ALTER TABLE capability.cognitive_model_descriptors
ADD COLUMN IF NOT EXISTS intelligence_score_millis BIGINT;
ALTER TABLE capability.cognitive_model_descriptors
DROP CONSTRAINT IF EXISTS cognitive_model_descriptors_intelligence_score_check,
ADD CONSTRAINT cognitive_model_descriptors_intelligence_score_check CHECK (intelligence_score_millis IS NULL OR (intelligence_score_millis >= 0 AND intelligence_score_millis <= 1000));
RESET ROLE;
COMMIT;
@@ -0,0 +1,43 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.agent_dispatch_intents (
dispatch_id TEXT PRIMARY KEY,
idempotency_key TEXT NOT NULL UNIQUE,
delegation_id TEXT NOT NULL,
run_id TEXT NOT NULL,
implementation_id TEXT NOT NULL,
implementation_revision BIGINT NOT NULL,
source_revision TEXT NOT NULL,
worktree_id TEXT NOT NULL,
verification_plan_ref TEXT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
CHECK (dispatch_id <> ''), CHECK (idempotency_key <> ''), CHECK (delegation_id <> ''),
CHECK (run_id <> ''), CHECK (implementation_id <> ''), CHECK (implementation_revision > 0),
CHECK (source_revision <> ''), CHECK (worktree_id <> ''), CHECK (verification_plan_ref <> ''),
CHECK (created_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.agent_leases (
lease_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL UNIQUE,
worker_id TEXT NOT NULL,
fencing_token BIGINT NOT NULL,
state TEXT NOT NULL,
acquired_at_unix_millis BIGINT NOT NULL,
expires_at_unix_millis BIGINT NOT NULL,
CHECK (lease_id <> ''), CHECK (run_id <> ''), CHECK (worker_id <> ''),
CHECK (fencing_token > 0), CHECK (state IN ('ACTIVE','EXPIRED')),
CHECK (acquired_at_unix_millis > 0), CHECK (expires_at_unix_millis > acquired_at_unix_millis)
);
CREATE TABLE IF NOT EXISTS work.agent_callback_heads (
run_id TEXT PRIMARY KEY,
last_sequence BIGINT NOT NULL DEFAULT 0,
updated_at_unix_millis BIGINT NOT NULL,
CHECK (run_id <> ''), CHECK (last_sequence >= 0), CHECK (updated_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,22 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.agent_dispatch_queue (
dispatch_id TEXT PRIMARY KEY REFERENCES work.agent_dispatch_intents(dispatch_id),
state TEXT NOT NULL,
available_at_unix_millis BIGINT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
claimed_by TEXT NOT NULL DEFAULT '',
updated_at_unix_millis BIGINT NOT NULL,
CHECK (state IN ('QUEUED','CLAIMED','COMPLETED','FAILED')),
CHECK (available_at_unix_millis > 0),
CHECK (attempts >= 0),
CHECK (updated_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS agent_dispatch_queue_claim_idx
ON work.agent_dispatch_queue(state, available_at_unix_millis, dispatch_id);
RESET ROLE;
COMMIT;
@@ -0,0 +1,41 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.graphs (
graph_id TEXT NOT NULL,
revision BIGINT NOT NULL,
state TEXT NOT NULL,
max_parallel INTEGER NOT NULL,
max_attempts INTEGER NOT NULL,
PRIMARY KEY (graph_id, revision),
CHECK (graph_id <> ''), CHECK (revision > 0),
CHECK (state IN ('DRAFT','ADMITTED','RUNNING','PAUSED','CANCELLING','PARTIAL','VERIFYING','COMPLETED','FAILED','UNKNOWN','RECOVERING')),
CHECK (max_parallel > 0), CHECK (max_attempts > 0)
);
CREATE TABLE IF NOT EXISTS work.graph_nodes (
graph_id TEXT NOT NULL,
revision BIGINT NOT NULL,
node_id TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
state TEXT NOT NULL,
attempts INTEGER NOT NULL,
PRIMARY KEY (graph_id, revision, node_id),
FOREIGN KEY (graph_id, revision) REFERENCES work.graphs(graph_id, revision),
CHECK (node_id <> ''), CHECK (idempotency_key <> ''), CHECK (attempts >= 0),
CHECK (state IN ('BLOCKED_PREREQUISITE','READY','LEASED','RUNNING','SUCCEEDED','FAILED','CANCELLED','UNKNOWN','RECOVERING'))
);
CREATE TABLE IF NOT EXISTS work.graph_edges (
graph_id TEXT NOT NULL,
revision BIGINT NOT NULL,
from_node_id TEXT NOT NULL,
to_node_id TEXT NOT NULL,
PRIMARY KEY (graph_id, revision, from_node_id, to_node_id),
FOREIGN KEY (graph_id, revision) REFERENCES work.graphs(graph_id, revision),
CHECK (from_node_id <> ''), CHECK (to_node_id <> ''), CHECK (from_node_id <> to_node_id)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,19 @@
BEGIN;
SET ROLE epistemics_role;
ALTER TABLE epistemics.reflection_candidates
ADD COLUMN IF NOT EXISTS claimed_by TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS claim_expires_at_unix_millis BIGINT NOT NULL DEFAULT 0;
ALTER TABLE epistemics.reflection_candidates
DROP CONSTRAINT IF EXISTS reflection_candidates_claim_expiry_check;
ALTER TABLE epistemics.reflection_candidates
ADD CONSTRAINT reflection_candidates_claim_expiry_check
CHECK (claim_expires_at_unix_millis >= 0);
CREATE INDEX IF NOT EXISTS reflection_candidates_claimable_idx
ON epistemics.reflection_candidates (state, claim_expires_at_unix_millis, recorded_at_unix_millis, reflection_candidate_id);
RESET ROLE;
COMMIT;

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