titan-agent 7.0.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -2
- package/dist/agent/agent.js +55 -10
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/contextManager.js +1 -1
- package/dist/agent/contextManager.js.map +1 -1
- package/dist/agent/honestyGuard.js +66 -0
- package/dist/agent/honestyGuard.js.map +1 -0
- package/dist/agent/selfCritique.js +101 -0
- package/dist/agent/selfCritique.js.map +1 -0
- package/dist/agent/toolSearch.js +9 -2
- package/dist/agent/toolSearch.js.map +1 -1
- package/dist/agent/trajectoryLogger.js +19 -2
- package/dist/agent/trajectoryLogger.js.map +1 -1
- package/dist/analytics/collector.js +1 -1
- package/dist/analytics/collector.js.map +1 -1
- package/dist/config/schema.js +32 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/eval/harness.js +4 -1
- package/dist/eval/harness.js.map +1 -1
- package/dist/gateway/server.js +24 -8
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/slashCommands.js +31 -0
- package/dist/gateway/slashCommands.js.map +1 -1
- package/dist/mcp/agentTools.js +135 -0
- package/dist/mcp/agentTools.js.map +1 -0
- package/dist/mcp/server.js +19 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory/graph.js +2 -0
- package/dist/memory/graph.js.map +1 -1
- package/dist/providers/errorTaxonomy.js +4 -0
- package/dist/providers/errorTaxonomy.js.map +1 -1
- package/dist/providers/moa.js +151 -0
- package/dist/providers/moa.js.map +1 -0
- package/dist/providers/modelCapabilities.js +65 -2
- package/dist/providers/modelCapabilities.js.map +1 -1
- package/dist/providers/ollama.js +1 -0
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai_compat.js +14 -2
- package/dist/providers/openai_compat.js.map +1 -1
- package/dist/providers/router.js +3 -1
- package/dist/providers/router.js.map +1 -1
- package/dist/utils/benchMode.js +8 -0
- package/dist/utils/benchMode.js.map +1 -0
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +26 -26
- package/ui/dist/assets/AboutYouMemory-57X5SqHQ.js +1 -0
- package/ui/dist/assets/AuditPanel-AyB8ciLZ.js +1 -0
- package/ui/dist/assets/AutonomyPanel-wmVPr-n3.js +1 -0
- package/ui/dist/assets/AutopilotPanel-CbLy4lJP.js +1 -0
- package/ui/dist/assets/AutoresearchPanel-DxRO1rBF.js +1 -0
- package/ui/dist/assets/BackupPanel-BUh9rQ-3.js +1 -0
- package/ui/dist/assets/BrowserPanel-OqKwGrIL.js +1 -0
- package/ui/dist/assets/CPActivity-DGfS7EY3.js +1 -0
- package/ui/dist/assets/CPAgentDetail-p0Kvzhhb.js +1 -0
- package/ui/dist/assets/CPAgents-B6wSSQZM.js +1 -0
- package/ui/dist/assets/CPApprovals-CYv9Do0i.js +1 -0
- package/ui/dist/assets/CPCosts-Dx6Q71rN.js +1 -0
- package/ui/dist/assets/CPDashboard-DA8-tE-l.js +1 -0
- package/ui/dist/assets/CPFiles-BJ49tzai.js +1 -0
- package/ui/dist/assets/CPGoals-h0dKL5V_.js +1 -0
- package/ui/dist/assets/CPInbox-CDvefgNH.js +1 -0
- package/ui/dist/assets/CPIssueDetail-CnmLzzDD.js +1 -0
- package/ui/dist/assets/CPIssues-DknAIMyl.js +1 -0
- package/ui/dist/assets/CPLayout-C2nqgkmY.js +2 -0
- package/ui/dist/assets/CPOrg-tDbHFphX.js +1 -0
- package/ui/dist/assets/CPRuns-UlrIURqG.js +1 -0
- package/ui/dist/assets/CPSocial-B-gHgvEw.js +1 -0
- package/ui/dist/assets/CPVoice-CJy938ZF.js +1 -0
- package/ui/dist/assets/ChannelsPanel-DLbxg1zO.js +1 -0
- package/ui/dist/assets/CheckpointsPanel-BB6TT0aE.js +1 -0
- package/ui/dist/assets/CommandPostHub-fIslwU_t.js +2 -0
- package/ui/dist/assets/ConfirmDialog-6hcNjmAh.js +9 -0
- package/ui/dist/assets/CronPanel-DLpw3Y25.js +1 -0
- package/ui/dist/assets/DataTable-KVqtDzvP.js +1 -0
- package/ui/dist/assets/DreamPanel-lcMXpbEb.js +5 -0
- package/ui/dist/assets/EmptyState-D-Dg8B8r.js +1 -0
- package/ui/dist/assets/EvalHarnessPanel-BKzRiiQw.js +2 -0
- package/ui/dist/assets/EvalPanel-Dtodacmj.js +1 -0
- package/ui/dist/assets/FileViewer-DjcVKkO2.js +60 -0
- package/ui/dist/assets/FilesPanel-BI90Pplh.js +1 -0
- package/ui/dist/assets/FleetPanel-tr0wjAwr.js +1 -0
- package/ui/dist/assets/HomelabPanel-m8vMm5rE.js +1 -0
- package/ui/dist/assets/InfraView-3WriJnyE.js +2 -0
- package/ui/dist/assets/IntegrationsPanel-BmL70fOw.js +1 -0
- package/ui/dist/assets/IntelligenceView-BGVxCKD3.js +2 -0
- package/ui/dist/assets/LearningPanel-CrYLQ6eV.js +1 -0
- package/ui/dist/assets/LiveStudio-CnW3eZYF.js +2 -0
- package/ui/dist/assets/LogsPanel-DbShr4Ic.js +1 -0
- package/ui/dist/assets/McpPanel-uMYEsPI4.js +1 -0
- package/ui/dist/assets/MemoryGraphPanel-C7-Z8f0v.js +1 -0
- package/ui/dist/assets/MemoryTaxonomyPanel-KrxN8U1G.js +1 -0
- package/ui/dist/assets/MemoryWikiPanel-DI3q6IHC.js +1 -0
- package/ui/dist/assets/MeshPanel-D4-g2tlu.js +1 -0
- package/ui/dist/assets/MissionCanvas-D8cjakJe.js +61 -0
- package/ui/dist/assets/MissionChat-BvqCUf1E.js +3 -0
- package/ui/dist/assets/MissionLibrary-CZoJQ9WD.js +1 -0
- package/ui/dist/assets/MissionStart-CXCq-_AD.js +1 -0
- package/ui/dist/assets/NvidiaPanel-BfI1K5Pg.js +1 -0
- package/ui/dist/assets/OrganismPanel-B7Gm9pP-.js +1 -0
- package/ui/dist/assets/OverviewPanel-B9tKWRK2.js +1 -0
- package/ui/dist/assets/PageHeader-CH85uoZG.js +1 -0
- package/ui/dist/assets/PersonaProfilesPanel-BPi8fl-U.js +3 -0
- package/ui/dist/assets/PersonasPanel-Dwr7sycT.js +1 -0
- package/ui/dist/assets/PhoneDeskPanel-D7qc6wUI.js +1 -0
- package/ui/dist/assets/RecipesPanel-DcqPWRSU.js +1 -0
- package/ui/dist/assets/SecurityPanel-Bv_B9Ahf.js +1 -0
- package/ui/dist/assets/SelfImprovePanel-Ca4rGwEG.js +1 -0
- package/ui/dist/assets/SelfProposalsPanel-DteMSASq.js +1 -0
- package/ui/dist/assets/SessionsPanel-BdbbKTbL.js +1 -0
- package/ui/dist/assets/SessionsTab-BJipiVJs.js +1 -0
- package/ui/dist/assets/SettingsPanel-mV7ykOCc.js +1 -0
- package/ui/dist/assets/SettingsView-Bu_wYTT5.js +2 -0
- package/ui/dist/assets/SkeletonLoader-9H7QWa9E.js +1 -0
- package/ui/dist/assets/SkillsPanel-CHwX2kDR.js +1 -0
- package/ui/dist/assets/SomaView-Bpsm4ZW7.js +5 -0
- package/ui/dist/assets/SomaView-Dghh9om4.css +1 -0
- package/ui/dist/assets/StatCard-wmSgMAM3.js +1 -0
- package/ui/dist/assets/TeamsPanel-XJiHxdDX.js +1 -0
- package/ui/dist/assets/TelemetryPanel-_pmGVxpO.js +1 -0
- package/ui/dist/assets/TitanCanvas-Cb7KG80W.css +1 -0
- package/ui/dist/assets/TitanCanvas-CmLy7p2N.js +883 -0
- package/ui/dist/assets/ToolsView-PHizHZBx.js +2 -0
- package/ui/dist/assets/TraceViewer-CYAzIcfB.js +1 -0
- package/ui/dist/assets/TracesPanel-CRSN_dzK.js +1 -0
- package/ui/dist/assets/TrainingPanel-B9rcuMsV.js +1 -0
- package/ui/dist/assets/VoiceOverlay-BL_ns1ZA.js +7 -0
- package/ui/dist/assets/VramPanel-BNPQ06_9.js +1 -0
- package/ui/dist/assets/WatchView-BgAkNnur.js +13 -0
- package/ui/dist/assets/WorkTab-RRpPpEOg.js +1 -0
- package/ui/dist/assets/WorkflowBuilderPanel-BPHIH8pf.js +1 -0
- package/ui/dist/assets/WorkflowsPanel-DqtmT0NB.js +1 -0
- package/ui/dist/assets/approvalHeadline-BRg3ReTi.js +1 -0
- package/ui/dist/assets/archive-DcK9HrbE.js +1 -0
- package/ui/dist/assets/arrow-left-4c2FnES1.js +1 -0
- package/ui/dist/assets/book-open-Ds_yciQm.js +1 -0
- package/ui/dist/assets/bot-CWDCc265.js +1 -0
- package/ui/dist/assets/chart-column-hhZMdCgB.js +1 -0
- package/ui/dist/assets/chevron-right-A_pvnRh_.js +1 -0
- package/ui/dist/assets/circle-check-big-C-JUM7wb.js +1 -0
- package/ui/dist/assets/circle-stop-yZuG9ZAE.js +1 -0
- package/ui/dist/assets/clipboard-list-C2HUEjZc.js +1 -0
- package/ui/dist/assets/clock-DSPgndSD.js +1 -0
- package/ui/dist/assets/clsx-CjueKrWZ.js +1 -0
- package/ui/dist/assets/createLucideIcon-qnlrxww7.js +1 -0
- package/ui/dist/assets/database-C1SgHa8T.js +1 -0
- package/ui/dist/assets/dollar-sign-D9siqK91.js +1 -0
- package/ui/dist/assets/download-CJZWQNfV.js +1 -0
- package/ui/dist/assets/external-link-CpQLWKKE.js +1 -0
- package/ui/dist/assets/file-text-BbGTi-Po.js +1 -0
- package/ui/dist/assets/flask-conical-B1YVft4E.js +1 -0
- package/ui/dist/assets/folder-EbR-w2Mq.js +1 -0
- package/ui/dist/assets/funnel-D2Dkn8Pf.js +1 -0
- package/ui/dist/assets/git-branch-CQ2yNSzU.js +1 -0
- package/ui/dist/assets/git-pull-request-BxL5wlGX.js +1 -0
- package/ui/dist/assets/index-Cz8190xO.css +2 -0
- package/ui/dist/assets/index-WI-E4I3h.js +243 -0
- package/ui/dist/assets/jsx-runtime-B15e6lOy.js +1 -0
- package/ui/dist/assets/layers-BPu5SRjI.js +1 -0
- package/ui/dist/assets/legacy-BIzifT-t.js +3 -0
- package/ui/dist/assets/lightbulb-BY7CpMte.js +1 -0
- package/ui/dist/assets/link-VDQkrGJw.js +1 -0
- package/ui/dist/assets/list-todo-DfnNUV_T.js +1 -0
- package/ui/dist/assets/message-square-BpzrINHz.js +1 -0
- package/ui/dist/assets/mic-De6oZFaV.js +1 -0
- package/ui/dist/assets/missions-y-qg_f9b.js +1 -0
- package/ui/dist/assets/network-CM7y1ats.js +1 -0
- package/ui/dist/assets/pause-C4TDJSfA.js +1 -0
- package/ui/dist/assets/pencil-Dd_vzHE-.js +1 -0
- package/ui/dist/assets/play-9WxZWVHA.js +1 -0
- package/ui/dist/assets/plug-Dm25Wbrc.js +1 -0
- package/ui/dist/assets/plus-CHryCIkk.js +1 -0
- package/ui/dist/assets/radio-D-XnoAVu.js +1 -0
- package/ui/dist/assets/refresh-cw-Disd5wRB.js +1 -0
- package/ui/dist/assets/rotate-ccw-Cw1T2W1T.js +1 -0
- package/ui/dist/assets/save-DP7oNar2.js +1 -0
- package/ui/dist/assets/search-aNVcX-nv.js +1 -0
- package/ui/dist/assets/send-D7iQD57b.js +1 -0
- package/ui/dist/assets/server-DFfeYjoM.js +1 -0
- package/ui/dist/assets/settings-5NWXzgyY.js +1 -0
- package/ui/dist/assets/shared-DLz_Jtg3.js +1 -0
- package/ui/dist/assets/shield-check-Cd50jCAn.js +1 -0
- package/ui/dist/assets/square-DlcoJ1ZM.js +1 -0
- package/ui/dist/assets/terminal-DgFz2Vsf.js +1 -0
- package/ui/dist/assets/test-tube-BimtS92_.js +1 -0
- package/ui/dist/assets/toggle-right-BMaR-sfT.js +1 -0
- package/ui/dist/assets/trash-2-CwUv7ww1.js +1 -0
- package/ui/dist/assets/trending-up-Ba9TwEow.js +1 -0
- package/ui/dist/assets/trophy-Gfv1QC45.js +1 -0
- package/ui/dist/assets/volume-2-BVlfVl2U.js +1 -0
- package/ui/dist/assets/wifi-off-CWLeusSV.js +1 -0
- package/ui/dist/assets/zoom-out-DeggJ7RM.js +1 -0
- package/ui/dist/index.html +4 -2
- package/ui/dist/sw.js +1 -1
- package/ui/dist/assets/AboutYouMemory-CZKEOd_O.js +0 -11
- package/ui/dist/assets/AuditPanel-Bu71N5Er.js +0 -1
- package/ui/dist/assets/AutonomyPanel-DB-39dX4.js +0 -11
- package/ui/dist/assets/AutopilotPanel-Cj8oKxWv.js +0 -1
- package/ui/dist/assets/AutoresearchPanel-3s8pjm6Z.js +0 -6
- package/ui/dist/assets/BackupPanel-EUQJMJE7.js +0 -1
- package/ui/dist/assets/BrowserPanel-Beo4F5Ar.js +0 -6
- package/ui/dist/assets/Button-MdXmb4_n.js +0 -1
- package/ui/dist/assets/CPActivity-B1Se9VVc.js +0 -1
- package/ui/dist/assets/CPAgentDetail-BYji6tT8.js +0 -1
- package/ui/dist/assets/CPAgents-BSUKucTe.js +0 -1
- package/ui/dist/assets/CPApprovals-B4v0em6e.js +0 -1
- package/ui/dist/assets/CPCosts-9ikh61Kv.js +0 -1
- package/ui/dist/assets/CPDashboard-sOok2T5v.js +0 -1
- package/ui/dist/assets/CPFiles-DB2nY5JE.js +0 -1
- package/ui/dist/assets/CPGoals-8Z1yOY8e.js +0 -1
- package/ui/dist/assets/CPInbox-Cq6-UY8F.js +0 -11
- package/ui/dist/assets/CPIssueDetail-NAGfmJg8.js +0 -1
- package/ui/dist/assets/CPIssues-BzX6Rckx.js +0 -1
- package/ui/dist/assets/CPLayout-BcRM6QSg.js +0 -2
- package/ui/dist/assets/CPOrg-B7DQ4UQP.js +0 -1
- package/ui/dist/assets/CPRuns-D-cpPw_M.js +0 -1
- package/ui/dist/assets/CPSocial-DshtmcK0.js +0 -11
- package/ui/dist/assets/CPVoice-DfXRnQVU.js +0 -1
- package/ui/dist/assets/ChannelsPanel-BnaTOeyf.js +0 -1
- package/ui/dist/assets/CheckpointsPanel-C62naE4U.js +0 -1
- package/ui/dist/assets/CommandPostHub-B_L1CAss.js +0 -24
- package/ui/dist/assets/ConfirmDialog-VErHWAbo.js +0 -1
- package/ui/dist/assets/CronPanel-BwMrrZ5d.js +0 -1
- package/ui/dist/assets/DataTable-BCqSUuN7.js +0 -1
- package/ui/dist/assets/DreamPanel-BNAcCLBB.js +0 -15
- package/ui/dist/assets/EmptyState-D9kQf1PX.js +0 -1
- package/ui/dist/assets/EvalHarnessPanel-BaMLdBNm.js +0 -2
- package/ui/dist/assets/EvalPanel-CW-SSjtg.js +0 -1
- package/ui/dist/assets/FileViewer-BL00nsd7.js +0 -61
- package/ui/dist/assets/FilesPanel-CeC6YUSd.js +0 -16
- package/ui/dist/assets/FleetPanel-EyWF7l3W.js +0 -1
- package/ui/dist/assets/HomelabPanel-BRfKRYe7.js +0 -6
- package/ui/dist/assets/InfraView-Jmqhg_Yc.js +0 -2
- package/ui/dist/assets/InlineEditableField-DgMrLDj1.js +0 -1
- package/ui/dist/assets/Input-CBTwa_Iw.js +0 -1
- package/ui/dist/assets/IntegrationsPanel-DQL-7Ec6.js +0 -1
- package/ui/dist/assets/IntelligenceView-DxD78qfP.js +0 -2
- package/ui/dist/assets/LearningPanel-C_JzSbJU.js +0 -1
- package/ui/dist/assets/LiveStudio-Db5yeaWo.js +0 -7
- package/ui/dist/assets/LogsPanel-CpinKwaZ.js +0 -1
- package/ui/dist/assets/McpPanel-D-MqN_f-.js +0 -1
- package/ui/dist/assets/MemoryGraphPanel-BnD5jRqf.js +0 -1
- package/ui/dist/assets/MemoryTaxonomyPanel-BBI202dd.js +0 -6
- package/ui/dist/assets/MemoryWikiPanel-Dn3XZHL0.js +0 -6
- package/ui/dist/assets/MeshPanel-C-mRJpr0.js +0 -11
- package/ui/dist/assets/MissionCanvas-CbZW83Ch.js +0 -72
- package/ui/dist/assets/MissionChat-Bs75jPAD.js +0 -7
- package/ui/dist/assets/MissionLibrary-BxQ6SxPe.js +0 -5
- package/ui/dist/assets/MissionStart-CBuBPasl.js +0 -1
- package/ui/dist/assets/Modal-ybPGvlQs.js +0 -9
- package/ui/dist/assets/NvidiaPanel-DdG13OX4.js +0 -1
- package/ui/dist/assets/OrganismPanel-xZWUJKyr.js +0 -1
- package/ui/dist/assets/OverviewPanel-B45ZYhJV.js +0 -1
- package/ui/dist/assets/PageHeader-DdrbS1aq.js +0 -1
- package/ui/dist/assets/PersonaProfilesPanel-CKER-EU5.js +0 -8
- package/ui/dist/assets/PersonasPanel-CMMQ3nh3.js +0 -1
- package/ui/dist/assets/PhoneDeskPanel-Bw_Y-cVE.js +0 -11
- package/ui/dist/assets/RecipesPanel-DzMcxpJJ.js +0 -1
- package/ui/dist/assets/SecurityPanel-BCq3Fzbr.js +0 -1
- package/ui/dist/assets/SelfImprovePanel-YE3hT8jK.js +0 -1
- package/ui/dist/assets/SelfProposalsPanel-DXuqBM6p.js +0 -1
- package/ui/dist/assets/SessionsPanel-DORz5goR.js +0 -1
- package/ui/dist/assets/SessionsTab-qYHDOuC8.js +0 -1
- package/ui/dist/assets/SettingsPanel-BbwPu1i7.js +0 -1
- package/ui/dist/assets/SettingsView-zgqkDDBJ.js +0 -2
- package/ui/dist/assets/SkeletonLoader-r6OGXaU9.js +0 -1
- package/ui/dist/assets/SkillsPanel-BjEOBMRo.js +0 -11
- package/ui/dist/assets/SomaView-BtCkeWfZ.css +0 -1
- package/ui/dist/assets/SomaView-DLzApZVW.js +0 -5
- package/ui/dist/assets/StatCard-DLq8aBps.js +0 -1
- package/ui/dist/assets/StatusBadge-BqlvIbdj.js +0 -1
- package/ui/dist/assets/Tabs-DdUThlp1.js +0 -1
- package/ui/dist/assets/TeamsPanel-uIIKoU7I.js +0 -1
- package/ui/dist/assets/TelemetryPanel-dVr3yQQr.js +0 -1
- package/ui/dist/assets/TitanCanvas-BfitWg9R.css +0 -1
- package/ui/dist/assets/TitanCanvas-xHAJqwG2.js +0 -945
- package/ui/dist/assets/ToolsView-CG1mZdiy.js +0 -2
- package/ui/dist/assets/Tooltip-C2tXJcrN.js +0 -6
- package/ui/dist/assets/TraceViewer-BIMD_Osr.js +0 -1
- package/ui/dist/assets/TracesPanel-nMUiyMj5.js +0 -1
- package/ui/dist/assets/TrainingPanel-CT5R4HWm.js +0 -1
- package/ui/dist/assets/VoiceOverlay-Br4QnCtw.js +0 -17
- package/ui/dist/assets/VramPanel-CdnJYytg.js +0 -1
- package/ui/dist/assets/WatchView-TNU6Ns1Q.js +0 -13
- package/ui/dist/assets/WorkTab-wTlX_UKP.js +0 -1
- package/ui/dist/assets/WorkflowBuilderPanel-CDpVt5ux.js +0 -6
- package/ui/dist/assets/WorkflowsPanel-zsFqsLdn.js +0 -11
- package/ui/dist/assets/approvalHeadline-DB9SgR-9.js +0 -1
- package/ui/dist/assets/archive-Bh689DNg.js +0 -6
- package/ui/dist/assets/arrow-left-WkVYLm80.js +0 -6
- package/ui/dist/assets/book-open-ohEXgUsS.js +0 -6
- package/ui/dist/assets/bot-DvEcyQcD.js +0 -6
- package/ui/dist/assets/chart-column-w3goFd5f.js +0 -6
- package/ui/dist/assets/check-CuRYh4Zh.js +0 -6
- package/ui/dist/assets/chevron-right-DbLSHmff.js +0 -6
- package/ui/dist/assets/circle-check-big-Bkba568b.js +0 -6
- package/ui/dist/assets/circle-stop-Bx2ohEg8.js +0 -6
- package/ui/dist/assets/clipboard-list-Dj9tORnu.js +0 -6
- package/ui/dist/assets/clock-JyQAYy6X.js +0 -6
- package/ui/dist/assets/clsx-B-dksMZM.js +0 -1
- package/ui/dist/assets/dollar-sign-XxwPAA2a.js +0 -6
- package/ui/dist/assets/download-D1izVEOL.js +0 -6
- package/ui/dist/assets/external-link-DZ_rZsIt.js +0 -6
- package/ui/dist/assets/file-text-Cfawu8Bs.js +0 -6
- package/ui/dist/assets/flask-conical-Ql8QxhgY.js +0 -6
- package/ui/dist/assets/folder-DAdIw-Pb.js +0 -6
- package/ui/dist/assets/funnel-FycfZcXo.js +0 -6
- package/ui/dist/assets/git-branch-C2qrhGGu.js +0 -6
- package/ui/dist/assets/index-Cml2x9pA.js +0 -507
- package/ui/dist/assets/index-yWybhcfI.css +0 -1
- package/ui/dist/assets/layers-yzjtGlHB.js +0 -6
- package/ui/dist/assets/legacy-D21WSMF1.js +0 -3
- package/ui/dist/assets/lightbulb-BZJNK69S.js +0 -6
- package/ui/dist/assets/link-CnFkBkaY.js +0 -6
- package/ui/dist/assets/list-todo-BlgfQHup.js +0 -6
- package/ui/dist/assets/message-square-CRsG9u6_.js +0 -6
- package/ui/dist/assets/mic-B32RHc7Y.js +0 -6
- package/ui/dist/assets/missions-1iw8Z2_0.js +0 -1
- package/ui/dist/assets/network-C9RXQGrV.js +0 -6
- package/ui/dist/assets/pause-BT1Bc2q3.js +0 -6
- package/ui/dist/assets/pencil-BK5IU6o0.js +0 -6
- package/ui/dist/assets/play-Cl1nC_gc.js +0 -6
- package/ui/dist/assets/plug-DvcpOalj.js +0 -6
- package/ui/dist/assets/plus-Dpo1qfrl.js +0 -6
- package/ui/dist/assets/radio-C5c9IyqX.js +0 -6
- package/ui/dist/assets/refresh-cw-C-iZkzuS.js +0 -6
- package/ui/dist/assets/rotate-ccw-qzrB3SNw.js +0 -6
- package/ui/dist/assets/save-DwR9wQVu.js +0 -6
- package/ui/dist/assets/search-D10l0m3-.js +0 -6
- package/ui/dist/assets/send-DtXGsaFW.js +0 -6
- package/ui/dist/assets/server-BzisF3LL.js +0 -6
- package/ui/dist/assets/settings-Dbwd1_-c.js +0 -6
- package/ui/dist/assets/shield-check-CXlDJyAW.js +0 -6
- package/ui/dist/assets/square-BBC-JKzY.js +0 -11
- package/ui/dist/assets/terminal-DaBIlWEM.js +0 -6
- package/ui/dist/assets/test-tube-CDATRD5t.js +0 -11
- package/ui/dist/assets/toggle-right-DTmrC5RD.js +0 -11
- package/ui/dist/assets/trash-2-CTeyjxD_.js +0 -6
- package/ui/dist/assets/trending-up-CXPRjRV4.js +0 -6
- package/ui/dist/assets/trophy-DbVdXRtU.js +0 -6
- package/ui/dist/assets/volume-2-DYDBmm3M.js +0 -6
- package/ui/dist/assets/wifi-off-BBuIGbiP.js +0 -6
- package/ui/dist/assets/zoom-out-D47_pRAO.js +0 -16
|
@@ -9,7 +9,7 @@ import { compressContext } from "./contextCompressor.js";
|
|
|
9
9
|
const COMPONENT = "Context";
|
|
10
10
|
function calculateBudget(maxContextTokens, systemPrompt, toolDefinitionsCount) {
|
|
11
11
|
const systemTokens = estimateTokens(systemPrompt);
|
|
12
|
-
const toolTokens = toolDefinitionsCount *
|
|
12
|
+
const toolTokens = toolDefinitionsCount * 250;
|
|
13
13
|
const reserveForResponse = 2e3;
|
|
14
14
|
return {
|
|
15
15
|
maxTokens: maxContextTokens,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/contextManager.ts"],"sourcesContent":["/**\n * TITAN — Smart Context Manager\n * Intelligent context window management that none of the competitors do well.\n *\n * Problems this solves:\n * - Auto-GPT wastes tokens on irrelevant history (high cost)\n * - CrewAI has prompt sprawl across agents (unpredictable cost)\n * - Open Interpreter has no context management at all\n *\n * TITAN's approach:\n * - Automatic summarization of old conversation history\n * - Priority-based context allocation (recent > relevant > old)\n * - Token budget tracking and enforcement\n * - Smart truncation that preserves tool call context\n */\nimport logger from '../utils/logger.js';\nimport type { ChatMessage } from '../providers/base.js';\nimport { estimateTokens } from '../utils/tokens.js';\nexport { estimateTokens };\nimport { flushMemoryBeforeCompaction } from '../memory/graph.js';\nimport { getRagContext } from '../memory/vectors.js';\nimport { getPlugins } from '../plugins/registry.js';\nimport { runAssemble, runCompact } from '../plugins/contextEngine.js';\nimport { compressContext, type StructuredSummary } from './contextCompressor.js';\n\nconst COMPONENT = 'Context';\n\ninterface ContextBudget {\n maxTokens: number;\n systemPromptTokens: number;\n toolDefinitionTokens: number;\n remainingForHistory: number;\n}\n\n\n\n/** Calculate the context budget */\nexport function calculateBudget(\n maxContextTokens: number,\n systemPrompt: string,\n toolDefinitionsCount: number,\n): ContextBudget {\n const systemTokens = estimateTokens(systemPrompt);\n const toolTokens = toolDefinitionsCount * 120; // ~120 tokens per tool definition\n const reserveForResponse = 2000; // Reserve for completion\n\n return {\n maxTokens: maxContextTokens,\n systemPromptTokens: systemTokens,\n toolDefinitionTokens: toolTokens,\n remainingForHistory: Math.max(0, maxContextTokens - systemTokens - toolTokens - reserveForResponse),\n };\n}\n\n/** Summarize a batch of messages into a single summary message */\nexport function summarizeMessages(messages: ChatMessage[]): ChatMessage {\n const userMessages = messages.filter((m) => m.role === 'user').map((m) => m.content).filter(Boolean);\n const assistantMessages = messages.filter((m) => m.role === 'assistant').map((m) => m.content).filter(Boolean);\n const toolMessages = messages.filter((m) => m.role === 'tool').length;\n\n const summary = [\n `[Earlier conversation summary — ${messages.length} messages, ${toolMessages} tool calls]`,\n userMessages.length > 0 ? `User discussed: ${userMessages.slice(-3).map((m) => m.slice(0, 80)).join('; ')}` : '',\n assistantMessages.length > 0 ? `Assistant actions: ${assistantMessages.slice(-2).map((m) => m.slice(0, 80)).join('; ')}` : '',\n ].filter(Boolean).join('\\n');\n\n return { role: 'system', content: summary };\n}\n\n/**\n * Compress verbose tool results to save context window space.\n * Keeps the last 5 tool results at full fidelity, summarizes older ones.\n */\nfunction compressToolResults(messages: ChatMessage[]): ChatMessage[] {\n // Count tool messages from the end to find the cutoff\n let toolCount = 0;\n const toolIndices: number[] = [];\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === 'tool') {\n toolCount++;\n toolIndices.push(i);\n }\n }\n\n // If 5 or fewer tool results, nothing to compress\n if (toolCount <= 5) return messages;\n\n // Indices of tool messages to keep at full fidelity (last 5)\n const keepFullSet = new Set(toolIndices.slice(0, 5));\n\n return messages.map((msg, i) => {\n if (msg.role !== 'tool' || keepFullSet.has(i)) return msg;\n const content = msg.content || '';\n if (content.length <= 500) return msg;\n\n // Compress: keep first 150 chars + success/error status\n const isError = content.toLowerCase().includes('error:');\n const summary = `[${msg.name || 'tool'}: ${isError ? 'FAILED' : 'OK'}] ${content.slice(0, 150)}... [${content.length} chars compressed]`;\n return { ...msg, content: summary };\n });\n}\n\n/** Persistent structured summary for iterative context compression */\nlet currentSummary: StructuredSummary | null = null;\n\n/**\n * TITAN pattern: MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES\n * Disables compaction after 3 consecutive failures per session to prevent\n * wasted API calls (This class of bug can waste 250K+ API calls/day).\n */\nlet consecutiveCompactFailures = 0;\nconst MAX_CONSECUTIVE_COMPACT_FAILURES = 3;\n\n/** Reset compact failure counter (call on new session) */\nexport function resetCompactFailures(): void {\n consecutiveCompactFailures = 0;\n}\n\n/** Get the current structured summary (for external access) */\nexport function getStructuredSummary(): StructuredSummary | null {\n return currentSummary;\n}\n\n/** Set the structured summary (e.g., from agent loop state) */\nexport function setStructuredSummary(summary: StructuredSummary | null): void {\n currentSummary = summary;\n}\n\n/**\n * Smart context builder — fits messages within token budget.\n * Uses the 5-phase compression pipeline (Hermes-inspired):\n * 1. Tool output pruning (no LLM call)\n * 2. Head protection (system + first 3 messages)\n * 3. Tail protection (last N messages, 5% of budget)\n * 4. Structured middle summary\n * 5. Iterative summary updates\n */\nexport function buildSmartContext(\n messages: ChatMessage[],\n tokenBudget: number,\n): ChatMessage[] {\n if (messages.length === 0) return [];\n\n // TITAN pattern: skip compaction after consecutive failures\n if (consecutiveCompactFailures >= MAX_CONSECUTIVE_COMPACT_FAILURES) {\n logger.warn(COMPONENT, `Compaction disabled after ${consecutiveCompactFailures} consecutive failures — returning raw messages`);\n return messages;\n }\n\n let result;\n try {\n result = compressContext(messages, tokenBudget, currentSummary);\n consecutiveCompactFailures = 0; // Reset on success\n } catch (err) {\n consecutiveCompactFailures++;\n logger.warn(COMPONENT, `Compaction failed (${consecutiveCompactFailures}/${MAX_CONSECUTIVE_COMPACT_FAILURES}): ${(err as Error).message}`);\n return messages; // Return uncompressed on failure\n }\n\n // Store summary for iterative updates on next compression\n if (result.summary) {\n currentSummary = result.summary;\n }\n\n if (result.savedTokens > 0) {\n logger.debug(COMPONENT, `Smart context: ${messages.length} → ${result.messages.length} messages, saved ~${result.savedTokens} tokens`);\n }\n\n return result.messages;\n}\n\n/**\n * Force context compaction (used by /compact command).\n * Uses the 5-phase pipeline with an aggressive budget to maximize compression.\n */\nexport function forceCompactContext(\n messages: ChatMessage[],\n): { messages: ChatMessage[]; savedTokens: number } {\n if (messages.length <= 4) {\n return { messages, savedTokens: 0 };\n }\n\n // Flush important context to graph memory before discarding (fire-and-forget)\n const toFlush = messages.filter(m => m.role !== 'system');\n flushMemoryBeforeCompaction(toFlush).catch((err) =>\n logger.warn(COMPONENT, `Memory flush before compaction failed: ${(err as Error).message}`),\n );\n\n // Aggressive budget: 4000 tokens forces maximum compression\n const result = compressContext(messages, 4000, currentSummary);\n\n if (result.summary) {\n currentSummary = result.summary;\n }\n\n logger.info(COMPONENT, `Force compacted: ${messages.length} → ${result.messages.length} messages, saved ~${result.savedTokens} tokens`);\n return { messages: result.messages, savedTokens: result.savedTokens };\n}\n\n/** Get context window stats */\nexport function getContextStats(messages: ChatMessage[]): {\n messageCount: number;\n estimatedTokens: number;\n userMessages: number;\n assistantMessages: number;\n toolCalls: number;\n} {\n let tokens = 0;\n let userMsgs = 0;\n let assistantMsgs = 0;\n let toolCalls = 0;\n\n for (const m of messages) {\n tokens += estimateTokens(m.content || '');\n if (m.role === 'user') userMsgs++;\n if (m.role === 'assistant') assistantMsgs++;\n if (m.toolCalls) toolCalls += m.toolCalls.length;\n }\n\n return {\n messageCount: messages.length,\n estimatedTokens: tokens,\n userMessages: userMsgs,\n assistantMessages: assistantMsgs,\n toolCalls,\n };\n}\n\n/**\n * Inject RAG context into messages before the agent turn.\n * Queries the RAG vector store with the latest user message and prepends\n * relevant chunks as a system message.\n */\nexport async function injectRagContext(\n messages: ChatMessage[],\n topK: number = 3,\n): Promise<ChatMessage[]> {\n // Find the last user message\n const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');\n if (!lastUserMsg?.content) return messages;\n\n try {\n const ragContext = await getRagContext(lastUserMsg.content, topK);\n if (!ragContext) return messages;\n\n logger.debug(COMPONENT, `Injecting RAG context (${ragContext.length} chars) for query: \"${lastUserMsg.content.slice(0, 80)}\"`);\n\n const ragMessage: ChatMessage = {\n role: 'system',\n content: `[Relevant knowledge from ingested documents]\\n${ragContext}`,\n };\n\n // Insert RAG context before the last user message\n const result = [...messages];\n const lastUserIndex = result.lastIndexOf(lastUserMsg);\n if (lastUserIndex >= 0) {\n result.splice(lastUserIndex, 0, ragMessage);\n } else {\n result.push(ragMessage);\n }\n return result;\n } catch (e) {\n logger.debug(COMPONENT, `RAG context injection failed: ${(e as Error).message}`);\n return messages;\n }\n}\n\n/**\n * Assemble context with plugin hooks.\n * Runs RAG injection first (built-in), then all registered plugins.\n */\nexport async function assembleContextWithPlugins(\n messages: ChatMessage[],\n userMessage: string,\n ragTopK: number = 3,\n): Promise<ChatMessage[]> {\n // Built-in RAG injection as first step\n let result = await injectRagContext(messages, ragTopK);\n\n // Run registered plugin assemble hooks\n const plugins = getPlugins();\n if (plugins.length > 0) {\n result = await runAssemble(plugins, result, userMessage);\n }\n\n return result;\n}\n\n/**\n * Compact context with plugin hooks.\n * Runs built-in smart context first, then all registered plugins.\n */\nexport async function compactContextWithPlugins(\n messages: ChatMessage[],\n maxTokens: number,\n): Promise<ChatMessage[]> {\n // Built-in smart context compaction\n let result = buildSmartContext(messages, maxTokens);\n\n // Run registered plugin compact hooks\n const plugins = getPlugins();\n if (plugins.length > 0) {\n result = await runCompact(plugins, result, maxTokens);\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/agent/contextManager.ts"],"sourcesContent":["/**\n * TITAN — Smart Context Manager\n * Intelligent context window management that none of the competitors do well.\n *\n * Problems this solves:\n * - Auto-GPT wastes tokens on irrelevant history (high cost)\n * - CrewAI has prompt sprawl across agents (unpredictable cost)\n * - Open Interpreter has no context management at all\n *\n * TITAN's approach:\n * - Automatic summarization of old conversation history\n * - Priority-based context allocation (recent > relevant > old)\n * - Token budget tracking and enforcement\n * - Smart truncation that preserves tool call context\n */\nimport logger from '../utils/logger.js';\nimport type { ChatMessage } from '../providers/base.js';\nimport { estimateTokens } from '../utils/tokens.js';\nexport { estimateTokens };\nimport { flushMemoryBeforeCompaction } from '../memory/graph.js';\nimport { getRagContext } from '../memory/vectors.js';\nimport { getPlugins } from '../plugins/registry.js';\nimport { runAssemble, runCompact } from '../plugins/contextEngine.js';\nimport { compressContext, type StructuredSummary } from './contextCompressor.js';\n\nconst COMPONENT = 'Context';\n\ninterface ContextBudget {\n maxTokens: number;\n systemPromptTokens: number;\n toolDefinitionTokens: number;\n remainingForHistory: number;\n}\n\n\n\n/** Calculate the context budget */\nexport function calculateBudget(\n maxContextTokens: number,\n systemPrompt: string,\n toolDefinitionsCount: number,\n): ContextBudget {\n const systemTokens = estimateTokens(systemPrompt);\n const toolTokens = toolDefinitionsCount * 250; // measured avg 887 chars (~250 tok) per definition — 120 undercounted 4x and blew small contexts\n const reserveForResponse = 2000; // Reserve for completion\n\n return {\n maxTokens: maxContextTokens,\n systemPromptTokens: systemTokens,\n toolDefinitionTokens: toolTokens,\n remainingForHistory: Math.max(0, maxContextTokens - systemTokens - toolTokens - reserveForResponse),\n };\n}\n\n/** Summarize a batch of messages into a single summary message */\nexport function summarizeMessages(messages: ChatMessage[]): ChatMessage {\n const userMessages = messages.filter((m) => m.role === 'user').map((m) => m.content).filter(Boolean);\n const assistantMessages = messages.filter((m) => m.role === 'assistant').map((m) => m.content).filter(Boolean);\n const toolMessages = messages.filter((m) => m.role === 'tool').length;\n\n const summary = [\n `[Earlier conversation summary — ${messages.length} messages, ${toolMessages} tool calls]`,\n userMessages.length > 0 ? `User discussed: ${userMessages.slice(-3).map((m) => m.slice(0, 80)).join('; ')}` : '',\n assistantMessages.length > 0 ? `Assistant actions: ${assistantMessages.slice(-2).map((m) => m.slice(0, 80)).join('; ')}` : '',\n ].filter(Boolean).join('\\n');\n\n return { role: 'system', content: summary };\n}\n\n/**\n * Compress verbose tool results to save context window space.\n * Keeps the last 5 tool results at full fidelity, summarizes older ones.\n */\nfunction compressToolResults(messages: ChatMessage[]): ChatMessage[] {\n // Count tool messages from the end to find the cutoff\n let toolCount = 0;\n const toolIndices: number[] = [];\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === 'tool') {\n toolCount++;\n toolIndices.push(i);\n }\n }\n\n // If 5 or fewer tool results, nothing to compress\n if (toolCount <= 5) return messages;\n\n // Indices of tool messages to keep at full fidelity (last 5)\n const keepFullSet = new Set(toolIndices.slice(0, 5));\n\n return messages.map((msg, i) => {\n if (msg.role !== 'tool' || keepFullSet.has(i)) return msg;\n const content = msg.content || '';\n if (content.length <= 500) return msg;\n\n // Compress: keep first 150 chars + success/error status\n const isError = content.toLowerCase().includes('error:');\n const summary = `[${msg.name || 'tool'}: ${isError ? 'FAILED' : 'OK'}] ${content.slice(0, 150)}... [${content.length} chars compressed]`;\n return { ...msg, content: summary };\n });\n}\n\n/** Persistent structured summary for iterative context compression */\nlet currentSummary: StructuredSummary | null = null;\n\n/**\n * TITAN pattern: MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES\n * Disables compaction after 3 consecutive failures per session to prevent\n * wasted API calls (This class of bug can waste 250K+ API calls/day).\n */\nlet consecutiveCompactFailures = 0;\nconst MAX_CONSECUTIVE_COMPACT_FAILURES = 3;\n\n/** Reset compact failure counter (call on new session) */\nexport function resetCompactFailures(): void {\n consecutiveCompactFailures = 0;\n}\n\n/** Get the current structured summary (for external access) */\nexport function getStructuredSummary(): StructuredSummary | null {\n return currentSummary;\n}\n\n/** Set the structured summary (e.g., from agent loop state) */\nexport function setStructuredSummary(summary: StructuredSummary | null): void {\n currentSummary = summary;\n}\n\n/**\n * Smart context builder — fits messages within token budget.\n * Uses the 5-phase compression pipeline (Hermes-inspired):\n * 1. Tool output pruning (no LLM call)\n * 2. Head protection (system + first 3 messages)\n * 3. Tail protection (last N messages, 5% of budget)\n * 4. Structured middle summary\n * 5. Iterative summary updates\n */\nexport function buildSmartContext(\n messages: ChatMessage[],\n tokenBudget: number,\n): ChatMessage[] {\n if (messages.length === 0) return [];\n\n // TITAN pattern: skip compaction after consecutive failures\n if (consecutiveCompactFailures >= MAX_CONSECUTIVE_COMPACT_FAILURES) {\n logger.warn(COMPONENT, `Compaction disabled after ${consecutiveCompactFailures} consecutive failures — returning raw messages`);\n return messages;\n }\n\n let result;\n try {\n result = compressContext(messages, tokenBudget, currentSummary);\n consecutiveCompactFailures = 0; // Reset on success\n } catch (err) {\n consecutiveCompactFailures++;\n logger.warn(COMPONENT, `Compaction failed (${consecutiveCompactFailures}/${MAX_CONSECUTIVE_COMPACT_FAILURES}): ${(err as Error).message}`);\n return messages; // Return uncompressed on failure\n }\n\n // Store summary for iterative updates on next compression\n if (result.summary) {\n currentSummary = result.summary;\n }\n\n if (result.savedTokens > 0) {\n logger.debug(COMPONENT, `Smart context: ${messages.length} → ${result.messages.length} messages, saved ~${result.savedTokens} tokens`);\n }\n\n return result.messages;\n}\n\n/**\n * Force context compaction (used by /compact command).\n * Uses the 5-phase pipeline with an aggressive budget to maximize compression.\n */\nexport function forceCompactContext(\n messages: ChatMessage[],\n): { messages: ChatMessage[]; savedTokens: number } {\n if (messages.length <= 4) {\n return { messages, savedTokens: 0 };\n }\n\n // Flush important context to graph memory before discarding (fire-and-forget)\n const toFlush = messages.filter(m => m.role !== 'system');\n flushMemoryBeforeCompaction(toFlush).catch((err) =>\n logger.warn(COMPONENT, `Memory flush before compaction failed: ${(err as Error).message}`),\n );\n\n // Aggressive budget: 4000 tokens forces maximum compression\n const result = compressContext(messages, 4000, currentSummary);\n\n if (result.summary) {\n currentSummary = result.summary;\n }\n\n logger.info(COMPONENT, `Force compacted: ${messages.length} → ${result.messages.length} messages, saved ~${result.savedTokens} tokens`);\n return { messages: result.messages, savedTokens: result.savedTokens };\n}\n\n/** Get context window stats */\nexport function getContextStats(messages: ChatMessage[]): {\n messageCount: number;\n estimatedTokens: number;\n userMessages: number;\n assistantMessages: number;\n toolCalls: number;\n} {\n let tokens = 0;\n let userMsgs = 0;\n let assistantMsgs = 0;\n let toolCalls = 0;\n\n for (const m of messages) {\n tokens += estimateTokens(m.content || '');\n if (m.role === 'user') userMsgs++;\n if (m.role === 'assistant') assistantMsgs++;\n if (m.toolCalls) toolCalls += m.toolCalls.length;\n }\n\n return {\n messageCount: messages.length,\n estimatedTokens: tokens,\n userMessages: userMsgs,\n assistantMessages: assistantMsgs,\n toolCalls,\n };\n}\n\n/**\n * Inject RAG context into messages before the agent turn.\n * Queries the RAG vector store with the latest user message and prepends\n * relevant chunks as a system message.\n */\nexport async function injectRagContext(\n messages: ChatMessage[],\n topK: number = 3,\n): Promise<ChatMessage[]> {\n // Find the last user message\n const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');\n if (!lastUserMsg?.content) return messages;\n\n try {\n const ragContext = await getRagContext(lastUserMsg.content, topK);\n if (!ragContext) return messages;\n\n logger.debug(COMPONENT, `Injecting RAG context (${ragContext.length} chars) for query: \"${lastUserMsg.content.slice(0, 80)}\"`);\n\n const ragMessage: ChatMessage = {\n role: 'system',\n content: `[Relevant knowledge from ingested documents]\\n${ragContext}`,\n };\n\n // Insert RAG context before the last user message\n const result = [...messages];\n const lastUserIndex = result.lastIndexOf(lastUserMsg);\n if (lastUserIndex >= 0) {\n result.splice(lastUserIndex, 0, ragMessage);\n } else {\n result.push(ragMessage);\n }\n return result;\n } catch (e) {\n logger.debug(COMPONENT, `RAG context injection failed: ${(e as Error).message}`);\n return messages;\n }\n}\n\n/**\n * Assemble context with plugin hooks.\n * Runs RAG injection first (built-in), then all registered plugins.\n */\nexport async function assembleContextWithPlugins(\n messages: ChatMessage[],\n userMessage: string,\n ragTopK: number = 3,\n): Promise<ChatMessage[]> {\n // Built-in RAG injection as first step\n let result = await injectRagContext(messages, ragTopK);\n\n // Run registered plugin assemble hooks\n const plugins = getPlugins();\n if (plugins.length > 0) {\n result = await runAssemble(plugins, result, userMessage);\n }\n\n return result;\n}\n\n/**\n * Compact context with plugin hooks.\n * Runs built-in smart context first, then all registered plugins.\n */\nexport async function compactContextWithPlugins(\n messages: ChatMessage[],\n maxTokens: number,\n): Promise<ChatMessage[]> {\n // Built-in smart context compaction\n let result = buildSmartContext(messages, maxTokens);\n\n // Run registered plugin compact hooks\n const plugins = getPlugins();\n if (plugins.length > 0) {\n result = await runCompact(plugins, result, maxTokens);\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const ACTION_CLAIM_RULES = [
|
|
3
|
+
{
|
|
4
|
+
action: "scheduling",
|
|
5
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:set|scheduled|created)|(?:reminder|schedule[d]?)\s+(?:is\s+)?(?:set|created|scheduled)|i(?:'|’)?ll remind you|scheduled a reminder)\b/i,
|
|
6
|
+
satisfiedBy: /(^|_)(reminder|cron|schedule|event_trigger|workflows?|social_scheduler)/i,
|
|
7
|
+
correction: "I described scheduling that, but I did not actually create a scheduled job this turn. Ask me again and I'll set it with the reminder tool properly."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
action: "sending a message/email",
|
|
11
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:sent|emailed|messaged|replied|forwarded)|(?:email|message|reply)\s+(?:has been|was)\s+sent|i(?:'|’)?ve dm(?:'|’)?d)\b/i,
|
|
12
|
+
satisfiedBy: /(^|_)(send|email|message|reply|forward|dm|sms|slack|discord|telegram|whatsapp|matrix|notify|post_message|messenger)/i,
|
|
13
|
+
correction: "I described sending that message, but no send tool ran this turn \u2014 nothing was actually sent. Tell me to send it and I will."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
action: "posting/publishing",
|
|
17
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:posted|published|tweeted|shared)|(?:posted|published)\s+(?:it|the|to)\b)/i,
|
|
18
|
+
satisfiedBy: /(^|_)(post|publish|tweet|share|x_|fb_|social|content_publish|deploy_website)/i,
|
|
19
|
+
correction: "I described posting/publishing that, but no publish tool ran this turn \u2014 nothing went live. Confirm and I'll actually publish it."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
action: "deleting/removing",
|
|
23
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:deleted|removed|cleared|wiped)|(?:has been|was)\s+(?:deleted|removed))\b/i,
|
|
24
|
+
satisfiedBy: /(^|_)(delete|remove|clear|wipe|purge|rm|trash|unlink)/i,
|
|
25
|
+
correction: "I described deleting that, but no delete tool ran this turn \u2014 nothing was actually removed."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
action: "deploying",
|
|
29
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:deployed|shipped|released|pushed live)|(?:has been|was)\s+deployed|deployment\s+(?:is\s+)?complete)\b/i,
|
|
30
|
+
satisfiedBy: /(^|_)(deploy|ship|release|publish|rollout|kubectl|docker_push)/i,
|
|
31
|
+
correction: "I described deploying that, but no deploy tool ran this turn \u2014 nothing was actually deployed."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
action: "writing a file",
|
|
35
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:saved|written|created|updated)\s+(?:the\s+)?(?:file|it to|config)|(?:file|it)\s+(?:has been|was)\s+(?:saved|written|created))\b/i,
|
|
36
|
+
satisfiedBy: /(^|_)(write|save|edit|create_file|file_write|append|patch|str_replace)/i,
|
|
37
|
+
correction: "I described saving/writing that file, but no file-write tool ran this turn \u2014 nothing was actually written to disk."
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
function detectFabricatedActions(finalContent, toolsUsed) {
|
|
41
|
+
if (!finalContent) return [];
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const rule of ACTION_CLAIM_RULES) {
|
|
44
|
+
if (!rule.claim.test(finalContent)) continue;
|
|
45
|
+
const satisfied = toolsUsed.some((t) => rule.satisfiedBy.test(t));
|
|
46
|
+
if (!satisfied) findings.push({ action: rule.action, correction: rule.correction });
|
|
47
|
+
}
|
|
48
|
+
return findings;
|
|
49
|
+
}
|
|
50
|
+
function applyHonestyGuard(finalContent, toolsUsed) {
|
|
51
|
+
const findings = detectFabricatedActions(finalContent, toolsUsed);
|
|
52
|
+
if (findings.length === 0) return { content: finalContent, flagged: [] };
|
|
53
|
+
const block = findings.length === 1 ? `
|
|
54
|
+
|
|
55
|
+
\u26A0\uFE0F Correction: ${findings[0].correction}` : `
|
|
56
|
+
|
|
57
|
+
\u26A0\uFE0F Correction: I described actions I didn't actually take this turn:
|
|
58
|
+
${findings.map((f) => `\u2022 ${f.correction}`).join("\n")}`;
|
|
59
|
+
return { content: finalContent + block, flagged: findings.map((f) => f.action) };
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
ACTION_CLAIM_RULES,
|
|
63
|
+
applyHonestyGuard,
|
|
64
|
+
detectFabricatedActions
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=honestyGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/honestyGuard.ts"],"sourcesContent":["/**\n * Honesty Guard — the verification wall (v7.1.x).\n *\n * The deterministic backstop that makes TITAN structurally honest: if a reply\n * CLAIMS it performed a side-effectful action (sent, posted, deleted, deployed,\n * saved a file, scheduled a job…) but no tool capable of that action ran this\n * turn, the claim is fabricated and gets a visible correction appended.\n *\n * Model-agnostic and deterministic by design — prompt steering tries to\n * prevent fabrication, but a weaker local model will still bluff; this\n * guarantees the user is never silently misled. It is the single most\n * important organ for running TITAN \"like Fable 5\" on any model: honesty is\n * not a capability, it's an enforced invariant.\n *\n * Pure function → fully unit-testable. Only APPENDS text; never blocks tools\n * or changes behavior, so its blast radius is a string concat.\n */\n\nexport interface ActionClaimRule {\n /** Short name of the action class (for the correction message + logs). */\n action: string;\n /** Reply-text pattern that indicates the model CLAIMED this action. */\n claim: RegExp;\n /** A tool name matching this satisfies the claim (segment-aware). */\n satisfiedBy: RegExp;\n /** What to tell the user when the claim is unbacked. */\n correction: string;\n}\n\n/**\n * Rules map \"the reply says it did X\" → \"a tool that does X must have run\".\n * Kept conservative: each `claim` targets explicit past/committed phrasing\n * (\"I've sent\", \"posted the\", \"deleted the\") — NOT hedged/intent phrasing\n * (\"I can send\", \"want me to post\") — so we never false-flag an offer.\n */\nexport const ACTION_CLAIM_RULES: ActionClaimRule[] = [\n {\n action: 'scheduling',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:set|scheduled|created)|(?:reminder|schedule[d]?)\\s+(?:is\\s+)?(?:set|created|scheduled)|i(?:'|’)?ll remind you|scheduled a reminder)\\b/i,\n satisfiedBy: /(^|_)(reminder|cron|schedule|event_trigger|workflows?|social_scheduler)/i,\n correction: 'I described scheduling that, but I did not actually create a scheduled job this turn. Ask me again and I\\'ll set it with the reminder tool properly.',\n },\n {\n action: 'sending a message/email',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:sent|emailed|messaged|replied|forwarded)|(?:email|message|reply)\\s+(?:has been|was)\\s+sent|i(?:'|’)?ve dm(?:'|’)?d)\\b/i,\n satisfiedBy: /(^|_)(send|email|message|reply|forward|dm|sms|slack|discord|telegram|whatsapp|matrix|notify|post_message|messenger)/i,\n correction: 'I described sending that message, but no send tool ran this turn — nothing was actually sent. Tell me to send it and I will.',\n },\n {\n action: 'posting/publishing',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:posted|published|tweeted|shared)|(?:posted|published)\\s+(?:it|the|to)\\b)/i,\n satisfiedBy: /(^|_)(post|publish|tweet|share|x_|fb_|social|content_publish|deploy_website)/i,\n correction: 'I described posting/publishing that, but no publish tool ran this turn — nothing went live. Confirm and I\\'ll actually publish it.',\n },\n {\n action: 'deleting/removing',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:deleted|removed|cleared|wiped)|(?:has been|was)\\s+(?:deleted|removed))\\b/i,\n satisfiedBy: /(^|_)(delete|remove|clear|wipe|purge|rm|trash|unlink)/i,\n correction: 'I described deleting that, but no delete tool ran this turn — nothing was actually removed.',\n },\n {\n action: 'deploying',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:deployed|shipped|released|pushed live)|(?:has been|was)\\s+deployed|deployment\\s+(?:is\\s+)?complete)\\b/i,\n satisfiedBy: /(^|_)(deploy|ship|release|publish|rollout|kubectl|docker_push)/i,\n correction: 'I described deploying that, but no deploy tool ran this turn — nothing was actually deployed.',\n },\n {\n action: 'writing a file',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:saved|written|created|updated)\\s+(?:the\\s+)?(?:file|it to|config)|(?:file|it)\\s+(?:has been|was)\\s+(?:saved|written|created))\\b/i,\n satisfiedBy: /(^|_)(write|save|edit|create_file|file_write|append|patch|str_replace)/i,\n correction: 'I described saving/writing that file, but no file-write tool ran this turn — nothing was actually written to disk.',\n },\n];\n\nexport interface HonestyFinding {\n action: string;\n correction: string;\n}\n\n/**\n * Detect fabricated action-claims. Pure: returns the unbacked claims, in rule\n * order, deduped by action. `toolsUsed` is the list of tool names that ran.\n */\nexport function detectFabricatedActions(finalContent: string, toolsUsed: string[]): HonestyFinding[] {\n if (!finalContent) return [];\n const findings: HonestyFinding[] = [];\n for (const rule of ACTION_CLAIM_RULES) {\n if (!rule.claim.test(finalContent)) continue;\n const satisfied = toolsUsed.some(t => rule.satisfiedBy.test(t));\n if (!satisfied) findings.push({ action: rule.action, correction: rule.correction });\n }\n return findings;\n}\n\n/**\n * Apply the guard: returns the (possibly corrected) content + which actions\n * were flagged. Appends ONE correction block for all findings.\n */\nexport function applyHonestyGuard(finalContent: string, toolsUsed: string[]): { content: string; flagged: string[] } {\n const findings = detectFabricatedActions(finalContent, toolsUsed);\n if (findings.length === 0) return { content: finalContent, flagged: [] };\n const block = findings.length === 1\n ? `\\n\\n⚠️ Correction: ${findings[0].correction}`\n : `\\n\\n⚠️ Correction: I described actions I didn't actually take this turn:\\n${findings.map(f => `• ${f.correction}`).join('\\n')}`;\n return { content: finalContent + block, flagged: findings.map(f => f.action) };\n}\n"],"mappings":";AAmCO,MAAM,qBAAwC;AAAA,EACjD;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AACJ;AAWO,SAAS,wBAAwB,cAAsB,WAAuC;AACjG,MAAI,CAAC,aAAc,QAAO,CAAC;AAC3B,QAAM,WAA6B,CAAC;AACpC,aAAW,QAAQ,oBAAoB;AACnC,QAAI,CAAC,KAAK,MAAM,KAAK,YAAY,EAAG;AACpC,UAAM,YAAY,UAAU,KAAK,OAAK,KAAK,YAAY,KAAK,CAAC,CAAC;AAC9D,QAAI,CAAC,UAAW,UAAS,KAAK,EAAE,QAAQ,KAAK,QAAQ,YAAY,KAAK,WAAW,CAAC;AAAA,EACtF;AACA,SAAO;AACX;AAMO,SAAS,kBAAkB,cAAsB,WAA6D;AACjH,QAAM,WAAW,wBAAwB,cAAc,SAAS;AAChE,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE,SAAS,cAAc,SAAS,CAAC,EAAE;AACvE,QAAM,QAAQ,SAAS,WAAW,IAC5B;AAAA;AAAA,2BAAsB,SAAS,CAAC,EAAE,UAAU,KAC5C;AAAA;AAAA;AAAA,EAA6E,SAAS,IAAI,OAAK,UAAK,EAAE,UAAU,EAAE,EAAE,KAAK,IAAI,CAAC;AACpI,SAAO,EAAE,SAAS,eAAe,OAAO,SAAS,SAAS,IAAI,OAAK,EAAE,MAAM,EAAE;AACjF;","names":[]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
const COMPONENT = "SelfCritique";
|
|
4
|
+
const DRAFT_CAP = 4e3;
|
|
5
|
+
const CRITIQUE_MAX_TOKENS = 400;
|
|
6
|
+
const CRITIQUE_TIMEOUT_MS = 3e4;
|
|
7
|
+
const DEFAULT_SELF_CRITIQUE = {
|
|
8
|
+
enabled: false,
|
|
9
|
+
minToolCalls: 1,
|
|
10
|
+
minDraftChars: 200,
|
|
11
|
+
model: ""
|
|
12
|
+
};
|
|
13
|
+
function resolveSelfCritique(selfCritique, reliabilityMode) {
|
|
14
|
+
const base = { ...DEFAULT_SELF_CRITIQUE, ...selfCritique || {} };
|
|
15
|
+
if (reliabilityMode && selfCritique?.enabled === void 0) base.enabled = true;
|
|
16
|
+
return base;
|
|
17
|
+
}
|
|
18
|
+
function shouldCritique(cfg, draft, toolsUsed) {
|
|
19
|
+
if (!cfg.enabled) return false;
|
|
20
|
+
if (!draft || draft.length < cfg.minDraftChars) return false;
|
|
21
|
+
if (toolsUsed.length < cfg.minToolCalls) return false;
|
|
22
|
+
if (/On reflection[:,]|⚠️ Correction:/.test(draft)) return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
function buildCritiquePrompt(task, draft, toolsUsed) {
|
|
26
|
+
return [
|
|
27
|
+
"You are reviewing your OWN draft answer adversarially, before it reaches the user.",
|
|
28
|
+
"Be skeptical: your job is to catch what the draft got wrong, not to praise it.",
|
|
29
|
+
"",
|
|
30
|
+
`USER TASK: ${task.slice(0, 800)}`,
|
|
31
|
+
`TOOLS YOU ACTUALLY RAN THIS TURN: ${toolsUsed.length ? toolsUsed.join(", ") : "(none)"}`,
|
|
32
|
+
"",
|
|
33
|
+
"YOUR DRAFT ANSWER:",
|
|
34
|
+
'"""',
|
|
35
|
+
draft.slice(0, DRAFT_CAP),
|
|
36
|
+
'"""',
|
|
37
|
+
"",
|
|
38
|
+
"Find ONLY concrete problems in these three classes:",
|
|
39
|
+
" 1. CLAIMED-BUT-UNVERIFIED: the draft states something happened or is true that the tools run above do NOT actually support.",
|
|
40
|
+
" 2. LIKELY-WRONG: a factual/logical error a careful reader would catch.",
|
|
41
|
+
" 3. UNSTATED-RISK: a caveat or failure case the user needs but the draft omits.",
|
|
42
|
+
"",
|
|
43
|
+
"If the draft is solid, reply with exactly: SOLID",
|
|
44
|
+
'Otherwise reply with 1-3 short bullet lines, each starting with "- ", naming a real issue. No preamble.'
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
function parseCritique(response) {
|
|
48
|
+
const text = (response || "").trim();
|
|
49
|
+
if (!text || /^solid\b/i.test(text)) return { solid: true, issues: [] };
|
|
50
|
+
const issues = text.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("-")).map((l) => l.replace(/^-\s*/, "").trim()).filter(Boolean).slice(0, 3);
|
|
51
|
+
if (issues.length === 0) return { solid: true, issues: [] };
|
|
52
|
+
return { solid: false, issues };
|
|
53
|
+
}
|
|
54
|
+
function formatReflection(issues) {
|
|
55
|
+
if (issues.length === 0) return "";
|
|
56
|
+
if (issues.length === 1) return `
|
|
57
|
+
|
|
58
|
+
_On reflection: ${issues[0]}_`;
|
|
59
|
+
return `
|
|
60
|
+
|
|
61
|
+
_On reflection, a couple of caveats:_
|
|
62
|
+
${issues.map((i) => `- ${i}`).join("\n")}`;
|
|
63
|
+
}
|
|
64
|
+
async function runSelfCritique(cfg, task, draft, toolsUsed, turnModel) {
|
|
65
|
+
if (!shouldCritique(cfg, draft, toolsUsed)) return { content: draft, critiqued: false, issues: [] };
|
|
66
|
+
try {
|
|
67
|
+
const { chat } = await import("../providers/router.js");
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const timer = setTimeout(() => controller.abort(), CRITIQUE_TIMEOUT_MS);
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
const res = await chat({
|
|
73
|
+
model: cfg.model || turnModel,
|
|
74
|
+
messages: [{ role: "user", content: buildCritiquePrompt(task, draft, toolsUsed) }],
|
|
75
|
+
maxTokens: CRITIQUE_MAX_TOKENS,
|
|
76
|
+
temperature: 0.2,
|
|
77
|
+
signal: controller.signal
|
|
78
|
+
});
|
|
79
|
+
response = res.content || "";
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
}
|
|
83
|
+
const verdict = parseCritique(response);
|
|
84
|
+
if (verdict.solid) return { content: draft, critiqued: true, issues: [] };
|
|
85
|
+
logger.info(COMPONENT, `Self-critique surfaced ${verdict.issues.length} issue(s); appended reflection`);
|
|
86
|
+
return { content: draft + formatReflection(verdict.issues), critiqued: true, issues: verdict.issues };
|
|
87
|
+
} catch (e) {
|
|
88
|
+
logger.debug(COMPONENT, `Self-critique skipped (error): ${e.message}`);
|
|
89
|
+
return { content: draft, critiqued: false, issues: [] };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
DEFAULT_SELF_CRITIQUE,
|
|
94
|
+
buildCritiquePrompt,
|
|
95
|
+
formatReflection,
|
|
96
|
+
parseCritique,
|
|
97
|
+
resolveSelfCritique,
|
|
98
|
+
runSelfCritique,
|
|
99
|
+
shouldCritique
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=selfCritique.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/selfCritique.ts"],"sourcesContent":["/**\n * Self-Critique — adversarial reflection pass (v7.1.x).\n *\n * Organ 5 of the Fable-5 rebuild plan (docs/handoff/REBUILD-FABLE-5.md): the\n * discipline that lets a capable local model reach frontier-*reliability*\n * without frontier-*IQ*. A single model can't out-think itself in one pass —\n * but it can catch its own mistakes in a SECOND pass framed adversarially.\n *\n * After a substantive turn, one bounded critique call asks the model to find,\n * in its own draft: claims it made but did NOT verify, likely errors, and\n * unstated risks. If the draft is solid → no change. If not → an honest\n * \"On reflection\" caveat is APPENDED (never a silent rewrite — revising can\n * make things worse; a caveat is strictly informative and safe).\n *\n * OFF BY DEFAULT (`agent.selfCritique.enabled`). Single extra call, hard\n * length caps, its own timeout — bounded latency/cost. Skips trivial turns.\n * Pure helpers (gate, prompt, parse) are exported for unit tests.\n */\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'SelfCritique';\nconst DRAFT_CAP = 4_000;\nconst CRITIQUE_MAX_TOKENS = 400;\nconst CRITIQUE_TIMEOUT_MS = 30_000;\n\nexport interface SelfCritiqueConfig {\n enabled: boolean;\n /** Only critique turns that ran at least this many tools (substance gate). */\n minToolCalls: number;\n /** Skip drafts shorter than this (trivial answers). */\n minDraftChars: number;\n /** Model for the critique pass; empty = same as the turn's model. */\n model: string;\n}\n\nexport const DEFAULT_SELF_CRITIQUE: SelfCritiqueConfig = {\n enabled: false,\n minToolCalls: 1,\n minDraftChars: 200,\n model: '',\n};\n\n/**\n * Resolve the effective self-critique config given the one-switch\n * `reliabilityMode`. Reliability Mode is the \"best harness\" preset: it turns\n * on self-critique (Organ 5) without the user editing nested config. An\n * explicit `selfCritique.enabled` still wins if set. Pure.\n */\nexport function resolveSelfCritique(\n selfCritique: Partial<SelfCritiqueConfig> | undefined,\n reliabilityMode: boolean,\n): SelfCritiqueConfig {\n const base = { ...DEFAULT_SELF_CRITIQUE, ...(selfCritique || {}) };\n // reliabilityMode flips the default ON; an explicit false in config still wins.\n if (reliabilityMode && selfCritique?.enabled === undefined) base.enabled = true;\n return base;\n}\n\n/** Should this turn be critiqued? Pure. */\nexport function shouldCritique(cfg: SelfCritiqueConfig, draft: string, toolsUsed: string[]): boolean {\n if (!cfg.enabled) return false;\n if (!draft || draft.length < cfg.minDraftChars) return false;\n if (toolsUsed.length < cfg.minToolCalls) return false;\n // Never critique a reply that's already a correction/caveat (avoid loops).\n if (/On reflection[:,]|⚠️ Correction:/.test(draft)) return false;\n return true;\n}\n\n/** Build the adversarial critique prompt. Pure. */\nexport function buildCritiquePrompt(task: string, draft: string, toolsUsed: string[]): string {\n return [\n 'You are reviewing your OWN draft answer adversarially, before it reaches the user.',\n 'Be skeptical: your job is to catch what the draft got wrong, not to praise it.',\n '',\n `USER TASK: ${task.slice(0, 800)}`,\n `TOOLS YOU ACTUALLY RAN THIS TURN: ${toolsUsed.length ? toolsUsed.join(', ') : '(none)'}`,\n '',\n 'YOUR DRAFT ANSWER:',\n '\"\"\"',\n draft.slice(0, DRAFT_CAP),\n '\"\"\"',\n '',\n 'Find ONLY concrete problems in these three classes:',\n ' 1. CLAIMED-BUT-UNVERIFIED: the draft states something happened or is true that the tools run above do NOT actually support.',\n ' 2. LIKELY-WRONG: a factual/logical error a careful reader would catch.',\n ' 3. UNSTATED-RISK: a caveat or failure case the user needs but the draft omits.',\n '',\n 'If the draft is solid, reply with exactly: SOLID',\n 'Otherwise reply with 1-3 short bullet lines, each starting with \"- \", naming a real issue. No preamble.',\n ].join('\\n');\n}\n\nexport interface CritiqueVerdict {\n solid: boolean;\n issues: string[];\n}\n\n/** Parse a critique response. Pure. */\nexport function parseCritique(response: string): CritiqueVerdict {\n const text = (response || '').trim();\n if (!text || /^solid\\b/i.test(text)) return { solid: true, issues: [] };\n const issues = text\n .split('\\n')\n .map(l => l.trim())\n .filter(l => l.startsWith('-'))\n .map(l => l.replace(/^-\\s*/, '').trim())\n .filter(Boolean)\n .slice(0, 3);\n // A response with no bullets and no \"SOLID\" is ambiguous → treat as solid\n // (don't append noise on a malformed critique).\n if (issues.length === 0) return { solid: true, issues: [] };\n return { solid: false, issues };\n}\n\n/** Format the appended caveat. Pure. */\nexport function formatReflection(issues: string[]): string {\n if (issues.length === 0) return '';\n if (issues.length === 1) return `\\n\\n_On reflection: ${issues[0]}_`;\n return `\\n\\n_On reflection, a couple of caveats:_\\n${issues.map(i => `- ${i}`).join('\\n')}`;\n}\n\n/**\n * Run the critique pass. Returns the (possibly caveated) content + whether it\n * fired. Never throws — a failed critique returns the draft unchanged.\n */\nexport async function runSelfCritique(\n cfg: SelfCritiqueConfig,\n task: string,\n draft: string,\n toolsUsed: string[],\n turnModel: string,\n): Promise<{ content: string; critiqued: boolean; issues: string[] }> {\n if (!shouldCritique(cfg, draft, toolsUsed)) return { content: draft, critiqued: false, issues: [] };\n try {\n const { chat } = await import('../providers/router.js');\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), CRITIQUE_TIMEOUT_MS);\n let response: string;\n try {\n const res = await chat({\n model: cfg.model || turnModel,\n messages: [{ role: 'user', content: buildCritiquePrompt(task, draft, toolsUsed) }],\n maxTokens: CRITIQUE_MAX_TOKENS,\n temperature: 0.2,\n signal: controller.signal,\n } as Parameters<typeof chat>[0]);\n response = res.content || '';\n } finally {\n clearTimeout(timer);\n }\n const verdict = parseCritique(response);\n if (verdict.solid) return { content: draft, critiqued: true, issues: [] };\n logger.info(COMPONENT, `Self-critique surfaced ${verdict.issues.length} issue(s); appended reflection`);\n return { content: draft + formatReflection(verdict.issues), critiqued: true, issues: verdict.issues };\n } catch (e) {\n logger.debug(COMPONENT, `Self-critique skipped (error): ${(e as Error).message}`);\n return { content: draft, critiqued: false, issues: [] };\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/agent/toolSearch.js
CHANGED
|
@@ -47,8 +47,15 @@ const DEFAULT_CORE_TOOLS = [
|
|
|
47
47
|
"list_spaces"
|
|
48
48
|
];
|
|
49
49
|
function buildToolCatalog() {
|
|
50
|
-
const tools = getRegisteredTools();
|
|
51
|
-
|
|
50
|
+
const tools = getRegisteredTools().filter((t) => t.name !== "tool_search");
|
|
51
|
+
const groups = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const t of tools) {
|
|
53
|
+
const ns = t.name.includes("_") ? t.name.split("_")[0] : t.name;
|
|
54
|
+
const g = groups.get(ns) || [];
|
|
55
|
+
g.push(t.name);
|
|
56
|
+
groups.set(ns, g);
|
|
57
|
+
}
|
|
58
|
+
return [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([ns, names]) => names.length === 1 ? names[0] : `${ns}: ${names.join(", ")}`).join(" | ");
|
|
52
59
|
}
|
|
53
60
|
function searchTools(query, metadataOnly = false) {
|
|
54
61
|
const tools = getRegisteredTools();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/toolSearch.ts"],"sourcesContent":["/**\n * TITAN — Tool Search\n *\n * Meta-tool that lets the LLM discover tools on demand instead of seeing\n * all 80+ tool schemas on every request. Reduces input tokens by 60-80%.\n *\n * How it works:\n * 1. Only core tools + tool_search are sent to the LLM (5-8 tools, ~600 tokens)\n * 2. tool_search description includes a compact catalog of ALL available tools\n * 3. When the LLM calls tool_search, matching tools are returned\n * 4. Agent loop adds discovered tools to activeTools for subsequent rounds\n *\n * Inspired by Anthropic's tool search pattern (2025) but works with ALL providers.\n */\nimport { getRegisteredTools, type ToolHandler } from './toolRunner.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'ToolSearch';\n\n/** Default core tools — always sent to the LLM without needing search */\nexport const DEFAULT_CORE_TOOLS = [\n 'shell',\n 'read_file',\n 'write_file',\n 'edit_file',\n 'list_dir',\n 'web_search',\n 'web_fetch',\n 'web_act',\n 'smart_form_fill',\n 'weather',\n 'memory',\n // v7.0: \"remind me at X\" is a core personal-assistant verb — models were\n // fabricating reminders precisely because this tool wasn't in their view.\n 'reminder',\n 'system_info',\n 'goal_list',\n 'spawn_agent',\n 'tool_search',\n 'tool_expand',\n 'self_doctor',\n 'ha_control',\n 'ha_devices',\n 'ha_status',\n 'gallery_search',\n 'gallery_get',\n 'gallery_list',\n // v6.0.2 — Canvas widget tools must always be visible so the model\n // reaches for them on \"build me X\" prompts (the v6.0 thesis). Before\n // this, only gallery_search was in core; create_widget was only\n // discoverable via tool_search, which the model rarely called for\n // novel asks like \"build me a solar system\" — it'd fall through to\n // shell + mkdir instead. These four tools are cheap (~80 tokens\n // combined) and they're load-bearing for the Living Canvas pitch.\n 'create_widget',\n 'update_widget',\n 'remove_widget',\n 'list_active_widgets',\n // Spaces tools — same logic: workspaces are a v6.0 first-class concept,\n // the model needs to know it can create / switch / list them by name.\n 'create_space',\n 'switch_space',\n 'list_spaces',\n];\n\n/** Build a compact one-line catalog of all tools for the tool_search description */\nexport function buildToolCatalog(): string {\n const tools = getRegisteredTools();\n return tools\n .filter(t => t.name !== 'tool_search')\n .map(t => `${t.name}: ${t.description.slice(0, 50)}`)\n .join(' | ');\n}\n\n/**\n * Search registered tools by keyword query.\n *\n * Progressive disclosure mode (Hermes competitive gap fix):\n * When metadataOnly=true, returns tools with truncated descriptions\n * and no parameter schemas — ~20 tokens each instead of ~200.\n * The model then calls tool_expand(name) to get the full schema\n * for just the tools it needs.\n */\nexport function searchTools(query: string, metadataOnly = false): ToolHandler[] {\n const tools = getRegisteredTools();\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n\n const scored = tools\n .filter(t => t.name !== 'tool_search' && t.name !== 'tool_expand')\n .map(t => {\n const text = `${t.name} ${t.description}`.toLowerCase();\n const score = terms.reduce((s, term) => s + (text.includes(term) ? 1 : 0), 0);\n return { tool: t, score };\n })\n .filter(({ score }) => score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, 8);\n\n if (metadataOnly) {\n // Return lightweight metadata — just name + one-line description\n return scored.map(({ tool }) => ({\n ...tool,\n description: tool.description.slice(0, 80),\n parameters: { type: 'object' as const, properties: {}, required: [] },\n }));\n }\n\n return scored.map(({ tool }) => tool);\n}\n\n/**\n * Expand a single tool's full schema by name.\n * Part of the progressive disclosure pattern — model calls tool_search\n * to get names, then tool_expand to get the full schema for just the\n * tools it needs.\n */\nexport function expandTool(name: string): ToolHandler | null {\n const tools = getRegisteredTools();\n return tools.find(t => t.name === name) || null;\n}\n\n/** Get the tool_search tool handler */\nexport function getToolSearchHandler(): ToolHandler {\n const catalog = buildToolCatalog();\n\n return {\n name: 'tool_search',\n description: `Search for tools by keyword to discover capabilities. Call this FIRST when you need a tool not in your current list. Available tools: ${catalog}`,\n parameters: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'Search query — keywords describing what you need (e.g. \"email send\", \"browser screenshot\", \"github pr\", \"cron schedule\")',\n },\n },\n required: ['query'],\n },\n execute: async (args: Record<string, unknown>) => {\n const query = (args.query as string) || '';\n if (!query.trim()) {\n return 'Please provide a search query. Example: tool_search({query: \"email\"})';\n }\n\n const results = searchTools(query);\n\n if (results.length === 0) {\n return `No tools found matching \"${query}\". Try broader keywords.`;\n }\n\n logger.info(COMPONENT, `Search \"${query}\" → ${results.length} tools: [${results.map(t => t.name).join(', ')}]`);\n\n const formatted = results.map(t => {\n const params = t.parameters as Record<string, unknown>;\n const props = (params.properties || {}) as Record<string, unknown>;\n const paramNames = Object.keys(props).join(', ');\n return `**${t.name}**(${paramNames}): ${t.description}`;\n }).join('\\n');\n\n return `Found ${results.length} tools:\\n${formatted}\\n\\nYou can now call these tools directly.`;\n },\n };\n}\n\n/**\n * Get the tool_expand handler — progressive disclosure (Hermes gap fix).\n * Returns the full JSON schema for a single tool by name.\n */\nexport function getToolExpandHandler(): ToolHandler {\n return {\n name: 'tool_expand',\n description: 'Get the full parameter schema for a specific tool. Use after tool_search to see the detailed parameters before calling the tool.',\n parameters: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'The exact tool name to expand (e.g. \"email_send\", \"freelance_search\")',\n },\n },\n required: ['name'],\n },\n execute: async (args: Record<string, unknown>) => {\n const name = (args.name as string) || '';\n if (!name.trim()) {\n return 'Please provide a tool name. Example: tool_expand({name: \"email_send\"})';\n }\n\n const tool = expandTool(name);\n if (!tool) {\n return `Tool \"${name}\" not found. Use tool_search to find the correct name.`;\n }\n\n logger.info(COMPONENT, `Expand \"${name}\" → full schema loaded`);\n\n const params = tool.parameters as Record<string, unknown>;\n const props = (params.properties || {}) as Record<string, { type?: string; description?: string }>;\n const required = (params.required || []) as string[];\n\n const paramDocs = Object.entries(props).map(([key, val]) => {\n const req = required.includes(key) ? ' (required)' : '';\n return ` - **${key}**${req}: ${val.description || val.type || 'any'}`;\n }).join('\\n');\n\n return `## ${tool.name}\\n${tool.description}\\n\\n### Parameters\\n${paramDocs || ' (no parameters)'}\\n\\nYou can now call ${tool.name}() with these parameters.`;\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/agent/toolSearch.ts"],"sourcesContent":["/**\n * TITAN — Tool Search\n *\n * Meta-tool that lets the LLM discover tools on demand instead of seeing\n * all 80+ tool schemas on every request. Reduces input tokens by 60-80%.\n *\n * How it works:\n * 1. Only core tools + tool_search are sent to the LLM (5-8 tools, ~600 tokens)\n * 2. tool_search description includes a compact catalog of ALL available tools\n * 3. When the LLM calls tool_search, matching tools are returned\n * 4. Agent loop adds discovered tools to activeTools for subsequent rounds\n *\n * Inspired by Anthropic's tool search pattern (2025) but works with ALL providers.\n */\nimport { getRegisteredTools, type ToolHandler } from './toolRunner.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'ToolSearch';\n\n/** Default core tools — always sent to the LLM without needing search */\nexport const DEFAULT_CORE_TOOLS = [\n 'shell',\n 'read_file',\n 'write_file',\n 'edit_file',\n 'list_dir',\n 'web_search',\n 'web_fetch',\n 'web_act',\n 'smart_form_fill',\n 'weather',\n 'memory',\n // v7.0: \"remind me at X\" is a core personal-assistant verb — models were\n // fabricating reminders precisely because this tool wasn't in their view.\n 'reminder',\n 'system_info',\n 'goal_list',\n 'spawn_agent',\n 'tool_search',\n 'tool_expand',\n 'self_doctor',\n 'ha_control',\n 'ha_devices',\n 'ha_status',\n 'gallery_search',\n 'gallery_get',\n 'gallery_list',\n // v6.0.2 — Canvas widget tools must always be visible so the model\n // reaches for them on \"build me X\" prompts (the v6.0 thesis). Before\n // this, only gallery_search was in core; create_widget was only\n // discoverable via tool_search, which the model rarely called for\n // novel asks like \"build me a solar system\" — it'd fall through to\n // shell + mkdir instead. These four tools are cheap (~80 tokens\n // combined) and they're load-bearing for the Living Canvas pitch.\n 'create_widget',\n 'update_widget',\n 'remove_widget',\n 'list_active_widgets',\n // Spaces tools — same logic: workspaces are a v6.0 first-class concept,\n // the model needs to know it can create / switch / list them by name.\n 'create_space',\n 'switch_space',\n 'list_spaces',\n];\n\n/** Build a compact one-line catalog of all tools for the tool_search description */\nexport function buildToolCatalog(): string {\n // v7.1: names-only, grouped by namespace prefix. The old form embedded a\n // 50-char description per tool — 18.5KB (~5K tokens) inside tool_search's\n // OWN description, the single fattest item in every request. Names alone\n // keep the full surface discoverable (tool_search returns full schemas on\n // demand) at ~1/5 the size — for every model, local or cloud.\n const tools = getRegisteredTools().filter(t => t.name !== 'tool_search');\n const groups = new Map<string, string[]>();\n for (const t of tools) {\n const ns = t.name.includes('_') ? t.name.split('_')[0] : t.name;\n const g = groups.get(ns) || [];\n g.push(t.name);\n groups.set(ns, g);\n }\n return [...groups.entries()]\n .sort((a, b) => a[0].localeCompare(b[0]))\n .map(([ns, names]) => (names.length === 1 ? names[0] : `${ns}: ${names.join(', ')}`))\n .join(' | ');\n}\n\n/**\n * Search registered tools by keyword query.\n *\n * Progressive disclosure mode (Hermes competitive gap fix):\n * When metadataOnly=true, returns tools with truncated descriptions\n * and no parameter schemas — ~20 tokens each instead of ~200.\n * The model then calls tool_expand(name) to get the full schema\n * for just the tools it needs.\n */\nexport function searchTools(query: string, metadataOnly = false): ToolHandler[] {\n const tools = getRegisteredTools();\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n\n const scored = tools\n .filter(t => t.name !== 'tool_search' && t.name !== 'tool_expand')\n .map(t => {\n const text = `${t.name} ${t.description}`.toLowerCase();\n const score = terms.reduce((s, term) => s + (text.includes(term) ? 1 : 0), 0);\n return { tool: t, score };\n })\n .filter(({ score }) => score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, 8);\n\n if (metadataOnly) {\n // Return lightweight metadata — just name + one-line description\n return scored.map(({ tool }) => ({\n ...tool,\n description: tool.description.slice(0, 80),\n parameters: { type: 'object' as const, properties: {}, required: [] },\n }));\n }\n\n return scored.map(({ tool }) => tool);\n}\n\n/**\n * Expand a single tool's full schema by name.\n * Part of the progressive disclosure pattern — model calls tool_search\n * to get names, then tool_expand to get the full schema for just the\n * tools it needs.\n */\nexport function expandTool(name: string): ToolHandler | null {\n const tools = getRegisteredTools();\n return tools.find(t => t.name === name) || null;\n}\n\n/** Get the tool_search tool handler */\nexport function getToolSearchHandler(): ToolHandler {\n const catalog = buildToolCatalog();\n\n return {\n name: 'tool_search',\n description: `Search for tools by keyword to discover capabilities. Call this FIRST when you need a tool not in your current list. Available tools: ${catalog}`,\n parameters: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'Search query — keywords describing what you need (e.g. \"email send\", \"browser screenshot\", \"github pr\", \"cron schedule\")',\n },\n },\n required: ['query'],\n },\n execute: async (args: Record<string, unknown>) => {\n const query = (args.query as string) || '';\n if (!query.trim()) {\n return 'Please provide a search query. Example: tool_search({query: \"email\"})';\n }\n\n const results = searchTools(query);\n\n if (results.length === 0) {\n return `No tools found matching \"${query}\". Try broader keywords.`;\n }\n\n logger.info(COMPONENT, `Search \"${query}\" → ${results.length} tools: [${results.map(t => t.name).join(', ')}]`);\n\n const formatted = results.map(t => {\n const params = t.parameters as Record<string, unknown>;\n const props = (params.properties || {}) as Record<string, unknown>;\n const paramNames = Object.keys(props).join(', ');\n return `**${t.name}**(${paramNames}): ${t.description}`;\n }).join('\\n');\n\n return `Found ${results.length} tools:\\n${formatted}\\n\\nYou can now call these tools directly.`;\n },\n };\n}\n\n/**\n * Get the tool_expand handler — progressive disclosure (Hermes gap fix).\n * Returns the full JSON schema for a single tool by name.\n */\nexport function getToolExpandHandler(): ToolHandler {\n return {\n name: 'tool_expand',\n description: 'Get the full parameter schema for a specific tool. Use after tool_search to see the detailed parameters before calling the tool.',\n parameters: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'The exact tool name to expand (e.g. \"email_send\", \"freelance_search\")',\n },\n },\n required: ['name'],\n },\n execute: async (args: Record<string, unknown>) => {\n const name = (args.name as string) || '';\n if (!name.trim()) {\n return 'Please provide a tool name. Example: tool_expand({name: \"email_send\"})';\n }\n\n const tool = expandTool(name);\n if (!tool) {\n return `Tool \"${name}\" not found. Use tool_search to find the correct name.`;\n }\n\n logger.info(COMPONENT, `Expand \"${name}\" → full schema loaded`);\n\n const params = tool.parameters as Record<string, unknown>;\n const props = (params.properties || {}) as Record<string, { type?: string; description?: string }>;\n const required = (params.required || []) as string[];\n\n const paramDocs = Object.entries(props).map(([key, val]) => {\n const req = required.includes(key) ? ' (required)' : '';\n return ` - **${key}**${req}: ${val.description || val.type || 'any'}`;\n }).join('\\n');\n\n return `## ${tool.name}\\n${tool.description}\\n\\n### Parameters\\n${paramDocs || ' (no parameters)'}\\n\\nYou can now call ${tool.name}() with these parameters.`;\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync, openSync, fstatSync, readSync, closeSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import logger from "../utils/logger.js";
|
|
5
5
|
import { TITAN_HOME } from "../utils/constants.js";
|
|
@@ -39,7 +39,24 @@ function logTrajectory(trajectory) {
|
|
|
39
39
|
function getRecentTrajectories(limit = 50, filter) {
|
|
40
40
|
if (!existsSync(TASK_TRAJECTORIES_FILE)) return [];
|
|
41
41
|
try {
|
|
42
|
-
const
|
|
42
|
+
const fd = openSync(TASK_TRAJECTORIES_FILE, "r");
|
|
43
|
+
let content = "";
|
|
44
|
+
try {
|
|
45
|
+
const size = fstatSync(fd).size;
|
|
46
|
+
let want = 512 * 1024;
|
|
47
|
+
for (; ; ) {
|
|
48
|
+
const start = Math.max(0, size - want);
|
|
49
|
+
const buf = Buffer.alloc(size - start);
|
|
50
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
51
|
+
content = buf.toString("utf-8");
|
|
52
|
+
if (start > 0) content = content.slice(content.indexOf("\n") + 1);
|
|
53
|
+
const enough = content.split("\n").filter((l) => l.trim()).length >= limit * 2;
|
|
54
|
+
if (enough || start === 0) break;
|
|
55
|
+
want *= 4;
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
closeSync(fd);
|
|
59
|
+
}
|
|
43
60
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
44
61
|
let trajectories = [];
|
|
45
62
|
for (let i = lines.length - 1; i >= 0 && trajectories.length < limit * 2; i--) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/trajectoryLogger.ts"],"sourcesContent":["/**\n * TITAN — Task-Level Trajectory Logger\n *\n * Records task-level trajectories (task, model, tool sequence, success, rounds, duration)\n * to ~/.titan/trajectories/task-trajectories.jsonl for auto-skill generation.\n *\n * Separate from trajectoryCapture.ts which produces ChatML format for LoRA fine-tuning.\n * Inspired by Hermes trajectory.py.\n */\nimport { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync } from 'fs';\nimport { join } from 'path';\nimport logger from '../utils/logger.js';\nimport { TITAN_HOME } from '../utils/constants.js';\n\nconst COMPONENT = 'TrajectoryLogger';\nconst TRAJECTORIES_DIR = join(TITAN_HOME, 'trajectories');\nconst TASK_TRAJECTORIES_FILE = join(TRAJECTORIES_DIR, 'task-trajectories.jsonl');\nconst MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\n\n// ── Types ─────────────────────────────────────────────────────────\nexport interface TaskTrajectory {\n id: string;\n timestamp: string;\n /** First 500 chars of the user message */\n task: string;\n taskType: string;\n model: string;\n /** Ordered tool names as executed */\n toolSequence: string[];\n /** Detailed tool call records */\n toolDetails: Array<{\n name: string;\n args: Record<string, unknown>;\n success: boolean;\n resultSnippet: string;\n }>;\n success: boolean;\n rounds: number;\n durationMs: number;\n sessionId: string;\n}\n\n// ── Ensure directory exists ───────────────────────────────────────\nfunction ensureDir(): void {\n if (!existsSync(TRAJECTORIES_DIR)) mkdirSync(TRAJECTORIES_DIR, { recursive: true });\n}\n\n// ── Auto-rotate ───────────────────────────────────────────────────\nfunction rotateIfNeeded(): void {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return;\n try {\n const stat = statSync(TASK_TRAJECTORIES_FILE);\n if (stat.size > MAX_FILE_SIZE) {\n logger.warn(COMPONENT, `Task trajectories file exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB — rotating`);\n const rotated = `${TASK_TRAJECTORIES_FILE}.${Date.now()}.bak`;\n try { writeFileSync(rotated, readFileSync(TASK_TRAJECTORIES_FILE)); } catch { /* ignore */ }\n writeFileSync(TASK_TRAJECTORIES_FILE, '');\n }\n } catch { /* ignore stat errors */ }\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\n/**\n * Log a task-level trajectory.\n * Fire-and-forget — errors are logged but never thrown.\n */\nexport function logTrajectory(trajectory: TaskTrajectory): void {\n try {\n ensureDir();\n rotateIfNeeded();\n appendFileSync(TASK_TRAJECTORIES_FILE, JSON.stringify(trajectory) + '\\n', 'utf-8');\n logger.debug(COMPONENT, `Logged trajectory: ${trajectory.taskType}, ${trajectory.toolSequence.length} tools, success=${trajectory.success}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to log trajectory: ${(err as Error).message}`);\n }\n}\n\n/**\n * Read recent trajectories, optionally filtered.\n */\nexport function getRecentTrajectories(\n limit: number = 50,\n filter?: { taskType?: string; success?: boolean },\n): TaskTrajectory[] {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return [];\n\n try {\n const
|
|
1
|
+
{"version":3,"sources":["../../src/agent/trajectoryLogger.ts"],"sourcesContent":["/**\n * TITAN — Task-Level Trajectory Logger\n *\n * Records task-level trajectories (task, model, tool sequence, success, rounds, duration)\n * to ~/.titan/trajectories/task-trajectories.jsonl for auto-skill generation.\n *\n * Separate from trajectoryCapture.ts which produces ChatML format for LoRA fine-tuning.\n * Inspired by Hermes trajectory.py.\n */\nimport { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync, openSync, fstatSync, readSync, closeSync } from 'fs';\nimport { join } from 'path';\nimport logger from '../utils/logger.js';\nimport { TITAN_HOME } from '../utils/constants.js';\n\nconst COMPONENT = 'TrajectoryLogger';\nconst TRAJECTORIES_DIR = join(TITAN_HOME, 'trajectories');\nconst TASK_TRAJECTORIES_FILE = join(TRAJECTORIES_DIR, 'task-trajectories.jsonl');\nconst MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\n\n// ── Types ─────────────────────────────────────────────────────────\nexport interface TaskTrajectory {\n id: string;\n timestamp: string;\n /** First 500 chars of the user message */\n task: string;\n taskType: string;\n model: string;\n /** Ordered tool names as executed */\n toolSequence: string[];\n /** Detailed tool call records */\n toolDetails: Array<{\n name: string;\n args: Record<string, unknown>;\n success: boolean;\n resultSnippet: string;\n }>;\n success: boolean;\n rounds: number;\n durationMs: number;\n sessionId: string;\n}\n\n// ── Ensure directory exists ───────────────────────────────────────\nfunction ensureDir(): void {\n if (!existsSync(TRAJECTORIES_DIR)) mkdirSync(TRAJECTORIES_DIR, { recursive: true });\n}\n\n// ── Auto-rotate ───────────────────────────────────────────────────\nfunction rotateIfNeeded(): void {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return;\n try {\n const stat = statSync(TASK_TRAJECTORIES_FILE);\n if (stat.size > MAX_FILE_SIZE) {\n logger.warn(COMPONENT, `Task trajectories file exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB — rotating`);\n const rotated = `${TASK_TRAJECTORIES_FILE}.${Date.now()}.bak`;\n try { writeFileSync(rotated, readFileSync(TASK_TRAJECTORIES_FILE)); } catch { /* ignore */ }\n writeFileSync(TASK_TRAJECTORIES_FILE, '');\n }\n } catch { /* ignore stat errors */ }\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\n/**\n * Log a task-level trajectory.\n * Fire-and-forget — errors are logged but never thrown.\n */\nexport function logTrajectory(trajectory: TaskTrajectory): void {\n try {\n ensureDir();\n rotateIfNeeded();\n appendFileSync(TASK_TRAJECTORIES_FILE, JSON.stringify(trajectory) + '\\n', 'utf-8');\n logger.debug(COMPONENT, `Logged trajectory: ${trajectory.taskType}, ${trajectory.toolSequence.length} tools, success=${trajectory.success}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to log trajectory: ${(err as Error).message}`);\n }\n}\n\n/**\n * Read recent trajectories, optionally filtered.\n */\nexport function getRecentTrajectories(\n limit: number = 50,\n filter?: { taskType?: string; success?: boolean },\n): TaskTrajectory[] {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return [];\n\n try {\n // PERF (v7.1.x): this runs on EVERY message (auto-skill gate) and the\n // file grows to 50MB before rotation — reading it whole cost tens of\n // ms + a 50MB string per turn. Read only the TAIL: start with 512KB\n // and double until we have enough parseable lines or hit the start.\n const fd = openSync(TASK_TRAJECTORIES_FILE, 'r');\n let content = '';\n try {\n const size = fstatSync(fd).size;\n let want = 512 * 1024;\n for (;;) {\n const start = Math.max(0, size - want);\n const buf = Buffer.alloc(size - start);\n readSync(fd, buf, 0, buf.length, start);\n content = buf.toString('utf-8');\n // Drop the first (possibly truncated) line unless we read from 0\n if (start > 0) content = content.slice(content.indexOf('\\n') + 1);\n const enough = content.split('\\n').filter(l => l.trim()).length >= limit * 2;\n if (enough || start === 0) break;\n want *= 4;\n }\n } finally {\n closeSync(fd);\n }\n const lines = content.split('\\n').filter(l => l.trim());\n let trajectories: TaskTrajectory[] = [];\n\n // Read from the end for efficiency\n for (let i = lines.length - 1; i >= 0 && trajectories.length < limit * 2; i--) {\n try {\n trajectories.push(JSON.parse(lines[i]));\n } catch { /* skip malformed lines */ }\n }\n\n // Apply filters\n if (filter?.taskType) {\n trajectories = trajectories.filter(t => t.taskType === filter.taskType);\n }\n if (filter?.success !== undefined) {\n trajectories = trajectories.filter(t => t.success === filter.success);\n }\n\n return trajectories.slice(0, limit);\n } catch {\n return [];\n }\n}\n\n/**\n * Get the tool sequence signature for matching (joined with →).\n */\nexport function getSequenceSignature(toolSequence: string[]): string {\n return toolSequence.join(' → ');\n}\n\n/**\n * Count how many successful trajectories match a given task type and tool sequence.\n */\nexport function countMatchingTrajectories(taskType: string, toolSequence: string[]): number {\n const recent = getRecentTrajectories(200, { taskType, success: true });\n const targetSig = getSequenceSignature(toolSequence);\n return recent.filter(t => getSequenceSignature(t.toolSequence) === targetSig).length;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/analytics/collector.ts"],"sourcesContent":["/**\n * TITAN — Analytics Collector\n * Collects anonymized system profile and usage heartbeat for product analytics.\n * All data is privacy-respecting: no IP, no personal data, no conversation content.\n */\nimport { platform, arch, release, cpus, totalmem } from 'os';\nimport { detectHardware } from '../hardware/autoConfig.js';\nimport { getOrCreateNodeId } from '../mesh/identity.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { sendPostHogEvent, getPostHogStatus } from './posthog.js';\n\nconst COMPONENT = 'Analytics';\n\nexport interface SystemProfile {\n /** Anonymous install ID — stable per ~/.titan/ */\n installId: string;\n /** TITAN version */\n version: string;\n /** Node.js version */\n nodeVersion: string;\n /** OS platform */\n os: string;\n /** OS release */\n osRelease: string;\n /** CPU architecture */\n arch: string;\n /** CPU cores */\n cpuCores: number;\n /** Total RAM in GB (bucketed to nearest GB) */\n ramTotalGB: number;\n /** GPU vendor */\n gpuVendor: string;\n /** GPU VRAM in GB (bucketed to nearest GB) */\n gpuVramGB: number;\n /** Install method: 'git' | 'npm' | 'unknown' */\n installMethod: string;\n /** Total disk GB (bucketed to nearest 10GB) */\n diskTotalGB: number;\n /** Timestamp of collection */\n collectedAt: string;\n}\n\nexport interface HeartbeatEvent {\n installId: string;\n version: string;\n event: 'heartbeat' | 'startup' | 'shutdown';\n timestamp: string;\n uptimeSeconds: number;\n activeSessions: number;\n /** Feature adoption snapshot — which major subsystems are enabled */\n features?: {\n voice: boolean;\n mesh: boolean;\n brain: boolean;\n tunnel: boolean;\n autopilot: boolean;\n selfImprove: boolean;\n training: boolean;\n daemon: boolean;\n nvidia: boolean;\n organism: boolean;\n selfMod: boolean;\n commandPost: boolean;\n mcpServer: boolean;\n /** Count of enabled channel adapters */\n channelsEnabled: number;\n /** Count of providers with at least one auth profile or apiKey */\n providersConfigured: number;\n };\n}\n\n/** Detect how TITAN was installed */\nasync function detectInstallMethod(): Promise<'git' | 'npm' | 'unknown'> {\n try {\n const { existsSync } = await import('fs');\n const { join } = await import('path');\n if (existsSync(join(process.cwd(), '.git'))) return 'git';\n if (process.cwd().includes('node_modules') || process.cwd().includes('/lib/node')) return 'npm';\n } catch { /* ignore */ }\n return 'unknown';\n}\n\n/** Bucket a value to the nearest step */\nfunction bucketToNearest(value: number, step: number): number {\n return Math.round(value / step) * step;\n}\n\n/** Collect full system profile (privacy-respecting: no cpuModel/gpuName, bucketed sizes) */\nexport async function collectSystemProfile(): Promise<SystemProfile> {\n const hw = await detectHardware();\n const installMethod = await detectInstallMethod();\n const cpuList = cpus();\n\n return {\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n nodeVersion: process.version,\n os: platform(),\n osRelease: release(),\n arch: arch(),\n cpuCores: cpuList.length || hw.cpuCores,\n ramTotalGB: bucketToNearest(hw.ramTotalMB / 1024, 4),\n gpuVendor: hw.gpuVendor,\n gpuVramGB: bucketToNearest(hw.gpuVramMB / 1024, 4),\n installMethod,\n diskTotalGB: bucketToNearest(hw.diskTotalGB, 50),\n collectedAt: new Date().toISOString(),\n };\n}\n\n/** Snapshot of which major features are enabled in the current config */\nfunction getFeatureSnapshot(cfg: ReturnType<typeof loadConfig>): HeartbeatEvent['features'] {\n const channels = cfg.channels || {};\n const providers = cfg.providers || {};\n const chEnabled = Object.values(channels).filter((c: unknown) => (c as Record<string, unknown>)?.enabled === true).length;\n const provConfigured = Object.entries(providers).filter(([, p]) => {\n const pr = p as Record<string, unknown>;\n return Boolean(pr?.apiKey) || (Array.isArray(pr?.authProfiles) && (pr.authProfiles as unknown[]).length > 0);\n }).length;\n return {\n voice: cfg.voice?.enabled === true,\n mesh: cfg.mesh?.enabled === true,\n brain: cfg.brain?.enabled === true,\n tunnel: cfg.tunnel?.enabled === true,\n autopilot: cfg.autopilot?.enabled === true,\n selfImprove: cfg.selfImprove?.enabled === true,\n training: cfg.training?.enabled === true,\n daemon: cfg.daemon?.enabled === true,\n nvidia: cfg.nvidia?.enabled === true,\n organism: cfg.organism?.enabled === true,\n selfMod: cfg.selfMod?.enabled === true,\n commandPost: cfg.commandPost?.enabled === true,\n mcpServer: cfg.mcp?.server?.enabled === true,\n channelsEnabled: chEnabled,\n providersConfigured: provConfigured,\n };\n}\n\n/** Build a heartbeat event */\nexport function buildHeartbeat(activeSessions = 0): HeartbeatEvent {\n const cfg = loadConfig();\n return {\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n event: 'heartbeat',\n timestamp: new Date().toISOString(),\n uptimeSeconds: Math.round(process.uptime()),\n activeSessions,\n features: getFeatureSnapshot(cfg),\n };\n}\n\n/**\n * Remote-send status. Exposed so the gateway and Mission Control can\n * show Tony whether telemetry is actually landing anywhere. Prior to\n * v5.0 the send function swallowed errors silently — the new state\n * struct records the last attempt so the Privacy widget can surface\n * \"last sent 14s ago ✓\" vs \"last send failed: connection refused\".\n */\nexport interface RemoteAnalyticsStatus {\n configured: boolean;\n remoteUrl?: string;\n sentCount: number;\n failedCount: number;\n lastAttemptAt?: string;\n lastSuccessAt?: string;\n lastError?: string;\n lastEventType?: string;\n /** PostHog bridge status (parallel send) */\n posthog?: { enabled: boolean; sentCount: number; failedCount: number; lastError?: string; lastSuccessAt?: string };\n}\nconst remoteStatus: RemoteAnalyticsStatus = {\n configured: false,\n sentCount: 0,\n failedCount: 0,\n};\nexport function getRemoteAnalyticsStatus(): RemoteAnalyticsStatus {\n return { ...remoteStatus, posthog: getPostHogStatus() };\n}\n\n/** Send analytics event to remote endpoint if configured */\nexport async function sendRemoteAnalytics(payload: Record<string, unknown>): Promise<void> {\n const cfg = loadConfig();\n const remoteUrl = (cfg.telemetry as Record<string, unknown> | undefined)?.remoteUrl as string | undefined;\n remoteStatus.configured = Boolean(remoteUrl);\n remoteStatus.remoteUrl = remoteUrl;\n if (!remoteUrl) return;\n\n const now = new Date().toISOString();\n remoteStatus.lastAttemptAt = now;\n remoteStatus.lastEventType = typeof payload.type === 'string' ? payload.type : 'unknown';\n try {\n const res = await fetch(remoteUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n remoteStatus.sentCount += 1;\n remoteStatus.lastSuccessAt = now;\n remoteStatus.lastError = undefined;\n } catch (err) {\n remoteStatus.failedCount += 1;\n remoteStatus.lastError = (err as Error).message || String(err);\n // Remote analytics is best-effort — we don't throw, but the\n // status struct lets callers see the failure.\n }\n\n // Parallel send to PostHog (if configured) — never blocks or throws.\n sendPostHogEvent(payload).catch(() => {});\n}\n\n/**\n * Read the postinstall marker (written by `scripts/postinstall.cjs`) and,\n * if the user has consented to telemetry, fire a one-shot `install` or\n * `update` event the first time the gateway boots after an install/upgrade.\n *\n * The marker stays on disk regardless of consent; we just won't SEND anything\n * until the user opts in. The `reported: true` flag prevents re-sending after\n * a gateway restart.\n */\nasync function reportInstallMarkerOnce(): Promise<void> {\n try {\n const { readFileSync, writeFileSync, existsSync } = await import('fs');\n const { join } = await import('path');\n const { homedir } = await import('os');\n const markerPath = join(homedir(), '.titan', 'install-marker.json');\n if (!existsSync(markerPath)) return;\n\n type Marker = {\n installedVersion?: string;\n previousVersion?: string | null;\n installedAt?: string;\n installMethod?: string;\n reported?: boolean;\n };\n const marker = JSON.parse(readFileSync(markerPath, 'utf-8')) as Marker;\n if (marker.reported) return;\n\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return; // Stays unreported until consent\n\n const isUpdate = Boolean(marker.previousVersion && marker.previousVersion !== marker.installedVersion);\n await sendRemoteAnalytics({\n type: isUpdate ? 'update' : 'install',\n installId: getOrCreateNodeId(),\n version: marker.installedVersion || TITAN_VERSION,\n fromVersion: isUpdate ? marker.previousVersion : null,\n installMethod: marker.installMethod || 'unknown',\n installedAt: marker.installedAt || new Date().toISOString(),\n });\n\n writeFileSync(markerPath, JSON.stringify({ ...marker, reported: true }, null, 2), 'utf-8');\n logger.info(COMPONENT, `Reported ${isUpdate ? 'update' : 'install'} event for ${marker.installedVersion}`);\n } catch (e) {\n logger.debug(COMPONENT, `Install-marker reporter skipped: ${(e as Error).message}`);\n }\n}\n\n/** Record startup profile + heartbeat via local telemetry */\nexport async function recordStartupAnalytics(): Promise<void> {\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return;\n\n try {\n const profile = await collectSystemProfile();\n const { getStorage } = await import('../storage/index.js');\n const storage = await getStorage();\n\n await storage.appendTelemetryEvent?.({\n event: 'system_profile',\n properties: profile as unknown as Record<string, unknown>,\n timestamp: new Date().toISOString(),\n sessionId: profile.installId,\n });\n\n await sendRemoteAnalytics({\n type: 'system_profile',\n ...profile,\n });\n\n logger.info(COMPONENT, `System profile recorded: ${profile.os} ${profile.arch}, ${profile.cpuCores}c/${profile.ramTotalGB}GB, GPU: ${profile.gpuVendor || 'none'}`);\n\n // Fire install/update event once per new install — only when consented.\n await reportInstallMarkerOnce();\n } catch (e) {\n logger.warn(COMPONENT, `Failed to record startup analytics: ${(e as Error).message}`);\n }\n}\n\n/** Schedule periodic heartbeat analytics */\nexport function startHeartbeatAnalytics(activeSessionsProvider: () => number): void {\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return;\n\n const HEARTBEAT_INTERVAL_MS = 300_000; // 5 minutes\n\n const tick = async () => {\n try {\n const heartbeat = buildHeartbeat(activeSessionsProvider());\n const { getStorage } = await import('../storage/index.js');\n const storage = await getStorage();\n\n await storage.appendTelemetryEvent?.({\n event: 'heartbeat',\n properties: heartbeat as unknown as Record<string, unknown>,\n timestamp: heartbeat.timestamp,\n sessionId: heartbeat.installId,\n });\n\n await sendRemoteAnalytics({\n type: 'heartbeat',\n ...heartbeat,\n });\n } catch {\n // Best-effort\n }\n };\n\n tick();\n setInterval(tick, HEARTBEAT_INTERVAL_MS);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/analytics/collector.ts"],"sourcesContent":["/**\n * TITAN — Analytics Collector\n * Collects anonymized system profile and usage heartbeat for product analytics.\n * All data is privacy-respecting: no IP, no personal data, no conversation content.\n */\nimport { platform, arch, release, cpus, totalmem } from 'os';\nimport { detectHardware } from '../hardware/autoConfig.js';\nimport { getOrCreateNodeId } from '../mesh/identity.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { sendPostHogEvent, getPostHogStatus } from './posthog.js';\n\nconst COMPONENT = 'Analytics';\n\nexport interface SystemProfile {\n /** Anonymous install ID — stable per ~/.titan/ */\n installId: string;\n /** TITAN version */\n version: string;\n /** Node.js version */\n nodeVersion: string;\n /** OS platform */\n os: string;\n /** OS release */\n osRelease: string;\n /** CPU architecture */\n arch: string;\n /** CPU cores */\n cpuCores: number;\n /** Total RAM in GB (bucketed to nearest GB) */\n ramTotalGB: number;\n /** GPU vendor */\n gpuVendor: string;\n /** GPU VRAM in GB (bucketed to nearest GB) */\n gpuVramGB: number;\n /** Install method: 'git' | 'npm' | 'unknown' */\n installMethod: string;\n /** Total disk GB (bucketed to nearest 10GB) */\n diskTotalGB: number;\n /** Timestamp of collection */\n collectedAt: string;\n}\n\nexport interface HeartbeatEvent {\n installId: string;\n version: string;\n event: 'heartbeat' | 'startup' | 'shutdown';\n timestamp: string;\n uptimeSeconds: number;\n activeSessions: number;\n /** Feature adoption snapshot — which major subsystems are enabled */\n features?: {\n voice: boolean;\n mesh: boolean;\n brain: boolean;\n tunnel: boolean;\n autopilot: boolean;\n selfImprove: boolean;\n training: boolean;\n daemon: boolean;\n nvidia: boolean;\n organism: boolean;\n selfMod: boolean;\n commandPost: boolean;\n mcpServer: boolean;\n /** Count of enabled channel adapters */\n channelsEnabled: number;\n /** Count of providers with at least one auth profile or apiKey */\n providersConfigured: number;\n };\n}\n\n/** Detect how TITAN was installed */\nasync function detectInstallMethod(): Promise<'git' | 'npm' | 'unknown'> {\n try {\n const { existsSync } = await import('fs');\n const { join } = await import('path');\n if (existsSync(join(process.cwd(), '.git'))) return 'git';\n if (process.cwd().includes('node_modules') || process.cwd().includes('/lib/node')) return 'npm';\n } catch { /* ignore */ }\n return 'unknown';\n}\n\n/** Bucket a value to the nearest step */\nfunction bucketToNearest(value: number, step: number): number {\n return Math.round(value / step) * step;\n}\n\n/** Collect full system profile (privacy-respecting: no cpuModel/gpuName, bucketed sizes) */\nexport async function collectSystemProfile(): Promise<SystemProfile> {\n const hw = await detectHardware();\n const installMethod = await detectInstallMethod();\n const cpuList = cpus();\n\n return {\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n nodeVersion: process.version,\n os: platform(),\n osRelease: release(),\n arch: arch(),\n cpuCores: cpuList.length || hw.cpuCores,\n ramTotalGB: bucketToNearest(hw.ramTotalMB / 1024, 4),\n gpuVendor: hw.gpuVendor,\n gpuVramGB: bucketToNearest(hw.gpuVramMB / 1024, 4),\n installMethod,\n diskTotalGB: bucketToNearest(hw.diskTotalGB, 50),\n collectedAt: new Date().toISOString(),\n };\n}\n\n/** Snapshot of which major features are enabled in the current config */\nfunction getFeatureSnapshot(cfg: ReturnType<typeof loadConfig>): HeartbeatEvent['features'] {\n const channels = cfg.channels || {};\n const providers = cfg.providers || {};\n const chEnabled = Object.values(channels).filter((c: unknown) => (c as Record<string, unknown>)?.enabled === true).length;\n const provConfigured = Object.entries(providers).filter(([, p]) => {\n const pr = p as Record<string, unknown>;\n return Boolean(pr?.apiKey) || (Array.isArray(pr?.authProfiles) && (pr.authProfiles as unknown[]).length > 0);\n }).length;\n return {\n voice: cfg.voice?.enabled === true,\n mesh: cfg.mesh?.enabled === true,\n brain: cfg.brain?.enabled === true,\n tunnel: cfg.tunnel?.enabled === true,\n autopilot: cfg.autopilot?.enabled === true,\n selfImprove: cfg.selfImprove?.enabled === true,\n training: cfg.training?.enabled === true,\n daemon: cfg.daemon?.enabled === true,\n nvidia: cfg.nvidia?.enabled === true,\n organism: cfg.organism?.enabled === true,\n selfMod: cfg.selfMod?.enabled === true,\n commandPost: cfg.commandPost?.enabled === true,\n mcpServer: cfg.mcp?.server?.enabled === true,\n channelsEnabled: chEnabled,\n providersConfigured: provConfigured,\n };\n}\n\n/** Build a heartbeat event */\nexport function buildHeartbeat(activeSessions = 0): HeartbeatEvent {\n const cfg = loadConfig();\n return {\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n event: 'heartbeat',\n timestamp: new Date().toISOString(),\n uptimeSeconds: Math.round(process.uptime()),\n activeSessions,\n features: getFeatureSnapshot(cfg),\n };\n}\n\n/**\n * Remote-send status. Exposed so the gateway and Mission Control can\n * show Tony whether telemetry is actually landing anywhere. Prior to\n * v5.0 the send function swallowed errors silently — the new state\n * struct records the last attempt so the Privacy widget can surface\n * \"last sent 14s ago ✓\" vs \"last send failed: connection refused\".\n */\nexport interface RemoteAnalyticsStatus {\n configured: boolean;\n remoteUrl?: string;\n sentCount: number;\n failedCount: number;\n lastAttemptAt?: string;\n lastSuccessAt?: string;\n lastError?: string;\n lastEventType?: string;\n /** PostHog bridge status (parallel send) */\n posthog?: { enabled: boolean; sentCount: number; failedCount: number; lastError?: string; lastSuccessAt?: string };\n}\nconst remoteStatus: RemoteAnalyticsStatus = {\n configured: false,\n sentCount: 0,\n failedCount: 0,\n};\nexport function getRemoteAnalyticsStatus(): RemoteAnalyticsStatus {\n return { ...remoteStatus, posthog: getPostHogStatus() };\n}\n\n/** Send analytics event to remote endpoint if configured */\nexport async function sendRemoteAnalytics(payload: Record<string, unknown>): Promise<void> {\n const cfg = loadConfig();\n const remoteUrl = (cfg.telemetry as Record<string, unknown> | undefined)?.remoteUrl as string | undefined;\n remoteStatus.configured = Boolean(remoteUrl);\n remoteStatus.remoteUrl = remoteUrl;\n if (!remoteUrl) return;\n\n const now = new Date().toISOString();\n remoteStatus.lastAttemptAt = now;\n remoteStatus.lastEventType = typeof payload.type === 'string' ? payload.type : 'unknown';\n try {\n const res = await fetch(remoteUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n remoteStatus.sentCount += 1;\n remoteStatus.lastSuccessAt = now;\n remoteStatus.lastError = undefined;\n } catch (err) {\n remoteStatus.failedCount += 1;\n remoteStatus.lastError = (err as Error).message || String(err);\n // Remote analytics is best-effort — we don't throw, but the\n // status struct lets callers see the failure.\n }\n\n // Parallel send to PostHog (if configured) — never blocks or throws.\n sendPostHogEvent(payload).catch(() => {});\n}\n\n/**\n * Read the postinstall marker (written by `scripts/postinstall.cjs`) and,\n * if the user has consented to telemetry, fire a one-shot `install` or\n * `update` event the first time the gateway boots after an install/upgrade.\n *\n * The marker stays on disk regardless of consent; we just won't SEND anything\n * until the user opts in. The `reported: true` flag prevents re-sending after\n * a gateway restart.\n */\nasync function reportInstallMarkerOnce(): Promise<void> {\n try {\n const { readFileSync, writeFileSync, existsSync } = await import('fs');\n const { join } = await import('path');\n const { homedir } = await import('os');\n const markerPath = join(homedir(), '.titan', 'install-marker.json');\n if (!existsSync(markerPath)) return;\n\n type Marker = {\n installedVersion?: string;\n previousVersion?: string | null;\n installedAt?: string;\n installMethod?: string;\n reported?: boolean;\n };\n const marker = JSON.parse(readFileSync(markerPath, 'utf-8')) as Marker;\n if (marker.reported) return;\n\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return; // Stays unreported until consent\n\n const isUpdate = Boolean(marker.previousVersion && marker.previousVersion !== marker.installedVersion);\n await sendRemoteAnalytics({\n type: isUpdate ? 'update' : 'install',\n installId: getOrCreateNodeId(),\n version: marker.installedVersion || TITAN_VERSION,\n fromVersion: isUpdate ? marker.previousVersion : null,\n installMethod: marker.installMethod || 'unknown',\n installedAt: marker.installedAt || new Date().toISOString(),\n });\n\n writeFileSync(markerPath, JSON.stringify({ ...marker, reported: true }, null, 2), 'utf-8');\n logger.info(COMPONENT, `Reported ${isUpdate ? 'update' : 'install'} event for ${marker.installedVersion}`);\n } catch (e) {\n logger.debug(COMPONENT, `Install-marker reporter skipped: ${(e as Error).message}`);\n }\n}\n\n/** Record startup profile + heartbeat via local telemetry */\nexport async function recordStartupAnalytics(): Promise<void> {\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return;\n\n try {\n const profile = await collectSystemProfile();\n const { getStorage } = await import('../storage/index.js');\n const storage = await getStorage();\n\n await storage.appendTelemetryEvent?.({\n event: 'system_profile',\n properties: profile as unknown as Record<string, unknown>,\n timestamp: new Date().toISOString(),\n sessionId: profile.installId,\n });\n\n await sendRemoteAnalytics({\n type: 'system_profile',\n ...profile,\n });\n\n logger.info(COMPONENT, `System profile recorded: ${profile.os} ${profile.arch}, ${profile.cpuCores}c/${profile.ramTotalGB}GB, GPU: ${profile.gpuVendor || 'none'}`);\n\n // Fire install/update event once per new install — only when consented.\n await reportInstallMarkerOnce();\n } catch (e) {\n logger.warn(COMPONENT, `Failed to record startup analytics: ${(e as Error).message}`);\n }\n}\n\n/** Schedule periodic heartbeat analytics */\nexport function startHeartbeatAnalytics(activeSessionsProvider: () => number): void {\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return;\n\n const HEARTBEAT_INTERVAL_MS = 300_000; // 5 minutes\n\n const tick = async () => {\n try {\n const heartbeat = buildHeartbeat(activeSessionsProvider());\n const { getStorage } = await import('../storage/index.js');\n const storage = await getStorage();\n\n await storage.appendTelemetryEvent?.({\n event: 'heartbeat',\n properties: heartbeat as unknown as Record<string, unknown>,\n timestamp: heartbeat.timestamp,\n sessionId: heartbeat.installId,\n });\n\n await sendRemoteAnalytics({\n type: 'heartbeat',\n ...heartbeat,\n });\n } catch {\n // Best-effort\n }\n };\n\n tick();\n setInterval(tick, HEARTBEAT_INTERVAL_MS).unref();\n}\n"],"mappings":";AAKA,SAAS,UAAU,MAAM,SAAS,YAAsB;AACxD,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AACnB,SAAS,kBAAkB,wBAAwB;AAEnD,MAAM,YAAY;AA6DlB,eAAe,sBAA0D;AACrE,MAAI;AACA,UAAM,EAAE,WAAW,IAAI,MAAM,OAAO,IAAI;AACxC,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO,MAAM;AACpC,QAAI,WAAW,KAAK,QAAQ,IAAI,GAAG,MAAM,CAAC,EAAG,QAAO;AACpD,QAAI,QAAQ,IAAI,EAAE,SAAS,cAAc,KAAK,QAAQ,IAAI,EAAE,SAAS,WAAW,EAAG,QAAO;AAAA,EAC9F,QAAQ;AAAA,EAAe;AACvB,SAAO;AACX;AAGA,SAAS,gBAAgB,OAAe,MAAsB;AAC1D,SAAO,KAAK,MAAM,QAAQ,IAAI,IAAI;AACtC;AAGA,eAAsB,uBAA+C;AACjE,QAAM,KAAK,MAAM,eAAe;AAChC,QAAM,gBAAgB,MAAM,oBAAoB;AAChD,QAAM,UAAU,KAAK;AAErB,SAAO;AAAA,IACH,WAAW,kBAAkB;AAAA,IAC7B,SAAS;AAAA,IACT,aAAa,QAAQ;AAAA,IACrB,IAAI,SAAS;AAAA,IACb,WAAW,QAAQ;AAAA,IACnB,MAAM,KAAK;AAAA,IACX,UAAU,QAAQ,UAAU,GAAG;AAAA,IAC/B,YAAY,gBAAgB,GAAG,aAAa,MAAM,CAAC;AAAA,IACnD,WAAW,GAAG;AAAA,IACd,WAAW,gBAAgB,GAAG,YAAY,MAAM,CAAC;AAAA,IACjD;AAAA,IACA,aAAa,gBAAgB,GAAG,aAAa,EAAE;AAAA,IAC/C,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,EACxC;AACJ;AAGA,SAAS,mBAAmB,KAAgE;AACxF,QAAM,WAAW,IAAI,YAAY,CAAC;AAClC,QAAM,YAAY,IAAI,aAAa,CAAC;AACpC,QAAM,YAAY,OAAO,OAAO,QAAQ,EAAE,OAAO,CAAC,MAAgB,GAA+B,YAAY,IAAI,EAAE;AACnH,QAAM,iBAAiB,OAAO,QAAQ,SAAS,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM;AAC/D,UAAM,KAAK;AACX,WAAO,QAAQ,IAAI,MAAM,KAAM,MAAM,QAAQ,IAAI,YAAY,KAAM,GAAG,aAA2B,SAAS;AAAA,EAC9G,CAAC,EAAE;AACH,SAAO;AAAA,IACH,OAAO,IAAI,OAAO,YAAY;AAAA,IAC9B,MAAM,IAAI,MAAM,YAAY;AAAA,IAC5B,OAAO,IAAI,OAAO,YAAY;AAAA,IAC9B,QAAQ,IAAI,QAAQ,YAAY;AAAA,IAChC,WAAW,IAAI,WAAW,YAAY;AAAA,IACtC,aAAa,IAAI,aAAa,YAAY;AAAA,IAC1C,UAAU,IAAI,UAAU,YAAY;AAAA,IACpC,QAAQ,IAAI,QAAQ,YAAY;AAAA,IAChC,QAAQ,IAAI,QAAQ,YAAY;AAAA,IAChC,UAAU,IAAI,UAAU,YAAY;AAAA,IACpC,SAAS,IAAI,SAAS,YAAY;AAAA,IAClC,aAAa,IAAI,aAAa,YAAY;AAAA,IAC1C,WAAW,IAAI,KAAK,QAAQ,YAAY;AAAA,IACxC,iBAAiB;AAAA,IACjB,qBAAqB;AAAA,EACzB;AACJ;AAGO,SAAS,eAAe,iBAAiB,GAAmB;AAC/D,QAAM,MAAM,WAAW;AACvB,SAAO;AAAA,IACH,WAAW,kBAAkB;AAAA,IAC7B,SAAS;AAAA,IACT,OAAO;AAAA,IACP,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,eAAe,KAAK,MAAM,QAAQ,OAAO,CAAC;AAAA,IAC1C;AAAA,IACA,UAAU,mBAAmB,GAAG;AAAA,EACpC;AACJ;AAqBA,MAAM,eAAsC;AAAA,EACxC,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AACjB;AACO,SAAS,2BAAkD;AAC9D,SAAO,EAAE,GAAG,cAAc,SAAS,iBAAiB,EAAE;AAC1D;AAGA,eAAsB,oBAAoB,SAAiD;AACvF,QAAM,MAAM,WAAW;AACvB,QAAM,YAAa,IAAI,WAAmD;AAC1E,eAAa,aAAa,QAAQ,SAAS;AAC3C,eAAa,YAAY;AACzB,MAAI,CAAC,UAAW;AAEhB,QAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,eAAa,gBAAgB;AAC7B,eAAa,gBAAgB,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;AAC/E,MAAI;AACA,UAAM,MAAM,MAAM,MAAM,WAAW;AAAA,MAC/B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,MAC5B,QAAQ,YAAY,QAAQ,GAAI;AAAA,IACpC,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,QAAQ,IAAI,MAAM,EAAE;AACjD,iBAAa,aAAa;AAC1B,iBAAa,gBAAgB;AAC7B,iBAAa,YAAY;AAAA,EAC7B,SAAS,KAAK;AACV,iBAAa,eAAe;AAC5B,iBAAa,YAAa,IAAc,WAAW,OAAO,GAAG;AAAA,EAGjE;AAGA,mBAAiB,OAAO,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AAC5C;AAWA,eAAe,0BAAyC;AACpD,MAAI;AACA,UAAM,EAAE,cAAc,eAAe,WAAW,IAAI,MAAM,OAAO,IAAI;AACrE,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO,MAAM;AACpC,UAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,IAAI;AACrC,UAAM,aAAa,KAAK,QAAQ,GAAG,UAAU,qBAAqB;AAClE,QAAI,CAAC,WAAW,UAAU,EAAG;AAS7B,UAAM,SAAS,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AAC3D,QAAI,OAAO,SAAU;AAErB,UAAM,MAAM,WAAW;AACvB,QAAI,CAAC,IAAI,WAAW,QAAS;AAE7B,UAAM,WAAW,QAAQ,OAAO,mBAAmB,OAAO,oBAAoB,OAAO,gBAAgB;AACrG,UAAM,oBAAoB;AAAA,MACtB,MAAM,WAAW,WAAW;AAAA,MAC5B,WAAW,kBAAkB;AAAA,MAC7B,SAAS,OAAO,oBAAoB;AAAA,MACpC,aAAa,WAAW,OAAO,kBAAkB;AAAA,MACjD,eAAe,OAAO,iBAAiB;AAAA,MACvC,aAAa,OAAO,gBAAe,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC9D,CAAC;AAED,kBAAc,YAAY,KAAK,UAAU,EAAE,GAAG,QAAQ,UAAU,KAAK,GAAG,MAAM,CAAC,GAAG,OAAO;AACzF,WAAO,KAAK,WAAW,YAAY,WAAW,WAAW,SAAS,cAAc,OAAO,gBAAgB,EAAE;AAAA,EAC7G,SAAS,GAAG;AACR,WAAO,MAAM,WAAW,oCAAqC,EAAY,OAAO,EAAE;AAAA,EACtF;AACJ;AAGA,eAAsB,yBAAwC;AAC1D,QAAM,MAAM,WAAW;AACvB,MAAI,CAAC,IAAI,WAAW,QAAS;AAE7B,MAAI;AACA,UAAM,UAAU,MAAM,qBAAqB;AAC3C,UAAM,EAAE,WAAW,IAAI,MAAM,OAAO,qBAAqB;AACzD,UAAM,UAAU,MAAM,WAAW;AAEjC,UAAM,QAAQ,uBAAuB;AAAA,MACjC,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,WAAW,QAAQ;AAAA,IACvB,CAAC;AAED,UAAM,oBAAoB;AAAA,MACtB,MAAM;AAAA,MACN,GAAG;AAAA,IACP,CAAC;AAED,WAAO,KAAK,WAAW,4BAA4B,QAAQ,EAAE,IAAI,QAAQ,IAAI,KAAK,QAAQ,QAAQ,KAAK,QAAQ,UAAU,YAAY,QAAQ,aAAa,MAAM,EAAE;AAGlK,UAAM,wBAAwB;AAAA,EAClC,SAAS,GAAG;AACR,WAAO,KAAK,WAAW,uCAAwC,EAAY,OAAO,EAAE;AAAA,EACxF;AACJ;AAGO,SAAS,wBAAwB,wBAA4C;AAChF,QAAM,MAAM,WAAW;AACvB,MAAI,CAAC,IAAI,WAAW,QAAS;AAE7B,QAAM,wBAAwB;AAE9B,QAAM,OAAO,YAAY;AACrB,QAAI;AACA,YAAM,YAAY,eAAe,uBAAuB,CAAC;AACzD,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,qBAAqB;AACzD,YAAM,UAAU,MAAM,WAAW;AAEjC,YAAM,QAAQ,uBAAuB;AAAA,QACjC,OAAO;AAAA,QACP,YAAY;AAAA,QACZ,WAAW,UAAU;AAAA,QACrB,WAAW,UAAU;AAAA,MACzB,CAAC;AAED,YAAM,oBAAoB;AAAA,QACtB,MAAM;AAAA,QACN,GAAG;AAAA,MACP,CAAC;AAAA,IACL,QAAQ;AAAA,IAER;AAAA,EACJ;AAEA,OAAK;AACL,cAAY,MAAM,qBAAqB,EAAE,MAAM;AACnD;","names":[]}
|
package/dist/config/schema.js
CHANGED
|
@@ -207,6 +207,20 @@ const AgentConfigSchema = z.object({
|
|
|
207
207
|
/** Active persona ID (filename stem from assets/personas/). Default 'default' = no persona override. */
|
|
208
208
|
persona: z.string().default("default"),
|
|
209
209
|
workspace: z.string().optional(),
|
|
210
|
+
/** v7.1.x Organ 5 — adversarial self-critique (Reflexion). OFF by default.
|
|
211
|
+
* When on, a substantive turn gets one bounded critique pass; real issues
|
|
212
|
+
* become an appended "On reflection" caveat. Trades latency for reliability
|
|
213
|
+
* — worth it for building/ops work on a local model. */
|
|
214
|
+
/** v7.2 one-switch "best harness" preset: turns on the honesty organs
|
|
215
|
+
* (self-critique; the verification wall is always on) without editing
|
|
216
|
+
* nested config. Trades a little latency for Fable-5-class reliability. */
|
|
217
|
+
reliabilityMode: z.boolean().default(false),
|
|
218
|
+
selfCritique: z.object({
|
|
219
|
+
enabled: z.boolean().optional(),
|
|
220
|
+
minToolCalls: z.number().int().min(0).default(1),
|
|
221
|
+
minDraftChars: z.number().int().min(0).default(200),
|
|
222
|
+
model: z.string().default("")
|
|
223
|
+
}).default({}),
|
|
210
224
|
sessionCompaction: z.object({
|
|
211
225
|
enabled: z.boolean().default(true).describe("Auto-rotate sessions when limits exceeded"),
|
|
212
226
|
maxSessionRuns: z.number().default(200).describe("Max interactions per session before rotation"),
|
|
@@ -575,6 +589,24 @@ const TitanConfigSchema = z.object({
|
|
|
575
589
|
})).default({})
|
|
576
590
|
}).default({}),
|
|
577
591
|
organism: OrganismConfigSchema.default({}),
|
|
592
|
+
/** v7.1 MoA — Mixture-of-Agents presets. A preset = N reference advisor
|
|
593
|
+
* models (trimmed context, no tools, parallel with timeouts) + 1 acting
|
|
594
|
+
* aggregator. Selectable anywhere as the virtual model `moa/<preset>`.
|
|
595
|
+
* Local-first: the default `local-council` mixes models across the
|
|
596
|
+
* user's own machines for $0 per turn. */
|
|
597
|
+
moa: z.object({
|
|
598
|
+
presets: z.record(z.string(), z.object({
|
|
599
|
+
references: z.array(z.object({
|
|
600
|
+
model: z.string(),
|
|
601
|
+
maxTokens: z.number().int().positive().optional(),
|
|
602
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
603
|
+
timeoutMs: z.number().int().positive().optional()
|
|
604
|
+
})).min(1),
|
|
605
|
+
aggregator: z.string(),
|
|
606
|
+
enabled: z.boolean().default(true),
|
|
607
|
+
maxTokens: z.number().int().positive().optional()
|
|
608
|
+
})).default({})
|
|
609
|
+
}).default({}),
|
|
578
610
|
/** v7.0 Muscle Memory — automatic, replay-verified skill learning.
|
|
579
611
|
* Safe to enable by default: proposals never auto-adopt; every learned
|
|
580
612
|
* skill must pass a deterministic replay exam before it is shown. */
|