@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.
- package/PLUGIN.mdl +25 -0
- package/dist/lib/AssistantCapabilities.mjs +8 -1
- package/dist/lib/AssistantCapabilities.mjs.map +1 -1
- package/dist/lib/Settings.mjs +4 -0
- package/dist/lib/Settings.mjs.map +1 -1
- package/dist/lib/TurnReviewSkill.mjs +55 -0
- package/dist/lib/TurnReviewSkill.mjs.map +1 -0
- package/dist/lib/chunk-AgentArticle.mjs +1 -1
- package/dist/lib/chunk-AssistantSettings.mjs +1 -1
- package/dist/lib/chunk-AssistantSettings.mjs.map +1 -1
- package/dist/lib/chunk-Chat.mjs +322 -174
- package/dist/lib/chunk-Chat.mjs.map +1 -1
- package/dist/lib/chunk-ChatArticle.mjs +14 -7
- package/dist/lib/chunk-ChatArticle.mjs.map +1 -1
- package/dist/lib/chunk-ChatDialog.mjs +5 -4
- package/dist/lib/chunk-ChatDialog.mjs.map +1 -1
- package/dist/lib/chunk-IntegrationPrompt.mjs +1 -1
- package/dist/lib/chunk-PLUGIN.mjs +1 -1
- package/dist/lib/chunk-PluginUrlPrompt.mjs +1 -1
- package/dist/lib/chunk-QuestionSurface.mjs +1 -1
- package/dist/lib/chunk-SpaceHomePrompt.mjs +116 -60
- package/dist/lib/chunk-SpaceHomePrompt.mjs.map +1 -1
- package/dist/lib/chunk-TriggerStatus.mjs +1 -1
- package/dist/lib/chunk-agent-service.mjs +3 -1
- package/dist/lib/chunk-agent-service.mjs.map +1 -1
- package/dist/lib/chunk-analyze-turn.mjs +220 -0
- package/dist/lib/chunk-analyze-turn.mjs.map +1 -0
- package/dist/lib/{chunk-processor.mjs → chunk-chat-model.mjs} +95 -43
- package/dist/lib/chunk-chat-model.mjs.map +1 -0
- package/dist/lib/chunk-context.mjs.map +1 -1
- package/dist/lib/chunk-create-chat.mjs +5 -19
- package/dist/lib/chunk-create-chat.mjs.map +1 -1
- package/dist/lib/chunk-create-object.mjs +1 -1
- package/dist/lib/chunk-default-skills.mjs +28 -0
- package/dist/lib/chunk-default-skills.mjs.map +1 -0
- package/dist/lib/chunk-generate-home-suggestions.mjs +8 -6
- package/dist/lib/chunk-generate-home-suggestions.mjs.map +1 -1
- package/dist/lib/chunk-presets.mjs +1 -1
- package/dist/lib/chunk-presets.mjs.map +1 -1
- package/dist/lib/chunk-question-resumer.mjs +1 -1
- package/dist/lib/chunk-react-surface.mjs +1 -1
- package/dist/lib/chunk-run-prompt-in-chat.mjs +2 -1
- package/dist/lib/chunk-run-prompt-in-chat.mjs.map +1 -1
- package/dist/lib/chunk-run-prompt-in-new-chat.mjs +1 -1
- package/dist/lib/chunk-schema-defs.mjs +2 -2
- package/dist/lib/chunk-skill-definition.mjs +3 -2
- package/dist/lib/chunk-skill-definition.mjs.map +1 -1
- package/dist/lib/components.mjs +13 -13
- package/dist/lib/components.mjs.map +1 -1
- package/dist/lib/hooks.mjs +40 -31
- package/dist/lib/hooks.mjs.map +1 -1
- package/dist/lib/operations.mjs +3 -1
- package/dist/lib/operations.mjs.map +1 -1
- package/dist/lib/skills.mjs +2 -1
- package/dist/lib/translations.mjs +4 -2
- package/dist/lib/translations.mjs.map +1 -1
- package/dist/types/src/Chat.d.ts +1 -1
- package/dist/types/src/capabilities/agent-service.d.ts.map +1 -1
- package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -1
- package/dist/types/src/{processor/processor.d.ts → chat-model/chat-model.d.ts} +24 -22
- package/dist/types/src/chat-model/chat-model.d.ts.map +1 -0
- package/dist/types/src/chat-model/chat-model.edge.test.d.ts +2 -0
- package/dist/types/src/chat-model/chat-model.edge.test.d.ts.map +1 -0
- package/dist/types/src/chat-model/chat-model.node.test.d.ts +2 -0
- package/dist/types/src/chat-model/chat-model.node.test.d.ts.map +1 -0
- package/dist/types/src/chat-model/chat-model.scripted.node.test.d.ts +2 -0
- package/dist/types/src/chat-model/chat-model.scripted.node.test.d.ts.map +1 -0
- package/dist/types/src/{processor → chat-model}/index.d.ts +1 -1
- package/dist/types/src/chat-model/index.d.ts.map +1 -0
- package/dist/types/src/chat-model/outbox.d.ts.map +1 -0
- package/dist/types/src/chat-model/outbox.test.d.ts.map +1 -0
- package/dist/types/src/chat-model/presets.d.ts.map +1 -0
- package/dist/types/src/chat-model/presets.test.d.ts.map +1 -0
- package/dist/types/src/{processor → chat-model}/prompt.d.ts +2 -2
- package/dist/types/src/chat-model/prompt.d.ts.map +1 -0
- package/dist/types/src/chat-model/prompt.test.d.ts.map +1 -0
- package/dist/types/src/chat-model/streaming.node.test.d.ts.map +1 -0
- package/dist/types/src/chat-model/testing/chat-model-recorder.d.ts +58 -0
- package/dist/types/src/chat-model/testing/chat-model-recorder.d.ts.map +1 -0
- package/dist/types/src/chat-model/testing/harness.d.ts +22 -0
- package/dist/types/src/chat-model/testing/harness.d.ts.map +1 -0
- package/dist/types/src/chat-model/thread.d.ts.map +1 -0
- package/dist/types/src/chat-model/thread.test.d.ts.map +1 -0
- package/dist/types/src/components/Chat/Chat.d.ts +6 -6
- package/dist/types/src/components/Chat/Chat.d.ts.map +1 -1
- package/dist/types/src/components/Chat/context.d.ts +5 -3
- package/dist/types/src/components/Chat/context.d.ts.map +1 -1
- package/dist/types/src/components/ChatPrompt/ChatActions.d.ts +1 -1
- package/dist/types/src/components/ChatPrompt/ChatActions.d.ts.map +1 -1
- package/dist/types/src/components/ChatPrompt/ChatMcpErrors.d.ts +3 -3
- package/dist/types/src/components/ChatPrompt/ChatMcpErrors.d.ts.map +1 -1
- package/dist/types/src/components/ChatPrompt/ChatOptions.d.ts +3 -1
- package/dist/types/src/components/ChatPrompt/ChatOptions.d.ts.map +1 -1
- package/dist/types/src/components/ChatPrompt/ChatOptions.stories.d.ts +4 -3
- package/dist/types/src/components/ChatPrompt/ChatOptions.stories.d.ts.map +1 -1
- package/dist/types/src/components/ChatPrompt/ChatPrompt.d.ts +15 -5
- package/dist/types/src/components/ChatPrompt/ChatPrompt.d.ts.map +1 -1
- package/dist/types/src/components/ChatPrompt/ChatPrompt.stories.d.ts +1 -1
- package/dist/types/src/components/ChatPrompt/ChatStatus.d.ts +1 -1
- package/dist/types/src/components/ChatPrompt/ChatStatus.d.ts.map +1 -1
- package/dist/types/src/components/ConnectorAuthMenu/ConnectorAuthMenu.stories.d.ts +9 -0
- package/dist/types/src/components/ConnectorAuthMenu/ConnectorAuthMenu.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbox/Toolbox.d.ts +3 -3
- package/dist/types/src/components/Toolbox/Toolbox.d.ts.map +1 -1
- package/dist/types/src/containers/ChatArticle/ChatArticle.d.ts.map +1 -1
- package/dist/types/src/containers/ChatArticle/ChatArticle.stories.d.ts +1 -1
- package/dist/types/src/containers/SpaceHomePrompt/SpaceHomePrompt.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/{useChatProcessor.d.ts → useChatModel.d.ts} +6 -6
- package/dist/types/src/hooks/useChatModel.d.ts.map +1 -0
- package/dist/types/src/hooks/useDebug.d.ts +3 -3
- package/dist/types/src/hooks/useDebug.d.ts.map +1 -1
- package/dist/types/src/hooks/useHomeSuggestions.d.ts +3 -3
- package/dist/types/src/hooks/useHomeSuggestions.d.ts.map +1 -1
- package/dist/types/src/hooks/usePresets.d.ts +1 -1
- package/dist/types/src/hooks/usePresets.d.ts.map +1 -1
- package/dist/types/src/hooks/useSelectionContext.d.ts +3 -3
- package/dist/types/src/hooks/useSelectionContext.d.ts.map +1 -1
- package/dist/types/src/operations/analyze-turn.d.ts +5 -0
- package/dist/types/src/operations/analyze-turn.d.ts.map +1 -0
- package/dist/types/src/operations/analyze-turn.test.d.ts +2 -0
- package/dist/types/src/operations/analyze-turn.test.d.ts.map +1 -0
- package/dist/types/src/operations/create-chat.d.ts.map +1 -1
- package/dist/types/src/operations/generate-home-suggestions.d.ts.map +1 -1
- package/dist/types/src/operations/index.d.ts.map +1 -1
- package/dist/types/src/operations/run-prompt-in-chat.d.ts.map +1 -1
- package/dist/types/src/review/turn-review.d.ts +58 -0
- package/dist/types/src/review/turn-review.d.ts.map +1 -0
- package/dist/types/src/review/turn-review.test.d.ts +2 -0
- package/dist/types/src/review/turn-review.test.d.ts.map +1 -0
- package/dist/types/src/skills/index.d.ts +1 -0
- package/dist/types/src/skills/index.d.ts.map +1 -1
- package/dist/types/src/skills/turn-review/TurnReviewSkill.d.ts +21 -0
- package/dist/types/src/skills/turn-review/TurnReviewSkill.d.ts.map +1 -0
- package/dist/types/src/skills/turn-review/index.d.ts +2 -0
- package/dist/types/src/skills/turn-review/index.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Assistant.d.ts +1 -0
- package/dist/types/src/types/Assistant.d.ts.map +1 -1
- package/dist/types/src/types/AssistantCapabilities.d.ts +7 -1
- package/dist/types/src/types/AssistantCapabilities.d.ts.map +1 -1
- package/dist/types/src/types/Settings.d.ts +1 -0
- package/dist/types/src/types/Settings.d.ts.map +1 -1
- package/dist/types/src/util/agent-options.d.ts +27 -0
- package/dist/types/src/util/agent-options.d.ts.map +1 -0
- package/dist/types/src/util/agent-options.test.d.ts +2 -0
- package/dist/types/src/util/agent-options.test.d.ts.map +1 -0
- package/dist/types/src/util/default-skills.d.ts +10 -0
- package/dist/types/src/util/default-skills.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +89 -80
- package/src/capabilities/agent-service.ts +2 -1
- package/src/capabilities/skill-definition.ts +2 -1
- package/src/chat-model/chat-model.edge.test.ts +301 -0
- package/src/{processor/processor.node.test.ts → chat-model/chat-model.node.test.ts} +19 -9
- package/src/chat-model/chat-model.scripted.node.test.ts +441 -0
- package/src/{processor/processor.ts → chat-model/chat-model.ts} +140 -45
- package/src/{processor → chat-model}/index.ts +1 -1
- package/src/{processor → chat-model}/prompt.ts +2 -2
- package/src/{processor → chat-model}/streaming.node.test.ts +108 -116
- package/src/chat-model/testing/chat-model-recorder.ts +249 -0
- package/src/chat-model/testing/harness.ts +81 -0
- package/src/{processor → chat-model}/thread.test.ts +1 -1
- package/src/components/Chat/Chat.tsx +44 -38
- package/src/components/Chat/Error.stories.tsx +1 -1
- package/src/components/Chat/context.ts +5 -3
- package/src/components/ChatPrompt/ChatActions.tsx +1 -1
- package/src/components/ChatPrompt/ChatMcpErrors.tsx +6 -6
- package/src/components/ChatPrompt/ChatOptions.stories.tsx +37 -3
- package/src/components/ChatPrompt/ChatOptions.tsx +118 -10
- package/src/components/ChatPrompt/ChatPrompt.stories.tsx +10 -10
- package/src/components/ChatPrompt/ChatPrompt.tsx +44 -27
- package/src/components/ChatPrompt/ChatStatus.tsx +8 -7
- package/src/components/Toolbox/Toolbox.tsx +12 -13
- package/src/containers/AssistantSettings/AssistantSettings.tsx +1 -1
- package/src/containers/ChatArticle/ChatArticle.stories.tsx +1 -1
- package/src/containers/ChatArticle/ChatArticle.tsx +16 -6
- package/src/containers/ChatDialog/ChatDialog.tsx +4 -4
- package/src/containers/SpaceHomePrompt/SpaceHomePrompt.tsx +110 -57
- package/src/hooks/index.ts +2 -2
- package/src/hooks/{useChatProcessor.ts → useChatModel.ts} +16 -14
- package/src/hooks/useDebug.ts +8 -8
- package/src/hooks/useHomeSuggestions.ts +14 -9
- package/src/hooks/usePresets.ts +2 -2
- package/src/hooks/useProcessEphemeralStatus.ts +1 -1
- package/src/hooks/useSelectionContext.ts +3 -5
- package/src/operations/analyze-turn.test.ts +194 -0
- package/src/operations/analyze-turn.ts +96 -0
- package/src/operations/create-chat.ts +4 -36
- package/src/operations/generate-home-suggestions.test.ts +1 -2
- package/src/operations/generate-home-suggestions.ts +17 -9
- package/src/operations/index.ts +2 -0
- package/src/operations/run-prompt-in-chat.ts +3 -2
- package/src/review/turn-review.test.ts +92 -0
- package/src/review/turn-review.ts +213 -0
- package/src/skills/index.ts +1 -0
- package/src/skills/turn-review/TurnReviewSkill.ts +46 -0
- package/src/skills/turn-review/index.ts +5 -0
- package/src/translations.ts +2 -0
- package/src/types/AssistantCapabilities.ts +8 -1
- package/src/types/Settings.ts +7 -0
- package/src/util/agent-options.test.ts +54 -0
- package/src/util/agent-options.ts +60 -0
- package/src/util/default-skills.ts +35 -0
- package/dist/lib/chunk-processor.mjs.map +0 -1
- package/dist/types/src/hooks/useChatProcessor.d.ts.map +0 -1
- package/dist/types/src/processor/index.d.ts.map +0 -1
- package/dist/types/src/processor/outbox.d.ts.map +0 -1
- package/dist/types/src/processor/outbox.test.d.ts.map +0 -1
- package/dist/types/src/processor/presets.d.ts.map +0 -1
- package/dist/types/src/processor/presets.test.d.ts.map +0 -1
- package/dist/types/src/processor/processor.d.ts.map +0 -1
- package/dist/types/src/processor/processor.node.test.d.ts +0 -2
- package/dist/types/src/processor/processor.node.test.d.ts.map +0 -1
- package/dist/types/src/processor/prompt.d.ts.map +0 -1
- package/dist/types/src/processor/prompt.test.d.ts.map +0 -1
- package/dist/types/src/processor/streaming.node.test.d.ts.map +0 -1
- package/dist/types/src/processor/thread.d.ts.map +0 -1
- package/dist/types/src/processor/thread.test.d.ts.map +0 -1
- /package/dist/types/src/{processor → chat-model}/outbox.d.ts +0 -0
- /package/dist/types/src/{processor → chat-model}/outbox.test.d.ts +0 -0
- /package/dist/types/src/{processor → chat-model}/presets.d.ts +0 -0
- /package/dist/types/src/{processor → chat-model}/presets.test.d.ts +0 -0
- /package/dist/types/src/{processor → chat-model}/prompt.test.d.ts +0 -0
- /package/dist/types/src/{processor → chat-model}/streaming.node.test.d.ts +0 -0
- /package/dist/types/src/{processor → chat-model}/thread.d.ts +0 -0
- /package/dist/types/src/{processor → chat-model}/thread.test.d.ts +0 -0
- /package/src/{processor → chat-model}/outbox.test.ts +0 -0
- /package/src/{processor → chat-model}/outbox.ts +0 -0
- /package/src/{processor → chat-model}/presets.test.ts +0 -0
- /package/src/{processor → chat-model}/presets.ts +0 -0
- /package/src/{processor → chat-model}/prompt.test.ts +0 -0
- /package/src/{processor → chat-model}/thread.ts +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from '@effect/vitest';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Function from 'effect/Function';
|
|
8
|
+
import * as Layer from 'effect/Layer';
|
|
9
|
+
import * as Atom from 'effect/reactivity/Atom';
|
|
10
|
+
import * as AtomRegistry from 'effect/reactivity/AtomRegistry';
|
|
11
|
+
|
|
12
|
+
import { AgentService } from '@dxos/agent-runtime';
|
|
13
|
+
import { AssistantTestLayer } from '@dxos/agent-runtime/testing';
|
|
14
|
+
import { ScriptedLanguageModel } from '@dxos/ai/testing';
|
|
15
|
+
import * as Capabilities from '@dxos/app-framework/Capabilities';
|
|
16
|
+
import * as Capability from '@dxos/app-framework/Capability';
|
|
17
|
+
import * as CapabilityManager from '@dxos/app-framework/CapabilityManager';
|
|
18
|
+
import * as Chat from '@dxos/assistant/Chat';
|
|
19
|
+
import { ProcessManager } from '@dxos/compute-runtime';
|
|
20
|
+
import * as Process from '@dxos/compute/Process';
|
|
21
|
+
import * as Skill from '@dxos/compute/Skill';
|
|
22
|
+
import { Feed, Obj } from '@dxos/echo';
|
|
23
|
+
import { TestHelpers } from '@dxos/effect/testing';
|
|
24
|
+
import { DXN, EntityId } from '@dxos/keys';
|
|
25
|
+
import * as Observability from '@dxos/observability/Observability';
|
|
26
|
+
import * as ObservabilityCapabilities from '@dxos/plugin-observability/ObservabilityCapabilities';
|
|
27
|
+
import { Message } from '@dxos/types';
|
|
28
|
+
|
|
29
|
+
import { AssistantOperationHandlerSet } from '#operations';
|
|
30
|
+
import { TurnReviewSkill } from '#skills';
|
|
31
|
+
import { type Assistant, AssistantCapabilities } from '#types';
|
|
32
|
+
|
|
33
|
+
import { REVIEW_SYSTEM_PROMPT, STRUGGLE_EVENT, type TurnVerdict } from '../review/turn-review.ts';
|
|
34
|
+
|
|
35
|
+
EntityId.dangerouslyDisableRandomness();
|
|
36
|
+
|
|
37
|
+
const VERDICT: TurnVerdict = {
|
|
38
|
+
struggled: true,
|
|
39
|
+
cause: 'tool_faulty',
|
|
40
|
+
severity: 'medium',
|
|
41
|
+
summary: 'The search tool rejected a valid query.',
|
|
42
|
+
tools: ['search'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type Captured = {
|
|
46
|
+
events: { event: string; attributes?: Record<string, unknown> }[];
|
|
47
|
+
uploads: { ndjson: string; kind: string }[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let captured: Captured = { events: [], uploads: [] };
|
|
51
|
+
let telemetryEnabled = true;
|
|
52
|
+
|
|
53
|
+
/** A real {@link Observability} over one recording extension, so the handler sees the production API. */
|
|
54
|
+
const observability = Effect.runSync(
|
|
55
|
+
Function.pipe(
|
|
56
|
+
Observability.make(),
|
|
57
|
+
Observability.addExtension(
|
|
58
|
+
Effect.succeed({
|
|
59
|
+
get enabled() {
|
|
60
|
+
return telemetryEnabled;
|
|
61
|
+
},
|
|
62
|
+
apis: [
|
|
63
|
+
{
|
|
64
|
+
kind: 'events',
|
|
65
|
+
isAvailable: () => Effect.succeed(true),
|
|
66
|
+
captureEvent: (event, attributes) => void captured.events.push({ event, attributes }),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
kind: 'support',
|
|
70
|
+
isAvailable: () => Effect.succeed(true),
|
|
71
|
+
uploadLogs: async () => undefined,
|
|
72
|
+
uploadNdjson: async (ndjson, kind) => {
|
|
73
|
+
captured.uploads.push({ ndjson, kind });
|
|
74
|
+
return 'trajectories/2026-10-06/test.ndjson.gz';
|
|
75
|
+
},
|
|
76
|
+
sessionContext: () => undefined,
|
|
77
|
+
flushLogs: async () => {},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// One manager for the file: the test layer resolves `Capability.Service` before a test body runs, so
|
|
86
|
+
// each test varies the state the manager holds rather than the manager itself.
|
|
87
|
+
const registry = AtomRegistry.make();
|
|
88
|
+
const settingsAtom = Atom.make<Assistant.Settings>({}).pipe(Atom.keepAlive);
|
|
89
|
+
const manager = CapabilityManager.make({ registry });
|
|
90
|
+
manager.contribute({ module: 'test', interface: Capabilities.AtomRegistry, implementation: registry });
|
|
91
|
+
manager.contribute({ module: 'test', interface: AssistantCapabilities.Settings, implementation: settingsAtom });
|
|
92
|
+
manager.contribute({
|
|
93
|
+
module: 'test',
|
|
94
|
+
interface: ObservabilityCapabilities.Observability,
|
|
95
|
+
implementation: observability,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const setup = ({ reportStruggles, telemetry }: { reportStruggles: boolean; telemetry: boolean }) => {
|
|
99
|
+
registry.set(settingsAtom, { reportStruggles, codeMode: true });
|
|
100
|
+
telemetryEnabled = telemetry;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const TestLayer = AssistantTestLayer({
|
|
104
|
+
operationHandlers: AssistantOperationHandlerSet,
|
|
105
|
+
types: [Chat.Chat, Feed.Feed, Message.Message, Skill.Skill],
|
|
106
|
+
skills: [TurnReviewSkill.make()],
|
|
107
|
+
// The reviewer gets the verdict; the conversational turn gets a plain reply.
|
|
108
|
+
aiService: ScriptedLanguageModel.scriptedAiService((request) => ({
|
|
109
|
+
parts: [
|
|
110
|
+
ScriptedLanguageModel.text(
|
|
111
|
+
request.system.includes(REVIEW_SYSTEM_PROMPT) ? JSON.stringify(VERDICT) : 'I could not search your notes.',
|
|
112
|
+
),
|
|
113
|
+
],
|
|
114
|
+
})),
|
|
115
|
+
extraServices: Layer.succeed(Capability.Service, manager),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const MODEL = DXN.make('com.anthropic.model.claude-sonnet-5.default');
|
|
119
|
+
|
|
120
|
+
/** Runs one turn with the turn-review skill bound and waits for the agent process, hooks included, to finish. */
|
|
121
|
+
const runTurn = Effect.fnUntraced(function* () {
|
|
122
|
+
const session = yield* AgentService.createSession({ skills: [TurnReviewSkill.make()], model: MODEL });
|
|
123
|
+
yield* session.submitPrompt('Find my notes.');
|
|
124
|
+
yield* session.waitForCompletion();
|
|
125
|
+
const processManager = yield* ProcessManager.ProcessManagerService;
|
|
126
|
+
// The agent process finishes only after its background hooks, so its exit is the review's.
|
|
127
|
+
const handles = yield* processManager.list({ target: Obj.getURI(session.chat) });
|
|
128
|
+
expect(handles.length).toBeGreaterThan(0);
|
|
129
|
+
yield* Effect.promise(async () => {
|
|
130
|
+
await expect
|
|
131
|
+
.poll(() => handles.every((handle) => handle.status.state === Process.State.SUCCEEDED), { timeout: 10_000 })
|
|
132
|
+
.toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('AnalyzeTurn', () => {
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
captured = { events: [], uploads: [] };
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it.effect(
|
|
142
|
+
'reports a prompting or tooling struggle with its trajectory',
|
|
143
|
+
Effect.fnUntraced(
|
|
144
|
+
function* (_) {
|
|
145
|
+
setup({ reportStruggles: true, telemetry: true });
|
|
146
|
+
yield* runTurn();
|
|
147
|
+
|
|
148
|
+
expect(captured.uploads).toHaveLength(1);
|
|
149
|
+
expect(captured.uploads[0].kind).toBe('trajectory');
|
|
150
|
+
const [header, ...messages] = captured.uploads[0].ndjson.split('\n').map((line) => JSON.parse(line));
|
|
151
|
+
expect(header).toMatchObject({ type: 'header', verdict: { cause: 'tool_faulty' } });
|
|
152
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
153
|
+
expect(messages.every((message) => message.type === 'message')).toBe(true);
|
|
154
|
+
expect(captured.events).toHaveLength(1);
|
|
155
|
+
expect(captured.events[0].event).toBe(STRUGGLE_EVENT);
|
|
156
|
+
expect(captured.events[0].attributes).toMatchObject({
|
|
157
|
+
model: 'dxn:com.anthropic.model.claude-sonnet-5.default',
|
|
158
|
+
code_mode: true,
|
|
159
|
+
cause: 'tool_faulty',
|
|
160
|
+
cause_group: 'tooling',
|
|
161
|
+
trajectory_key: 'trajectories/2026-10-06/test.ndjson.gz',
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
Effect.provide(TestLayer),
|
|
165
|
+
TestHelpers.provideTestContext,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
it.effect(
|
|
170
|
+
'does nothing unless the user opted in',
|
|
171
|
+
Effect.fnUntraced(
|
|
172
|
+
function* (_) {
|
|
173
|
+
setup({ reportStruggles: false, telemetry: true });
|
|
174
|
+
yield* runTurn();
|
|
175
|
+
expect(captured).toEqual({ events: [], uploads: [] });
|
|
176
|
+
},
|
|
177
|
+
Effect.provide(TestLayer),
|
|
178
|
+
TestHelpers.provideTestContext,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
it.effect(
|
|
183
|
+
'does nothing while telemetry is off',
|
|
184
|
+
Effect.fnUntraced(
|
|
185
|
+
function* (_) {
|
|
186
|
+
setup({ reportStruggles: true, telemetry: false });
|
|
187
|
+
yield* runTurn();
|
|
188
|
+
expect(captured).toEqual({ events: [], uploads: [] });
|
|
189
|
+
},
|
|
190
|
+
Effect.provide(TestLayer),
|
|
191
|
+
TestHelpers.provideTestContext,
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
6
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import * as Option from 'effect/Option';
|
|
9
|
+
|
|
10
|
+
import { AiService } from '@dxos/ai';
|
|
11
|
+
import * as Capabilities from '@dxos/app-framework/Capabilities';
|
|
12
|
+
import * as Capability from '@dxos/app-framework/Capability';
|
|
13
|
+
import * as Harness from '@dxos/assistant/Harness';
|
|
14
|
+
import * as Operation from '@dxos/compute/Operation';
|
|
15
|
+
import { Database, Obj } from '@dxos/echo';
|
|
16
|
+
import { log } from '@dxos/log';
|
|
17
|
+
import { contentCaptureAllowed } from '@dxos/observability/AiObservability';
|
|
18
|
+
import * as ObservabilityCapabilities from '@dxos/plugin-observability/ObservabilityCapabilities';
|
|
19
|
+
|
|
20
|
+
import { TurnReviewSkill } from '#skills';
|
|
21
|
+
import { AssistantCapabilities } from '#types';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
REVIEW_MODEL,
|
|
25
|
+
REVIEW_SYSTEM_PROMPT,
|
|
26
|
+
STRUGGLE_EVENT,
|
|
27
|
+
type TrajectoryHeader,
|
|
28
|
+
TurnVerdict,
|
|
29
|
+
formatReviewPrompt,
|
|
30
|
+
isReportable,
|
|
31
|
+
toStruggleEventProperties,
|
|
32
|
+
toTrajectoryNdjson,
|
|
33
|
+
} from '../review/turn-review.ts';
|
|
34
|
+
|
|
35
|
+
// Never fails: it runs as a background hook after the user's turn, so a review that cannot complete
|
|
36
|
+
// is only logged.
|
|
37
|
+
const handler: Operation.WithHandler<typeof TurnReviewSkill.AnalyzeTurn> = TurnReviewSkill.AnalyzeTurn.pipe(
|
|
38
|
+
Operation.withHandler(
|
|
39
|
+
Effect.fnUntraced(
|
|
40
|
+
function* () {
|
|
41
|
+
const settings = Option.getOrUndefined(yield* Capabilities.getAtomValueOption(AssistantCapabilities.Settings));
|
|
42
|
+
const observability = Option.getOrUndefined(
|
|
43
|
+
yield* Capability.getOption(ObservabilityCapabilities.Observability),
|
|
44
|
+
);
|
|
45
|
+
// Both opt-ins are re-read here: the skill stays bound to a chat after the user opts out.
|
|
46
|
+
if (!settings?.reportStruggles || !observability?.enabled) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const chat = yield* Harness.getChat;
|
|
51
|
+
const spaceId = Obj.getDatabase(chat)?.spaceId;
|
|
52
|
+
if (!spaceId || !contentCaptureAllowed(spaceId)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const history = yield* Harness.history;
|
|
57
|
+
if (history.length === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { value: verdict } = yield* LanguageModel.generateObject({
|
|
62
|
+
schema: TurnVerdict,
|
|
63
|
+
objectName: 'turn_verdict',
|
|
64
|
+
prompt: Prompt.setSystem(Prompt.make(formatReviewPrompt(history)), REVIEW_SYSTEM_PROMPT),
|
|
65
|
+
}).pipe(Effect.provide(AiService.languageModel(REVIEW_MODEL)));
|
|
66
|
+
log.info('turn reviewed', { chat: chat.id, struggled: verdict.struggled, cause: verdict.cause });
|
|
67
|
+
if (!isReportable(verdict)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const binder = yield* Harness.binder;
|
|
72
|
+
const feed = yield* Database.load(chat.feed);
|
|
73
|
+
const header: TrajectoryHeader = {
|
|
74
|
+
sessionId: Obj.getURI(feed, { prefer: 'absolute' }),
|
|
75
|
+
model: chat.session?.model,
|
|
76
|
+
codeMode: settings.codeMode ?? false,
|
|
77
|
+
skills: binder
|
|
78
|
+
.getSkills()
|
|
79
|
+
.filter((skill) => Obj.getMeta(skill).key !== TurnReviewSkill.key)
|
|
80
|
+
.map((skill) => skill.name),
|
|
81
|
+
verdict,
|
|
82
|
+
};
|
|
83
|
+
const trajectoryKey = yield* Effect.promise(() =>
|
|
84
|
+
observability.support.uploadNdjson(toTrajectoryNdjson(header, history), 'trajectory'),
|
|
85
|
+
);
|
|
86
|
+
observability.events.captureEvent(
|
|
87
|
+
STRUGGLE_EVENT,
|
|
88
|
+
toStruggleEventProperties({ ...header, history, trajectoryKey }),
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
Effect.catchCause((cause) => Effect.sync(() => log.warn('turn review failed', { cause }))),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
export default handler;
|
|
@@ -8,18 +8,14 @@ import * as Capabilities from '@dxos/app-framework/Capabilities';
|
|
|
8
8
|
import * as Capability from '@dxos/app-framework/Capability';
|
|
9
9
|
import * as AppCapabilities from '@dxos/app-toolkit/AppCapabilities';
|
|
10
10
|
import { AiContext } from '@dxos/assistant';
|
|
11
|
-
import * as AlarmSkill from '@dxos/assistant-toolkit/AlarmSkill';
|
|
12
|
-
import * as ChatContextSkill from '@dxos/assistant-toolkit/ChatContextSkill';
|
|
13
|
-
import * as PlanningSkill from '@dxos/assistant-toolkit/PlanningSkill';
|
|
14
11
|
import * as Chat from '@dxos/assistant/Chat';
|
|
15
12
|
import * as Operation from '@dxos/compute/Operation';
|
|
16
|
-
import * as Skill from '@dxos/compute/Skill';
|
|
17
13
|
import { Database, Feed, Ref } from '@dxos/echo';
|
|
18
|
-
import * as DatabaseSkill from '@dxos/plugin-space/DatabaseSkill';
|
|
19
14
|
|
|
20
|
-
import { AssistantSkill, PluginManagerSkill } from '#skills';
|
|
21
15
|
import { AssistantOperation } from '#types';
|
|
22
16
|
|
|
17
|
+
import { bindChatDefaults, contributesPluginManager } from '../util/default-skills.ts';
|
|
18
|
+
|
|
23
19
|
const handler: Operation.WithHandler<typeof AssistantOperation.CreateChat> = AssistantOperation.CreateChat.pipe(
|
|
24
20
|
Operation.withHandler(
|
|
25
21
|
Effect.fnUntraced(function* ({ name, instructions }) {
|
|
@@ -33,39 +29,11 @@ const handler: Operation.WithHandler<typeof AssistantOperation.CreateChat> = Ass
|
|
|
33
29
|
const feed = db.add(Feed.make());
|
|
34
30
|
const chat = Chat.make({ name, feed: Ref.make(feed), instructions });
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
const { SkillManagerSkill } = yield* Effect.promise(() => import('@dxos/assistant-toolkit'));
|
|
38
|
-
|
|
39
|
-
// Only an extensible host contributes the plugin-manager skill, since its tools resolve to the
|
|
40
|
-
// registry plugin's handlers; binding it elsewhere would bind a skill that cannot run.
|
|
41
|
-
const contributed = yield* Capability.getAll(AppCapabilities.SkillDefinition);
|
|
42
|
-
const pluginManagerContributed = contributed.some(({ key }) => key === PluginManagerSkill.key);
|
|
32
|
+
const pluginManager = contributesPluginManager(yield* Capability.getAll(AppCapabilities.SkillDefinition));
|
|
43
33
|
|
|
44
34
|
const runtime = yield* Effect.context<Database.Service>();
|
|
45
35
|
const binder = new AiContext.Binder({ feed, runtime, registry });
|
|
46
|
-
|
|
47
|
-
// Bind default skills via registry refs — no DB clone needed since the ECHO ref
|
|
48
|
-
// resolver already spans the hypergraph registry.
|
|
49
|
-
yield* Effect.promise(() =>
|
|
50
|
-
binder.use((b: AiContext.Binder) =>
|
|
51
|
-
b.bind({
|
|
52
|
-
skills: [
|
|
53
|
-
AssistantSkill,
|
|
54
|
-
DatabaseSkill,
|
|
55
|
-
ChatContextSkill,
|
|
56
|
-
SkillManagerSkill,
|
|
57
|
-
AlarmSkill,
|
|
58
|
-
// Bound by default rather than agent-enabled: the conversation's checklist is durable
|
|
59
|
-
// and invisible to the model, so a chat without this skill cannot read or update the
|
|
60
|
-
// tasks it is already carrying — and a model that enables it mid-turn has already
|
|
61
|
-
// answered a task question from nothing.
|
|
62
|
-
PlanningSkill,
|
|
63
|
-
...(pluginManagerContributed ? [PluginManagerSkill] : []),
|
|
64
|
-
].map(({ key }) => Ref.fromURI(Skill.registryURI(key))),
|
|
65
|
-
objects: [Ref.make(chat)],
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
68
|
-
);
|
|
36
|
+
yield* Effect.promise(() => binder.use((b: AiContext.Binder) => bindChatDefaults(b, { chat, pluginManager })));
|
|
69
37
|
|
|
70
38
|
return { object: chat };
|
|
71
39
|
}),
|
|
@@ -122,11 +122,10 @@ describe('GenerateHomeSuggestions', () => {
|
|
|
122
122
|
);
|
|
123
123
|
|
|
124
124
|
it.effect(
|
|
125
|
-
'
|
|
125
|
+
'cached prompts are reused within the refresh interval whatever the recent set is',
|
|
126
126
|
Effect.fnUntraced(
|
|
127
127
|
function* (_) {
|
|
128
128
|
const { db } = yield* Database.Service;
|
|
129
|
-
yield* addNotes(5);
|
|
130
129
|
const stored = ['Refine the proposal', 'Review the schedule', 'Draft a summary'];
|
|
131
130
|
testRegistry.set(testCacheAtom, {
|
|
132
131
|
[db.spaceId]: { generatedAt: Date.now() - HOUR_MS / 2, prompts: stored, fingerprint: 'another set' },
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import * as LanguageModel from 'effect/ai/LanguageModel';
|
|
6
|
+
import * as Array from 'effect/Array';
|
|
6
7
|
import * as Effect from 'effect/Effect';
|
|
7
8
|
import * as Schema from 'effect/Schema';
|
|
8
9
|
|
|
@@ -44,14 +45,24 @@ const handler: Operation.WithHandler<typeof AssistantOperation.GenerateHomeSugge
|
|
|
44
45
|
const { db } = yield* Database.Service;
|
|
45
46
|
const spaceId = db.spaceId;
|
|
46
47
|
|
|
48
|
+
const cache = yield* Capabilities.getAtomValue(AssistantCapabilities.HomeSuggestionsCache);
|
|
49
|
+
const entry = cache[spaceId];
|
|
50
|
+
const age = entry ? Date.now() - entry.generatedAt : Infinity;
|
|
51
|
+
if (entry && age < MIN_REFRESH_MS) {
|
|
52
|
+
return { prompts: [...entry.prompts] };
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
// Build the recent-objects filter (mirrors SpaceHomeRecent).
|
|
48
56
|
const schemas = yield* Capability.getAll(AppCapabilities.Schema);
|
|
49
57
|
const collectionTypename = Type.getTypename(Collection.Collection);
|
|
50
|
-
const types =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
const types = Array.dedupeWith(
|
|
59
|
+
schemas
|
|
60
|
+
.flat()
|
|
61
|
+
.filter(Type.isType)
|
|
62
|
+
.filter((type) => TypeOptions.isUserType(type))
|
|
63
|
+
.filter((type) => Type.getTypename(type) !== collectionTypename),
|
|
64
|
+
(a, b) => Type.getURI(a) === Type.getURI(b),
|
|
65
|
+
);
|
|
55
66
|
if (types.length === 0) {
|
|
56
67
|
return { prompts: [] };
|
|
57
68
|
}
|
|
@@ -85,10 +96,7 @@ const handler: Operation.WithHandler<typeof AssistantOperation.GenerateHomeSugge
|
|
|
85
96
|
.map(({ label, typename }) => `${typename}:${label}`)
|
|
86
97
|
.sort()
|
|
87
98
|
.join('\n');
|
|
88
|
-
|
|
89
|
-
const entry = cache[spaceId];
|
|
90
|
-
const age = entry ? Date.now() - entry.generatedAt : Infinity;
|
|
91
|
-
if (entry && (age < MIN_REFRESH_MS || (entry.fingerprint === fingerprint && age < CACHE_TTL_MS))) {
|
|
99
|
+
if (entry && entry.fingerprint === fingerprint && age < CACHE_TTL_MS) {
|
|
92
100
|
return { prompts: [...entry.prompts] };
|
|
93
101
|
}
|
|
94
102
|
|
package/src/operations/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import * as Operation from '@dxos/compute/Operation';
|
|
|
6
6
|
import * as OperationHandlerSet from '@dxos/compute/OperationHandlerSet';
|
|
7
7
|
import * as RoutineOperation from '@dxos/plugin-routine/RoutineOperation';
|
|
8
8
|
|
|
9
|
+
import { TurnReviewSkill } from '#skills';
|
|
9
10
|
import { AssistantOperation } from '#types';
|
|
10
11
|
|
|
11
12
|
export const AssistantOperationHandlerSet = OperationHandlerSet.lazy([
|
|
@@ -23,4 +24,5 @@ export const AssistantOperationHandlerSet = OperationHandlerSet.lazy([
|
|
|
23
24
|
AssistantOperation.SetCurrentChat.pipe(Operation.lazyHandler(() => import('./set-current-chat.ts'))),
|
|
24
25
|
AssistantOperation.SetTracePanelDebug.pipe(Operation.lazyHandler(() => import('./set-trace-panel-debug.ts'))),
|
|
25
26
|
AssistantOperation.UpdateChatName.pipe(Operation.lazyHandler(() => import('./update-chat-name.ts'))),
|
|
27
|
+
TurnReviewSkill.AnalyzeTurn.pipe(Operation.lazyHandler(() => import('./analyze-turn.ts'))),
|
|
26
28
|
]);
|
|
@@ -8,6 +8,7 @@ import * as Option from 'effect/Option';
|
|
|
8
8
|
import * as Capabilities from '@dxos/app-framework/Capabilities';
|
|
9
9
|
import * as Capability from '@dxos/app-framework/Capability';
|
|
10
10
|
import * as Plugin from '@dxos/app-framework/Plugin';
|
|
11
|
+
import * as Agent from '@dxos/assistant/Agent';
|
|
11
12
|
import * as Chat from '@dxos/assistant/Chat';
|
|
12
13
|
import * as AgentService from '@dxos/compute/AgentService';
|
|
13
14
|
import * as Operation from '@dxos/compute/Operation';
|
|
@@ -17,8 +18,8 @@ import { ContentBlock } from '@dxos/types';
|
|
|
17
18
|
|
|
18
19
|
import { AssistantCapabilities, AssistantEvents, AssistantOperation } from '#types';
|
|
19
20
|
|
|
21
|
+
import { defaultPreset, providerForModel } from '../chat-model/index.ts';
|
|
20
22
|
import { ChatNotSpecifiedError } from '../errors.ts';
|
|
21
|
-
import { defaultPreset, providerForModel } from '../processor/index.ts';
|
|
22
23
|
|
|
23
24
|
const handler: Operation.WithHandler<typeof AssistantOperation.RunPromptInChat> =
|
|
24
25
|
AssistantOperation.RunPromptInChat.pipe(
|
|
@@ -62,7 +63,7 @@ const handler: Operation.WithHandler<typeof AssistantOperation.RunPromptInChat>
|
|
|
62
63
|
const model = chat.session?.model ?? preset?.model;
|
|
63
64
|
const session = yield* AgentService.getSession(chat, {
|
|
64
65
|
provider: model ? providerForModel(model, preset?.provider) : preset?.provider,
|
|
65
|
-
location: chat
|
|
66
|
+
location: Agent.chatLocation(chat),
|
|
66
67
|
});
|
|
67
68
|
// A plain string is submitted as-is so the default path keeps its existing shape; a stated
|
|
68
69
|
// disposition needs the block form, which is the only place it can be carried.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { Message } from '@dxos/types';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type TurnVerdict,
|
|
11
|
+
causeGroup,
|
|
12
|
+
formatReviewPrompt,
|
|
13
|
+
isReportable,
|
|
14
|
+
splitTurn,
|
|
15
|
+
toStruggleEventProperties,
|
|
16
|
+
toTrajectoryNdjson,
|
|
17
|
+
} from './turn-review.ts';
|
|
18
|
+
|
|
19
|
+
const text = (created: string, sender: 'user' | 'assistant', value: string) =>
|
|
20
|
+
Message.make({ created, sender, blocks: [{ _tag: 'text', text: value }] });
|
|
21
|
+
|
|
22
|
+
const HISTORY = [
|
|
23
|
+
text('2026-10-06T09:00:00.000Z', 'user', 'earlier question'),
|
|
24
|
+
text('2026-10-06T09:00:05.000Z', 'assistant', 'earlier answer'),
|
|
25
|
+
text('2026-10-06T10:00:00.000Z', 'user', 'latest question'),
|
|
26
|
+
Message.make({
|
|
27
|
+
created: '2026-10-06T10:00:02.000Z',
|
|
28
|
+
sender: 'assistant',
|
|
29
|
+
blocks: [
|
|
30
|
+
{ _tag: 'toolCall', toolCallId: '1', name: 'search', input: '{}', providerExecuted: false },
|
|
31
|
+
{ _tag: 'toolResult', toolCallId: '1', name: 'search', error: 'boom', providerExecuted: false },
|
|
32
|
+
],
|
|
33
|
+
}),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const VERDICT: TurnVerdict = {
|
|
37
|
+
struggled: true,
|
|
38
|
+
cause: 'system_prompt_missing',
|
|
39
|
+
severity: 'high',
|
|
40
|
+
summary: 'The agent did not know how to find notes.',
|
|
41
|
+
tools: [],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('turn review', () => {
|
|
45
|
+
test('the turn starts at the last user message', ({ expect }) => {
|
|
46
|
+
const { context, turn } = splitTurn(HISTORY);
|
|
47
|
+
expect(context).toHaveLength(2);
|
|
48
|
+
expect(turn).toHaveLength(2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('only prompting and tooling causes are reported', ({ expect }) => {
|
|
52
|
+
expect(causeGroup('system_prompt_wrong')).toBe('system_prompt');
|
|
53
|
+
expect(causeGroup('tool_missing')).toBe('tooling');
|
|
54
|
+
expect(isReportable(VERDICT)).toBe(true);
|
|
55
|
+
expect(isReportable({ ...VERDICT, cause: 'model' })).toBe(false);
|
|
56
|
+
expect(isReportable({ ...VERDICT, struggled: false })).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the prompt marks the latest turn', ({ expect }) => {
|
|
60
|
+
const [before, after] = formatReviewPrompt(HISTORY).split('<latest_turn>');
|
|
61
|
+
expect(before).toContain('earlier question');
|
|
62
|
+
expect(after).toContain('latest question');
|
|
63
|
+
expect(after).toContain('[tool error search] boom');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('trajectory and event carry the turn metadata', ({ expect }) => {
|
|
67
|
+
const header = {
|
|
68
|
+
sessionId: 'echo://space/feed',
|
|
69
|
+
model: 'dxn:com.anthropic.model.claude-sonnet-5.default',
|
|
70
|
+
codeMode: false,
|
|
71
|
+
skills: ['Markdown', 'Tables'],
|
|
72
|
+
verdict: VERDICT,
|
|
73
|
+
};
|
|
74
|
+
const lines = toTrajectoryNdjson(header, HISTORY)
|
|
75
|
+
.split('\n')
|
|
76
|
+
.map((line) => JSON.parse(line));
|
|
77
|
+
expect(lines).toHaveLength(1 + HISTORY.length);
|
|
78
|
+
expect(lines[0]).toMatchObject({ type: 'header', skills: ['Markdown', 'Tables'] });
|
|
79
|
+
|
|
80
|
+
expect(toStruggleEventProperties({ ...header, history: HISTORY, trajectoryKey: 'k' })).toMatchObject({
|
|
81
|
+
$ai_session_id: 'echo://space/feed',
|
|
82
|
+
model: 'dxn:com.anthropic.model.claude-sonnet-5.default',
|
|
83
|
+
code_mode: false,
|
|
84
|
+
skills: 'Markdown,Tables',
|
|
85
|
+
cause_group: 'system_prompt',
|
|
86
|
+
turn_message_count: 2,
|
|
87
|
+
turn_tool_call_count: 1,
|
|
88
|
+
turn_tool_error_count: 1,
|
|
89
|
+
trajectory_key: 'k',
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|