@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
|
@@ -23,12 +23,15 @@ import {
|
|
|
23
23
|
createSystemPrompt,
|
|
24
24
|
formatSystemPrompt,
|
|
25
25
|
} from '@dxos/assistant';
|
|
26
|
+
import * as Agent from '@dxos/assistant/Agent';
|
|
26
27
|
import type * as Chat from '@dxos/assistant/Chat';
|
|
27
28
|
import * as AgentService from '@dxos/compute/AgentService';
|
|
28
29
|
import type * as Credential from '@dxos/compute/Credential';
|
|
29
30
|
import type * as Instructions from '@dxos/compute/Instructions';
|
|
30
31
|
import * as Operation from '@dxos/compute/Operation';
|
|
32
|
+
import type * as Process from '@dxos/compute/Process';
|
|
31
33
|
import type * as ServiceResolver from '@dxos/compute/ServiceResolver';
|
|
34
|
+
import * as Skill from '@dxos/compute/Skill';
|
|
32
35
|
import * as Trace from '@dxos/compute/Trace';
|
|
33
36
|
import { Database, Feed, Filter, Obj, Query, Ref, type Registry } from '@dxos/echo';
|
|
34
37
|
import { UsageQuotaExceededError } from '@dxos/edge-client';
|
|
@@ -38,18 +41,19 @@ import { log } from '@dxos/log';
|
|
|
38
41
|
import { ContentBlock, Message } from '@dxos/types';
|
|
39
42
|
import { markWork } from '@dxos/util';
|
|
40
43
|
|
|
44
|
+
import { TurnReviewSkill } from '#skills';
|
|
41
45
|
import { AssistantOperation } from '#types';
|
|
42
46
|
|
|
43
47
|
import { findInCause } from '../util/error-cause.ts';
|
|
44
48
|
import { Outbox, type OutboxEntry, PromptCancelledError } from './outbox.ts';
|
|
45
49
|
import { providerForModel } from './presets.ts';
|
|
46
|
-
import { type
|
|
50
|
+
import { type ChatRequestContext, createPromptContent } from './prompt.ts';
|
|
47
51
|
import { EMPTY_THREAD, type ThreadProjection, projectThread } from './thread.ts';
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
54
|
* Space-scoped services materialised by the layer passed into
|
|
51
|
-
* {@link
|
|
52
|
-
* {@link
|
|
55
|
+
* {@link ChatModel}. Mirrors the tag list that
|
|
56
|
+
* {@link useChatModel} passes to {@link ServiceResolver.provide}.
|
|
53
57
|
*/
|
|
54
58
|
export type SpaceServices =
|
|
55
59
|
| Database.Service
|
|
@@ -59,7 +63,7 @@ export type SpaceServices =
|
|
|
59
63
|
| Registry.Service
|
|
60
64
|
| OpaqueToolkit.OpaqueToolkitProvider;
|
|
61
65
|
|
|
62
|
-
export type
|
|
66
|
+
export type ChatModelOptions = {
|
|
63
67
|
/**
|
|
64
68
|
* The model the chat's picker shows selected. The agent process reads the model off the chat, so
|
|
65
69
|
* this is stamped onto a chat that has not selected one before its first request — otherwise the
|
|
@@ -77,26 +81,28 @@ export type AiChatProcessorOptions = {
|
|
|
77
81
|
*/
|
|
78
82
|
chat?: Ref.Ref<Chat.Chat>;
|
|
79
83
|
system?: string;
|
|
80
|
-
/** Who this
|
|
84
|
+
/** Who this chat model's prompts come from, when the chat is one of several people's with one agent. */
|
|
81
85
|
sender?: AgentService.PromptSender;
|
|
86
|
+
/** Binds {@link TurnReviewSkill} so each turn is reviewed for struggles (opt-in setting). */
|
|
87
|
+
reportStruggles?: boolean;
|
|
82
88
|
};
|
|
83
89
|
|
|
84
|
-
const defaultOptions: Partial<
|
|
90
|
+
const defaultOptions: Partial<ChatModelOptions> = {
|
|
85
91
|
model: Model.DEFAULT_EDGE,
|
|
86
92
|
};
|
|
87
93
|
|
|
88
|
-
export type
|
|
94
|
+
export type ChatRequestOptions = {};
|
|
89
95
|
|
|
90
|
-
export type
|
|
96
|
+
export type ChatRequest = {
|
|
91
97
|
message: string;
|
|
92
98
|
/** `synthetic` marks system-generated turn content (e.g. a completed inline flow reporting itself). */
|
|
93
99
|
disposition?: ContentBlock.Text['disposition'];
|
|
94
100
|
/** Ephemeral context (e.g. companion-document selection) captured at submit time. */
|
|
95
|
-
context?:
|
|
96
|
-
options?:
|
|
101
|
+
context?: ChatRequestContext;
|
|
102
|
+
options?: ChatRequestOptions;
|
|
97
103
|
};
|
|
98
104
|
|
|
99
|
-
export type
|
|
105
|
+
export type ChatSendOptions = {
|
|
100
106
|
/**
|
|
101
107
|
* Runs before the prompt is dispatched — after it is already showing — and a rejection fails the
|
|
102
108
|
* prompt. Lets a transient chat persist and flush its conversation feed so the agent can resolve it.
|
|
@@ -107,7 +113,7 @@ export type ProcessorSendOptions = {
|
|
|
107
113
|
/** Settles the dispatch of one prompt: resolved once the agent holds it, rejected if it never will. */
|
|
108
114
|
export type PromptSubmission = { resolve: () => void; reject: (error: unknown) => void };
|
|
109
115
|
|
|
110
|
-
type OutboxPayload = { request:
|
|
116
|
+
type OutboxPayload = { request: ChatRequest; prepare?: () => Promise<void> | void };
|
|
111
117
|
|
|
112
118
|
/** User-facing message shown when an AI request is rejected for exceeding the account usage quota (HTTP 429). */
|
|
113
119
|
const QUOTA_EXCEEDED_MESSAGE = 'You have reached your AI usage limit for this period.';
|
|
@@ -157,6 +163,30 @@ const describeAiError = (err: AiError.AiError): string =>
|
|
|
157
163
|
*/
|
|
158
164
|
const MODEL_UNAVAILABLE_PATTERN = /AI Model not available:\s*(\S+?):?(?=\s|$)/i;
|
|
159
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Matches the first line of a stringified {@link AiError.AiError} ("…AiError: Module.method: detail"), the
|
|
168
|
+
* form a provider failure takes once it has crossed the agent-process boundary.
|
|
169
|
+
*/
|
|
170
|
+
const AI_ERROR_PATTERN = /AiError: [\w$.]+: ([^\n]+)/;
|
|
171
|
+
|
|
172
|
+
/** The provider's own description of a failure that arrived stringified, anywhere in the cause chain. */
|
|
173
|
+
const stringifiedAiErrorText = (err: unknown): string | undefined => {
|
|
174
|
+
const visited = new Set<unknown>();
|
|
175
|
+
for (
|
|
176
|
+
let current = err;
|
|
177
|
+
current && !visited.has(current);
|
|
178
|
+
current = current instanceof Error ? current.cause : undefined
|
|
179
|
+
) {
|
|
180
|
+
visited.add(current);
|
|
181
|
+
const text = typeof current === 'string' ? current : current instanceof Error ? current.message : '';
|
|
182
|
+
const detail = text.match(AI_ERROR_PATTERN)?.[1]?.trim();
|
|
183
|
+
if (detail) {
|
|
184
|
+
return detail;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
};
|
|
189
|
+
|
|
160
190
|
/** The displayable text of a failure, which reaches the chat either typed or already stringified. */
|
|
161
191
|
const errorText = (err: unknown): string => (typeof err === 'string' ? err : err instanceof Error ? err.message : '');
|
|
162
192
|
|
|
@@ -204,6 +234,10 @@ export const parseError = (err: unknown): Error => {
|
|
|
204
234
|
}
|
|
205
235
|
}
|
|
206
236
|
|
|
237
|
+
if (!message) {
|
|
238
|
+
message = stringifiedAiErrorText(err);
|
|
239
|
+
}
|
|
240
|
+
|
|
207
241
|
if (!message) {
|
|
208
242
|
message = 'An unexpected error occurred.';
|
|
209
243
|
}
|
|
@@ -215,7 +249,7 @@ export const parseError = (err: unknown): Error => {
|
|
|
215
249
|
* Handles interactions with the AI service.
|
|
216
250
|
* Uses AgentService to spawn a process-backed agent and subscribes to ephemeral trace events for streaming.
|
|
217
251
|
*/
|
|
218
|
-
export class
|
|
252
|
+
export class ChatModel {
|
|
219
253
|
readonly #registry: AtomRegistry.AtomRegistry;
|
|
220
254
|
|
|
221
255
|
/** Pending messages (finalized, non-streaming). */
|
|
@@ -227,14 +261,18 @@ export class AiChatProcessor {
|
|
|
227
261
|
/** Set of message IDs that have been finalized (non-pending delivered via ephemeral). */
|
|
228
262
|
readonly #finalizedIds = new Set<string>();
|
|
229
263
|
|
|
264
|
+
/** Timestamp of the newest trace event applied this turn, overall and per streamed message. */
|
|
265
|
+
#appliedAt = 0;
|
|
266
|
+
readonly #appliedAtByMessage = new Map<string, number>();
|
|
267
|
+
|
|
230
268
|
/** Currently active request fiber. */
|
|
231
269
|
#requestFiber: Fiber.Fiber<void, unknown> | undefined;
|
|
232
270
|
|
|
233
|
-
/** Fiber following a turn this
|
|
271
|
+
/** Fiber following a turn this chat model did not issue ({@link adopt}). */
|
|
234
272
|
#observeFiber: Fiber.Fiber<void, unknown> | undefined;
|
|
235
273
|
|
|
236
274
|
/** Last request (for retries). */
|
|
237
|
-
#lastRequest:
|
|
275
|
+
#lastRequest: ChatRequest | undefined;
|
|
238
276
|
|
|
239
277
|
/** Streaming state. */
|
|
240
278
|
public readonly streaming = Atom.make<boolean>((get) => get(this.#streaming).length > 0);
|
|
@@ -287,16 +325,16 @@ export class AiChatProcessor {
|
|
|
287
325
|
* {@link ServiceResolver.provide} with the {@link ServiceResolver} already
|
|
288
326
|
* supplied (hence `RIn = never`); the {@link ServiceNotAvailableError}
|
|
289
327
|
* error channel surfaces when a tag is not available for the space.
|
|
290
|
-
* Provided to every effect run by the
|
|
328
|
+
* Provided to every effect run by the chat model so the underlying
|
|
291
329
|
* {@link ProcessManagerRuntime} has access to space-affinity services.
|
|
292
330
|
*/
|
|
293
331
|
private readonly _spaceLayer: Layer.Layer<SpaceServices, ServiceResolver.ServiceNotAvailableError, never>,
|
|
294
|
-
private readonly _options:
|
|
332
|
+
private readonly _options: ChatModelOptions = defaultOptions,
|
|
295
333
|
) {
|
|
296
334
|
this.#registry = this._options.observableRegistry ?? AtomRegistry.make();
|
|
297
335
|
this.#outbox = new Outbox(this.#registry, (payload) => this.#dispatch(payload));
|
|
298
336
|
this.outbox = this.#outbox.entries;
|
|
299
|
-
// Held for the
|
|
337
|
+
// Held for the chat model's life: a query's atom is memoized per result, so the query must be too.
|
|
300
338
|
const db = Obj.getDatabase(this._feed);
|
|
301
339
|
this.#feedMessages = db
|
|
302
340
|
? db.query(Query.select(Filter.type(Message.Message)).from(this._feed)).atom
|
|
@@ -379,7 +417,7 @@ export class AiChatProcessor {
|
|
|
379
417
|
*
|
|
380
418
|
* Returns the outbox id, which is the prompt's thread row for its whole life.
|
|
381
419
|
*/
|
|
382
|
-
send(request:
|
|
420
|
+
send(request: ChatRequest, { prepare }: ChatSendOptions = {}): string {
|
|
383
421
|
const content = createPromptContent(request);
|
|
384
422
|
const blocks = typeof content === 'string' ? [ContentBlock.Text.make({ text: content })] : content;
|
|
385
423
|
// What the feed holds now cannot be this prompt's echo, however alike it reads.
|
|
@@ -420,7 +458,7 @@ export class AiChatProcessor {
|
|
|
420
458
|
* `submission` settles once the prompt is submitted (or cannot be), which is long before the turn
|
|
421
459
|
* this resolves after.
|
|
422
460
|
*/
|
|
423
|
-
async request(requestProp:
|
|
461
|
+
async request(requestProp: ChatRequest, submission?: PromptSubmission): Promise<void> {
|
|
424
462
|
if (this.#requestFiber) {
|
|
425
463
|
await this.cancel();
|
|
426
464
|
}
|
|
@@ -433,6 +471,7 @@ export class AiChatProcessor {
|
|
|
433
471
|
// that resolve is itself part of the wait the reader is watching.
|
|
434
472
|
this.#registry.set(this.activity, { phase: 'starting' });
|
|
435
473
|
this.#registry.set(this.active, true);
|
|
474
|
+
await this.#ensureTurnReview();
|
|
436
475
|
|
|
437
476
|
const effect = Effect.gen({ self: this }, function* () {
|
|
438
477
|
// NOTE: Gets or creates a session for the feed.
|
|
@@ -443,13 +482,15 @@ export class AiChatProcessor {
|
|
|
443
482
|
});
|
|
444
483
|
const session = yield* this.#getSession();
|
|
445
484
|
markWork('chat.session-ready');
|
|
446
|
-
|
|
485
|
+
// Subscribed before the prompt is submitted, so nothing of this turn needs replaying, and a
|
|
486
|
+
// replay would carry the process's earlier turns.
|
|
487
|
+
yield* this.#forkEphemeralCollector(session, { replay: false });
|
|
447
488
|
|
|
448
|
-
log('chat
|
|
489
|
+
log('chat model submitting prompt', { length: requestProp.message.length });
|
|
449
490
|
yield* session.submitPrompt(createPromptContent(requestProp), { sender: this._options.sender });
|
|
450
491
|
markWork('chat.prompt-submitted');
|
|
451
492
|
submission?.resolve();
|
|
452
|
-
log('chat
|
|
493
|
+
log('chat model submitPrompt returned, waiting for agent', {});
|
|
453
494
|
|
|
454
495
|
// On the first message (no name yet), schedule rename immediately so it
|
|
455
496
|
// runs concurrently with the AI response rather than waiting for completion. A synthetic
|
|
@@ -507,7 +548,7 @@ export class AiChatProcessor {
|
|
|
507
548
|
* `waitForCompletion` already covers the queued turn, since the agent does not report completion
|
|
508
549
|
* while its queue is non-empty.
|
|
509
550
|
*/
|
|
510
|
-
async enqueue(requestProp:
|
|
551
|
+
async enqueue(requestProp: ChatRequest): Promise<void> {
|
|
511
552
|
try {
|
|
512
553
|
await this.#enqueue(requestProp);
|
|
513
554
|
} catch (err) {
|
|
@@ -516,7 +557,7 @@ export class AiChatProcessor {
|
|
|
516
557
|
}
|
|
517
558
|
}
|
|
518
559
|
|
|
519
|
-
async #enqueue(requestProp:
|
|
560
|
+
async #enqueue(requestProp: ChatRequest): Promise<void> {
|
|
520
561
|
await this._runtime.runPromise(
|
|
521
562
|
Effect.gen({ self: this }, function* () {
|
|
522
563
|
const session = yield* this.#getSession();
|
|
@@ -527,8 +568,8 @@ export class AiChatProcessor {
|
|
|
527
568
|
}
|
|
528
569
|
|
|
529
570
|
/**
|
|
530
|
-
* Mirrors turns this
|
|
531
|
-
* per-
|
|
571
|
+
* Mirrors turns this chat model did not initiate into its own state: active/streaming state is
|
|
572
|
+
* per-chat model ({@link useChatModel} builds one per mount) while the agent process outlives
|
|
532
573
|
* the mount, so a chat remounted mid-turn would otherwise render as idle.
|
|
533
574
|
*
|
|
534
575
|
* Returns a disposer that stops observing.
|
|
@@ -571,7 +612,7 @@ export class AiChatProcessor {
|
|
|
571
612
|
|
|
572
613
|
/**
|
|
573
614
|
* Follows a turn started elsewhere to completion, surfacing its streamed blocks here.
|
|
574
|
-
* A turn this
|
|
615
|
+
* A turn this chat model issued is owned by {@link request}, which reports its own errors.
|
|
575
616
|
*/
|
|
576
617
|
async #observe(session: AgentService.Session): Promise<void> {
|
|
577
618
|
if (this.#requestFiber || this.#observeFiber || this.#registry.get(this.active)) {
|
|
@@ -580,9 +621,12 @@ export class AiChatProcessor {
|
|
|
580
621
|
|
|
581
622
|
log.info('observing agent turn', { feed: Obj.getURI(this._feed) });
|
|
582
623
|
try {
|
|
624
|
+
// Attaching is the `starting` phase; the agent's next report names the real one.
|
|
625
|
+
this.#registry.set(this.activity, { phase: 'starting' });
|
|
583
626
|
this.#registry.set(this.active, true);
|
|
584
627
|
const effect = Effect.gen({ self: this }, function* () {
|
|
585
|
-
|
|
628
|
+
// Replayed: the turn being adopted is already under way.
|
|
629
|
+
yield* this.#forkEphemeralCollector(session, { replay: true });
|
|
586
630
|
yield* session.waitForCompletion();
|
|
587
631
|
this.#flushStreaming();
|
|
588
632
|
});
|
|
@@ -612,9 +656,9 @@ export class AiChatProcessor {
|
|
|
612
656
|
return Effect.gen({ self: this }, function* () {
|
|
613
657
|
const chat = this._options.chat?.target;
|
|
614
658
|
if (!chat) {
|
|
615
|
-
// The agent process is bound to a chat; a
|
|
659
|
+
// The agent process is bound to a chat; a chat model constructed without one has no
|
|
616
660
|
// conversation to run.
|
|
617
|
-
return yield* Effect.die(new Error('Chat
|
|
661
|
+
return yield* Effect.die(new Error('Chat model requires a chat.'));
|
|
618
662
|
}
|
|
619
663
|
const selected = this._options.model;
|
|
620
664
|
if (!chat.session?.model && selected) {
|
|
@@ -627,7 +671,7 @@ export class AiChatProcessor {
|
|
|
627
671
|
const model = chat.session?.model ?? selected;
|
|
628
672
|
return yield* AgentService.getSession(chat, {
|
|
629
673
|
provider: model ? providerForModel(model, this._options.provider) : this._options.provider,
|
|
630
|
-
location: chat
|
|
674
|
+
location: Agent.chatLocation(chat),
|
|
631
675
|
});
|
|
632
676
|
});
|
|
633
677
|
}
|
|
@@ -636,14 +680,28 @@ export class AiChatProcessor {
|
|
|
636
680
|
* Forks the collector for the session's ephemeral trace events (streaming blocks and MCP
|
|
637
681
|
* failures) as a child of the calling fiber.
|
|
638
682
|
*/
|
|
639
|
-
#forkEphemeralCollector(
|
|
640
|
-
|
|
683
|
+
#forkEphemeralCollector(
|
|
684
|
+
session: AgentService.Session,
|
|
685
|
+
options: Process.SubscribeEphemeralOptions,
|
|
686
|
+
): Effect.Effect<void> {
|
|
687
|
+
return session.subscribeEphemeral(options).pipe(
|
|
641
688
|
Stream.runForEach((message) =>
|
|
642
689
|
Effect.sync(() => {
|
|
643
690
|
for (const event of message.events) {
|
|
691
|
+
// A remote agent's trace is relayed over the swarm, which does not preserve order, so a
|
|
692
|
+
// phase or partial older than one already applied is stale rather than news.
|
|
644
693
|
if (Trace.isOfType(Trace.PartialBlock, event)) {
|
|
645
|
-
|
|
694
|
+
// Read before `#isStale` records it: a block can be news for its message yet older than a
|
|
695
|
+
// phase already shown, which it must not turn back into `generating`.
|
|
696
|
+
const current = event.timestamp >= this.#appliedAt;
|
|
697
|
+
if (this.#isStale(event.timestamp, event.data.messageId)) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
this.#handleEphemeralMessage(event.data, current);
|
|
646
701
|
} else if (Trace.isOfType(Trace.RequestPhase, event)) {
|
|
702
|
+
if (this.#isStale(event.timestamp)) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
647
705
|
this.#registry.set(this.activity, event.data);
|
|
648
706
|
} else if (Trace.isOfType(Trace.McpServerError, event)) {
|
|
649
707
|
this.#handleMcpError(event.data);
|
|
@@ -712,17 +770,35 @@ export class AiChatProcessor {
|
|
|
712
770
|
);
|
|
713
771
|
}
|
|
714
772
|
|
|
773
|
+
/**
|
|
774
|
+
* Whether a trace event is older than one already applied (per message for a streamed block, whose
|
|
775
|
+
* text is the whole reply so far), recording it as the newest when it is not.
|
|
776
|
+
*/
|
|
777
|
+
#isStale(timestamp: number, messageId?: string): boolean {
|
|
778
|
+
const appliedAt = messageId === undefined ? this.#appliedAt : (this.#appliedAtByMessage.get(messageId) ?? 0);
|
|
779
|
+
if (timestamp < appliedAt) {
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
this.#appliedAt = Math.max(this.#appliedAt, timestamp);
|
|
783
|
+
if (messageId !== undefined) {
|
|
784
|
+
this.#appliedAtByMessage.set(messageId, timestamp);
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
|
|
715
789
|
/**
|
|
716
790
|
* Handles an ephemeral message from the agent process.
|
|
717
791
|
* Both pending and completed blocks arrive here. Completed blocks are deduped
|
|
718
792
|
* against messages already written to the feed queue to handle the race between
|
|
719
793
|
* ephemeral delivery and feed replication.
|
|
720
794
|
*/
|
|
721
|
-
#handleEphemeralMessage(event: Trace.PayloadType<typeof Trace.PartialBlock
|
|
795
|
+
#handleEphemeralMessage(event: Trace.PayloadType<typeof Trace.PartialBlock>, current = true) {
|
|
722
796
|
// Content arriving is what "generating" means, and deriving it here keeps it out of the agent's
|
|
723
797
|
// streaming pipeline, where the extra yield a trace write costs is observable to the turn's
|
|
724
798
|
// tools. A tool call the agent reports supersedes it for as long as the tool runs.
|
|
725
|
-
|
|
799
|
+
if (current) {
|
|
800
|
+
this.#registry.set(this.activity, { phase: 'generating' });
|
|
801
|
+
}
|
|
726
802
|
|
|
727
803
|
const isPending = event.block.pending;
|
|
728
804
|
const message = Obj.make(Message.Message, {
|
|
@@ -778,19 +854,42 @@ export class AiChatProcessor {
|
|
|
778
854
|
this.#registry.set(this.#streaming, []);
|
|
779
855
|
this.#registry.set(this.activity, undefined);
|
|
780
856
|
this.#finalizedIds.clear();
|
|
857
|
+
this.#resetApplied();
|
|
781
858
|
}
|
|
782
859
|
|
|
783
860
|
/**
|
|
784
861
|
* Move remaining streaming messages to pending (called when agent completes).
|
|
785
862
|
*/
|
|
786
863
|
#flushStreaming() {
|
|
787
|
-
|
|
864
|
+
// The activity line is left to the settle that clears `active`: cleared here, it would vanish a tick
|
|
865
|
+
// before the turn reads as finished.
|
|
788
866
|
const remaining = this.#registry.get(this.#streaming);
|
|
789
867
|
if (remaining.length > 0) {
|
|
790
868
|
this.#registry.update(this.#pending, (pending) => [...pending, ...remaining]);
|
|
791
869
|
this.#registry.set(this.#streaming, []);
|
|
792
870
|
}
|
|
793
871
|
this.#finalizedIds.clear();
|
|
872
|
+
this.#resetApplied();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
#resetApplied() {
|
|
876
|
+
this.#appliedAt = 0;
|
|
877
|
+
this.#appliedAtByMessage.clear();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Binds the turn-review skill before a request while the user is opted in; its background
|
|
882
|
+
* end-request hook does the review, so nothing here waits on it. Left bound after an opt-out,
|
|
883
|
+
* since the hook re-checks the setting.
|
|
884
|
+
*/
|
|
885
|
+
async #ensureTurnReview(): Promise<void> {
|
|
886
|
+
if (!this._options.reportStruggles) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const bound = this.context.getSkills().some((skill) => Obj.getMeta(skill).key === TurnReviewSkill.key);
|
|
890
|
+
if (!bound) {
|
|
891
|
+
await this.context.bind({ skills: [Ref.fromURI(Skill.registryURI(TurnReviewSkill.key))] });
|
|
892
|
+
}
|
|
794
893
|
}
|
|
795
894
|
|
|
796
895
|
/**
|
|
@@ -813,12 +912,9 @@ export class AiChatProcessor {
|
|
|
813
912
|
}
|
|
814
913
|
}
|
|
815
914
|
|
|
816
|
-
export type
|
|
817
|
-
AiChatProcessor,
|
|
818
|
-
'streaming' | 'active' | 'thread' | 'error' | 'mcpErrors' | 'activity'
|
|
819
|
-
>;
|
|
915
|
+
export type ChatModelState = Pick<ChatModel, 'streaming' | 'active' | 'thread' | 'error' | 'mcpErrors' | 'activity'>;
|
|
820
916
|
|
|
821
|
-
const
|
|
917
|
+
const idleChatModelState: ChatModelState = {
|
|
822
918
|
streaming: Atom.make(false),
|
|
823
919
|
active: Atom.make(false),
|
|
824
920
|
thread: Atom.make<ThreadProjection>(EMPTY_THREAD),
|
|
@@ -827,5 +923,4 @@ const idleProcessorState: AiChatProcessorState = {
|
|
|
827
923
|
activity: Atom.make<Trace.PayloadType<typeof Trace.RequestPhase> | undefined>(undefined),
|
|
828
924
|
};
|
|
829
925
|
|
|
830
|
-
export const
|
|
831
|
-
processor ?? idleProcessorState;
|
|
926
|
+
export const getChatModelState = (chatModel: ChatModel | undefined): ChatModelState => chatModel ?? idleChatModelState;
|
|
@@ -6,7 +6,7 @@ import { ContentBlock } from '@dxos/types';
|
|
|
6
6
|
import { trim } from '@dxos/util';
|
|
7
7
|
|
|
8
8
|
/** Ephemeral, per-request context captured at submit time (not part of the durable chat state). */
|
|
9
|
-
export type
|
|
9
|
+
export type ChatRequestContext = {
|
|
10
10
|
selection?: {
|
|
11
11
|
/** Anchor strings (`"${from}:${to}"` cursor pairs) the text was resolved from; enables future in-place actions. */
|
|
12
12
|
anchors: string[];
|
|
@@ -23,7 +23,7 @@ export type ProcessorRequestContext = {
|
|
|
23
23
|
export const createPromptContent = (request: {
|
|
24
24
|
message: string;
|
|
25
25
|
disposition?: ContentBlock.Text['disposition'];
|
|
26
|
-
context?:
|
|
26
|
+
context?: ChatRequestContext;
|
|
27
27
|
}): string | ContentBlock.Any[] => {
|
|
28
28
|
if (request.disposition === 'synthetic') {
|
|
29
29
|
return [ContentBlock.Text.make({ text: request.message, disposition: 'synthetic' })];
|