@genesislcap/ai-assistant 15.19.0 → 15.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -18
- package/api-extractor.json +0 -11
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
- package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
- package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
- package/dist/dts/config/validate-providers.test.d.ts +0 -2
- package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
- package/dist/dts/main/blocked-state.test.d.ts +0 -2
- package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
- package/dist/dts/main/budget-meter.test.d.ts +0 -2
- package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
- package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
- package/dist/dts/main/file-attachments.test.d.ts +0 -2
- package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
- package/dist/dts/main/interaction-cost.test.d.ts +0 -2
- package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
- package/dist/dts/main/main.styles.test.d.ts +0 -2
- package/dist/dts/main/main.styles.test.d.ts.map +0 -1
- package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
- package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
- package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
- package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
- package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
- package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
- package/dist/dts/state/debug-event-log.test.d.ts +0 -2
- package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
- package/dist/dts/state/driver-registry.test.d.ts +0 -2
- package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
- package/dist/dts/state/interaction-context.test.d.ts +0 -2
- package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
- package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
- package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
- package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
- package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
- package/dist/dts/utils/condense-history.test.d.ts +0 -2
- package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
- package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
- package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
- package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
- package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
- package/dist/dts/utils/history-transform.test.d.ts +0 -2
- package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
- package/dist/dts/utils/message-partition.test.d.ts +0 -2
- package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
- package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
- package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-costs.test.d.ts +0 -2
- package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
- package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-usage.test.d.ts +0 -2
- package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
- package/dist/dts/utils/usage-rows.test.d.ts +0 -2
- package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
- package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
- package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
- package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
- package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
- package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
- package/dist/esm/config/define-stateful-agent.test.js +0 -53
- package/dist/esm/config/validate-providers.test.js +0 -100
- package/dist/esm/main/blocked-state.test.js +0 -1065
- package/dist/esm/main/budget-meter.test.js +0 -359
- package/dist/esm/main/cost-session-banking.test.js +0 -538
- package/dist/esm/main/file-attachments.test.js +0 -164
- package/dist/esm/main/interaction-cost.test.js +0 -107
- package/dist/esm/main/main.styles.test.js +0 -161
- package/dist/esm/main/persistence-broken-sources.test.js +0 -180
- package/dist/esm/main/popout-interaction-gate.test.js +0 -54
- package/dist/esm/state/ai-assistant-slice.test.js +0 -229
- package/dist/esm/state/debug-event-log.test.js +0 -117
- package/dist/esm/state/driver-registry.test.js +0 -55
- package/dist/esm/state/interaction-context.test.js +0 -78
- package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
- package/dist/esm/state/persistence/diagnostics.test.js +0 -280
- package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
- package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
- package/dist/esm/state/persistence/session-persister.test.js +0 -613
- package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
- package/dist/esm/utils/animation-exclusivity.test.js +0 -52
- package/dist/esm/utils/collect-session-models.test.js +0 -53
- package/dist/esm/utils/condense-history.test.js +0 -547
- package/dist/esm/utils/cost-session-history.test.js +0 -174
- package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
- package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
- package/dist/esm/utils/history-transform.test.js +0 -158
- package/dist/esm/utils/message-partition.test.js +0 -90
- package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
- package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
- package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
- package/dist/esm/utils/sum-costs.test.js +0 -114
- package/dist/esm/utils/sum-tokens.test.js +0 -51
- package/dist/esm/utils/sum-usage.test.js +0 -164
- package/dist/esm/utils/usage-rows.test.js +0 -189
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/license.txt +0 -46
- package/sandbox/tsconfig.json +0 -8
- package/src/channel/ai-activity-bus.ts +0 -209
- package/src/channel/ai-activity-channel.ts +0 -146
- package/src/chat-driver-node.ts +0 -159
- package/src/components/activity-halo/activity-halo.ts +0 -106
- package/src/components/agent-picker/agent-picker.constants.ts +0 -2
- package/src/components/agent-picker/agent-picker.styles.ts +0 -24
- package/src/components/agent-picker/agent-picker.template.ts +0 -54
- package/src/components/agent-picker/agent-picker.ts +0 -99
- package/src/components/agent-picker/index.ts +0 -1
- package/src/components/ai-driver/ai-driver.ts +0 -166
- package/src/components/ai-driver/index.ts +0 -1
- package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
- package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
- package/src/components/chat-bubble/chat-bubble.ts +0 -436
- package/src/components/chat-bubble/index.ts +0 -1
- package/src/components/chat-driver/align-event-globals.ts +0 -23
- package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
- package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
- package/src/components/chat-driver/chat-driver.test.ts +0 -3679
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
- package/src/components/chat-driver/chat-driver.ts +0 -3925
- package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
- package/src/components/chat-driver/index.ts +0 -1
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
- package/src/components/chat-interaction-wrapper/index.ts +0 -3
- package/src/components/chat-markdown/chat-markdown.ts +0 -89
- package/src/components/chat-markdown/index.ts +0 -1
- package/src/components/flowing-waves-indicator.ts +0 -286
- package/src/components/halo-overlay.ts +0 -184
- package/src/components/orchestrating-driver/index.ts +0 -1
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
- package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
- package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
- package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
- package/src/components/plasma-orb-indicator.ts +0 -281
- package/src/components/popout-manager/index.ts +0 -1
- package/src/components/popout-manager/popout-manager.ts +0 -212
- package/src/components/settings-modal/index.ts +0 -2
- package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
- package/src/components/settings-modal/settings-modal.styles.ts +0 -853
- package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
- package/src/components/settings-modal/settings-modal.template.ts +0 -943
- package/src/components/waves-indicator.ts +0 -241
- package/src/config/config.ts +0 -608
- package/src/config/define-stateful-agent.test.ts +0 -64
- package/src/config/define-stateful-agent.ts +0 -642
- package/src/config/fallback-agents.ts +0 -29
- package/src/config/index.ts +0 -2
- package/src/config/validate-providers.test.ts +0 -148
- package/src/config/validate-providers.ts +0 -58
- package/src/index.ts +0 -29
- package/src/main/blocked-state.test.ts +0 -1444
- package/src/main/budget-meter.test.ts +0 -477
- package/src/main/cost-session-banking.test.ts +0 -679
- package/src/main/file-attachments.test.ts +0 -215
- package/src/main/index.ts +0 -3
- package/src/main/interaction-cost.test.ts +0 -140
- package/src/main/main.styles.test.ts +0 -216
- package/src/main/main.styles.ts +0 -995
- package/src/main/main.template.ts +0 -1119
- package/src/main/main.ts +0 -5432
- package/src/main/main.types.ts +0 -225
- package/src/main/persistence-broken-sources.test.ts +0 -219
- package/src/main/popout-interaction-gate.test.ts +0 -87
- package/src/provider/ai-provider-switcher.ts +0 -40
- package/src/provider/assistant-app-settings.ts +0 -84
- package/src/state/ai-assistant-slice.test.ts +0 -336
- package/src/state/ai-assistant-slice.ts +0 -604
- package/src/state/debug-event-log.test.ts +0 -154
- package/src/state/debug-event-log.ts +0 -358
- package/src/state/driver-registry.test.ts +0 -70
- package/src/state/driver-registry.ts +0 -79
- package/src/state/interaction-context.test.ts +0 -90
- package/src/state/interaction-context.ts +0 -101
- package/src/state/persistence/build-timeline-entries.ts +0 -66
- package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
- package/src/state/persistence/diagnostics-cursors.ts +0 -80
- package/src/state/persistence/diagnostics.test.ts +0 -346
- package/src/state/persistence/diagnostics.ts +0 -220
- package/src/state/persistence/index.ts +0 -10
- package/src/state/persistence/persister-registry.ts +0 -55
- package/src/state/persistence/session-persistence-provider.test.ts +0 -136
- package/src/state/persistence/session-persistence-provider.ts +0 -226
- package/src/state/persistence/session-persistence.integration.test.ts +0 -141
- package/src/state/persistence/session-persister.test.ts +0 -787
- package/src/state/persistence/session-persister.ts +0 -917
- package/src/state/persistence/session-snapshot.test.ts +0 -352
- package/src/state/persistence/session-snapshot.ts +0 -344
- package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
- package/src/state/session-store.ts +0 -56
- package/src/styles/ai-colours.ts +0 -30
- package/src/styles/menu.ts +0 -187
- package/src/styles/settings-section.ts +0 -40
- package/src/suggestions/chat-suggestions.ts +0 -186
- package/src/tags/index.ts +0 -1
- package/src/types/ai-chat-widget.ts +0 -67
- package/src/types/interaction-context.ts +0 -82
- package/src/utils/animated-panel-toggle.ts +0 -62
- package/src/utils/animation-exclusivity.test.ts +0 -72
- package/src/utils/animation-exclusivity.ts +0 -40
- package/src/utils/banked-usage-baselines.ts +0 -90
- package/src/utils/collect-session-models.test.ts +0 -82
- package/src/utils/collect-session-models.ts +0 -65
- package/src/utils/condense-history.test.ts +0 -746
- package/src/utils/condense-history.ts +0 -448
- package/src/utils/cost-session-history.test.ts +0 -226
- package/src/utils/cost-session-history.ts +0 -218
- package/src/utils/derive-cost-session-title.test.ts +0 -63
- package/src/utils/derive-cost-session-title.ts +0 -76
- package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
- package/src/utils/flatten-sub-agent-messages.ts +0 -88
- package/src/utils/format-usd.ts +0 -26
- package/src/utils/history-transform.test.ts +0 -228
- package/src/utils/history-transform.ts +0 -163
- package/src/utils/index.ts +0 -3
- package/src/utils/logger.ts +0 -3
- package/src/utils/message-partition.test.ts +0 -129
- package/src/utils/message-partition.ts +0 -115
- package/src/utils/resolve-cost-history-config.test.ts +0 -38
- package/src/utils/resolve-cost-history-config.ts +0 -38
- package/src/utils/resolve-preference-baseline.test.ts +0 -84
- package/src/utils/resolve-preference-baseline.ts +0 -80
- package/src/utils/strip-agent-handlers.test.ts +0 -99
- package/src/utils/strip-agent-handlers.ts +0 -53
- package/src/utils/sum-costs.test.ts +0 -136
- package/src/utils/sum-costs.ts +0 -18
- package/src/utils/sum-tokens.test.ts +0 -93
- package/src/utils/sum-tokens.ts +0 -68
- package/src/utils/sum-usage.test.ts +0 -191
- package/src/utils/sum-usage.ts +0 -208
- package/src/utils/tool-fold.ts +0 -181
- package/src/utils/usage-rows.test.ts +0 -237
- package/src/utils/usage-rows.ts +0 -187
- package/src/utils/with-timeout.ts +0 -24
- package/tsconfig.json +0 -9
|
@@ -1,813 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AIProviderRegistry,
|
|
3
|
-
ChatAttachment,
|
|
4
|
-
ChatDriverResult,
|
|
5
|
-
ChatMessage,
|
|
6
|
-
ChatRequestOptions,
|
|
7
|
-
} from '@genesislcap/foundation-ai';
|
|
8
|
-
import { BudgetExhaustedError } from '@genesislcap/foundation-ai';
|
|
9
|
-
import type { ActivityBus } from '../../channel/ai-activity-bus';
|
|
10
|
-
import type {
|
|
11
|
-
AgentConfig,
|
|
12
|
-
FallbackAgentConfig,
|
|
13
|
-
SpecialistAgentConfig,
|
|
14
|
-
SystemPromptContext,
|
|
15
|
-
SystemPromptInput,
|
|
16
|
-
} from '../../config/config';
|
|
17
|
-
import { validateStaticAgentProviders } from '../../config/validate-providers';
|
|
18
|
-
import { recordMetaEvent } from '../../state/debug-event-log';
|
|
19
|
-
import type { DiagnosticEntry } from '../../state/persistence/diagnostics';
|
|
20
|
-
import type { InteractionContext } from '../../types/interaction-context';
|
|
21
|
-
import { transformHistoryForAgent } from '../../utils/history-transform';
|
|
22
|
-
import { logger } from '../../utils/logger';
|
|
23
|
-
import type { AiDriver, AllAgentSummary } from '../ai-driver/ai-driver';
|
|
24
|
-
import {
|
|
25
|
-
ChatDriver,
|
|
26
|
-
REQUEST_CONTINUATION_TOOL,
|
|
27
|
-
type TurnSnapshot,
|
|
28
|
-
} from '../chat-driver/chat-driver';
|
|
29
|
-
|
|
30
|
-
const DEFAULT_MAX_HANDOFFS = 3;
|
|
31
|
-
const DEFAULT_CLASSIFIER_HISTORY_LENGTH = 4;
|
|
32
|
-
const DEFAULT_CLASSIFIER_RETRIES = 2;
|
|
33
|
-
|
|
34
|
-
const REQUEST_CONTINUATION_DEFINITION = {
|
|
35
|
-
name: REQUEST_CONTINUATION_TOOL,
|
|
36
|
-
description:
|
|
37
|
-
"Call this when you have completed your part of the task but fulfilling the user's full request requires capabilities outside your domain. Do not call this if you can complete the request yourself. Pass a plain-language description of what still needs to be done — another specialist will be selected automatically.",
|
|
38
|
-
parameters: {
|
|
39
|
-
type: 'object',
|
|
40
|
-
required: ['summary', 'remaining_task'],
|
|
41
|
-
properties: {
|
|
42
|
-
summary: {
|
|
43
|
-
type: 'string',
|
|
44
|
-
description: 'What you found or did — passed as context to the next specialist.',
|
|
45
|
-
},
|
|
46
|
-
remaining_task: {
|
|
47
|
-
type: 'string',
|
|
48
|
-
description:
|
|
49
|
-
'What still needs to be done, in plain language. Used to route to the right specialist.',
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
function isSpecialist(agent: AgentConfig): agent is SpecialistAgentConfig {
|
|
56
|
-
return !agent.fallback;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isFallback(agent: AgentConfig): agent is FallbackAgentConfig {
|
|
60
|
-
return agent.fallback === true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function buildFallbackSystemPrompt(
|
|
64
|
-
fallback: FallbackAgentConfig,
|
|
65
|
-
specialists: SpecialistAgentConfig[],
|
|
66
|
-
): SystemPromptInput | undefined {
|
|
67
|
-
const agentList = specialists.map((s) => `- ${s.name}: ${s.description}`).join('\n');
|
|
68
|
-
if (typeof fallback.systemPrompt === 'string') {
|
|
69
|
-
return fallback.systemPrompt.replace('{{agents}}', agentList);
|
|
70
|
-
}
|
|
71
|
-
if (typeof fallback.systemPrompt === 'function') {
|
|
72
|
-
// Function-form fallback prompt — pass through unchanged. The `{{agents}}`
|
|
73
|
-
// substitution is a string-template convenience; consumers using the
|
|
74
|
-
// function form can compose the agent list themselves if they want it.
|
|
75
|
-
return fallback.systemPrompt;
|
|
76
|
-
}
|
|
77
|
-
return `You are a helpful assistant. You cannot directly help with the user's request, but the following specialists are available:\n\n${agentList}\n\nPolitely let the user know what you can help with and invite them to rephrase their request.`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Orchestrates multiple specialist agents. Sits between `FoundationAiAssistant`
|
|
82
|
-
* and `ChatDriver`, classifying each user message and routing it to the right
|
|
83
|
-
* specialist — each with its own focused system prompt, tools, and primer.
|
|
84
|
-
*
|
|
85
|
-
* @beta
|
|
86
|
-
*/
|
|
87
|
-
export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
88
|
-
private readonly chatDriver: ChatDriver;
|
|
89
|
-
private readonly specialists: SpecialistAgentConfig[];
|
|
90
|
-
private readonly fallback?: FallbackAgentConfig;
|
|
91
|
-
private readonly maxHandoffs: number;
|
|
92
|
-
private readonly classifierHistoryLength: number;
|
|
93
|
-
private readonly classifierRetries: number;
|
|
94
|
-
private readonly sessionKey: string;
|
|
95
|
-
/**
|
|
96
|
-
* Aborted on driver disposal. Threaded into `AgentLifecycleContext.signal`
|
|
97
|
-
* so long-running `onActivate` work can bail if the session disconnects.
|
|
98
|
-
*/
|
|
99
|
-
private readonly lifecycleAbortController = new AbortController();
|
|
100
|
-
/**
|
|
101
|
-
* Set by `cancel()` for the duration of a `sendMessage` run. Breaks the
|
|
102
|
-
* handoff loop so we don't classify or start another agent turn after the
|
|
103
|
-
* user stops. Reset at the start of each `sendMessage`.
|
|
104
|
-
*/
|
|
105
|
-
private cancelled = false;
|
|
106
|
-
/**
|
|
107
|
-
* Whether the CURRENT turn's user message has reached the inner driver's
|
|
108
|
-
* history. False through the pre-first-turn classify (where the only echo of
|
|
109
|
-
* the message is an optimistic `history-updated` dispatch), true from the
|
|
110
|
-
* moment `chatDriver.sendMessage` is entered — including through every later
|
|
111
|
-
* handoff classify. Read by the budget-wall catch in `sendMessage` to decide
|
|
112
|
-
* whether `reportBudgetExhausted` must append the message itself: appending
|
|
113
|
-
* it when already appended duplicated it; not appending it when unappended
|
|
114
|
-
* made it vanish. Reset at the top of each `runOrchestratedTurn`.
|
|
115
|
-
*/
|
|
116
|
-
private userMessageAppended = false;
|
|
117
|
-
/**
|
|
118
|
-
* Sticky user pick from the picker (or the host's `setAgent` API). Only
|
|
119
|
-
* changes on explicit user action. Survives flow completion: when a stateful
|
|
120
|
-
* agent releases, the user-pin stays put and routing falls back to it on
|
|
121
|
-
* the next turn.
|
|
122
|
-
*/
|
|
123
|
-
private pinnedAgentName: string | null = null;
|
|
124
|
-
/**
|
|
125
|
-
* Transient flow lock — set when a stateful agent's `onActivate` fires,
|
|
126
|
-
* cleared by `releaseActiveAgent`. While non-null, routing precedence goes
|
|
127
|
-
* to this agent and the picker is locked. Independent of
|
|
128
|
-
* `pinnedAgentName` so completing a flow doesn't wipe the user's sticky
|
|
129
|
-
* choice.
|
|
130
|
-
*/
|
|
131
|
-
private flowOwnerAgentName: string | null = null;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Per-agent restored state for a rehydrated session, keyed by agent name.
|
|
135
|
-
* Primed by the host on restore; each entry is passed to that agent's
|
|
136
|
-
* `onActivate` as `ctx.restored` on its next activation, then deleted so a
|
|
137
|
-
* later re-activation starts fresh.
|
|
138
|
-
*/
|
|
139
|
-
private restoredAgentStates: Record<string, unknown> = {};
|
|
140
|
-
|
|
141
|
-
activeAgent?: AgentConfig;
|
|
142
|
-
|
|
143
|
-
constructor(
|
|
144
|
-
private readonly providerRegistry: AIProviderRegistry,
|
|
145
|
-
private readonly agents: AgentConfig[],
|
|
146
|
-
options: {
|
|
147
|
-
sessionKey?: string;
|
|
148
|
-
maxHandoffs?: number;
|
|
149
|
-
classifierHistoryLength?: number;
|
|
150
|
-
classifierRetries?: number;
|
|
151
|
-
maxToolIterations?: number;
|
|
152
|
-
maxFoldOperations?: number;
|
|
153
|
-
maxTurnSnapshots?: number;
|
|
154
|
-
/** Activity bus passed through to the inner ChatDriver (browser host injects the singleton). */
|
|
155
|
-
activityBus?: ActivityBus;
|
|
156
|
-
/** Budget-wall transcript copy, passed through to the inner ChatDriver (GENC-1464). */
|
|
157
|
-
budgetExhaustedMessage?: string;
|
|
158
|
-
} = {},
|
|
159
|
-
) {
|
|
160
|
-
super();
|
|
161
|
-
this.sessionKey = options.sessionKey ?? '';
|
|
162
|
-
this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
|
|
163
|
-
this.classifierHistoryLength =
|
|
164
|
-
options.classifierHistoryLength ?? DEFAULT_CLASSIFIER_HISTORY_LENGTH;
|
|
165
|
-
this.classifierRetries = options.classifierRetries ?? DEFAULT_CLASSIFIER_RETRIES;
|
|
166
|
-
|
|
167
|
-
// Static-name validation: walk every agent (and nested sub-agents). Any
|
|
168
|
-
// `provider: '<name>'` that isn't registered, or resolves to a provider
|
|
169
|
-
// without `chat()`, throws now rather than at first turn.
|
|
170
|
-
validateStaticAgentProviders(agents, providerRegistry);
|
|
171
|
-
|
|
172
|
-
// Specialists drive the classifier. `excludeFromClassifier` agents are still
|
|
173
|
-
// resolvable by name (so manual pinning works) but never auto-routed.
|
|
174
|
-
this.specialists = agents.filter(isSpecialist).filter((a) => !a.excludeFromClassifier);
|
|
175
|
-
const fallbacks = agents.filter(isFallback);
|
|
176
|
-
if (fallbacks.length > 1) {
|
|
177
|
-
logger.warn(
|
|
178
|
-
'OrchestratingDriver: multiple fallback agents found — only the first will be used.',
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const rawFallback = fallbacks[0];
|
|
183
|
-
this.fallback = rawFallback
|
|
184
|
-
? { ...rawFallback, systemPrompt: buildFallbackSystemPrompt(rawFallback, this.specialists) }
|
|
185
|
-
: undefined;
|
|
186
|
-
|
|
187
|
-
this.chatDriver = new ChatDriver(providerRegistry, {
|
|
188
|
-
maxToolIterations: options.maxToolIterations,
|
|
189
|
-
maxFoldOperations: options.maxFoldOperations,
|
|
190
|
-
maxTurnSnapshots: options.maxTurnSnapshots,
|
|
191
|
-
sessionKey: this.sessionKey,
|
|
192
|
-
activityBus: options.activityBus,
|
|
193
|
-
budgetExhaustedMessage: options.budgetExhaustedMessage,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// Proxy events from the shared driver
|
|
197
|
-
this.chatDriver.addEventListener('history-updated', (e: Event) => {
|
|
198
|
-
this.dispatchEvent(new CustomEvent('history-updated', { detail: (e as CustomEvent).detail }));
|
|
199
|
-
});
|
|
200
|
-
this.chatDriver.addEventListener('sub-agent-history-updated', (e: Event) => {
|
|
201
|
-
this.dispatchEvent(
|
|
202
|
-
new CustomEvent('sub-agent-history-updated', { detail: (e as CustomEvent).detail }),
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
this.chatDriver.addEventListener('sub-agent-start', (e: Event) => {
|
|
206
|
-
this.dispatchEvent(new CustomEvent('sub-agent-start', { detail: (e as CustomEvent).detail }));
|
|
207
|
-
});
|
|
208
|
-
this.chatDriver.addEventListener('sub-agent-stop', (e: Event) => {
|
|
209
|
-
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: (e as CustomEvent).detail }));
|
|
210
|
-
});
|
|
211
|
-
this.chatDriver.addEventListener('interaction-start', (e: Event) => {
|
|
212
|
-
this.dispatchEvent(
|
|
213
|
-
new CustomEvent('interaction-start', { detail: (e as CustomEvent).detail }),
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
this.chatDriver.addEventListener('interaction-stop', (e: Event) => {
|
|
217
|
-
this.dispatchEvent(
|
|
218
|
-
new CustomEvent('interaction-stop', { detail: (e as CustomEvent).detail }),
|
|
219
|
-
);
|
|
220
|
-
});
|
|
221
|
-
this.chatDriver.addEventListener('provider-changed', (e: Event) => {
|
|
222
|
-
this.dispatchEvent(
|
|
223
|
-
new CustomEvent('provider-changed', { detail: (e as CustomEvent).detail }),
|
|
224
|
-
);
|
|
225
|
-
});
|
|
226
|
-
// Post-resolve external cost. The host only ever holds THIS wrapper (`createDriver` returns
|
|
227
|
-
// one for any non-empty `agents`), so an event the inner driver raises and this list omits is
|
|
228
|
-
// an event the host never sees: `recordExternalCost` still delegates and the money still lands
|
|
229
|
-
// on the message, but the totals never refresh and the save is never scheduled — and in the
|
|
230
|
-
// flow this exists for the interaction is the last thing in the session, so "stale until the
|
|
231
|
-
// next history change" means permanently. Reviewer catch on #2474.
|
|
232
|
-
this.chatDriver.addEventListener('external-cost-recorded', (e: Event) => {
|
|
233
|
-
this.dispatchEvent(
|
|
234
|
-
new CustomEvent('external-cost-recorded', { detail: (e as CustomEvent).detail }),
|
|
235
|
-
);
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
resolveInteraction(interactionId: string, result: unknown): void {
|
|
240
|
-
this.chatDriver.resolveInteraction(interactionId, result);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** Delegated for the same reason as `resolveInteraction`: the history that owns the
|
|
244
|
-
* interaction — and therefore the cost — lives on the wrapped driver, not here. */
|
|
245
|
-
recordExternalCost(interactionId: string, costUsd: number): boolean {
|
|
246
|
-
return this.chatDriver.recordExternalCost(interactionId, costUsd);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
getInteractionContext(interactionId: string): InteractionContext | undefined {
|
|
250
|
-
return this.chatDriver.getInteractionContext(interactionId);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
isBusy(): boolean {
|
|
254
|
-
return this.chatDriver.isBusy();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Currently active provider name from the underlying ChatDriver. */
|
|
258
|
-
getActiveProviderName(): string {
|
|
259
|
-
return this.chatDriver.getActiveProviderName();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Pins routing to a specific agent by name. While pinned, the classifier is
|
|
264
|
-
* skipped and the continuation tool is hidden from the agent's tool list, so
|
|
265
|
-
* the agent cannot quietly hand back to the orchestrator. Pass `null` to
|
|
266
|
-
* return to automatic routing.
|
|
267
|
-
*/
|
|
268
|
-
setPinnedAgent(name: string | null): void {
|
|
269
|
-
this.pinnedAgentName = name;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Set the flow-owner lock — used to restore state from Redux on driver
|
|
274
|
-
* reconstruction (agents-array changes, pop-in/pop-out). Not intended for
|
|
275
|
-
* external callers to manipulate routing; that's what `setPinnedAgent` is
|
|
276
|
-
* for.
|
|
277
|
-
*/
|
|
278
|
-
setFlowOwner(name: string | null): void {
|
|
279
|
-
this.flowOwnerAgentName = name;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
loadHistory(messages: ChatMessage[]): void {
|
|
283
|
-
this.chatDriver.loadHistory(messages);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
getRawHistory(): readonly ChatMessage[] {
|
|
287
|
-
return this.chatDriver.getHistory();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** {@inheritDoc AiDriver.compact} */
|
|
291
|
-
compact(): Promise<ChatMessage | null> {
|
|
292
|
-
return this.chatDriver.compact();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** {@inheritDoc AiDriver.canCompact} */
|
|
296
|
-
canCompact(): boolean {
|
|
297
|
-
return this.chatDriver.canCompact();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** Delegates to the inner {@link ChatDriver} — turns are captured there. */
|
|
301
|
-
getTurnSnapshots(): ReadonlyArray<TurnSnapshot> {
|
|
302
|
-
return this.chatDriver.getTurnSnapshots();
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** Delegates to the inner {@link ChatDriver} — interactions resolve there, so it holds the buffer. */
|
|
306
|
-
getExternalDiagnostics(): ReadonlyArray<DiagnosticEntry> {
|
|
307
|
-
return this.chatDriver.getExternalDiagnostics();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async getSuggestions(
|
|
311
|
-
history: ChatMessage[],
|
|
312
|
-
prompt: string,
|
|
313
|
-
count: number,
|
|
314
|
-
allAgentInfo?: AllAgentSummary[],
|
|
315
|
-
): Promise<string[]> {
|
|
316
|
-
// When pinned to a specialist, scope suggestions to that agent so the
|
|
317
|
-
// proposed prompts reflect the active routing instead of the full panel.
|
|
318
|
-
const pinned = this.resolvePinnedAgent();
|
|
319
|
-
const candidates = pinned && isSpecialist(pinned) ? [pinned] : this.specialists;
|
|
320
|
-
const agentInfo = candidates.map((s) => ({
|
|
321
|
-
name: s.name,
|
|
322
|
-
description: s.description,
|
|
323
|
-
// Suggestions use tool names for prompt hints. Dynamic agents resolve
|
|
324
|
-
// their tools per-turn against a SystemPromptContext we don't have here
|
|
325
|
-
// — pass an empty list rather than invoke the factory with a fake one.
|
|
326
|
-
tools: Array.isArray(s.toolDefinitions) ? s.toolDefinitions : [],
|
|
327
|
-
}));
|
|
328
|
-
return this.chatDriver.getSuggestions(history, prompt, count, agentInfo);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async sendMessage(input: string, attachments?: ChatAttachment[]): Promise<ChatDriverResult> {
|
|
332
|
-
try {
|
|
333
|
-
return await this.runOrchestratedTurn(input, attachments);
|
|
334
|
-
} catch (e) {
|
|
335
|
-
// The classifier calls the provider directly, outside any ChatDriver turn,
|
|
336
|
-
// so a budget wall raised there would escape as an unhandled rejection —
|
|
337
|
-
// `FoundationAiAssistant.send()` has a `finally` but no `catch`. Land it on
|
|
338
|
-
// the driver that owns the transcript, which appends the user-facing copy
|
|
339
|
-
// and returns the same terminal result an in-loop wall would.
|
|
340
|
-
//
|
|
341
|
-
// The user's message rides along ONLY while it is genuinely unappended.
|
|
342
|
-
// `classify()` runs at two seams: before the first agent turn — where the
|
|
343
|
-
// `history-updated` dispatch in `runOrchestratedTurn` is optimistic only
|
|
344
|
-
// and the real append lives inside `chatDriver.sendMessage`, which the
|
|
345
|
-
// wall prevents from ever running — and again at the END of every handoff
|
|
346
|
-
// iteration, by which point `chatDriver.sendMessage` HAS appended it.
|
|
347
|
-
// Passing the message unconditionally duplicated it on the handoff seam:
|
|
348
|
-
// `['user','assistant','tool','user','assistant']` in the transcript, the
|
|
349
|
-
// snapshot, and the history sent to the model once the budget is raised.
|
|
350
|
-
if (e instanceof BudgetExhaustedError) {
|
|
351
|
-
return this.chatDriver.reportBudgetExhausted(
|
|
352
|
-
e,
|
|
353
|
-
this.userMessageAppended
|
|
354
|
-
? undefined
|
|
355
|
-
: {
|
|
356
|
-
role: 'user',
|
|
357
|
-
content: input,
|
|
358
|
-
...(attachments ? { attachments } : {}),
|
|
359
|
-
},
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
throw e;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private async runOrchestratedTurn(
|
|
367
|
-
input: string,
|
|
368
|
-
attachments?: ChatAttachment[],
|
|
369
|
-
): Promise<ChatDriverResult> {
|
|
370
|
-
this.cancelled = false;
|
|
371
|
-
this.userMessageAppended = false;
|
|
372
|
-
const history = this.chatDriver.getHistory() as ChatMessage[];
|
|
373
|
-
|
|
374
|
-
// Emit the user message immediately so the UI reflects it during the classify
|
|
375
|
-
// round-trip — without this the chat appears frozen until classify returns.
|
|
376
|
-
this.dispatchEvent(
|
|
377
|
-
new CustomEvent('history-updated', {
|
|
378
|
-
detail: [...history, { role: 'user', content: input, attachments }],
|
|
379
|
-
}),
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
const pinned = this.resolvePinnedAgent();
|
|
383
|
-
if (!pinned) this.dispatchEvent(new CustomEvent('orchestrating-start'));
|
|
384
|
-
let currentAgent = pinned ?? (await this.classify(input, history, this.activeAgent));
|
|
385
|
-
let isHandoff = false;
|
|
386
|
-
let handoffs = 0;
|
|
387
|
-
let handoffSummary = '';
|
|
388
|
-
let remainingTask = '';
|
|
389
|
-
// The outcome of the agent turn that ENDS the loop — see the return below.
|
|
390
|
-
let lastResult: ChatDriverResult | undefined;
|
|
391
|
-
|
|
392
|
-
while (true) {
|
|
393
|
-
// Cancelled before a (next) agent turn started — e.g. during classify.
|
|
394
|
-
// The chat driver appends its own "Stopped." marker when a turn it was
|
|
395
|
-
// running is cancelled, so we just stop here without a duplicate.
|
|
396
|
-
if (this.cancelled) break;
|
|
397
|
-
|
|
398
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
399
|
-
await this.applyAgent(currentAgent);
|
|
400
|
-
|
|
401
|
-
let result: ChatDriverResult;
|
|
402
|
-
if (isHandoff) {
|
|
403
|
-
const contextPrimer: ChatMessage[] = handoffSummary
|
|
404
|
-
? [{ role: 'user', content: `[Context from previous agent]: ${handoffSummary}` }]
|
|
405
|
-
: [];
|
|
406
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
407
|
-
result = await this.chatDriver.continueFromHistory(contextPrimer);
|
|
408
|
-
} else {
|
|
409
|
-
// From here the inner driver owns the user message — it appends it as
|
|
410
|
-
// its first act, before any provider call can throw. Set BEFORE the
|
|
411
|
-
// await so a wall raised anywhere inside the turn (or on a later
|
|
412
|
-
// handoff classify) sees the append as done.
|
|
413
|
-
this.userMessageAppended = true;
|
|
414
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
415
|
-
result = await this.chatDriver.sendMessage(input, attachments);
|
|
416
|
-
}
|
|
417
|
-
lastResult = result;
|
|
418
|
-
|
|
419
|
-
// Release check: a stateful agent called `releaseAgent` from a terminal
|
|
420
|
-
// tool handler. Fire onDeactivate, clear the pin, drop the user back to
|
|
421
|
-
// classifier-mode. The LLM has already emitted its final wrap-up message
|
|
422
|
-
// by the time we get here — release is purely a teardown.
|
|
423
|
-
if (this.chatDriver.getAgentReleaseRequested()) {
|
|
424
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
425
|
-
await this.releaseActiveAgent();
|
|
426
|
-
break;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Pinned agents never hand off — the continuation tool is filtered out in
|
|
430
|
-
// applyAgent, but this guards against a model hallucinating a handoff result.
|
|
431
|
-
if (result.reason !== 'agent-handoff' || isFallback(currentAgent) || pinned) {
|
|
432
|
-
break;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
handoffs += 1;
|
|
436
|
-
if (handoffs > this.maxHandoffs) {
|
|
437
|
-
this.appendInlineMessage(
|
|
438
|
-
`I wasn't able to fully complete your request — the task required more hand-offs between specialists than allowed. Please try breaking your request into smaller steps.`,
|
|
439
|
-
);
|
|
440
|
-
break;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
handoffSummary = result.summary;
|
|
444
|
-
remainingTask = result.remainingTask;
|
|
445
|
-
isHandoff = true;
|
|
446
|
-
|
|
447
|
-
const updatedHistory = this.chatDriver.getHistory() as ChatMessage[];
|
|
448
|
-
this.dispatchEvent(new CustomEvent('orchestrating-start'));
|
|
449
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
450
|
-
currentAgent = await this.classify(remainingTask, updatedHistory);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// The orchestrated turn reports the outcome of the agent turn that ENDED it.
|
|
454
|
-
// The hardcoded `{ reason: 'done' }` that used to live here made every
|
|
455
|
-
// typed failure unreachable for an agents-configured host — including
|
|
456
|
-
// `'budget-exhausted'`, which the element latches its blocked state off.
|
|
457
|
-
//
|
|
458
|
-
// "Last wins" cannot hide an earlier failure behind a later success: a
|
|
459
|
-
// failing inner turn always ends the loop, because the loop only continues
|
|
460
|
-
// on `reason === 'agent-handoff'`. The two breaks that hold a non-'done'
|
|
461
|
-
// result (the handoff cap, and the pinned/fallback break) both carry
|
|
462
|
-
// `'agent-handoff'`, which is an internal routing protocol that must never
|
|
463
|
-
// escape to the host — the collapse below strips it. A cancel before the
|
|
464
|
-
// first turn leaves `lastResult` undefined; the release break returns the
|
|
465
|
-
// inner turn's real result, which is correct, because that turn ran.
|
|
466
|
-
return lastResult?.reason === 'done' ? lastResult : { reason: 'done' };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
async continueFromHistory(transientPrimer?: ChatMessage[]): Promise<ChatDriverResult> {
|
|
470
|
-
return this.chatDriver.continueFromHistory(transientPrimer);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/** {@inheritDoc AiDriver.primeRestoredAgentState} */
|
|
474
|
-
primeRestoredAgentState(snapshots: Record<string, unknown>): void {
|
|
475
|
-
this.restoredAgentStates = { ...snapshots };
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/** {@inheritDoc AiDriver.getActiveAgent} */
|
|
479
|
-
getActiveAgent(): AgentConfig | undefined {
|
|
480
|
-
return this.activeAgent;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
private async applyAgent(agent: AgentConfig): Promise<void> {
|
|
484
|
-
const previousAgent = this.activeAgent;
|
|
485
|
-
const isSwitch = !previousAgent || previousAgent.name !== agent.name;
|
|
486
|
-
|
|
487
|
-
// Record the agent transition on the debug-log timeline. A `null` `from`
|
|
488
|
-
// marks the initial activation; a named `from` is a handoff between agents.
|
|
489
|
-
if (isSwitch) {
|
|
490
|
-
recordMetaEvent(this.sessionKey, 'agent.handoff', {
|
|
491
|
-
from: previousAgent?.name ?? null,
|
|
492
|
-
to: agent.name,
|
|
493
|
-
// GENC-1351: was a restored snapshot waiting for this agent at activation?
|
|
494
|
-
restored: this.restoredAgentStates[agent.name] !== undefined,
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Fire lifecycle hooks around the swap — outgoing first, then incoming.
|
|
499
|
-
// Both are awaited so a heavy `onActivate` (e.g. machine restore) completes
|
|
500
|
-
// before the agent's first turn runs.
|
|
501
|
-
if (isSwitch && previousAgent?.onDeactivate) {
|
|
502
|
-
try {
|
|
503
|
-
await previousAgent.onDeactivate({
|
|
504
|
-
agentName: previousAgent.name,
|
|
505
|
-
sessionKey: this.sessionKey,
|
|
506
|
-
signal: this.lifecycleAbortController.signal,
|
|
507
|
-
});
|
|
508
|
-
} catch (e) {
|
|
509
|
-
logger.warn(`OrchestratingDriver: onDeactivate("${previousAgent.name}") threw:`, e);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
if (isSwitch && agent.onActivate) {
|
|
513
|
-
// On a session restore, hand this agent its persisted state exactly once
|
|
514
|
-
// (consumed here) so a stateful machine resumes at the right step; a later
|
|
515
|
-
// re-activation of the same agent then starts fresh.
|
|
516
|
-
const restored = this.restoredAgentStates[agent.name];
|
|
517
|
-
if (restored !== undefined) delete this.restoredAgentStates[agent.name];
|
|
518
|
-
try {
|
|
519
|
-
await agent.onActivate({
|
|
520
|
-
agentName: agent.name,
|
|
521
|
-
sessionKey: this.sessionKey,
|
|
522
|
-
previousAgentName: previousAgent?.name,
|
|
523
|
-
signal: this.lifecycleAbortController.signal,
|
|
524
|
-
restored,
|
|
525
|
-
});
|
|
526
|
-
} catch (e) {
|
|
527
|
-
logger.warn(`OrchestratingDriver: onActivate("${agent.name}") threw:`, e);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const hasLifecycleHooks = !!(agent.onActivate || agent.onDeactivate);
|
|
532
|
-
|
|
533
|
-
// Stateful agents auto-lock the flow on activation. The lock guarantees
|
|
534
|
-
// the machine survives subsequent turns (the classifier would otherwise be
|
|
535
|
-
// free to route away mid-flow, tearing the machine down). Release happens
|
|
536
|
-
// when the agent calls `releaseAgent` from a terminal-state tool handler
|
|
537
|
-
// — see the post-sendMessage check below. We write `flowOwnerAgentName`
|
|
538
|
-
// rather than `pinnedAgentName` so completing a flow doesn't wipe the
|
|
539
|
-
// user's sticky picker choice.
|
|
540
|
-
if (isSwitch && hasLifecycleHooks && this.flowOwnerAgentName !== agent.name) {
|
|
541
|
-
this.flowOwnerAgentName = agent.name;
|
|
542
|
-
this.dispatchEvent(new CustomEvent('flow-owner-changed', { detail: agent.name }));
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Terminal agents do not get the cross-agent handoff tool. Four cases:
|
|
546
|
-
// • fallback — already a leaf; handoff would loop
|
|
547
|
-
// • user-pinned — user explicitly selected this agent; do not auto-route away
|
|
548
|
-
// • flow-owner — a stateful flow is mid-stream; do not auto-route away
|
|
549
|
-
// • stateful — agents with lifecycle hooks own state for the duration of
|
|
550
|
-
// their flow. Initiating a handoff mid-flow would abandon
|
|
551
|
-
// that state with no clean exit and dump the user into the
|
|
552
|
-
// classifier mid-machine. Capture the tool loop until the
|
|
553
|
-
// user (or the agent itself, via `releaseAgent`) releases.
|
|
554
|
-
const isTerminal =
|
|
555
|
-
isFallback(agent) ||
|
|
556
|
-
this.pinnedAgentName !== null ||
|
|
557
|
-
this.flowOwnerAgentName !== null ||
|
|
558
|
-
hasLifecycleHooks;
|
|
559
|
-
|
|
560
|
-
let agentToApply: AgentConfig = agent;
|
|
561
|
-
if (!isTerminal) {
|
|
562
|
-
const declaredTools = agent.toolDefinitions;
|
|
563
|
-
agentToApply = {
|
|
564
|
-
...agent,
|
|
565
|
-
toolDefinitions:
|
|
566
|
-
typeof declaredTools === 'function'
|
|
567
|
-
? async (ctx: SystemPromptContext) => [
|
|
568
|
-
...(await declaredTools(ctx)),
|
|
569
|
-
REQUEST_CONTINUATION_DEFINITION,
|
|
570
|
-
]
|
|
571
|
-
: [...(declaredTools ?? []), REQUEST_CONTINUATION_DEFINITION],
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (previousAgent && previousAgent.name !== agent.name) {
|
|
576
|
-
const rawHistory = this.chatDriver.getHistory() as ChatMessage[];
|
|
577
|
-
this.chatDriver.loadHistory([
|
|
578
|
-
...rawHistory,
|
|
579
|
-
{ role: 'system-event', content: agent.name, timestamp: new Date().toISOString() },
|
|
580
|
-
]);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
|
|
584
|
-
this.chatDriver.applyAgent(agentToApply);
|
|
585
|
-
this.activeAgent = agent;
|
|
586
|
-
this.dispatchEvent(new CustomEvent('orchestrating-stop'));
|
|
587
|
-
this.dispatchEvent(new CustomEvent('agent-changed', { detail: agent }));
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Release the current stateful agent: fire `onDeactivate`, clear the
|
|
592
|
-
* flow-owner lock, dispatch events so the host (and Redux) reflect the
|
|
593
|
-
* unlocked state. Called automatically when a tool handler invokes
|
|
594
|
-
* `context.releaseAgent`.
|
|
595
|
-
*
|
|
596
|
-
* The user-pin (`pinnedAgentName`) is intentionally *not* cleared — a user
|
|
597
|
-
* who explicitly picked this agent should stay on it after the flow ends,
|
|
598
|
-
* so the next message starts a fresh flow on the same agent rather than
|
|
599
|
-
* routing through the classifier.
|
|
600
|
-
*/
|
|
601
|
-
private async releaseActiveAgent(): Promise<void> {
|
|
602
|
-
const agent = this.activeAgent;
|
|
603
|
-
if (!agent) return;
|
|
604
|
-
if (agent.onDeactivate) {
|
|
605
|
-
try {
|
|
606
|
-
await agent.onDeactivate({
|
|
607
|
-
agentName: agent.name,
|
|
608
|
-
sessionKey: this.sessionKey,
|
|
609
|
-
signal: this.lifecycleAbortController.signal,
|
|
610
|
-
});
|
|
611
|
-
} catch (e) {
|
|
612
|
-
logger.warn(`OrchestratingDriver: release onDeactivate("${agent.name}") threw:`, e);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
this.activeAgent = undefined;
|
|
616
|
-
if (this.flowOwnerAgentName !== null) {
|
|
617
|
-
this.flowOwnerAgentName = null;
|
|
618
|
-
this.dispatchEvent(new CustomEvent('flow-owner-changed', { detail: null }));
|
|
619
|
-
}
|
|
620
|
-
this.dispatchEvent(new CustomEvent('agent-released', { detail: agent }));
|
|
621
|
-
this.dispatchEvent(new CustomEvent('agent-changed', { detail: undefined }));
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Stop the current turn (user "stop" button). Cancels the inner chat driver's
|
|
626
|
-
* in-flight request and breaks the handoff loop so no further agent turn or
|
|
627
|
-
* classify runs. The driver stays usable for the next message.
|
|
628
|
-
*/
|
|
629
|
-
cancel(): void {
|
|
630
|
-
this.cancelled = true;
|
|
631
|
-
this.chatDriver.cancel();
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Fire `onDeactivate` on the current active agent and abort any pending
|
|
636
|
-
* lifecycle work. Called by the host on session teardown so machines can
|
|
637
|
-
* release resources cleanly.
|
|
638
|
-
*/
|
|
639
|
-
async dispose(): Promise<void> {
|
|
640
|
-
const previousAgent = this.activeAgent;
|
|
641
|
-
if (previousAgent?.onDeactivate) {
|
|
642
|
-
try {
|
|
643
|
-
await previousAgent.onDeactivate({
|
|
644
|
-
agentName: previousAgent.name,
|
|
645
|
-
sessionKey: this.sessionKey,
|
|
646
|
-
signal: this.lifecycleAbortController.signal,
|
|
647
|
-
});
|
|
648
|
-
} catch (e) {
|
|
649
|
-
logger.warn(`OrchestratingDriver: dispose onDeactivate("${previousAgent.name}") threw:`, e);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
this.lifecycleAbortController.abort();
|
|
653
|
-
// Tear down the inner driver too, so any in-flight provider request aborts.
|
|
654
|
-
this.chatDriver.dispose();
|
|
655
|
-
this.activeAgent = undefined;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
private async classify(
|
|
659
|
-
input: string,
|
|
660
|
-
history: ChatMessage[],
|
|
661
|
-
contextAgent?: AgentConfig,
|
|
662
|
-
): Promise<AgentConfig> {
|
|
663
|
-
// Single-candidate short-circuits. No point asking the LLM to route
|
|
664
|
-
// when there's only one viable choice. Skipped if a fallback is
|
|
665
|
-
// configured — that's an explicit "escape hatch" signal from the
|
|
666
|
-
// consumer, and we preserve the LLM's ability to send unrelated
|
|
667
|
-
// messages there by returning -1 from select_agent.
|
|
668
|
-
if (this.specialists.length === 1 && !this.fallback) {
|
|
669
|
-
return this.specialists[0];
|
|
670
|
-
}
|
|
671
|
-
if (this.specialists.length === 0) {
|
|
672
|
-
// No classifier-eligible specialists. If exactly one non-fallback
|
|
673
|
-
// agent exists (typically a stateful agent flagged
|
|
674
|
-
// `excludeFromClassifier`) and there's no fallback to preserve as an
|
|
675
|
-
// escape hatch, route to it — this is what fixes the previously-
|
|
676
|
-
// silent single-stateful-agent case. Otherwise drop to the fallback;
|
|
677
|
-
// excluded specialists remain reachable via manual pin.
|
|
678
|
-
if (!this.fallback) {
|
|
679
|
-
const routable = this.agents.filter((a) => !isFallback(a));
|
|
680
|
-
if (routable.length === 1) {
|
|
681
|
-
return routable[0];
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
return this.fallback ?? { name: 'Assistant', fallback: true };
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
const agentList = this.specialists
|
|
688
|
-
.map((a, i) => `${i}: ${a.name} — ${a.description}`)
|
|
689
|
-
.join('\n');
|
|
690
|
-
|
|
691
|
-
const recentMessages = history
|
|
692
|
-
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
693
|
-
.slice(-this.classifierHistoryLength)
|
|
694
|
-
.map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
|
|
695
|
-
.join('\n');
|
|
696
|
-
|
|
697
|
-
const continuationHint =
|
|
698
|
-
contextAgent && isSpecialist(contextAgent)
|
|
699
|
-
? `\n\nThe previous turn was handled by "${contextAgent.name}". If the new message is a refinement, correction, or continuation of that work, prefer "${contextAgent.name}" — unless the topic has clearly shifted to another specialist's domain.`
|
|
700
|
-
: '';
|
|
701
|
-
|
|
702
|
-
const classifierPrompt = `You are a routing classifier. Call select_agent with the index of the best matching agent for the user message. Use -1 only if the request is entirely unrelated to every agent listed — prefer the closest match when in doubt. If the request spans multiple agents, pick the one that should act first; I will re-classify and route again once that agent is done.\n\nAgents:\n${agentList}${continuationHint}${recentMessages ? `\n\nRecent conversation:\n${recentMessages}` : ''}`;
|
|
703
|
-
|
|
704
|
-
const routingTool = {
|
|
705
|
-
name: 'select_agent',
|
|
706
|
-
description: 'Select the most appropriate agent for the user message.',
|
|
707
|
-
parameters: {
|
|
708
|
-
type: 'object',
|
|
709
|
-
required: ['agent_index'],
|
|
710
|
-
properties: {
|
|
711
|
-
agent_index: {
|
|
712
|
-
type: 'integer',
|
|
713
|
-
description: 'Zero-based index of the matching agent, or -1 if no agent matches.',
|
|
714
|
-
},
|
|
715
|
-
},
|
|
716
|
-
},
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
// True only when the classifier RAN and answered "no match" (index -1). The
|
|
720
|
-
// no-fallback apology below is gated on it: an exhausted-retries exit never
|
|
721
|
-
// earned that apology, and emitting it there put a routing excuse ahead of
|
|
722
|
-
// the real error the routed turn is about to produce.
|
|
723
|
-
let routedNoMatch = false;
|
|
724
|
-
|
|
725
|
-
for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
|
|
726
|
-
try {
|
|
727
|
-
const options: ChatRequestOptions = {
|
|
728
|
-
systemPrompt: classifierPrompt,
|
|
729
|
-
tools: [routingTool],
|
|
730
|
-
};
|
|
731
|
-
// Classification is orchestrator-level, not bound to any single agent —
|
|
732
|
-
// always run against the registry default.
|
|
733
|
-
const classifierProvider = this.providerRegistry.default();
|
|
734
|
-
if (!classifierProvider.chat) {
|
|
735
|
-
logger.warn(
|
|
736
|
-
'OrchestratingDriver: default AI provider does not implement chat() — cannot classify.',
|
|
737
|
-
);
|
|
738
|
-
break;
|
|
739
|
-
}
|
|
740
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
741
|
-
const response = await classifierProvider.chat([], input, options);
|
|
742
|
-
const tc = response.toolCalls?.[0];
|
|
743
|
-
const index = tc?.name === 'select_agent' ? (tc.args.agent_index as number) : -1;
|
|
744
|
-
|
|
745
|
-
if (index >= 0 && index < this.specialists.length) {
|
|
746
|
-
return this.specialists[index];
|
|
747
|
-
}
|
|
748
|
-
// index === -1 — fall through to fallback
|
|
749
|
-
routedNoMatch = true;
|
|
750
|
-
break;
|
|
751
|
-
} catch (e) {
|
|
752
|
-
// Terminal: no ladder clears a budget wall. Short-circuit the
|
|
753
|
-
// classifier's own retries the same way 402 short-circuits the
|
|
754
|
-
// transport's — this loop is a SECOND retry ladder stacked on that one,
|
|
755
|
-
// so without this a single wall costs `classifierRetries + 1` doomed
|
|
756
|
-
// classifier calls and then a doomed fallback turn on top. Deliberately
|
|
757
|
-
// narrow: a generic classifier failure must still retry and fall back.
|
|
758
|
-
if (e instanceof BudgetExhaustedError) throw e;
|
|
759
|
-
logger.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
|
|
760
|
-
if (attempt === this.classifierRetries) {
|
|
761
|
-
logger.error('OrchestratingDriver: classifier failed after all retries, using fallback');
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
if (this.fallback) return this.fallback;
|
|
767
|
-
|
|
768
|
-
// No fallback configured. Route to the first specialist as a no-op — but
|
|
769
|
-
// only APOLOGISE when the classifier actually said "none of these". On an
|
|
770
|
-
// exhausted-retries exit the classifier never expressed an opinion, so the
|
|
771
|
-
// apology would be a routing excuse the user reads immediately above the
|
|
772
|
-
// real error that turn is about to produce.
|
|
773
|
-
if (routedNoMatch) {
|
|
774
|
-
const specialistNames = this.specialists.map((s) => s.name).join(', ');
|
|
775
|
-
this.appendInlineMessage(
|
|
776
|
-
`I'm not sure how to help with that. I can assist with: ${specialistNames}.`,
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
return this.specialists[0];
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Returns the agent that the current turn is locked to, if any. Flow-owner
|
|
784
|
-
* (transient, set by stateful-agent activation) wins over user-pin (sticky,
|
|
785
|
-
* set by picker click). `undefined` falls back to the classifier.
|
|
786
|
-
*
|
|
787
|
-
* Logs and returns `undefined` if a name no longer matches the agents array
|
|
788
|
-
* — protects against stale state after an agents-array reassignment.
|
|
789
|
-
*/
|
|
790
|
-
private resolvePinnedAgent(): AgentConfig | undefined {
|
|
791
|
-
const name = this.flowOwnerAgentName ?? this.pinnedAgentName;
|
|
792
|
-
if (name === null) return undefined;
|
|
793
|
-
const match = this.agents.find((a) => a.name === name);
|
|
794
|
-
if (!match) {
|
|
795
|
-
logger.warn(
|
|
796
|
-
`OrchestratingDriver: pinned agent "${name}" not found in agents array — falling back to classifier.`,
|
|
797
|
-
);
|
|
798
|
-
return undefined;
|
|
799
|
-
}
|
|
800
|
-
return match;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
private appendInlineMessage(content: string): void {
|
|
804
|
-
const history = this.chatDriver.getHistory() as ChatMessage[];
|
|
805
|
-
this.chatDriver.loadHistory([
|
|
806
|
-
...history,
|
|
807
|
-
{ role: 'assistant', content, timestamp: new Date().toISOString() },
|
|
808
|
-
]);
|
|
809
|
-
this.dispatchEvent(
|
|
810
|
-
new CustomEvent('history-updated', { detail: this.chatDriver.getHistory() }),
|
|
811
|
-
);
|
|
812
|
-
}
|
|
813
|
-
}
|