@dxos/plugin-assistant 0.13.0 → 0.14.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.
Files changed (234) hide show
  1. package/PLUGIN.mdl +25 -0
  2. package/dist/lib/AssistantCapabilities.mjs +8 -1
  3. package/dist/lib/AssistantCapabilities.mjs.map +1 -1
  4. package/dist/lib/Settings.mjs +4 -0
  5. package/dist/lib/Settings.mjs.map +1 -1
  6. package/dist/lib/TurnReviewSkill.mjs +55 -0
  7. package/dist/lib/TurnReviewSkill.mjs.map +1 -0
  8. package/dist/lib/chunk-AgentArticle.mjs +1 -1
  9. package/dist/lib/chunk-AssistantSettings.mjs +1 -1
  10. package/dist/lib/chunk-AssistantSettings.mjs.map +1 -1
  11. package/dist/lib/chunk-Chat.mjs +322 -174
  12. package/dist/lib/chunk-Chat.mjs.map +1 -1
  13. package/dist/lib/chunk-ChatArticle.mjs +14 -7
  14. package/dist/lib/chunk-ChatArticle.mjs.map +1 -1
  15. package/dist/lib/chunk-ChatDialog.mjs +5 -4
  16. package/dist/lib/chunk-ChatDialog.mjs.map +1 -1
  17. package/dist/lib/chunk-IntegrationPrompt.mjs +1 -1
  18. package/dist/lib/chunk-PLUGIN.mjs +1 -1
  19. package/dist/lib/chunk-PluginUrlPrompt.mjs +1 -1
  20. package/dist/lib/chunk-QuestionSurface.mjs +1 -1
  21. package/dist/lib/chunk-SpaceHomePrompt.mjs +116 -60
  22. package/dist/lib/chunk-SpaceHomePrompt.mjs.map +1 -1
  23. package/dist/lib/chunk-TriggerStatus.mjs +1 -1
  24. package/dist/lib/chunk-agent-service.mjs +3 -1
  25. package/dist/lib/chunk-agent-service.mjs.map +1 -1
  26. package/dist/lib/chunk-analyze-turn.mjs +220 -0
  27. package/dist/lib/chunk-analyze-turn.mjs.map +1 -0
  28. package/dist/lib/{chunk-processor.mjs → chunk-chat-model.mjs} +95 -43
  29. package/dist/lib/chunk-chat-model.mjs.map +1 -0
  30. package/dist/lib/chunk-context.mjs.map +1 -1
  31. package/dist/lib/chunk-create-chat.mjs +5 -19
  32. package/dist/lib/chunk-create-chat.mjs.map +1 -1
  33. package/dist/lib/chunk-create-object.mjs +1 -1
  34. package/dist/lib/chunk-default-skills.mjs +28 -0
  35. package/dist/lib/chunk-default-skills.mjs.map +1 -0
  36. package/dist/lib/chunk-generate-home-suggestions.mjs +8 -6
  37. package/dist/lib/chunk-generate-home-suggestions.mjs.map +1 -1
  38. package/dist/lib/chunk-presets.mjs +1 -1
  39. package/dist/lib/chunk-presets.mjs.map +1 -1
  40. package/dist/lib/chunk-question-resumer.mjs +1 -1
  41. package/dist/lib/chunk-react-surface.mjs +1 -1
  42. package/dist/lib/chunk-run-prompt-in-chat.mjs +2 -1
  43. package/dist/lib/chunk-run-prompt-in-chat.mjs.map +1 -1
  44. package/dist/lib/chunk-run-prompt-in-new-chat.mjs +1 -1
  45. package/dist/lib/chunk-schema-defs.mjs +2 -2
  46. package/dist/lib/chunk-skill-definition.mjs +3 -2
  47. package/dist/lib/chunk-skill-definition.mjs.map +1 -1
  48. package/dist/lib/components.mjs +13 -13
  49. package/dist/lib/components.mjs.map +1 -1
  50. package/dist/lib/hooks.mjs +40 -31
  51. package/dist/lib/hooks.mjs.map +1 -1
  52. package/dist/lib/operations.mjs +3 -1
  53. package/dist/lib/operations.mjs.map +1 -1
  54. package/dist/lib/skills.mjs +2 -1
  55. package/dist/lib/translations.mjs +4 -2
  56. package/dist/lib/translations.mjs.map +1 -1
  57. package/dist/types/src/Chat.d.ts +1 -1
  58. package/dist/types/src/capabilities/agent-service.d.ts.map +1 -1
  59. package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -1
  60. package/dist/types/src/{processor/processor.d.ts → chat-model/chat-model.d.ts} +24 -22
  61. package/dist/types/src/chat-model/chat-model.d.ts.map +1 -0
  62. package/dist/types/src/chat-model/chat-model.edge.test.d.ts +2 -0
  63. package/dist/types/src/chat-model/chat-model.edge.test.d.ts.map +1 -0
  64. package/dist/types/src/chat-model/chat-model.node.test.d.ts +2 -0
  65. package/dist/types/src/chat-model/chat-model.node.test.d.ts.map +1 -0
  66. package/dist/types/src/chat-model/chat-model.scripted.node.test.d.ts +2 -0
  67. package/dist/types/src/chat-model/chat-model.scripted.node.test.d.ts.map +1 -0
  68. package/dist/types/src/{processor → chat-model}/index.d.ts +1 -1
  69. package/dist/types/src/chat-model/index.d.ts.map +1 -0
  70. package/dist/types/src/chat-model/outbox.d.ts.map +1 -0
  71. package/dist/types/src/chat-model/outbox.test.d.ts.map +1 -0
  72. package/dist/types/src/chat-model/presets.d.ts.map +1 -0
  73. package/dist/types/src/chat-model/presets.test.d.ts.map +1 -0
  74. package/dist/types/src/{processor → chat-model}/prompt.d.ts +2 -2
  75. package/dist/types/src/chat-model/prompt.d.ts.map +1 -0
  76. package/dist/types/src/chat-model/prompt.test.d.ts.map +1 -0
  77. package/dist/types/src/chat-model/streaming.node.test.d.ts.map +1 -0
  78. package/dist/types/src/chat-model/testing/chat-model-recorder.d.ts +58 -0
  79. package/dist/types/src/chat-model/testing/chat-model-recorder.d.ts.map +1 -0
  80. package/dist/types/src/chat-model/testing/harness.d.ts +22 -0
  81. package/dist/types/src/chat-model/testing/harness.d.ts.map +1 -0
  82. package/dist/types/src/chat-model/thread.d.ts.map +1 -0
  83. package/dist/types/src/chat-model/thread.test.d.ts.map +1 -0
  84. package/dist/types/src/components/Chat/Chat.d.ts +6 -6
  85. package/dist/types/src/components/Chat/Chat.d.ts.map +1 -1
  86. package/dist/types/src/components/Chat/context.d.ts +5 -3
  87. package/dist/types/src/components/Chat/context.d.ts.map +1 -1
  88. package/dist/types/src/components/ChatPrompt/ChatActions.d.ts +1 -1
  89. package/dist/types/src/components/ChatPrompt/ChatActions.d.ts.map +1 -1
  90. package/dist/types/src/components/ChatPrompt/ChatMcpErrors.d.ts +3 -3
  91. package/dist/types/src/components/ChatPrompt/ChatMcpErrors.d.ts.map +1 -1
  92. package/dist/types/src/components/ChatPrompt/ChatOptions.d.ts +3 -1
  93. package/dist/types/src/components/ChatPrompt/ChatOptions.d.ts.map +1 -1
  94. package/dist/types/src/components/ChatPrompt/ChatOptions.stories.d.ts +4 -3
  95. package/dist/types/src/components/ChatPrompt/ChatOptions.stories.d.ts.map +1 -1
  96. package/dist/types/src/components/ChatPrompt/ChatPrompt.d.ts +15 -5
  97. package/dist/types/src/components/ChatPrompt/ChatPrompt.d.ts.map +1 -1
  98. package/dist/types/src/components/ChatPrompt/ChatPrompt.stories.d.ts +1 -1
  99. package/dist/types/src/components/ChatPrompt/ChatStatus.d.ts +1 -1
  100. package/dist/types/src/components/ChatPrompt/ChatStatus.d.ts.map +1 -1
  101. package/dist/types/src/components/ConnectorAuthMenu/ConnectorAuthMenu.stories.d.ts +9 -0
  102. package/dist/types/src/components/ConnectorAuthMenu/ConnectorAuthMenu.stories.d.ts.map +1 -1
  103. package/dist/types/src/components/Toolbox/Toolbox.d.ts +3 -3
  104. package/dist/types/src/components/Toolbox/Toolbox.d.ts.map +1 -1
  105. package/dist/types/src/containers/ChatArticle/ChatArticle.d.ts.map +1 -1
  106. package/dist/types/src/containers/ChatArticle/ChatArticle.stories.d.ts +1 -1
  107. package/dist/types/src/containers/SpaceHomePrompt/SpaceHomePrompt.d.ts.map +1 -1
  108. package/dist/types/src/hooks/index.d.ts +2 -2
  109. package/dist/types/src/hooks/index.d.ts.map +1 -1
  110. package/dist/types/src/hooks/{useChatProcessor.d.ts → useChatModel.d.ts} +6 -6
  111. package/dist/types/src/hooks/useChatModel.d.ts.map +1 -0
  112. package/dist/types/src/hooks/useDebug.d.ts +3 -3
  113. package/dist/types/src/hooks/useDebug.d.ts.map +1 -1
  114. package/dist/types/src/hooks/useHomeSuggestions.d.ts +3 -3
  115. package/dist/types/src/hooks/useHomeSuggestions.d.ts.map +1 -1
  116. package/dist/types/src/hooks/usePresets.d.ts +1 -1
  117. package/dist/types/src/hooks/usePresets.d.ts.map +1 -1
  118. package/dist/types/src/hooks/useSelectionContext.d.ts +3 -3
  119. package/dist/types/src/hooks/useSelectionContext.d.ts.map +1 -1
  120. package/dist/types/src/operations/analyze-turn.d.ts +5 -0
  121. package/dist/types/src/operations/analyze-turn.d.ts.map +1 -0
  122. package/dist/types/src/operations/analyze-turn.test.d.ts +2 -0
  123. package/dist/types/src/operations/analyze-turn.test.d.ts.map +1 -0
  124. package/dist/types/src/operations/create-chat.d.ts.map +1 -1
  125. package/dist/types/src/operations/generate-home-suggestions.d.ts.map +1 -1
  126. package/dist/types/src/operations/index.d.ts.map +1 -1
  127. package/dist/types/src/operations/run-prompt-in-chat.d.ts.map +1 -1
  128. package/dist/types/src/review/turn-review.d.ts +58 -0
  129. package/dist/types/src/review/turn-review.d.ts.map +1 -0
  130. package/dist/types/src/review/turn-review.test.d.ts +2 -0
  131. package/dist/types/src/review/turn-review.test.d.ts.map +1 -0
  132. package/dist/types/src/skills/index.d.ts +1 -0
  133. package/dist/types/src/skills/index.d.ts.map +1 -1
  134. package/dist/types/src/skills/turn-review/TurnReviewSkill.d.ts +21 -0
  135. package/dist/types/src/skills/turn-review/TurnReviewSkill.d.ts.map +1 -0
  136. package/dist/types/src/skills/turn-review/index.d.ts +2 -0
  137. package/dist/types/src/skills/turn-review/index.d.ts.map +1 -0
  138. package/dist/types/src/translations.d.ts.map +1 -1
  139. package/dist/types/src/types/Assistant.d.ts +1 -0
  140. package/dist/types/src/types/Assistant.d.ts.map +1 -1
  141. package/dist/types/src/types/AssistantCapabilities.d.ts +7 -1
  142. package/dist/types/src/types/AssistantCapabilities.d.ts.map +1 -1
  143. package/dist/types/src/types/Settings.d.ts +1 -0
  144. package/dist/types/src/types/Settings.d.ts.map +1 -1
  145. package/dist/types/src/util/agent-options.d.ts +27 -0
  146. package/dist/types/src/util/agent-options.d.ts.map +1 -0
  147. package/dist/types/src/util/agent-options.test.d.ts +2 -0
  148. package/dist/types/src/util/agent-options.test.d.ts.map +1 -0
  149. package/dist/types/src/util/default-skills.d.ts +10 -0
  150. package/dist/types/src/util/default-skills.d.ts.map +1 -0
  151. package/dist/types/tsconfig.tsbuildinfo +1 -1
  152. package/package.json +89 -80
  153. package/src/capabilities/agent-service.ts +2 -1
  154. package/src/capabilities/skill-definition.ts +2 -1
  155. package/src/chat-model/chat-model.edge.test.ts +301 -0
  156. package/src/{processor/processor.node.test.ts → chat-model/chat-model.node.test.ts} +19 -9
  157. package/src/chat-model/chat-model.scripted.node.test.ts +441 -0
  158. package/src/{processor/processor.ts → chat-model/chat-model.ts} +140 -45
  159. package/src/{processor → chat-model}/index.ts +1 -1
  160. package/src/{processor → chat-model}/prompt.ts +2 -2
  161. package/src/{processor → chat-model}/streaming.node.test.ts +108 -116
  162. package/src/chat-model/testing/chat-model-recorder.ts +249 -0
  163. package/src/chat-model/testing/harness.ts +81 -0
  164. package/src/{processor → chat-model}/thread.test.ts +1 -1
  165. package/src/components/Chat/Chat.tsx +44 -38
  166. package/src/components/Chat/Error.stories.tsx +1 -1
  167. package/src/components/Chat/context.ts +5 -3
  168. package/src/components/ChatPrompt/ChatActions.tsx +1 -1
  169. package/src/components/ChatPrompt/ChatMcpErrors.tsx +6 -6
  170. package/src/components/ChatPrompt/ChatOptions.stories.tsx +37 -3
  171. package/src/components/ChatPrompt/ChatOptions.tsx +118 -10
  172. package/src/components/ChatPrompt/ChatPrompt.stories.tsx +10 -10
  173. package/src/components/ChatPrompt/ChatPrompt.tsx +44 -27
  174. package/src/components/ChatPrompt/ChatStatus.tsx +8 -7
  175. package/src/components/Toolbox/Toolbox.tsx +12 -13
  176. package/src/containers/AssistantSettings/AssistantSettings.tsx +1 -1
  177. package/src/containers/ChatArticle/ChatArticle.stories.tsx +1 -1
  178. package/src/containers/ChatArticle/ChatArticle.tsx +16 -6
  179. package/src/containers/ChatDialog/ChatDialog.tsx +4 -4
  180. package/src/containers/SpaceHomePrompt/SpaceHomePrompt.tsx +110 -57
  181. package/src/hooks/index.ts +2 -2
  182. package/src/hooks/{useChatProcessor.ts → useChatModel.ts} +16 -14
  183. package/src/hooks/useDebug.ts +8 -8
  184. package/src/hooks/useHomeSuggestions.ts +14 -9
  185. package/src/hooks/usePresets.ts +2 -2
  186. package/src/hooks/useProcessEphemeralStatus.ts +1 -1
  187. package/src/hooks/useSelectionContext.ts +3 -5
  188. package/src/operations/analyze-turn.test.ts +194 -0
  189. package/src/operations/analyze-turn.ts +96 -0
  190. package/src/operations/create-chat.ts +4 -36
  191. package/src/operations/generate-home-suggestions.test.ts +1 -2
  192. package/src/operations/generate-home-suggestions.ts +17 -9
  193. package/src/operations/index.ts +2 -0
  194. package/src/operations/run-prompt-in-chat.ts +3 -2
  195. package/src/review/turn-review.test.ts +92 -0
  196. package/src/review/turn-review.ts +213 -0
  197. package/src/skills/index.ts +1 -0
  198. package/src/skills/turn-review/TurnReviewSkill.ts +46 -0
  199. package/src/skills/turn-review/index.ts +5 -0
  200. package/src/translations.ts +2 -0
  201. package/src/types/AssistantCapabilities.ts +8 -1
  202. package/src/types/Settings.ts +7 -0
  203. package/src/util/agent-options.test.ts +54 -0
  204. package/src/util/agent-options.ts +60 -0
  205. package/src/util/default-skills.ts +35 -0
  206. package/dist/lib/chunk-processor.mjs.map +0 -1
  207. package/dist/types/src/hooks/useChatProcessor.d.ts.map +0 -1
  208. package/dist/types/src/processor/index.d.ts.map +0 -1
  209. package/dist/types/src/processor/outbox.d.ts.map +0 -1
  210. package/dist/types/src/processor/outbox.test.d.ts.map +0 -1
  211. package/dist/types/src/processor/presets.d.ts.map +0 -1
  212. package/dist/types/src/processor/presets.test.d.ts.map +0 -1
  213. package/dist/types/src/processor/processor.d.ts.map +0 -1
  214. package/dist/types/src/processor/processor.node.test.d.ts +0 -2
  215. package/dist/types/src/processor/processor.node.test.d.ts.map +0 -1
  216. package/dist/types/src/processor/prompt.d.ts.map +0 -1
  217. package/dist/types/src/processor/prompt.test.d.ts.map +0 -1
  218. package/dist/types/src/processor/streaming.node.test.d.ts.map +0 -1
  219. package/dist/types/src/processor/thread.d.ts.map +0 -1
  220. package/dist/types/src/processor/thread.test.d.ts.map +0 -1
  221. /package/dist/types/src/{processor → chat-model}/outbox.d.ts +0 -0
  222. /package/dist/types/src/{processor → chat-model}/outbox.test.d.ts +0 -0
  223. /package/dist/types/src/{processor → chat-model}/presets.d.ts +0 -0
  224. /package/dist/types/src/{processor → chat-model}/presets.test.d.ts +0 -0
  225. /package/dist/types/src/{processor → chat-model}/prompt.test.d.ts +0 -0
  226. /package/dist/types/src/{processor → chat-model}/streaming.node.test.d.ts +0 -0
  227. /package/dist/types/src/{processor → chat-model}/thread.d.ts +0 -0
  228. /package/dist/types/src/{processor → chat-model}/thread.test.d.ts +0 -0
  229. /package/src/{processor → chat-model}/outbox.test.ts +0 -0
  230. /package/src/{processor → chat-model}/outbox.ts +0 -0
  231. /package/src/{processor → chat-model}/presets.test.ts +0 -0
  232. /package/src/{processor → chat-model}/presets.ts +0 -0
  233. /package/src/{processor → chat-model}/prompt.test.ts +0 -0
  234. /package/src/{processor → chat-model}/thread.ts +0 -0
@@ -0,0 +1,220 @@
1
+ import * as Capability from "@dxos/app-framework/Capability";
2
+ import { AssistantCapabilities } from "#types";
3
+ import * as Capabilities from "@dxos/app-framework/Capabilities";
4
+ import * as Option from "effect/Option";
5
+ import { Database, Obj } from "@dxos/echo";
6
+ import { AiService } from "@dxos/ai";
7
+ import { AiSession } from "@dxos/assistant";
8
+ import { log } from "@dxos/log";
9
+ import { trim } from "@dxos/util";
10
+ import * as Effect from "effect/Effect";
11
+ import * as Operation from "@dxos/compute/Operation";
12
+ import { TurnReviewSkill } from "#skills";
13
+ import * as Schema from "effect/Schema";
14
+ import * as Harness$1 from "@dxos/assistant/Harness";
15
+ import * as LanguageModel from "effect/ai/LanguageModel";
16
+ import * as Prompt from "effect/ai/Prompt";
17
+ import { contentCaptureAllowed } from "@dxos/observability/AiObservability";
18
+ import * as ObservabilityCapabilities from "@dxos/plugin-observability/ObservabilityCapabilities";
19
+ //#region src/review/turn-review.ts
20
+ /** PostHog event emitted when the reviewer finds a turn the agent struggled through. */
21
+ var STRUGGLE_EVENT = "agent_struggle";
22
+ /** Small enough to be cheap per turn, strong enough to judge an agentic transcript. */
23
+ var REVIEW_MODEL = "com.anthropic.model.claude-sonnet-5.default";
24
+ /** Bounds the transcript the reviewer reads; the uploaded trajectory is not cut. */
25
+ var MAX_TRANSCRIPT_CHARS = 12e4;
26
+ var MAX_BLOCK_CHARS = 4e3;
27
+ /**
28
+ * Why the agent struggled. Only the first four are reported: they are the ones fixable on our side
29
+ * (prompting or tooling); the model's own reasoning and an unclear user request are not.
30
+ */
31
+ var StruggleCause = Schema.Literals([
32
+ "system_prompt_missing",
33
+ "system_prompt_wrong",
34
+ "tool_missing",
35
+ "tool_faulty",
36
+ "model",
37
+ "user",
38
+ "none"
39
+ ]);
40
+ var TurnVerdict = Schema.Struct({
41
+ struggled: Schema.Boolean.annotate({ description: "Whether the agent visibly struggled in the latest turn." }),
42
+ cause: StruggleCause.annotate({ description: "The root cause of the struggle, or \"none\"." }),
43
+ severity: Schema.Literals([
44
+ "low",
45
+ "medium",
46
+ "high"
47
+ ]).annotate({ description: "low: recovered quickly; medium: wasted several steps; high: failed the request." }),
48
+ summary: Schema.String.annotate({ description: "One sentence naming what went wrong and what instruction or tool change would fix it." }),
49
+ tools: Schema.Array(Schema.String).annotate({ description: "Names of the tools implicated, if any." })
50
+ });
51
+ /** The dashboard's top-level split; `undefined` for a cause that is not reported. */
52
+ var causeGroup = (cause) => {
53
+ switch (cause) {
54
+ case "system_prompt_missing":
55
+ case "system_prompt_wrong": return "system_prompt";
56
+ case "tool_missing":
57
+ case "tool_faulty": return "tooling";
58
+ default: return;
59
+ }
60
+ };
61
+ var isReportable = (verdict) => verdict.struggled && causeGroup(verdict.cause) !== void 0;
62
+ var REVIEW_SYSTEM_PROMPT = trim`
63
+ You review transcripts of an AI agent working inside Composer, a collaborative workspace app.
64
+ Decide whether the agent struggled in the LATEST TURN (messages inside the <latest_turn> tags).
65
+ Earlier messages are context only.
66
+
67
+ Signs of struggling: repeated or looping tool calls, tool errors, calling tools with invalid
68
+ arguments, guessing at tool names or object ids, apologizing and retrying, giving up, asking the
69
+ user for information it should have been able to obtain, or giving an incomplete answer.
70
+
71
+ Classify the root cause:
72
+ - system_prompt_missing: the agent lacked instructions or context it needed (e.g. how a feature
73
+ works, which tool to use for a task, conventions of the app).
74
+ - system_prompt_wrong: instructions were present but misleading, contradictory, or outdated.
75
+ - tool_missing: no tool existed for an action the request clearly required.
76
+ - tool_faulty: a tool errored, returned unusable output, had a confusing schema or description.
77
+ - model: the tools and instructions were adequate; the model reasoned poorly.
78
+ - user: the request was ambiguous or impossible.
79
+ - none: the agent did not struggle.
80
+
81
+ Be conservative: a single quickly-recovered tool error is not a struggle. When unsure, answer
82
+ struggled=false with cause=none.
83
+ `;
84
+ var clip = (text, max) => text.length <= max ? text : `${text.slice(0, max)}… [${text.length - max} chars truncated]`;
85
+ var stringify = (value) => typeof value === "string" ? value : JSON.stringify(value);
86
+ /** One line per block, so the reviewer sees the shape of the turn without a tool result drowning it. */
87
+ var formatMessage = (message) => {
88
+ const lines = message.blocks.map((block) => {
89
+ switch (block._tag) {
90
+ case "text": return clip(block.text, MAX_BLOCK_CHARS);
91
+ case "toolCall": return `[tool call ${block.name}] ${clip(stringify(block.input), MAX_BLOCK_CHARS)}`;
92
+ case "toolResult": return block.error ? `[tool error ${block.name}] ${clip(stringify(block.error), MAX_BLOCK_CHARS)}` : `[tool result ${block.name}] ${clip(stringify(block.result), MAX_BLOCK_CHARS)}`;
93
+ default: return `[${block._tag}]`;
94
+ }
95
+ });
96
+ return `${(message.sender.role ?? "unknown").toUpperCase()}:\n${lines.join("\n")}`;
97
+ };
98
+ /**
99
+ * Splits the history at the turn that just ended: it starts at the last user message, which is what
100
+ * the agent was answering. Hooks fire per request, so everything after it is this turn's work.
101
+ */
102
+ var splitTurn = (history) => {
103
+ const index = history.findLastIndex((message) => message.sender.role === "user");
104
+ return index === -1 ? {
105
+ context: [],
106
+ turn: history
107
+ } : {
108
+ context: history.slice(0, index),
109
+ turn: history.slice(index)
110
+ };
111
+ };
112
+ /** The reviewer's prompt; context is trimmed from the front when the transcript runs long. */
113
+ var formatReviewPrompt = (history) => {
114
+ const { context, turn } = splitTurn(history);
115
+ const turnText = turn.map(formatMessage).join("\n\n");
116
+ const budget = Math.max(0, MAX_TRANSCRIPT_CHARS - turnText.length);
117
+ const contextText = context.map(formatMessage).join("\n\n");
118
+ return [
119
+ contextText.length > budget ? `…${contextText.slice(contextText.length - budget)}` : contextText,
120
+ "<latest_turn>",
121
+ clip(turnText, MAX_TRANSCRIPT_CHARS),
122
+ "</latest_turn>"
123
+ ].filter((part) => part.length > 0).join("\n\n");
124
+ };
125
+ var countToolCalls = (messages) => {
126
+ let calls = 0;
127
+ let errors = 0;
128
+ for (const message of messages) for (const block of message.blocks) if (block._tag === "toolCall") calls++;
129
+ else if (block._tag === "toolResult" && block.error) errors++;
130
+ return {
131
+ calls,
132
+ errors
133
+ };
134
+ };
135
+ /** A header line then one line per message, in the span serialization the LLM analytics traces use. */
136
+ var toTrajectoryNdjson = (header, history) => [JSON.stringify({
137
+ type: "header",
138
+ ...header
139
+ }), ...history.map((message) => JSON.stringify({
140
+ type: "message",
141
+ created: message.created,
142
+ ...AiSession.serializeMessage(message)
143
+ }))].join("\n");
144
+ /**
145
+ * Flat properties so every one is a PostHog breakdown; lists are comma-joined for the same reason.
146
+ * `$ai_session_id` joins the event to the conversation's LLM analytics traces.
147
+ */
148
+ var toStruggleEventProperties = ({ sessionId, model, codeMode, skills, verdict, history, trajectoryKey }) => {
149
+ const { turn } = splitTurn(history);
150
+ const { calls, errors } = countToolCalls(turn);
151
+ return {
152
+ $ai_session_id: sessionId,
153
+ model,
154
+ code_mode: codeMode,
155
+ skills: skills.join(","),
156
+ cause: verdict.cause,
157
+ cause_group: causeGroup(verdict.cause),
158
+ severity: verdict.severity,
159
+ summary: verdict.summary,
160
+ tools: verdict.tools.join(","),
161
+ turn_message_count: turn.length,
162
+ turn_tool_call_count: calls,
163
+ turn_tool_error_count: errors,
164
+ review_model: REVIEW_MODEL,
165
+ trajectory_key: trajectoryKey
166
+ };
167
+ };
168
+ //#endregion
169
+ //#region src/operations/analyze-turn.ts
170
+ var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-assistant/src/operations/analyze-turn.ts";
171
+ var handler = TurnReviewSkill.AnalyzeTurn.pipe(Operation.withHandler(Effect.fnUntraced(function* () {
172
+ const settings = Option.getOrUndefined(yield* Capabilities.getAtomValueOption(AssistantCapabilities.Settings));
173
+ const observability = Option.getOrUndefined(yield* Capability.getOption(ObservabilityCapabilities.Observability));
174
+ if (!settings?.reportStruggles || !observability?.enabled) return;
175
+ const chat = yield* Harness$1.getChat;
176
+ const spaceId = Obj.getDatabase(chat)?.spaceId;
177
+ if (!spaceId || !contentCaptureAllowed(spaceId)) return;
178
+ const history = yield* Harness$1.history;
179
+ if (history.length === 0) return;
180
+ const { value: verdict } = yield* LanguageModel.generateObject({
181
+ schema: TurnVerdict,
182
+ objectName: "turn_verdict",
183
+ prompt: Prompt.setSystem(Prompt.make(formatReviewPrompt(history)), REVIEW_SYSTEM_PROMPT)
184
+ }).pipe(Effect.provide(AiService.languageModel(REVIEW_MODEL)));
185
+ log.info("turn reviewed", {
186
+ chat: chat.id,
187
+ struggled: verdict.struggled,
188
+ cause: verdict.cause
189
+ }, {
190
+ "~LogMeta": "~LogMeta",
191
+ F: __dxlog_file,
192
+ L: 66,
193
+ S: this
194
+ });
195
+ if (!isReportable(verdict)) return;
196
+ const binder = yield* Harness$1.binder;
197
+ const feed = yield* Database.load(chat.feed);
198
+ const header = {
199
+ sessionId: Obj.getURI(feed, { prefer: "absolute" }),
200
+ model: chat.session?.model,
201
+ codeMode: settings.codeMode ?? false,
202
+ skills: binder.getSkills().filter((skill) => Obj.getMeta(skill).key !== TurnReviewSkill.key).map((skill) => skill.name),
203
+ verdict
204
+ };
205
+ const trajectoryKey = yield* Effect.promise(() => observability.support.uploadNdjson(toTrajectoryNdjson(header, history), "trajectory"));
206
+ observability.events.captureEvent(STRUGGLE_EVENT, toStruggleEventProperties({
207
+ ...header,
208
+ history,
209
+ trajectoryKey
210
+ }));
211
+ }, Effect.catchCause((cause) => Effect.sync(() => log.warn("turn review failed", { cause }, {
212
+ "~LogMeta": "~LogMeta",
213
+ F: __dxlog_file,
214
+ L: 91,
215
+ S: void 0
216
+ }))))));
217
+ //#endregion
218
+ export { handler as default };
219
+
220
+ //# sourceMappingURL=chunk-analyze-turn.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-analyze-turn.mjs","names":[],"sources":["../../src/review/turn-review.ts","../../src/operations/analyze-turn.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Schema from 'effect/Schema';\n\nimport { AiSession } from '@dxos/assistant';\nimport { type Message } from '@dxos/types';\nimport { trim } from '@dxos/util';\n\n/** PostHog event emitted when the reviewer finds a turn the agent struggled through. */\nexport const STRUGGLE_EVENT = 'agent_struggle';\n\n/** Small enough to be cheap per turn, strong enough to judge an agentic transcript. */\nexport const REVIEW_MODEL = 'com.anthropic.model.claude-sonnet-5.default';\n\n/** Bounds the transcript the reviewer reads; the uploaded trajectory is not cut. */\nconst MAX_TRANSCRIPT_CHARS = 120_000;\nconst MAX_BLOCK_CHARS = 4_000;\n\n/**\n * Why the agent struggled. Only the first four are reported: they are the ones fixable on our side\n * (prompting or tooling); the model's own reasoning and an unclear user request are not.\n */\nexport const StruggleCause = Schema.Literals([\n 'system_prompt_missing',\n 'system_prompt_wrong',\n 'tool_missing',\n 'tool_faulty',\n 'model',\n 'user',\n 'none',\n]);\nexport type StruggleCause = Schema.Schema.Type<typeof StruggleCause>;\n\nexport const TurnVerdict = Schema.Struct({\n struggled: Schema.Boolean.annotate({\n description: 'Whether the agent visibly struggled in the latest turn.',\n }),\n cause: StruggleCause.annotate({\n description: 'The root cause of the struggle, or \"none\".',\n }),\n severity: Schema.Literals(['low', 'medium', 'high']).annotate({\n description: 'low: recovered quickly; medium: wasted several steps; high: failed the request.',\n }),\n summary: Schema.String.annotate({\n description: 'One sentence naming what went wrong and what instruction or tool change would fix it.',\n }),\n tools: Schema.Array(Schema.String).annotate({\n description: 'Names of the tools implicated, if any.',\n }),\n});\nexport type TurnVerdict = Schema.Schema.Type<typeof TurnVerdict>;\n\nexport type CauseGroup = 'system_prompt' | 'tooling';\n\n/** The dashboard's top-level split; `undefined` for a cause that is not reported. */\nexport const causeGroup = (cause: StruggleCause): CauseGroup | undefined => {\n switch (cause) {\n case 'system_prompt_missing':\n case 'system_prompt_wrong':\n return 'system_prompt';\n case 'tool_missing':\n case 'tool_faulty':\n return 'tooling';\n default:\n return undefined;\n }\n};\n\nexport const isReportable = (verdict: TurnVerdict): boolean =>\n verdict.struggled && causeGroup(verdict.cause) !== undefined;\n\nexport const REVIEW_SYSTEM_PROMPT = trim`\n You review transcripts of an AI agent working inside Composer, a collaborative workspace app.\n Decide whether the agent struggled in the LATEST TURN (messages inside the <latest_turn> tags).\n Earlier messages are context only.\n\n Signs of struggling: repeated or looping tool calls, tool errors, calling tools with invalid\n arguments, guessing at tool names or object ids, apologizing and retrying, giving up, asking the\n user for information it should have been able to obtain, or giving an incomplete answer.\n\n Classify the root cause:\n - system_prompt_missing: the agent lacked instructions or context it needed (e.g. how a feature\n works, which tool to use for a task, conventions of the app).\n - system_prompt_wrong: instructions were present but misleading, contradictory, or outdated.\n - tool_missing: no tool existed for an action the request clearly required.\n - tool_faulty: a tool errored, returned unusable output, had a confusing schema or description.\n - model: the tools and instructions were adequate; the model reasoned poorly.\n - user: the request was ambiguous or impossible.\n - none: the agent did not struggle.\n\n Be conservative: a single quickly-recovered tool error is not a struggle. When unsure, answer\n struggled=false with cause=none.\n`;\n\nconst clip = (text: string, max: number): string =>\n text.length <= max ? text : `${text.slice(0, max)}… [${text.length - max} chars truncated]`;\n\nconst stringify = (value: unknown): string => (typeof value === 'string' ? value : JSON.stringify(value));\n\n/** One line per block, so the reviewer sees the shape of the turn without a tool result drowning it. */\nconst formatMessage = (message: Message.Message): string => {\n const lines = message.blocks.map((block) => {\n switch (block._tag) {\n case 'text':\n return clip(block.text, MAX_BLOCK_CHARS);\n case 'toolCall':\n return `[tool call ${block.name}] ${clip(stringify(block.input), MAX_BLOCK_CHARS)}`;\n case 'toolResult':\n return block.error\n ? `[tool error ${block.name}] ${clip(stringify(block.error), MAX_BLOCK_CHARS)}`\n : `[tool result ${block.name}] ${clip(stringify(block.result), MAX_BLOCK_CHARS)}`;\n default:\n return `[${block._tag}]`;\n }\n });\n return `${(message.sender.role ?? 'unknown').toUpperCase()}:\\n${lines.join('\\n')}`;\n};\n\n/**\n * Splits the history at the turn that just ended: it starts at the last user message, which is what\n * the agent was answering. Hooks fire per request, so everything after it is this turn's work.\n */\nexport const splitTurn = (history: readonly Message.Message[]) => {\n const index = history.findLastIndex((message) => message.sender.role === 'user');\n return index === -1\n ? { context: [] as readonly Message.Message[], turn: history }\n : { context: history.slice(0, index), turn: history.slice(index) };\n};\n\n/** The reviewer's prompt; context is trimmed from the front when the transcript runs long. */\nexport const formatReviewPrompt = (history: readonly Message.Message[]): string => {\n const { context, turn } = splitTurn(history);\n const turnText = turn.map(formatMessage).join('\\n\\n');\n const budget = Math.max(0, MAX_TRANSCRIPT_CHARS - turnText.length);\n const contextText = context.map(formatMessage).join('\\n\\n');\n const clippedContext =\n contextText.length > budget ? `…${contextText.slice(contextText.length - budget)}` : contextText;\n return [clippedContext, '<latest_turn>', clip(turnText, MAX_TRANSCRIPT_CHARS), '</latest_turn>']\n .filter((part) => part.length > 0)\n .join('\\n\\n');\n};\n\nexport const countToolCalls = (messages: readonly Message.Message[]) => {\n let calls = 0;\n let errors = 0;\n for (const message of messages) {\n for (const block of message.blocks) {\n if (block._tag === 'toolCall') {\n calls++;\n } else if (block._tag === 'toolResult' && block.error) {\n errors++;\n }\n }\n }\n return { calls, errors };\n};\n\nexport type TrajectoryHeader = {\n sessionId: string;\n model?: string;\n codeMode: boolean;\n skills: readonly string[];\n verdict: TurnVerdict;\n};\n\n/** A header line then one line per message, in the span serialization the LLM analytics traces use. */\nexport const toTrajectoryNdjson = (header: TrajectoryHeader, history: readonly Message.Message[]): string =>\n [\n JSON.stringify({ type: 'header', ...header }),\n ...history.map((message) =>\n JSON.stringify({ type: 'message', created: message.created, ...(AiSession.serializeMessage(message) as object) }),\n ),\n ].join('\\n');\n\nexport type StruggleEventInput = TrajectoryHeader & {\n history: readonly Message.Message[];\n trajectoryKey?: string;\n};\n\n/**\n * Flat properties so every one is a PostHog breakdown; lists are comma-joined for the same reason.\n * `$ai_session_id` joins the event to the conversation's LLM analytics traces.\n */\nexport const toStruggleEventProperties = ({\n sessionId,\n model,\n codeMode,\n skills,\n verdict,\n history,\n trajectoryKey,\n}: StruggleEventInput): Record<string, unknown> => {\n const { turn } = splitTurn(history);\n const { calls, errors } = countToolCalls(turn);\n return {\n $ai_session_id: sessionId,\n model,\n code_mode: codeMode,\n skills: skills.join(','),\n cause: verdict.cause,\n cause_group: causeGroup(verdict.cause),\n severity: verdict.severity,\n summary: verdict.summary,\n tools: verdict.tools.join(','),\n turn_message_count: turn.length,\n turn_tool_call_count: calls,\n turn_tool_error_count: errors,\n review_model: REVIEW_MODEL,\n trajectory_key: trajectoryKey,\n };\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as LanguageModel from 'effect/ai/LanguageModel';\nimport * as Prompt from 'effect/ai/Prompt';\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\n\nimport { AiService } from '@dxos/ai';\nimport * as Capabilities from '@dxos/app-framework/Capabilities';\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as Harness from '@dxos/assistant/Harness';\nimport * as Operation from '@dxos/compute/Operation';\nimport { Database, Obj } from '@dxos/echo';\nimport { log } from '@dxos/log';\nimport { contentCaptureAllowed } from '@dxos/observability/AiObservability';\nimport * as ObservabilityCapabilities from '@dxos/plugin-observability/ObservabilityCapabilities';\n\nimport { TurnReviewSkill } from '#skills';\nimport { AssistantCapabilities } from '#types';\n\nimport {\n REVIEW_MODEL,\n REVIEW_SYSTEM_PROMPT,\n STRUGGLE_EVENT,\n type TrajectoryHeader,\n TurnVerdict,\n formatReviewPrompt,\n isReportable,\n toStruggleEventProperties,\n toTrajectoryNdjson,\n} from '../review/turn-review.ts';\n\n// Never fails: it runs as a background hook after the user's turn, so a review that cannot complete\n// is only logged.\nconst handler: Operation.WithHandler<typeof TurnReviewSkill.AnalyzeTurn> = TurnReviewSkill.AnalyzeTurn.pipe(\n Operation.withHandler(\n Effect.fnUntraced(\n function* () {\n const settings = Option.getOrUndefined(yield* Capabilities.getAtomValueOption(AssistantCapabilities.Settings));\n const observability = Option.getOrUndefined(\n yield* Capability.getOption(ObservabilityCapabilities.Observability),\n );\n // Both opt-ins are re-read here: the skill stays bound to a chat after the user opts out.\n if (!settings?.reportStruggles || !observability?.enabled) {\n return;\n }\n\n const chat = yield* Harness.getChat;\n const spaceId = Obj.getDatabase(chat)?.spaceId;\n if (!spaceId || !contentCaptureAllowed(spaceId)) {\n return;\n }\n\n const history = yield* Harness.history;\n if (history.length === 0) {\n return;\n }\n\n const { value: verdict } = yield* LanguageModel.generateObject({\n schema: TurnVerdict,\n objectName: 'turn_verdict',\n prompt: Prompt.setSystem(Prompt.make(formatReviewPrompt(history)), REVIEW_SYSTEM_PROMPT),\n }).pipe(Effect.provide(AiService.languageModel(REVIEW_MODEL)));\n log.info('turn reviewed', { chat: chat.id, struggled: verdict.struggled, cause: verdict.cause });\n if (!isReportable(verdict)) {\n return;\n }\n\n const binder = yield* Harness.binder;\n const feed = yield* Database.load(chat.feed);\n const header: TrajectoryHeader = {\n sessionId: Obj.getURI(feed, { prefer: 'absolute' }),\n model: chat.session?.model,\n codeMode: settings.codeMode ?? false,\n skills: binder\n .getSkills()\n .filter((skill) => Obj.getMeta(skill).key !== TurnReviewSkill.key)\n .map((skill) => skill.name),\n verdict,\n };\n const trajectoryKey = yield* Effect.promise(() =>\n observability.support.uploadNdjson(toTrajectoryNdjson(header, history), 'trajectory'),\n );\n observability.events.captureEvent(\n STRUGGLE_EVENT,\n toStruggleEventProperties({ ...header, history, trajectoryKey }),\n );\n },\n Effect.catchCause((cause) => Effect.sync(() => log.warn('turn review failed', { cause }))),\n ),\n ),\n);\n\nexport default handler;\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { a as providerForModel } from "./chunk-presets.mjs";
2
2
  import { AssistantOperation } from "#types";
3
3
  import * as Option from "effect/Option";
4
4
  import * as Atom from "effect/reactivity/Atom";
5
- import { Annotation, Database, Feed, Filter, Obj, Query } from "@dxos/echo";
5
+ import { Annotation, Database, Feed, Filter, Obj, Query, Ref } from "@dxos/echo";
6
6
  import { AiModelNotAvailableError, Model } from "@dxos/ai";
7
7
  import { Harness, ToolExecutionServices, createSystemPrompt, formatSystemPrompt, isConsumed, isInFlight, isQueued } from "@dxos/assistant";
8
8
  import { log } from "@dxos/log";
@@ -16,16 +16,19 @@ import * as Exit from "effect/Exit";
16
16
  import * as Fiber from "effect/Fiber";
17
17
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
18
18
  import * as Stream from "effect/Stream";
19
+ import * as Agent from "@dxos/assistant/Agent";
19
20
  import * as AgentService from "@dxos/compute/AgentService";
20
21
  import * as Operation from "@dxos/compute/Operation";
22
+ import * as Skill from "@dxos/compute/Skill";
21
23
  import * as Trace from "@dxos/compute/Trace";
22
24
  import { UsageQuotaExceededError } from "@dxos/edge-client";
23
25
  import * as EffectEx from "@dxos/effect/EffectEx";
26
+ import { TurnReviewSkill } from "#skills";
24
27
  import * as Array from "effect/Array";
25
28
  import * as Order$1 from "effect/Order";
26
29
  import { DeliveryAnnotation } from "@dxos/react-ui-assistant/types";
27
- //#region src/processor/outbox.ts
28
- var __dxlog_file$1 = "/__w/dxos/dxos/packages/plugins/plugin-assistant/src/processor/outbox.ts";
30
+ //#region src/chat-model/outbox.ts
31
+ var __dxlog_file$1 = "/__w/dxos/dxos/packages/plugins/plugin-assistant/src/chat-model/outbox.ts";
29
32
  /** A dispatch stopped before the prompt reached the agent, by the reader: the prompt is dropped, not failed. */
30
33
  var PromptCancelledError = class extends BaseError.extend("PromptCancelledError", "Prompt cancelled before it was sent.") {};
31
34
  /**
@@ -117,7 +120,7 @@ var findInCause = (error, predicate) => {
117
120
  }
118
121
  };
119
122
  //#endregion
120
- //#region src/processor/prompt.ts
123
+ //#region src/chat-model/prompt.ts
121
124
  /**
122
125
  * Prompt content for a request: the bare message, or — when the request carries selection context —
123
126
  * a synthetic block (system-generated user-turn content, hidden from the summary view) ahead of it.
@@ -142,7 +145,7 @@ var createPromptContent = (request) => {
142
145
  }), ContentBlock.Text.make({ text: request.message })];
143
146
  };
144
147
  //#endregion
145
- //#region src/processor/thread.ts
148
+ //#region src/chat-model/thread.ts
146
149
  /**
147
150
  * Append order for {@link Feed.history}, which walks lineage positionally rather than by time.
148
151
  *
@@ -373,8 +376,8 @@ var resolveRewind = (messages, messageId) => {
373
376
  };
374
377
  };
375
378
  //#endregion
376
- //#region src/processor/processor.ts
377
- var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-assistant/src/processor/processor.ts";
379
+ //#region src/chat-model/chat-model.ts
380
+ var __dxlog_file = "/__w/dxos/dxos/packages/plugins/plugin-assistant/src/chat-model/chat-model.ts";
378
381
  var defaultOptions = { model: Model.DEFAULT_EDGE };
379
382
  /** User-facing message shown when an AI request is rejected for exceeding the account usage quota (HTTP 429). */
380
383
  var QUOTA_EXCEEDED_MESSAGE = "You have reached your AI usage limit for this period.";
@@ -410,6 +413,20 @@ var describeAiError = (err) => ("description" in err.reason ? err.reason.descrip
410
413
  * which drops nested causes — so detection also reads the message it stringifies to.
411
414
  */
412
415
  var MODEL_UNAVAILABLE_PATTERN = /AI Model not available:\s*(\S+?):?(?=\s|$)/i;
416
+ /**
417
+ * Matches the first line of a stringified {@link AiError.AiError} ("…AiError: Module.method: detail"), the
418
+ * form a provider failure takes once it has crossed the agent-process boundary.
419
+ */
420
+ var AI_ERROR_PATTERN = /AiError: [\w$.]+: ([^\n]+)/;
421
+ /** The provider's own description of a failure that arrived stringified, anywhere in the cause chain. */
422
+ var stringifiedAiErrorText = (err) => {
423
+ const visited = /* @__PURE__ */ new Set();
424
+ for (let current = err; current && !visited.has(current); current = current instanceof Error ? current.cause : void 0) {
425
+ visited.add(current);
426
+ const detail = (typeof current === "string" ? current : current instanceof Error ? current.message : "").match(AI_ERROR_PATTERN)?.[1]?.trim();
427
+ if (detail) return detail;
428
+ }
429
+ };
413
430
  /** The displayable text of a failure, which reaches the chat either typed or already stringified. */
414
431
  var errorText = (err) => typeof err === "string" ? err : err instanceof Error ? err.message : "";
415
432
  /** The model of a model-unavailable rejection, by typed context or message text. */
@@ -441,6 +458,7 @@ var parseError = (err) => {
441
458
  const [, model] = err.match(/model\s+'([^']+)'\s+not\s+found/i) || [];
442
459
  if (model) message = `The model is not available: ${model}`;
443
460
  }
461
+ if (!message) message = stringifiedAiErrorText(err);
444
462
  if (!message) message = "An unexpected error occurred.";
445
463
  return new Error(message, { cause: err });
446
464
  };
@@ -448,7 +466,7 @@ var parseError = (err) => {
448
466
  * Handles interactions with the AI service.
449
467
  * Uses AgentService to spawn a process-backed agent and subscribes to ephemeral trace events for streaming.
450
468
  */
451
- var AiChatProcessor = class {
469
+ var ChatModel = class {
452
470
  _conversation;
453
471
  _runtime;
454
472
  _feed;
@@ -461,9 +479,12 @@ var AiChatProcessor = class {
461
479
  #streaming = Atom.make([]);
462
480
  /** Set of message IDs that have been finalized (non-pending delivered via ephemeral). */
463
481
  #finalizedIds = /* @__PURE__ */ new Set();
482
+ /** Timestamp of the newest trace event applied this turn, overall and per streamed message. */
483
+ #appliedAt = 0;
484
+ #appliedAtByMessage = /* @__PURE__ */ new Map();
464
485
  /** Currently active request fiber. */
465
486
  #requestFiber;
466
- /** Fiber following a turn this processor did not issue ({@link adopt}). */
487
+ /** Fiber following a turn this chat model did not issue ({@link adopt}). */
467
488
  #observeFiber;
468
489
  /** Last request (for retries). */
469
490
  #lastRequest;
@@ -625,6 +646,7 @@ var AiChatProcessor = class {
625
646
  this.#registry.set(this.mcpErrors, []);
626
647
  this.#registry.set(this.activity, { phase: "starting" });
627
648
  this.#registry.set(this.active, true);
649
+ await this.#ensureTurnReview();
628
650
  const effect = Effect.gen({ self: this }, function* () {
629
651
  log.info("init agent session", {
630
652
  feed: Obj.getURI(this._feed),
@@ -633,25 +655,25 @@ var AiChatProcessor = class {
633
655
  }, {
634
656
  "~LogMeta": "~LogMeta",
635
657
  F: __dxlog_file,
636
- L: 439,
658
+ L: 478,
637
659
  S: this
638
660
  });
639
661
  const session = yield* this.#getSession();
640
662
  markWork("chat.session-ready");
641
- yield* this.#forkEphemeralCollector(session);
642
- log("chat processor submitting prompt", { length: requestProp.message.length }, {
663
+ yield* this.#forkEphemeralCollector(session, { replay: false });
664
+ log("chat model submitting prompt", { length: requestProp.message.length }, {
643
665
  "~LogMeta": "~LogMeta",
644
666
  F: __dxlog_file,
645
- L: 448,
667
+ L: 489,
646
668
  S: this
647
669
  });
648
670
  yield* session.submitPrompt(createPromptContent(requestProp), { sender: this._options.sender });
649
671
  markWork("chat.prompt-submitted");
650
672
  submission?.resolve();
651
- log("chat processor submitPrompt returned, waiting for agent", {}, {
673
+ log("chat model submitPrompt returned, waiting for agent", {}, {
652
674
  "~LogMeta": "~LogMeta",
653
675
  F: __dxlog_file,
654
- L: 452,
676
+ L: 493,
655
677
  S: this
656
678
  });
657
679
  if (!this._options.chat?.target?.name && requestProp.disposition !== "synthetic") yield* this.#updateChatName(requestProp.message);
@@ -659,7 +681,7 @@ var AiChatProcessor = class {
659
681
  log.info("session complete", void 0, {
660
682
  "~LogMeta": "~LogMeta",
661
683
  F: __dxlog_file,
662
- L: 462,
684
+ L: 503,
663
685
  S: this
664
686
  });
665
687
  this.#flushStreaming();
@@ -681,7 +703,7 @@ var AiChatProcessor = class {
681
703
  log.error("request failed", { error: err }, {
682
704
  "~LogMeta": "~LogMeta",
683
705
  F: __dxlog_file,
684
- L: 489,
706
+ L: 530,
685
707
  S: this
686
708
  });
687
709
  submission?.reject(err);
@@ -690,7 +712,7 @@ var AiChatProcessor = class {
690
712
  log.info("setting active to false", void 0, {
691
713
  "~LogMeta": "~LogMeta",
692
714
  F: __dxlog_file,
693
- L: 494,
715
+ L: 535,
694
716
  S: this
695
717
  });
696
718
  this.#registry.set(this.active, false);
@@ -714,7 +736,7 @@ var AiChatProcessor = class {
714
736
  log.error("enqueue failed", { error: err }, {
715
737
  "~LogMeta": "~LogMeta",
716
738
  F: __dxlog_file,
717
- L: 514,
739
+ L: 555,
718
740
  S: this
719
741
  });
720
742
  this.#registry.set(this.error, Option.some(parseError(err)));
@@ -727,8 +749,8 @@ var AiChatProcessor = class {
727
749
  }).pipe(Effect.provide(this._spaceLayer)));
728
750
  }
729
751
  /**
730
- * Mirrors turns this processor did not initiate into its own state: active/streaming state is
731
- * per-processor ({@link useChatProcessor} builds one per mount) while the agent process outlives
752
+ * Mirrors turns this chat model did not initiate into its own state: active/streaming state is
753
+ * per-chat model ({@link useChatModel} builds one per mount) while the agent process outlives
732
754
  * the mount, so a chat remounted mid-turn would otherwise render as idle.
733
755
  *
734
756
  * Returns a disposer that stops observing.
@@ -744,7 +766,7 @@ var AiChatProcessor = class {
744
766
  }).catch((err) => log.warn("failed to attach to agent session", { error: err }, {
745
767
  "~LogMeta": "~LogMeta",
746
768
  F: __dxlog_file,
747
- L: 557,
769
+ L: 598,
748
770
  S: this
749
771
  }));
750
772
  return () => {
@@ -759,20 +781,21 @@ var AiChatProcessor = class {
759
781
  }
760
782
  /**
761
783
  * Follows a turn started elsewhere to completion, surfacing its streamed blocks here.
762
- * A turn this processor issued is owned by {@link request}, which reports its own errors.
784
+ * A turn this chat model issued is owned by {@link request}, which reports its own errors.
763
785
  */
764
786
  async #observe(session) {
765
787
  if (this.#requestFiber || this.#observeFiber || this.#registry.get(this.active)) return;
766
788
  log.info("observing agent turn", { feed: Obj.getURI(this._feed) }, {
767
789
  "~LogMeta": "~LogMeta",
768
790
  F: __dxlog_file,
769
- L: 581,
791
+ L: 622,
770
792
  S: this
771
793
  });
772
794
  try {
795
+ this.#registry.set(this.activity, { phase: "starting" });
773
796
  this.#registry.set(this.active, true);
774
797
  const effect = Effect.gen({ self: this }, function* () {
775
- yield* this.#forkEphemeralCollector(session);
798
+ yield* this.#forkEphemeralCollector(session, { replay: true });
776
799
  yield* session.waitForCompletion();
777
800
  this.#flushStreaming();
778
801
  });
@@ -783,7 +806,7 @@ var AiChatProcessor = class {
783
806
  log.warn("failed to observe agent turn", { error: err }, {
784
807
  "~LogMeta": "~LogMeta",
785
808
  F: __dxlog_file,
786
- L: 599,
809
+ L: 643,
787
810
  S: this
788
811
  });
789
812
  } finally {
@@ -799,7 +822,7 @@ var AiChatProcessor = class {
799
822
  #getSession() {
800
823
  return Effect.gen({ self: this }, function* () {
801
824
  const chat = this._options.chat?.target;
802
- if (!chat) return yield* Effect.die(/* @__PURE__ */ new Error("Chat processor requires a chat."));
825
+ if (!chat) return yield* Effect.die(/* @__PURE__ */ new Error("Chat model requires a chat."));
803
826
  const selected = this._options.model;
804
827
  if (!chat.session?.model && selected) Obj.update(chat, (chat) => {
805
828
  chat.session = {
@@ -810,7 +833,7 @@ var AiChatProcessor = class {
810
833
  const model = chat.session?.model ?? selected;
811
834
  return yield* AgentService.getSession(chat, {
812
835
  provider: model ? providerForModel(model, this._options.provider) : this._options.provider,
813
- location: chat.remote ? "edge" : "local"
836
+ location: Agent.chatLocation(chat)
814
837
  });
815
838
  });
816
839
  }
@@ -818,11 +841,16 @@ var AiChatProcessor = class {
818
841
  * Forks the collector for the session's ephemeral trace events (streaming blocks and MCP
819
842
  * failures) as a child of the calling fiber.
820
843
  */
821
- #forkEphemeralCollector(session) {
822
- return session.subscribeEphemeral().pipe(Stream.runForEach((message) => Effect.sync(() => {
823
- for (const event of message.events) if (Trace.isOfType(Trace.PartialBlock, event)) this.#handleEphemeralMessage(event.data);
824
- else if (Trace.isOfType(Trace.RequestPhase, event)) this.#registry.set(this.activity, event.data);
825
- else if (Trace.isOfType(Trace.McpServerError, event)) this.#handleMcpError(event.data);
844
+ #forkEphemeralCollector(session, options) {
845
+ return session.subscribeEphemeral(options).pipe(Stream.runForEach((message) => Effect.sync(() => {
846
+ for (const event of message.events) if (Trace.isOfType(Trace.PartialBlock, event)) {
847
+ const current = event.timestamp >= this.#appliedAt;
848
+ if (this.#isStale(event.timestamp, event.data.messageId)) continue;
849
+ this.#handleEphemeralMessage(event.data, current);
850
+ } else if (Trace.isOfType(Trace.RequestPhase, event)) {
851
+ if (this.#isStale(event.timestamp)) continue;
852
+ this.#registry.set(this.activity, event.data);
853
+ } else if (Trace.isOfType(Trace.McpServerError, event)) this.#handleMcpError(event.data);
826
854
  })), Effect.forkChild, Effect.asVoid);
827
855
  }
828
856
  /**
@@ -833,7 +861,7 @@ var AiChatProcessor = class {
833
861
  log.info("cancelling request", { fiber: this.#requestFiber }, {
834
862
  "~LogMeta": "~LogMeta",
835
863
  F: __dxlog_file,
836
- L: 665,
864
+ L: 723,
837
865
  S: this
838
866
  });
839
867
  if (this.#requestFiber) yield* Fiber.interrupt(this.#requestFiber);
@@ -864,13 +892,23 @@ var AiChatProcessor = class {
864
892
  EffectEx.unwrapExit(await this._runtime.runPromiseExit(Operation.invoke(AssistantOperation.UpdateChatName, { chat }, { spaceId }).pipe(Effect.provide(this._spaceLayer))));
865
893
  }
866
894
  /**
895
+ * Whether a trace event is older than one already applied (per message for a streamed block, whose
896
+ * text is the whole reply so far), recording it as the newest when it is not.
897
+ */
898
+ #isStale(timestamp, messageId) {
899
+ if (timestamp < (messageId === void 0 ? this.#appliedAt : this.#appliedAtByMessage.get(messageId) ?? 0)) return true;
900
+ this.#appliedAt = Math.max(this.#appliedAt, timestamp);
901
+ if (messageId !== void 0) this.#appliedAtByMessage.set(messageId, timestamp);
902
+ return false;
903
+ }
904
+ /**
867
905
  * Handles an ephemeral message from the agent process.
868
906
  * Both pending and completed blocks arrive here. Completed blocks are deduped
869
907
  * against messages already written to the feed queue to handle the race between
870
908
  * ephemeral delivery and feed replication.
871
909
  */
872
- #handleEphemeralMessage(event) {
873
- this.#registry.set(this.activity, { phase: "generating" });
910
+ #handleEphemeralMessage(event, current = true) {
911
+ if (current) this.#registry.set(this.activity, { phase: "generating" });
874
912
  const isPending = event.block.pending;
875
913
  const message = Obj.make(Message.Message, {
876
914
  id: event.messageId,
@@ -906,7 +944,7 @@ var AiChatProcessor = class {
906
944
  log.warn("MCP server error", event, {
907
945
  "~LogMeta": "~LogMeta",
908
946
  F: __dxlog_file,
909
- L: 765,
947
+ L: 841,
910
948
  S: this
911
949
  });
912
950
  this.#registry.update(this.mcpErrors, (errors) => {
@@ -921,18 +959,32 @@ var AiChatProcessor = class {
921
959
  this.#registry.set(this.#streaming, []);
922
960
  this.#registry.set(this.activity, void 0);
923
961
  this.#finalizedIds.clear();
962
+ this.#resetApplied();
924
963
  }
925
964
  /**
926
965
  * Move remaining streaming messages to pending (called when agent completes).
927
966
  */
928
967
  #flushStreaming() {
929
- this.#registry.set(this.activity, void 0);
930
968
  const remaining = this.#registry.get(this.#streaming);
931
969
  if (remaining.length > 0) {
932
970
  this.#registry.update(this.#pending, (pending) => [...pending, ...remaining]);
933
971
  this.#registry.set(this.#streaming, []);
934
972
  }
935
973
  this.#finalizedIds.clear();
974
+ this.#resetApplied();
975
+ }
976
+ #resetApplied() {
977
+ this.#appliedAt = 0;
978
+ this.#appliedAtByMessage.clear();
979
+ }
980
+ /**
981
+ * Binds the turn-review skill before a request while the user is opted in; its background
982
+ * end-request hook does the review, so nothing here waits on it. Left bound after an opt-out,
983
+ * since the hook re-checks the setting.
984
+ */
985
+ async #ensureTurnReview() {
986
+ if (!this._options.reportStruggles) return;
987
+ if (!this.context.getSkills().some((skill) => Obj.getMeta(skill).key === TurnReviewSkill.key)) await this.context.bind({ skills: [Ref.fromURI(Skill.registryURI(TurnReviewSkill.key))] });
936
988
  }
937
989
  /**
938
990
  * Schedule a chat name update as a detached (fire-and-forget) operation.
@@ -946,7 +998,7 @@ var AiChatProcessor = class {
946
998
  log.info("scheduling chat name update", void 0, {
947
999
  "~LogMeta": "~LogMeta",
948
1000
  F: __dxlog_file,
949
- L: 811,
1001
+ L: 910,
950
1002
  S: this
951
1003
  });
952
1004
  return Operation.schedule(AssistantOperation.UpdateChatName, {
@@ -955,7 +1007,7 @@ var AiChatProcessor = class {
955
1007
  }, { spaceId });
956
1008
  }
957
1009
  };
958
- var idleProcessorState = {
1010
+ var idleChatModelState = {
959
1011
  streaming: Atom.make(false),
960
1012
  active: Atom.make(false),
961
1013
  thread: Atom.make(EMPTY_THREAD),
@@ -963,8 +1015,8 @@ var idleProcessorState = {
963
1015
  mcpErrors: Atom.make([]),
964
1016
  activity: Atom.make(void 0)
965
1017
  };
966
- var getProcessorState = (processor) => processor ?? idleProcessorState;
1018
+ var getChatModelState = (chatModel) => chatModel ?? idleChatModelState;
967
1019
  //#endregion
968
- export { projectSelfWakes as a, projectAlarms as i, AiUsageQuotaError as n, resolveRewind as o, getProcessorState as r, AiChatProcessor as t };
1020
+ export { projectSelfWakes as a, projectAlarms as i, ChatModel as n, resolveRewind as o, getChatModelState as r, AiUsageQuotaError as t };
969
1021
 
970
- //# sourceMappingURL=chunk-processor.mjs.map
1022
+ //# sourceMappingURL=chunk-chat-model.mjs.map