mixdog 1.2.6 → 1.3.1

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.
Files changed (294) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +71 -9
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +14 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +178 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent/AGENT.md +4 -0
  18. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  19. package/src/rules/agent-mode/MIXY.md +28 -0
  20. package/src/rules/agent-mode/ROUTINE.md +6 -0
  21. package/src/rules/lead/LEAD.md +7 -3
  22. package/src/rules/shared/70-delivery.md +3 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  24. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  25. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
  26. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  27. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
  28. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  29. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  30. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  31. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  34. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  35. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  36. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  37. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
  44. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
  46. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
  49. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  50. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  52. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  53. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  54. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
  58. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  61. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
  63. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
  64. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  66. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  67. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  68. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  69. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  70. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
  71. package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
  72. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  73. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  75. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
  76. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  77. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  78. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  79. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  80. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  81. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  82. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  83. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
  84. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
  85. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  86. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  87. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  88. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  89. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
  90. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  91. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  92. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  93. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  94. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  96. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  97. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  98. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  99. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  100. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
  101. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
  102. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  103. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  104. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  105. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  106. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
  108. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  109. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
  110. package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
  111. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  112. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  114. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  115. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  116. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
  118. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
  119. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  121. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  123. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  124. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  125. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  127. package/src/runtime/attachments/store.mjs +6 -0
  128. package/src/runtime/browser-bridge/client.mjs +24 -0
  129. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  130. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  131. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  132. package/src/runtime/channels/lib/webhook.mjs +0 -1
  133. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  134. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  135. package/src/runtime/computer-bridge/client.mjs +28 -1
  136. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  137. package/src/runtime/connectors/google-mcp.mjs +446 -0
  138. package/src/runtime/effort-judge/judge-client.mjs +1 -1
  139. package/src/runtime/effort-judge/model-manifest.json +7 -7
  140. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  141. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  142. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  143. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  144. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  145. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  146. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  147. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  148. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  149. package/src/runtime/memory/lib/memory.mjs +7 -1
  150. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  151. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  152. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  153. package/src/runtime/memory/tool-defs.mjs +5 -0
  154. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  155. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  156. package/src/runtime/shared/agent-avatar.mjs +320 -0
  157. package/src/runtime/shared/agent-kind.mjs +152 -0
  158. package/src/runtime/shared/agent-route-config.d.mts +4 -0
  159. package/src/runtime/shared/agent-route-config.mjs +30 -0
  160. package/src/runtime/shared/agent-store.mjs +132 -0
  161. package/src/runtime/shared/atomic-file.mjs +14 -1
  162. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  163. package/src/runtime/shared/config-patch.mjs +20 -1
  164. package/src/runtime/shared/config.mjs +8 -1
  165. package/src/runtime/shared/connector-accounts.mjs +15 -0
  166. package/src/runtime/shared/connector-catalog.d.mts +51 -0
  167. package/src/runtime/shared/connector-catalog.mjs +228 -0
  168. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  169. package/src/runtime/shared/connector-triggers.mjs +45 -0
  170. package/src/runtime/shared/err-text.mjs +3 -1
  171. package/src/runtime/shared/error-presentation.mjs +91 -6
  172. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  173. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  174. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  175. package/src/runtime/shared/held-request-host.d.mts +12 -0
  176. package/src/runtime/shared/held-request-host.mjs +71 -0
  177. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  178. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  179. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  180. package/src/runtime/shared/plain-preview.d.mts +1 -0
  181. package/src/runtime/shared/plain-preview.mjs +33 -0
  182. package/src/runtime/shared/routine-limits.d.mts +11 -0
  183. package/src/runtime/shared/routine-limits.mjs +21 -0
  184. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  185. package/src/runtime/shared/routine-trigger.mjs +44 -0
  186. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  187. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  188. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  189. package/src/runtime/shared/slow-span.mjs +4 -12
  190. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  191. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  192. package/src/runtime/shared/turn-changes.mjs +0 -8
  193. package/src/runtime/shared/usage-readout-totals.mjs +58 -0
  194. package/src/runtime/shared/user-data-guard.mjs +119 -38
  195. package/src/session-runtime/agent-home-delivery.mjs +826 -0
  196. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  197. package/src/session-runtime/agent-room-delivery.mjs +133 -0
  198. package/src/session-runtime/agent-task-sessions.mjs +460 -0
  199. package/src/session-runtime/boot/apis.mjs +63 -18
  200. package/src/session-runtime/boot/facade.mjs +5 -8
  201. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  202. package/src/session-runtime/boot/providers.mjs +14 -4
  203. package/src/session-runtime/boot/tools.mjs +3 -0
  204. package/src/session-runtime/channel-config-api.mjs +53 -5
  205. package/src/session-runtime/config-lifecycle.mjs +43 -2
  206. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  207. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  208. package/src/session-runtime/handoff-attachments.mjs +593 -0
  209. package/src/session-runtime/held-requests.mjs +228 -0
  210. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  211. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  212. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  213. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
  214. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  215. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  216. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  217. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  218. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  219. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  220. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  221. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  222. package/src/session-runtime/model-route/route-tuning.mjs +7 -1
  223. package/src/session-runtime/model-route/set-route.mjs +35 -4
  224. package/src/session-runtime/native-web-search.mjs +2 -2
  225. package/src/session-runtime/new-session-config.mjs +6 -1
  226. package/src/session-runtime/owned-automation-run.mjs +229 -0
  227. package/src/session-runtime/resource-api.mjs +5 -0
  228. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  229. package/src/session-runtime/resource-skills-api.mjs +12 -0
  230. package/src/session-runtime/room-member-gate.mjs +48 -0
  231. package/src/session-runtime/route-resolution.mjs +5 -11
  232. package/src/session-runtime/routine-home-turn.mjs +156 -0
  233. package/src/session-runtime/routine-owner.mjs +48 -0
  234. package/src/session-runtime/schedule-session-run.mjs +30 -64
  235. package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
  236. package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
  237. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  238. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  239. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  240. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  241. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  242. package/src/session-runtime/services/channel-admin.mjs +93 -24
  243. package/src/session-runtime/services/memory-access.mjs +37 -0
  244. package/src/session-runtime/services/provider-admin.mjs +74 -39
  245. package/src/session-runtime/services/schedule-access.mjs +91 -0
  246. package/src/session-runtime/session-create/session-options.mjs +34 -7
  247. package/src/session-runtime/session-hooks.mjs +16 -8
  248. package/src/session-runtime/setup-tool/executor.mjs +79 -6
  249. package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
  250. package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
  251. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  252. package/src/session-runtime/skills-api.mjs +16 -1
  253. package/src/session-runtime/tool-policy-refresh.mjs +249 -29
  254. package/src/session-runtime/turn/session-ops.mjs +8 -1
  255. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  256. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
  257. package/src/session-runtime/webhook-session-run.mjs +49 -67
  258. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  259. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
  260. package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
  261. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  262. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  263. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  264. package/src/standalone/daemon-agent-control.mjs +1 -8
  265. package/src/standalone/daemon-session-control.mjs +21 -4
  266. package/src/standalone/daemon.mjs +31 -5
  267. package/src/standalone/session-protocol.mjs +27 -0
  268. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  269. package/src/standalone/session-service/agent-tree.mjs +4 -3
  270. package/src/standalone/session-service/session-calls/session-turns.mjs +28 -0
  271. package/src/standalone/session-service.mjs +19 -0
  272. package/src/tui/app/slash-commands.mjs +20 -0
  273. package/src/tui/dist/index.mjs +234 -95
  274. package/src/tui/session/context-state.mjs +6 -1
  275. package/src/tui/session/notice-surface.mjs +34 -4
  276. package/src/tui/session/queue-helpers.mjs +2 -1
  277. package/src/tui/session/session-api/extensions.mjs +4 -0
  278. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  279. package/src/tui/session/session-api/integrations.mjs +8 -0
  280. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  281. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  282. package/src/tui/session/session-api/routes.mjs +61 -5
  283. package/src/tui/session/session-flow/drain.mjs +12 -3
  284. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  285. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  286. package/src/tui/session/session-flow/steering.mjs +58 -24
  287. package/src/tui/session/tool-approval.mjs +26 -5
  288. package/src/tui/session/transcript-intake.mjs +8 -2
  289. package/src/tui/session/transcript-spill.mjs +5 -1
  290. package/src/tui/session/tui-steering-persist.mjs +65 -42
  291. package/src/tui/session/turn-live-call.mjs +64 -0
  292. package/src/tui/session/turn.mjs +34 -8
  293. package/src/session-runtime/automation-agents.mjs +0 -94
  294. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -0,0 +1,320 @@
1
+ // The Mixdog mascot: a blocky pixel creature on a 16x12 grid, painted from the
2
+ // string layers in agent-avatar-sprites.mjs (design/mockups/mixy-art). Every
3
+ // look is the same body block; the species (ears, marks, tail, legs), an
4
+ // accessory, the expression and the colour vary. An agent's `agent.json`
5
+ // stores its pick as `avatar: { kind: 'mascot', species, accessory,
6
+ // expression, color }` (an older `variant` reads as its mapped look), or its
7
+ // own images per motion state as `avatar: { kind: 'custom', states: { idle,
8
+ // typing?, message? } }`. An agent without one gets a stable look from its id.
9
+ import { MIXY_AGENT_ID } from '../agent/orchestrator/session/agent-home.mjs';
10
+ import { MASCOT_SPRITES } from './agent-avatar-sprites.mjs';
11
+
12
+ /** The palette an avatar colour is chosen from. */
13
+ export const AGENT_AVATAR_COLORS = Object.freeze([
14
+ '#e6e6e6',
15
+ '#9a9a9a',
16
+ '#f06a5f',
17
+ '#f59a42',
18
+ '#f2c94c',
19
+ '#5cc98a',
20
+ '#3fc1c9',
21
+ '#5b9bff',
22
+ '#9b7bff',
23
+ '#ef6fb0',
24
+ ]);
25
+
26
+ /** Every species a user can pick: the six dogs. Mixy's own (`mixy`) is not one of them. */
27
+ export const MASCOT_SPECIES = Object.freeze(['labrador', 'shepherd', 'terrier', 'corgi', 'husky', 'shiba']);
28
+ export const MASCOT_ACCESSORIES = Object.freeze(['none', 'collar', 'glasses', 'beanie', 'scarf', 'bow']);
29
+ /** Picks that were removed, read as their nearest remaining option. */
30
+ const RETIRED_SPECIES = { dalmatian: 'labrador', cat: 'labrador', bird: 'labrador', robot: 'labrador' };
31
+ const RETIRED_ACCESSORIES = { headset: 'glasses' };
32
+ export const MASCOT_EXPRESSIONS = Object.freeze(['calm', 'happy', 'wink', 'sleepy']);
33
+
34
+ /** Mixy, the primary assistant: its own silver pup with one ear up and a
35
+ * slate collar with the gold star medal. Never offered in the picker. */
36
+ export const MIXY_AVATAR = Object.freeze({
37
+ kind: 'mascot',
38
+ species: 'mixy',
39
+ accessory: 'none',
40
+ expression: 'calm',
41
+ color: '#e6e6e6',
42
+ });
43
+
44
+ /** The fixed inks a sprite letter names; B, D, L and E depend on the look. */
45
+ export const MASCOT_INKS = Object.freeze({
46
+ K: '#141414',
47
+ G: '#f2c94c',
48
+ R: '#e5534b',
49
+ O: '#f59a42',
50
+ W: '#f4f4f4',
51
+ C: '#3fc1c9',
52
+ M: '#8a8a90',
53
+ S: '#2a2b30',
54
+ P: '#ef8fb4',
55
+ N: '#f06a5f',
56
+ n: '#c4544c',
57
+ r: '#b94039',
58
+ F: '#ef6fb0',
59
+ f: '#b8497f',
60
+ });
61
+
62
+ /** `hex` with every channel scaled by `k`. */
63
+ export function shadeHex(hex, k) {
64
+ const n = Number.parseInt(hex.slice(1), 16);
65
+ const channel = (shift) =>
66
+ Math.round(((n >> shift) & 255) * k)
67
+ .toString(16)
68
+ .padStart(2, '0');
69
+ return `#${channel(16)}${channel(8)}${channel(0)}`;
70
+ }
71
+
72
+ /** `hex` mixed `k` of the way to white. */
73
+ function liftHex(hex, k) {
74
+ const n = Number.parseInt(hex.slice(1), 16);
75
+ const channel = (shift) => {
76
+ const value = (n >> shift) & 255;
77
+ return Math.round(value + (255 - value) * k)
78
+ .toString(16)
79
+ .padStart(2, '0');
80
+ };
81
+ return `#${channel(16)}${channel(8)}${channel(0)}`;
82
+ }
83
+
84
+ // The 12 looks before species existed, `<ears>-<expression>-<plain|collar>`:
85
+ // each reads as the species with those ears, the collar as an accessory.
86
+ const LEGACY_EARS = { floppy: 'labrador', perky: 'shepherd', folded: 'terrier' };
87
+ const LEGACY_VARIANTS = Object.keys(LEGACY_EARS).flatMap((ears) =>
88
+ ['calm', 'happy'].flatMap((expression) => ['plain', 'collar'].map((extra) => `${ears}-${expression}-${extra}`))
89
+ );
90
+
91
+ /** An older `variant` as its species, accessory and expression; null if unknown. */
92
+ export function mascotFromVariant(variant) {
93
+ if (!LEGACY_VARIANTS.includes(variant)) return null;
94
+ const [ears, expression, extra] = variant.split('-');
95
+ return { species: LEGACY_EARS[ears], accessory: extra === 'collar' ? 'collar' : 'none', expression };
96
+ }
97
+
98
+ /** `species-accessory-expression`: a look's key (data attributes, tests). */
99
+ export const mascotLook = (avatar) => `${avatar.species}-${avatar.accessory}-${avatar.expression}`;
100
+
101
+ function layerCells(rows, color, eye) {
102
+ if (!rows) return [];
103
+ const ink = (key) => {
104
+ if (key === 'B') return color;
105
+ if (key === 'D') return shadeHex(color, 0.7);
106
+ if (key === 'L') return liftHex(color, 0.35);
107
+ return MASCOT_INKS[key === 'E' ? eye : key];
108
+ };
109
+ const cells = [];
110
+ rows.forEach((row, y) => {
111
+ for (let x = 0; x < row.length; x += 1) if (row[x] !== '.') cells.push({ x, y, fill: ink(row[x]) });
112
+ });
113
+ return cells;
114
+ }
115
+
116
+ /** A look's cells by part, in paint order — the groups the renderer draws
117
+ * (tail, legs, then the bobbing body with its ears, marks, eyes and
118
+ * accessory). `blink` is the closed eyes an idle blink shows. Null for an
119
+ * unknown species. */
120
+ export function mascotParts(avatar) {
121
+ const species = MASCOT_SPRITES.species[avatar.species];
122
+ if (!species) return null;
123
+ const paint = (rows) => layerCells(rows, avatar.color, species.eye);
124
+ return {
125
+ tail: paint(species.tail && MASCOT_SPRITES.tails[species.tail]),
126
+ legs: paint(MASCOT_SPRITES.legs[species.legs]),
127
+ body: paint(MASCOT_SPRITES.body),
128
+ ear: paint(species.ears),
129
+ marks: paint(species.marks),
130
+ eyes: paint(MASCOT_SPRITES.eyes[avatar.expression]),
131
+ blink: paint(MASCOT_SPRITES.eyes.sleepy),
132
+ accessory: paint(MASCOT_SPRITES.accessories[avatar.accessory]),
133
+ };
134
+ }
135
+
136
+ /** The look as one still frame: each cell's final fill. [] when unknown. */
137
+ export function mascotCells(avatar) {
138
+ const parts = mascotParts(avatar);
139
+ if (!parts) return [];
140
+ const cells = new Map();
141
+ for (const part of ['tail', 'legs', 'body', 'ear', 'marks', 'eyes', 'accessory']) {
142
+ for (const cell of parts[part]) cells.set(`${cell.y},${cell.x}`, cell);
143
+ }
144
+ return [...cells.values()];
145
+ }
146
+
147
+ /** Cells as one SVG path per fill, each row's run one rectangle. */
148
+ export function cellPaths(cells) {
149
+ const byFill = new Map();
150
+ for (const { x, y, fill } of cells) {
151
+ const rows = byFill.get(fill) ?? new Map();
152
+ byFill.set(fill, rows);
153
+ rows.set(y, [...(rows.get(y) ?? []), x]);
154
+ }
155
+ return [...byFill].map(([fill, rows]) => {
156
+ let d = '';
157
+ for (const [y, xs] of [...rows].sort(([a], [b]) => a - b)) {
158
+ const sorted = [...new Set(xs)].sort((a, b) => a - b);
159
+ for (let i = 0; i < sorted.length; ) {
160
+ let j = i;
161
+ while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1) j += 1;
162
+ const run = j - i + 1;
163
+ d += `M${sorted[i]} ${y}h${run}v1h-${run}z`;
164
+ i = j + 1;
165
+ }
166
+ }
167
+ return { fill, d };
168
+ });
169
+ }
170
+
171
+ /** The motion states an avatar shows today, `idle` first (a custom avatar's
172
+ * one required image, and every missing state's fallback). */
173
+ export const AGENT_AVATAR_STATES = Object.freeze(['idle', 'typing', 'message']);
174
+
175
+ /** The single image avatar before per-state images: a PNG beside AGENT.md
176
+ * under a content-hash name. Still read, as a custom avatar's idle image. */
177
+ export const AGENT_AVATAR_IMAGE_FILE = /^avatar-[0-9a-f]{12}\.png$/;
178
+ /** One state's image in `agents/<id>/avatar/`, under a content-hash name (a
179
+ * new upload is a new name, so caches by name never go stale). An animated
180
+ * one keeps its still first frame beside it (avatarPosterFile). */
181
+ export const AGENT_AVATAR_STATE_FILE = /^(idle|typing|message)-[0-9a-f]{12}\.(png|gif|webp)$/;
182
+ export const AGENT_AVATAR_DIR = 'avatar';
183
+ /** The largest image upload accepted, and the largest side of an animation. */
184
+ export const AGENT_AVATAR_IMAGE_MAX_BYTES = 1024 * 1024;
185
+ export const AGENT_AVATAR_IMAGE_MAX_SIDE = 512;
186
+
187
+ /** A state image's path under the agent's folder ('/'-separated). */
188
+ export const agentAvatarStatePath = (file) =>
189
+ AGENT_AVATAR_IMAGE_FILE.test(file) ? file : `${AGENT_AVATAR_DIR}/${file}`;
190
+ /** The still first frame stored beside an animated state image. */
191
+ export const avatarPosterFile = (file) => file.replace(/\.(png|gif|webp)$/, '.poster.png');
192
+ /** The image a custom avatar shows in `state`: its own, else idle's. */
193
+ export const customAvatarStateFile = (avatar, state) => avatar.states[state] ?? avatar.states.idle;
194
+
195
+ const u16le = (bytes, at) => bytes[at] | (bytes[at + 1] << 8);
196
+ const u24le = (bytes, at) => bytes[at] | (bytes[at + 1] << 8) | (bytes[at + 2] << 16);
197
+ const u32be = (bytes, at) => ((bytes[at] << 24) | (bytes[at + 1] << 16) | (bytes[at + 2] << 8) | bytes[at + 3]) >>> 0;
198
+ const ascii = (bytes, at, length) => String.fromCharCode(...bytes.subarray(at, at + length));
199
+
200
+ /** An upload's format and size, read from its header bytes: a PNG (an APNG
201
+ * counts as animated), a GIF (always treated as animated) or a WebP; null
202
+ * for anything else — SVG and every other markup included. */
203
+ export function avatarImageInfo(bytes) {
204
+ if (!(bytes instanceof Uint8Array) || bytes.length < 30) return null;
205
+ if (u32be(bytes, 0) === 0x89504e47 && u32be(bytes, 4) === 0x0d0a1a0a && ascii(bytes, 12, 4) === 'IHDR') {
206
+ let animated = false;
207
+ for (let at = 8; at + 8 <= bytes.length; at += 12 + u32be(bytes, at)) {
208
+ const type = ascii(bytes, at + 4, 4);
209
+ if (type === 'acTL') animated = true;
210
+ if (type === 'IDAT' || type === 'IEND') break;
211
+ }
212
+ return { format: 'png', width: u32be(bytes, 16), height: u32be(bytes, 20), animated };
213
+ }
214
+ if (ascii(bytes, 0, 6) === 'GIF87a' || ascii(bytes, 0, 6) === 'GIF89a') {
215
+ return { format: 'gif', width: u16le(bytes, 6), height: u16le(bytes, 8), animated: true };
216
+ }
217
+ if (ascii(bytes, 0, 4) === 'RIFF' && ascii(bytes, 8, 4) === 'WEBP') {
218
+ const chunk = ascii(bytes, 12, 4);
219
+ if (chunk === 'VP8X') {
220
+ const animated = (bytes[20] & 0x02) !== 0;
221
+ return { format: 'webp', width: u24le(bytes, 24) + 1, height: u24le(bytes, 27) + 1, animated };
222
+ }
223
+ if (chunk === 'VP8 ') {
224
+ return { format: 'webp', width: u16le(bytes, 26) & 0x3fff, height: u16le(bytes, 28) & 0x3fff, animated: false };
225
+ }
226
+ if (chunk === 'VP8L') {
227
+ const bits = (bytes[21] | (bytes[22] << 8) | (bytes[23] << 16) | (bytes[24] << 24)) >>> 0;
228
+ return { format: 'webp', width: (bits & 0x3fff) + 1, height: ((bits >>> 14) & 0x3fff) + 1, animated: false };
229
+ }
230
+ }
231
+ return null;
232
+ }
233
+
234
+ /** Why bytes cannot be a state image ('' when they can): not a PNG, GIF or
235
+ * WebP, over 1 MB, not square, or over 512px. */
236
+ export function agentAvatarImageProblem(bytes) {
237
+ const info = avatarImageInfo(bytes);
238
+ if (!info) return 'must be a PNG, GIF or WebP image';
239
+ if (bytes.length > AGENT_AVATAR_IMAGE_MAX_BYTES) return 'is larger than 1 MB';
240
+ if (!info.width || info.width !== info.height) return 'must be square';
241
+ if (info.width > AGENT_AVATAR_IMAGE_MAX_SIDE) return 'must be 512px or smaller';
242
+ return '';
243
+ }
244
+
245
+ function normalizeCustom(states) {
246
+ if (!states || typeof states !== 'object') return null;
247
+ const kept = {};
248
+ for (const state of AGENT_AVATAR_STATES) {
249
+ const file = states[state];
250
+ if (file == null) continue;
251
+ const own = typeof file === 'string' && AGENT_AVATAR_STATE_FILE.test(file) && file.startsWith(`${state}-`);
252
+ const legacy = state === 'idle' && typeof file === 'string' && AGENT_AVATAR_IMAGE_FILE.test(file);
253
+ if (!own && !legacy) return null;
254
+ kept[state] = file;
255
+ }
256
+ return kept.idle ? { kind: 'custom', states: kept } : null;
257
+ }
258
+
259
+ const pick = (value, list, fallback) => (value == null ? fallback : list.includes(value) ? value : null);
260
+
261
+ /** A stored avatar as the app understands it, or null when it is not one. An
262
+ * older single image reads as a custom avatar with only its idle image; an
263
+ * older mascot `variant` as its mapped species, accessory and expression. */
264
+ export function normalizeAgentAvatar(value) {
265
+ if (!value || typeof value !== 'object') return null;
266
+ if (value.kind === 'image') {
267
+ return typeof value.file === 'string' && AGENT_AVATAR_IMAGE_FILE.test(value.file)
268
+ ? { kind: 'custom', states: { idle: value.file } }
269
+ : null;
270
+ }
271
+ if (value.kind === 'custom') return normalizeCustom(value.states);
272
+ if (value.kind !== 'mascot') return null;
273
+ const color = typeof value.color === 'string' ? value.color.toLowerCase() : '';
274
+ if (!AGENT_AVATAR_COLORS.includes(color)) return null;
275
+ const look =
276
+ value.species == null && typeof value.variant === 'string'
277
+ ? mascotFromVariant(value.variant)
278
+ : {
279
+ species: pick(
280
+ RETIRED_SPECIES[value.species] ?? value.species,
281
+ [...MASCOT_SPECIES, MIXY_AVATAR.species],
282
+ null
283
+ ),
284
+ accessory: pick(RETIRED_ACCESSORIES[value.accessory] ?? value.accessory, MASCOT_ACCESSORIES, 'none'),
285
+ expression: pick(value.expression, MASCOT_EXPRESSIONS, 'calm'),
286
+ };
287
+ if (!look?.species || !look.accessory || !look.expression) return null;
288
+ return { kind: 'mascot', ...look, color };
289
+ }
290
+
291
+ // FNV-1a: a small stable string hash, the same on every platform.
292
+ function hashId(id) {
293
+ let hash = 0x811c9dc5;
294
+ for (const char of String(id)) {
295
+ hash ^= char.codePointAt(0);
296
+ hash = Math.imul(hash, 0x01000193) >>> 0;
297
+ }
298
+ return hash;
299
+ }
300
+
301
+ /** The look of an agent that never picked one: fixed by its id — the
302
+ * species, accessory and colour it had before species existed (its old
303
+ * variant, mapped), with the neutral face every default wears. */
304
+ export function defaultAgentAvatar(id) {
305
+ const hash = hashId(id);
306
+ return {
307
+ kind: 'mascot',
308
+ ...mascotFromVariant(LEGACY_VARIANTS[hash % LEGACY_VARIANTS.length]),
309
+ expression: 'calm',
310
+ color: AGENT_AVATAR_COLORS[Math.floor(hash / LEGACY_VARIANTS.length) % AGENT_AVATAR_COLORS.length],
311
+ };
312
+ }
313
+
314
+ /** What an agent shows: its own images, else Mixy's fixed look (for Mixy),
315
+ * its own mascot pick, or its id default. */
316
+ export function agentAvatar(id, stored) {
317
+ const own = normalizeAgentAvatar(stored);
318
+ if (id === MIXY_AGENT_ID) return own ?? { ...MIXY_AVATAR };
319
+ return own ?? defaultAgentAvatar(id);
320
+ }
@@ -0,0 +1,152 @@
1
+ // An agent definition's kind (isContactAgentId reads it), decided read-only —
2
+ // the one rule for the runtime and the desktop (which reads `kind` from the
3
+ // runtime's catalog rows). Nothing is ever stamped on disk.
4
+ //
5
+ // An explicit frontmatter `kind` wins (`bot` from the bot dialog and Mixy's
6
+ // setup tool, `agent` from the Workflows page and other chats). v1.3.0 wrote
7
+ // no marker for bots, and v1.2.6 agents have none either, so an unmarked
8
+ // user-made agent is a bot only on evidence that only a v1.3.0 bot creation
9
+ // produced:
10
+ // T1 its home thread holds the creation greeting (agentGreetingKickoff,
11
+ // sent only to bots a dialog or the host greeted on creation; agents
12
+ // older than 1.3 were recorded without one);
13
+ // T2 its agent.json has an `avatar` (the bot dialog always sent one) AND
14
+ // its AGENT.md body is the bot dialog's default (agentBodyFromBrief).
15
+ // Anything else is a workflow agent, as in v1.2.6.
16
+ import { existsSync, readFileSync, statSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { agentHomeSessionId } from '../agent/orchestrator/session/agent-home.mjs';
19
+ import { readMarkdownDocument } from './markdown-frontmatter.mjs';
20
+ import { BOT_AGENT_KIND } from './agent-route-config.mjs';
21
+ import { botStoreActive, isStoreBot } from './agent-store.mjs';
22
+
23
+ const RUNTIME_MARKER = '[mixdog-runtime]';
24
+ /** The creation greeting's opening (shared/agent-greeting.ts, v1.3.0 on). */
25
+ const CREATION_GREETING = /(^|\n)\[mixdog-runtime\] The user just created you as /;
26
+ /** The closing lines of the bot dialog's default AGENT.md (agentBodyFromBrief). */
27
+ const DIALOG_BODY_TAIL = [
28
+ /^- Help the user( with this role)? in a natural, friendly conversation, in the language they write in\.$/,
29
+ /^- Ask a short question when a request is unclear, and say plainly when something is outside what you do\.$/,
30
+ /^- Keep replies as short or as detailed as the moment needs\.$/,
31
+ ];
32
+
33
+ function messageText(content) {
34
+ if (typeof content === 'string') return content;
35
+ if (!Array.isArray(content)) return '';
36
+ return content
37
+ .map((part) => (typeof part === 'string' ? part : typeof part?.text === 'string' ? part.text : ''))
38
+ .join('\n');
39
+ }
40
+
41
+ const NO_HOME = Object.freeze({ exists: false, greeted: false, chatted: false });
42
+ // Per session file, keyed by its size and mtime: a home thread is read once
43
+ // until it changes.
44
+ const homeEvidenceCache = new Map();
45
+
46
+ /** What `agentId`'s home thread (sessions/sess_home_<id>.json) holds:
47
+ * whether it exists, whether it has the creation greeting, and whether the
48
+ * user and the agent talked (any user message that is not a runtime turn,
49
+ * or any assistant message). A missing or unreadable file holds nothing. */
50
+ export function homeThreadEvidence(dataDir, agentId) {
51
+ const sessionId = agentHomeSessionId(agentId);
52
+ if (!dataDir || !sessionId) return NO_HOME;
53
+ const file = join(dataDir, 'sessions', `${sessionId}.json`);
54
+ let stat;
55
+ try {
56
+ stat = statSync(file);
57
+ } catch {
58
+ return NO_HOME;
59
+ }
60
+ const cached = homeEvidenceCache.get(file);
61
+ if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) return cached.value;
62
+ let messages = [];
63
+ try {
64
+ const doc = JSON.parse(readFileSync(file, 'utf8'));
65
+ if (Array.isArray(doc?.messages)) messages = doc.messages;
66
+ } catch {
67
+ // unreadable: it proves nothing
68
+ }
69
+ let greeted = false;
70
+ let chatted = false;
71
+ for (const message of messages) {
72
+ if (message?.role === 'assistant') chatted = true;
73
+ if (message?.role !== 'user') continue;
74
+ const text = messageText(message.content);
75
+ if (CREATION_GREETING.test(text)) greeted = true;
76
+ else if (text.trim() && !text.trimStart().startsWith(RUNTIME_MARKER)) chatted = true;
77
+ }
78
+ const value = Object.freeze({ exists: true, greeted, chatted });
79
+ homeEvidenceCache.set(file, { size: stat.size, mtimeMs: stat.mtimeMs, value });
80
+ return value;
81
+ }
82
+
83
+ function isDialogDefaultBody(body) {
84
+ const lines = String(body || '')
85
+ .trim()
86
+ .split(/\r?\n/);
87
+ const tail = lines.slice(-DIALOG_BODY_TAIL.length);
88
+ return tail.length === DIALOG_BODY_TAIL.length && DIALOG_BODY_TAIL.every((line, index) => line.test(tail[index]));
89
+ }
90
+
91
+ /** The definition's kind: the explicit frontmatter kind (lower-cased); else,
92
+ * for an unmarked user-made agent (`dataDir`/`id` given, not a shipped role),
93
+ * `bot` on T1 or T2 evidence; else null (a workflow agent). `manifest` and
94
+ * `body` are the user copy's agent.json and AGENT.md body. */
95
+ export function agentDefinitionKind(definition) {
96
+ const { dataDir, id } = definition;
97
+ // Once bots have their own store the folder decides: bots/ holds bots,
98
+ // agents/ workflow agents (an explicit marker still read as written).
99
+ if (dataDir && id && isStoreBot(dataDir, id)) return BOT_AGENT_KIND;
100
+ if (dataDir && botStoreActive(dataDir)) return explicitKind(definition.frontmatterKind);
101
+ return evidenceKind(definition);
102
+ }
103
+
104
+ const explicitKind = (frontmatterKind) =>
105
+ (typeof frontmatterKind === 'string' ? frontmatterKind.trim().toLowerCase() : '') || null;
106
+
107
+ /** The pre-store rule alone (the split's decision input): the explicit kind,
108
+ * else `bot` on T1/T2 evidence for an unmarked user-made agent, else null. */
109
+ export function evidenceKind({ frontmatterKind, dataDir, shippedRoot, id, manifest, body }) {
110
+ const explicit = explicitKind(frontmatterKind);
111
+ if (explicit) return explicit;
112
+ if (!dataDir || !id || id === 'lead') return null;
113
+ if (shippedRoot && existsSync(join(shippedRoot, 'agents', id, 'AGENT.md'))) return null;
114
+ if (manifest?.avatar && isDialogDefaultBody(body)) return BOT_AGENT_KIND;
115
+ return homeThreadEvidence(dataDir, id).greeted ? BOT_AGENT_KIND : null;
116
+ }
117
+
118
+ /** agentDefinitionKind from the user copy on disk, for readers that have no
119
+ * loaded definition at hand: a store bot (bots/<id>/) is a bot; any other id
120
+ * is read from agents/<id>/, else from the shipped role. */
121
+ export function readAgentDefinitionKind({ dataDir, shippedRoot, id }) {
122
+ if (isStoreBot(dataDir, id)) return BOT_AGENT_KIND;
123
+ const dir = join(dataDir, 'agents', id);
124
+ let doc;
125
+ try {
126
+ doc = readMarkdownDocument(readFileSync(join(dir, 'AGENT.md'), 'utf8'));
127
+ } catch {
128
+ // No user copy: the shipped role's own frontmatter decides.
129
+ try {
130
+ return agentDefinitionKind({
131
+ frontmatterKind: readMarkdownDocument(readFileSync(join(shippedRoot, 'agents', id, 'AGENT.md'), 'utf8'))
132
+ .frontmatter.kind,
133
+ });
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ let manifest = {};
139
+ try {
140
+ manifest = JSON.parse(readFileSync(join(dir, 'agent.json'), 'utf8')) || {};
141
+ } catch {
142
+ manifest = {};
143
+ }
144
+ return agentDefinitionKind({
145
+ frontmatterKind: doc.frontmatter.kind,
146
+ dataDir,
147
+ shippedRoot,
148
+ id,
149
+ manifest,
150
+ body: doc.body,
151
+ });
152
+ }
@@ -0,0 +1,4 @@
1
+ // Types for the desktop's one import of agent-route-config.mjs.
2
+ export const WORKFLOW_AGENT_KIND: 'agent';
3
+ export const BOT_AGENT_KIND: 'bot';
4
+ export function isContactAgentId(value: unknown, kind?: unknown): boolean;
@@ -16,6 +16,26 @@ export const DEFAULT_DISABLED_AGENT_IDS = Object.freeze([
16
16
  // runs only once the user turns it on and picks its model.
17
17
  export const DEFAULT_DISABLED_AGENT_ROSTER = Object.freeze([...DEFAULT_DISABLED_AGENT_IDS, 'maintainer']);
18
18
 
19
+ /** The AGENT.md frontmatter `kind` the Workflows page writes when it creates
20
+ * an agent: a workflow delegation role, never a contact (봇). */
21
+ export const WORKFLOW_AGENT_KIND = 'agent';
22
+
23
+ /** The AGENT.md frontmatter `kind` every bot creation path writes: the bot
24
+ * dialog, and the setup tool when Mixy (agent mode) creates an agent. */
25
+ export const BOT_AGENT_KIND = 'bot';
26
+
27
+ /** A contact agent (a 봇): one explicitly marked `kind: bot` at creation —
28
+ * never Mixy ('lead') nor a shipped role (the starter sub-agents Worker,
29
+ * Reviewer, … and the Maintainer). `kind` is the definition's frontmatter
30
+ * `kind`; an unmarked custom agent (every agent made before bots existed)
31
+ * stays a workflow agent. The one rule for the runtime (delegation, agent
32
+ * catalogs) and the desktop (contacts, agent lists). */
33
+ export function isContactAgentId(value, kind) {
34
+ const id = typeof value === 'string' ? value.trim().toLowerCase() : '';
35
+ const bot = typeof kind === 'string' && kind.trim().toLowerCase() === BOT_AGENT_KIND;
36
+ return Boolean(id) && id !== 'lead' && !DEFAULT_DISABLED_AGENT_ROSTER.includes(id) && bot;
37
+ }
38
+
19
39
  function record(value) {
20
40
  return isPlainObject(value) ? value : {};
21
41
  }
@@ -112,9 +132,19 @@ export function withAgentDisabled(config, agentId, disabled) {
112
132
  // left under retired slots (agents.maintenance, workflowRoutes.*,
113
133
  // maintenance.memory, generated workflow-* presets) are scrubbed by
114
134
  // canonicalizeAgentRouteStorage, never folded in.
135
+ // A bot's route lives with the bot (agent-store.mjs registers the reader in
136
+ // the runtime): the resolver answers a route, null (a bot without one), or
137
+ // undefined (no bot — the config decides).
138
+ let botRouteResolver = null;
139
+ export function setBotRouteResolver(resolver) {
140
+ botRouteResolver = typeof resolver === 'function' ? resolver : null;
141
+ }
142
+
115
143
  export function configuredAgentRouteCandidates(config, agentId) {
116
144
  const id = String(agentId || '').trim();
117
145
  if (!id) return [];
146
+ const bot = botRouteResolver?.(id);
147
+ if (bot !== undefined) return bot ? [bot] : [];
118
148
  const route = record(config?.agents)[id];
119
149
  return route ? [route] : [];
120
150
  }
@@ -0,0 +1,132 @@
1
+ // Where a user-made definition lives once bots have their own store (1.3.1):
2
+ // bots under <dataDir>/bots/<id>/ (with their own model route in agent.json),
3
+ // workflow agents under <dataDir>/agents/<id>/. The store is active once the
4
+ // one-time split finished (bots/migration.json, bot-split-migration.mjs);
5
+ // before that — or if it never could run — every reader sees agents/ as
6
+ // before. The ids the split copied are bots for good: their agents/ originals
7
+ // stay on disk for rollback but no catalog reads them again.
8
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { setBotRouteResolver } from './agent-route-config.mjs';
11
+
12
+ export const BOTS_DIR = 'bots';
13
+ export const BOT_MIGRATION_MANIFEST = 'migration.json';
14
+ export const BOT_MIGRATION_VERSION = 1;
15
+ /** Mixy's bot id once it has its own folder. */
16
+ export const MIXY_BOT_ID = 'mixy';
17
+
18
+ function readJson(file) {
19
+ try {
20
+ const value = JSON.parse(readFileSync(file, 'utf8'));
21
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ // Small stat-keyed caches: the store is read on route and catalog paths.
28
+ const fileCache = new Map();
29
+ function cachedJson(file) {
30
+ let stat;
31
+ try {
32
+ stat = statSync(file);
33
+ } catch {
34
+ fileCache.delete(file);
35
+ return null;
36
+ }
37
+ const hit = fileCache.get(file);
38
+ if (hit && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size) return hit.value;
39
+ const value = readJson(file);
40
+ fileCache.set(file, { mtimeMs: stat.mtimeMs, size: stat.size, value });
41
+ return value;
42
+ }
43
+
44
+ /** The finished split's record, or null before it ran. */
45
+ export function readBotMigration(dataDir) {
46
+ if (!dataDir) return null;
47
+ const manifest = cachedJson(join(dataDir, BOTS_DIR, BOT_MIGRATION_MANIFEST));
48
+ return manifest?.version === BOT_MIGRATION_VERSION ? manifest : null;
49
+ }
50
+
51
+ /** Whether `dataDir` has its bot store (the split finished). */
52
+ export function botStoreActive(dataDir) {
53
+ return readBotMigration(dataDir) !== null;
54
+ }
55
+
56
+ /** The agents/ ids the split copied to bots/: the agent catalog skips them. */
57
+ export function migratedBotIds(dataDir) {
58
+ return new Set(readBotMigration(dataDir)?.bots || []);
59
+ }
60
+
61
+ export const botDir = (dataDir, id) => join(dataDir, BOTS_DIR, id);
62
+
63
+ /** Whether `id` is a store bot: the store is active and the bot has its
64
+ * folder, or the split copied it (an id it moved is never an agent again).
65
+ * Mixy's own folder is no ordinary bot. */
66
+ export function isStoreBot(dataDir, id) {
67
+ if (!dataDir || !id || id === MIXY_BOT_ID || id.startsWith('.')) return false;
68
+ const manifest = readBotMigration(dataDir);
69
+ if (!manifest) return false;
70
+ return existsSync(botDir(dataDir, id)) || (manifest.bots || []).includes(id);
71
+ }
72
+
73
+ /** The folder of a user-made definition: bots/<id> for a store bot, else agents/<id>. */
74
+ export function userDefinitionDir(dataDir, id) {
75
+ return isStoreBot(dataDir, id) ? botDir(dataDir, id) : join(dataDir, 'agents', id);
76
+ }
77
+
78
+ const isCompleteRoute = (route) =>
79
+ Boolean(
80
+ route && typeof route === 'object' && String(route.provider || '').trim() && String(route.model || '').trim()
81
+ );
82
+
83
+ /** A store bot's own route (agent.json `route`): the route, null when it has
84
+ * none, undefined when `id` is no store bot. */
85
+ export function readBotRoute(dataDir, id) {
86
+ if (!isStoreBot(dataDir, id)) return undefined;
87
+ const route = cachedJson(join(botDir(dataDir, id), 'agent.json'))?.route;
88
+ return isCompleteRoute(route) ? route : null;
89
+ }
90
+
91
+ /** Stores (or with null removes) a store bot's route in its agent.json; every
92
+ * other key is kept. */
93
+ export function writeBotRoute(dataDir, id, route) {
94
+ const dir = botDir(dataDir, id);
95
+ const file = join(dir, 'agent.json');
96
+ const manifest = readJson(file) || {};
97
+ if (route) manifest.route = route;
98
+ else delete manifest.route;
99
+ mkdirSync(dir, { recursive: true });
100
+ writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
101
+ fileCache.delete(file);
102
+ }
103
+
104
+ // --- Mixy: its own bot (bots/mixy), apart from the Lead ---
105
+ // Mixy's identity (name, face, persona, 일하는 방식, connectors, `created`)
106
+ // lives in bots/mixy once the store is active, in agents/lead before. Its
107
+ // model route is always its own (bots/mixy/agent.json `route`), never the
108
+ // Lead's: changing one never changes the other. Its home thread keeps the id
109
+ // sess_home_lead, and its runtime role id stays `lead`.
110
+
111
+ /** The folder Mixy's identity is read from and written to. */
112
+ export function mixyIdentityDir(dataDir) {
113
+ return botStoreActive(dataDir) ? botDir(dataDir, MIXY_BOT_ID) : join(dataDir, 'agents', 'lead');
114
+ }
115
+
116
+ /** Mixy's own route, or null when it has none (its thread keeps its own). */
117
+ export function readMixyRoute(dataDir) {
118
+ if (!dataDir) return null;
119
+ const route = cachedJson(join(botDir(dataDir, MIXY_BOT_ID), 'agent.json'))?.route;
120
+ return isCompleteRoute(route) ? route : null;
121
+ }
122
+
123
+ /** Stores (or with null removes) Mixy's own route; nothing of the Lead's. */
124
+ export function writeMixyRoute(dataDir, route) {
125
+ writeBotRoute(dataDir, MIXY_BOT_ID, route);
126
+ }
127
+
128
+ /** Every config route read (configuredAgentRouteCandidates) asks the bot
129
+ * store first: a bot's route lives with the bot, never in the config. */
130
+ export function registerBotRouteStore(getDataDir) {
131
+ setBotRouteResolver((id) => readBotRoute(getDataDir(), id));
132
+ }