@eclipse-docks/extension-ai-system 0.7.139 → 0.7.141
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/README.md +72 -0
- package/dist/api.d.ts +1 -7
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- package/dist/cognitive-extension-CG_d39K5.js +2129 -0
- package/dist/cognitive-extension-CG_d39K5.js.map +1 -0
- package/dist/cognitive-extension.d.ts +1 -0
- package/dist/cognitive-extension.d.ts.map +1 -0
- package/dist/i18n.de-BrLTiN56.js +11 -0
- package/dist/i18n.de-BrLTiN56.js.map +1 -0
- package/dist/i18n.en-MkwuLlOD.js +11 -0
- package/dist/i18n.en-MkwuLlOD.js.map +1 -0
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/attachments.d.ts +89 -0
- package/dist/runtime/attachments.d.ts.map +1 -0
- package/dist/runtime/cognitive-runtime.d.ts +83 -0
- package/dist/runtime/cognitive-runtime.d.ts.map +1 -0
- package/dist/runtime/compact/compaction.d.ts +29 -0
- package/dist/runtime/compact/compaction.d.ts.map +1 -0
- package/dist/runtime/constants.d.ts +9 -0
- package/dist/runtime/constants.d.ts.map +1 -0
- package/dist/runtime/context/usage.d.ts +35 -0
- package/dist/runtime/context/usage.d.ts.map +1 -0
- package/dist/runtime/core/index.d.ts +4 -0
- package/dist/runtime/core/index.d.ts.map +1 -0
- package/dist/runtime/core/needs.d.ts +5 -0
- package/dist/runtime/core/needs.d.ts.map +1 -0
- package/dist/runtime/core/plutchik.d.ts +8 -0
- package/dist/runtime/core/plutchik.d.ts.map +1 -0
- package/dist/runtime/core/tick.d.ts +23 -0
- package/dist/runtime/core/tick.d.ts.map +1 -0
- package/dist/runtime/events.d.ts +21 -0
- package/dist/runtime/events.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +14 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/memory/index.d.ts +2 -0
- package/dist/runtime/memory/index.d.ts.map +1 -0
- package/dist/runtime/memory/store.d.ts +27 -0
- package/dist/runtime/memory/store.d.ts.map +1 -0
- package/dist/runtime/models/effort-support.d.ts +31 -0
- package/dist/runtime/models/effort-support.d.ts.map +1 -0
- package/dist/runtime/models/index.d.ts +7 -0
- package/dist/runtime/models/index.d.ts.map +1 -0
- package/dist/runtime/models/openai-compatible.d.ts +39 -0
- package/dist/runtime/models/openai-compatible.d.ts.map +1 -0
- package/dist/runtime/models/provider-config.d.ts +23 -0
- package/dist/runtime/models/provider-config.d.ts.map +1 -0
- package/dist/runtime/models/provider-registry.d.ts +16 -0
- package/dist/runtime/models/provider-registry.d.ts.map +1 -0
- package/dist/runtime/models/router.d.ts +12 -0
- package/dist/runtime/models/router.d.ts.map +1 -0
- package/dist/runtime/models/types.d.ts +88 -0
- package/dist/runtime/models/types.d.ts.map +1 -0
- package/dist/runtime/session/index.d.ts +2 -0
- package/dist/runtime/session/index.d.ts.map +1 -0
- package/dist/runtime/session/session-runtime.d.ts +48 -0
- package/dist/runtime/session/session-runtime.d.ts.map +1 -0
- package/dist/runtime/specialists/classify-risk.d.ts +15 -0
- package/dist/runtime/specialists/classify-risk.d.ts.map +1 -0
- package/dist/runtime/specialists/index.d.ts +12 -0
- package/dist/runtime/specialists/index.d.ts.map +1 -0
- package/dist/runtime/specialists/loop.d.ts +25 -0
- package/dist/runtime/specialists/loop.d.ts.map +1 -0
- package/dist/runtime/tools/attachment-adapter.d.ts +6 -0
- package/dist/runtime/tools/attachment-adapter.d.ts.map +1 -0
- package/dist/runtime/tools/broker.d.ts +52 -0
- package/dist/runtime/tools/broker.d.ts.map +1 -0
- package/dist/runtime/tools/command-adapter.d.ts +5 -0
- package/dist/runtime/tools/command-adapter.d.ts.map +1 -0
- package/dist/runtime/tools/index.d.ts +5 -0
- package/dist/runtime/tools/index.d.ts.map +1 -0
- package/dist/runtime/tools/workspace-adapter.d.ts +4 -0
- package/dist/runtime/tools/workspace-adapter.d.ts.map +1 -0
- package/dist/runtime/types.d.ts +145 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime-DfNGzEjU.js +2952 -0
- package/dist/runtime-DfNGzEjU.js.map +1 -0
- package/dist/view/cognitive-settings-editor.d.ts +40 -0
- package/dist/view/cognitive-settings-editor.d.ts.map +1 -0
- package/dist/view/cognitive-view.d.ts +83 -0
- package/dist/view/cognitive-view.d.ts.map +1 -0
- package/package.json +15 -11
- package/dist/agents/agent-registry.d.ts +0 -8
- package/dist/agents/agent-registry.d.ts.map +0 -1
- package/dist/agents/index.d.ts +0 -6
- package/dist/agents/index.d.ts.map +0 -1
- package/dist/agents/message-processor.d.ts +0 -9
- package/dist/agents/message-processor.d.ts.map +0 -1
- package/dist/agents/orchestrator.d.ts +0 -12
- package/dist/agents/orchestrator.d.ts.map +0 -1
- package/dist/agents/prompt-builder.d.ts +0 -21
- package/dist/agents/prompt-builder.d.ts.map +0 -1
- package/dist/agents/reviewer.d.ts +0 -15
- package/dist/agents/reviewer.d.ts.map +0 -1
- package/dist/ai-service-Cmafte5S.js +0 -1866
- package/dist/ai-service-Cmafte5S.js.map +0 -1
- package/dist/ai-system-extension-CsfSiwNJ.js +0 -2324
- package/dist/ai-system-extension-CsfSiwNJ.js.map +0 -1
- package/dist/ai-system-extension.d.ts +0 -1
- package/dist/ai-system-extension.d.ts.map +0 -1
- package/dist/chat-provider-contributions.d.ts +0 -2
- package/dist/chat-provider-contributions.d.ts.map +0 -1
- package/dist/core/constants.d.ts +0 -21
- package/dist/core/constants.d.ts.map +0 -1
- package/dist/core/index.d.ts +0 -4
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/interfaces.d.ts +0 -138
- package/dist/core/interfaces.d.ts.map +0 -1
- package/dist/core/message-utils.d.ts +0 -4
- package/dist/core/message-utils.d.ts.map +0 -1
- package/dist/core/types.d.ts +0 -128
- package/dist/core/types.d.ts.map +0 -1
- package/dist/prompt-enhancer-contributions.d.ts +0 -2
- package/dist/prompt-enhancer-contributions.d.ts.map +0 -1
- package/dist/providers/base-provider.d.ts +0 -15
- package/dist/providers/base-provider.d.ts.map +0 -1
- package/dist/providers/index.d.ts +0 -9
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/ollama-provider.d.ts +0 -7
- package/dist/providers/ollama-provider.d.ts.map +0 -1
- package/dist/providers/openai-provider.d.ts +0 -7
- package/dist/providers/openai-provider.d.ts.map +0 -1
- package/dist/providers/provider-factory.d.ts +0 -10
- package/dist/providers/provider-factory.d.ts.map +0 -1
- package/dist/providers/provider-utils.d.ts +0 -5
- package/dist/providers/provider-utils.d.ts.map +0 -1
- package/dist/providers/streaming/ollama-parser.d.ts +0 -9
- package/dist/providers/streaming/ollama-parser.d.ts.map +0 -1
- package/dist/providers/streaming/sse-parser.d.ts +0 -10
- package/dist/providers/streaming/sse-parser.d.ts.map +0 -1
- package/dist/providers/streaming/stream-parser.d.ts +0 -10
- package/dist/providers/streaming/stream-parser.d.ts.map +0 -1
- package/dist/service/ai-service.d.ts +0 -47
- package/dist/service/ai-service.d.ts.map +0 -1
- package/dist/service/token-usage-tracker.d.ts +0 -16
- package/dist/service/token-usage-tracker.d.ts.map +0 -1
- package/dist/task/task-checkpoint.d.ts +0 -12
- package/dist/task/task-checkpoint.d.ts.map +0 -1
- package/dist/task/task-plan.d.ts +0 -7
- package/dist/task/task-plan.d.ts.map +0 -1
- package/dist/task/task-runner.d.ts +0 -10
- package/dist/task/task-runner.d.ts.map +0 -1
- package/dist/tools/index.d.ts +0 -6
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/token-estimator.d.ts +0 -12
- package/dist/tools/token-estimator.d.ts.map +0 -1
- package/dist/tools/tool-call-accumulator.d.ts +0 -9
- package/dist/tools/tool-call-accumulator.d.ts.map +0 -1
- package/dist/tools/tool-executor.d.ts +0 -13
- package/dist/tools/tool-executor.d.ts.map +0 -1
- package/dist/tools/tool-name-utils.d.ts +0 -2
- package/dist/tools/tool-name-utils.d.ts.map +0 -1
- package/dist/tools/tool-registry.d.ts +0 -7
- package/dist/tools/tool-registry.d.ts.map +0 -1
- package/dist/utils/token-estimator.d.ts +0 -2
- package/dist/utils/token-estimator.d.ts.map +0 -1
- package/dist/utils/tool-detector.d.ts +0 -10
- package/dist/utils/tool-detector.d.ts.map +0 -1
- package/dist/view/agent-group-manager.d.ts +0 -37
- package/dist/view/agent-group-manager.d.ts.map +0 -1
- package/dist/view/aiview.d.ts +0 -42
- package/dist/view/aiview.d.ts.map +0 -1
- package/dist/view/components/ai-agent-response-group.d.ts +0 -18
- package/dist/view/components/ai-agent-response-group.d.ts.map +0 -1
- package/dist/view/components/ai-chat-input.d.ts +0 -20
- package/dist/view/components/ai-chat-input.d.ts.map +0 -1
- package/dist/view/components/ai-chat-message.d.ts +0 -22
- package/dist/view/components/ai-chat-message.d.ts.map +0 -1
- package/dist/view/components/ai-config-editor.d.ts +0 -33
- package/dist/view/components/ai-config-editor.d.ts.map +0 -1
- package/dist/view/components/ai-empty-state.d.ts +0 -13
- package/dist/view/components/ai-empty-state.d.ts.map +0 -1
- package/dist/view/components/ai-tool-approval.d.ts +0 -26
- package/dist/view/components/ai-tool-approval.d.ts.map +0 -1
- package/dist/view/components/index.d.ts +0 -7
- package/dist/view/components/index.d.ts.map +0 -1
- package/dist/view/provider-manager.d.ts +0 -30
- package/dist/view/provider-manager.d.ts.map +0 -1
- package/dist/view/session-manager.d.ts +0 -25
- package/dist/view/session-manager.d.ts.map +0 -1
- package/dist/view/stream-manager.d.ts +0 -23
- package/dist/view/stream-manager.d.ts.map +0 -1
- package/dist/view/task-progress-panel.d.ts +0 -14
- package/dist/view/task-progress-panel.d.ts.map +0 -1
- package/dist/view/token-usage.d.ts +0 -18
- package/dist/view/token-usage.d.ts.map +0 -1
- package/dist/view/workspace-panel.d.ts +0 -15
- package/dist/view/workspace-panel.d.ts.map +0 -1
- package/dist/workflows/base-sequential-workflow.d.ts +0 -13
- package/dist/workflows/base-sequential-workflow.d.ts.map +0 -1
- package/dist/workflows/conditional-workflow.d.ts +0 -7
- package/dist/workflows/conditional-workflow.d.ts.map +0 -1
- package/dist/workflows/index.d.ts +0 -10
- package/dist/workflows/index.d.ts.map +0 -1
- package/dist/workflows/orchestrated-workflow.d.ts +0 -10
- package/dist/workflows/orchestrated-workflow.d.ts.map +0 -1
- package/dist/workflows/parallel-workflow.d.ts +0 -6
- package/dist/workflows/parallel-workflow.d.ts.map +0 -1
- package/dist/workflows/pipeline-workflow.d.ts +0 -7
- package/dist/workflows/pipeline-workflow.d.ts.map +0 -1
- package/dist/workflows/review-workflow.d.ts +0 -10
- package/dist/workflows/review-workflow.d.ts.map +0 -1
- package/dist/workflows/sequential-workflow.d.ts +0 -7
- package/dist/workflows/sequential-workflow.d.ts.map +0 -1
- package/dist/workflows/workflow-engine.d.ts +0 -8
- package/dist/workflows/workflow-engine.d.ts.map +0 -1
- package/dist/workflows/workflow-strategy.d.ts +0 -8
- package/dist/workflows/workflow-strategy.d.ts.map +0 -1
- package/dist/workspace/workspace.d.ts +0 -20
- package/dist/workspace/workspace.d.ts.map +0 -1
|
@@ -0,0 +1,2952 @@
|
|
|
1
|
+
import { Directory, File, commandRegistry, persistenceService, workspaceService } from "@eclipse-docks/core";
|
|
2
|
+
import { Models } from "@opencode-ai/models";
|
|
3
|
+
//#region src/runtime/events.ts
|
|
4
|
+
/** Tiny in-process bus so runtime stays UI-agnostic. */
|
|
5
|
+
var RuntimeEvents = class {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
8
|
+
}
|
|
9
|
+
publish(topic, payload) {
|
|
10
|
+
const set = this.handlers.get(topic);
|
|
11
|
+
if (!set) return;
|
|
12
|
+
for (const handler of set) handler(payload);
|
|
13
|
+
}
|
|
14
|
+
subscribe(topic, handler) {
|
|
15
|
+
let set = this.handlers.get(topic);
|
|
16
|
+
if (!set) {
|
|
17
|
+
set = /* @__PURE__ */ new Set();
|
|
18
|
+
this.handlers.set(topic, set);
|
|
19
|
+
}
|
|
20
|
+
set.add(handler);
|
|
21
|
+
return () => set.delete(handler);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var TOPIC = {
|
|
25
|
+
sessionChanged: "cognitive/session/changed",
|
|
26
|
+
messagePart: "cognitive/message/part",
|
|
27
|
+
messageDone: "cognitive/message/done",
|
|
28
|
+
permissionAsked: "cognitive/permission/asked",
|
|
29
|
+
modelRouted: "cognitive/model/routed",
|
|
30
|
+
coreTick: "cognitive/core/tick",
|
|
31
|
+
checkpoint: "cognitive/checkpoint",
|
|
32
|
+
providersChanged: "cognitive/providers/changed",
|
|
33
|
+
approvalModeChanged: "cognitive/approval-mode/changed"
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/runtime/constants.ts
|
|
37
|
+
/** Sidebar contribution / tab panel name for the AI chat view. */
|
|
38
|
+
var AI_VIEW_NAME = "cognitiveview";
|
|
39
|
+
/** EditorInput.key for the AI settings editor. */
|
|
40
|
+
var AI_SETTINGS_KEY = ".system.cognitive-ai-settings";
|
|
41
|
+
/** editorRegistry editorId for the AI settings editor. */
|
|
42
|
+
var AI_SETTINGS_EDITOR_ID = "system.cognitive-ai-settings-editor";
|
|
43
|
+
/** Command that opens AI settings. */
|
|
44
|
+
var OPEN_AI_SETTINGS_COMMAND = "open_cognitive_ai_settings";
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/runtime/core/plutchik.ts
|
|
47
|
+
var PLUTCHIK_EMOTIONS = [
|
|
48
|
+
"joy",
|
|
49
|
+
"trust",
|
|
50
|
+
"fear",
|
|
51
|
+
"surprise",
|
|
52
|
+
"sadness",
|
|
53
|
+
"disgust",
|
|
54
|
+
"anger",
|
|
55
|
+
"anticipation"
|
|
56
|
+
];
|
|
57
|
+
function createNeutralState() {
|
|
58
|
+
return Object.fromEntries(PLUTCHIK_EMOTIONS.map((e) => [e, 0]));
|
|
59
|
+
}
|
|
60
|
+
function clampEmotion(value) {
|
|
61
|
+
if (Number.isNaN(value)) return 0;
|
|
62
|
+
return Math.max(-1, Math.min(1, value));
|
|
63
|
+
}
|
|
64
|
+
function blendStates(base, delta, weight = 1) {
|
|
65
|
+
const next = { ...base };
|
|
66
|
+
for (const key of PLUTCHIK_EMOTIONS) {
|
|
67
|
+
const d = delta[key];
|
|
68
|
+
if (d === void 0) continue;
|
|
69
|
+
next[key] = clampEmotion(base[key] + d * weight);
|
|
70
|
+
}
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
function valenceScore(state) {
|
|
74
|
+
return (state.joy + state.trust + state.anticipation + state.surprise - (state.fear + state.sadness + state.disgust + state.anger)) / 4;
|
|
75
|
+
}
|
|
76
|
+
function diffStates(prev, next) {
|
|
77
|
+
const out = createNeutralState();
|
|
78
|
+
for (const key of PLUTCHIK_EMOTIONS) out[key] = clampEmotion(next[key] - prev[key]);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/runtime/core/needs.ts
|
|
83
|
+
function defaultNeeds() {
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
id: "progress",
|
|
87
|
+
label: "Make progress on goals",
|
|
88
|
+
weight: 1,
|
|
89
|
+
urgency: .4
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "clarity",
|
|
93
|
+
label: "Reduce uncertainty",
|
|
94
|
+
weight: .8,
|
|
95
|
+
urgency: .3
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "safety",
|
|
99
|
+
label: "Avoid harmful actions",
|
|
100
|
+
weight: 1.2,
|
|
101
|
+
urgency: .5
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "coherence",
|
|
105
|
+
label: "Keep context coherent",
|
|
106
|
+
weight: .7,
|
|
107
|
+
urgency: .2
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
function dominantNeed(needs) {
|
|
112
|
+
let best = needs[0];
|
|
113
|
+
for (const need of needs) {
|
|
114
|
+
const score = need.weight * need.urgency;
|
|
115
|
+
if (!best || score > best.weight * best.urgency) best = need;
|
|
116
|
+
}
|
|
117
|
+
return best;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/runtime/core/tick.ts
|
|
121
|
+
function contextDelta(context) {
|
|
122
|
+
if (!context) return {};
|
|
123
|
+
const kind = String(context.slots.kind ?? context.type);
|
|
124
|
+
if (kind === "error" || kind === "failure") return {
|
|
125
|
+
fear: .15,
|
|
126
|
+
sadness: .1,
|
|
127
|
+
anticipation: -.05
|
|
128
|
+
};
|
|
129
|
+
if (kind === "success" || kind === "progress") return {
|
|
130
|
+
joy: .2,
|
|
131
|
+
anticipation: .1,
|
|
132
|
+
trust: .05
|
|
133
|
+
};
|
|
134
|
+
if (kind === "isolation" || kind === "empty") return {
|
|
135
|
+
sadness: .2,
|
|
136
|
+
fear: .05,
|
|
137
|
+
surprise: .1
|
|
138
|
+
};
|
|
139
|
+
if (kind === "user") return {
|
|
140
|
+
anticipation: .15,
|
|
141
|
+
trust: .05
|
|
142
|
+
};
|
|
143
|
+
return { surprise: .05 };
|
|
144
|
+
}
|
|
145
|
+
function createCoreState(needs) {
|
|
146
|
+
return {
|
|
147
|
+
conscious: createNeutralState(),
|
|
148
|
+
subconscious: createNeutralState(),
|
|
149
|
+
needs: [...needs]
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function emptyBuffers() {
|
|
153
|
+
return {
|
|
154
|
+
goal: null,
|
|
155
|
+
retrieval: null,
|
|
156
|
+
imaginal: null,
|
|
157
|
+
emotion: null,
|
|
158
|
+
motor: null
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** One ICOM-inspired Core tick: update valence from context + needs, emit intent when useful. */
|
|
162
|
+
function coreTick(input) {
|
|
163
|
+
const prev = input.state.conscious;
|
|
164
|
+
const need = dominantNeed(input.state.needs);
|
|
165
|
+
const delta = contextDelta(input.context ?? input.buffers.retrieval);
|
|
166
|
+
const needBias = need.id === "safety" ? {
|
|
167
|
+
fear: .05 * need.urgency,
|
|
168
|
+
trust: .02
|
|
169
|
+
} : need.id === "progress" ? {
|
|
170
|
+
anticipation: .08 * need.urgency,
|
|
171
|
+
joy: .03 * need.urgency
|
|
172
|
+
} : need.id === "clarity" ? {
|
|
173
|
+
surprise: .05 * need.urgency,
|
|
174
|
+
anticipation: .04 * need.urgency
|
|
175
|
+
} : { trust: .03 * need.urgency };
|
|
176
|
+
const conscious = blendStates(blendStates(prev, delta), needBias);
|
|
177
|
+
const subconscious = blendStates(input.state.subconscious, delta, .5);
|
|
178
|
+
const qualia = diffStates(prev, conscious);
|
|
179
|
+
const valenceDelta = valenceScore(conscious) - valenceScore(prev);
|
|
180
|
+
const summary = input.context?.slots.summary ?? input.buffers.goal?.slots.summary ?? need.label;
|
|
181
|
+
const intent = valenceDelta !== 0 || Boolean(input.context) || Boolean(input.buffers.goal) ? {
|
|
182
|
+
id: `intent-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
183
|
+
summary: String(summary),
|
|
184
|
+
motivation: need.id,
|
|
185
|
+
valenceDelta,
|
|
186
|
+
createdAt: Date.now()
|
|
187
|
+
} : null;
|
|
188
|
+
const goalChunk = intent ? {
|
|
189
|
+
id: `goal-${intent.id}`,
|
|
190
|
+
type: "goal",
|
|
191
|
+
slots: {
|
|
192
|
+
summary: intent.summary,
|
|
193
|
+
motivation: intent.motivation,
|
|
194
|
+
intentId: intent.id
|
|
195
|
+
},
|
|
196
|
+
activation: 1,
|
|
197
|
+
createdAt: intent.createdAt,
|
|
198
|
+
updatedAt: intent.createdAt,
|
|
199
|
+
scope: "session"
|
|
200
|
+
} : input.buffers.goal;
|
|
201
|
+
const emotionChunk = {
|
|
202
|
+
id: `emotion-${Date.now()}`,
|
|
203
|
+
type: "emotion",
|
|
204
|
+
slots: {
|
|
205
|
+
conscious,
|
|
206
|
+
valence: valenceScore(conscious)
|
|
207
|
+
},
|
|
208
|
+
activation: 1,
|
|
209
|
+
createdAt: Date.now(),
|
|
210
|
+
updatedAt: Date.now(),
|
|
211
|
+
scope: "run"
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
state: {
|
|
215
|
+
conscious,
|
|
216
|
+
subconscious,
|
|
217
|
+
needs: input.state.needs
|
|
218
|
+
},
|
|
219
|
+
buffers: {
|
|
220
|
+
...input.buffers,
|
|
221
|
+
goal: goalChunk,
|
|
222
|
+
emotion: emotionChunk,
|
|
223
|
+
motor: null
|
|
224
|
+
},
|
|
225
|
+
intent,
|
|
226
|
+
valenceDelta,
|
|
227
|
+
qualia
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/runtime/memory/store.ts
|
|
232
|
+
function now() {
|
|
233
|
+
return Date.now();
|
|
234
|
+
}
|
|
235
|
+
function id() {
|
|
236
|
+
return `chunk-${now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
237
|
+
}
|
|
238
|
+
/** In-memory declarative/episodic store with ACT-R-like activation ranking. */
|
|
239
|
+
var CognitiveMemory = class {
|
|
240
|
+
constructor() {
|
|
241
|
+
this.chunks = /* @__PURE__ */ new Map();
|
|
242
|
+
}
|
|
243
|
+
upsert(input) {
|
|
244
|
+
const existing = input.id ? this.chunks.get(input.id) : void 0;
|
|
245
|
+
const chunk = {
|
|
246
|
+
id: input.id ?? existing?.id ?? id(),
|
|
247
|
+
type: input.type,
|
|
248
|
+
slots: { ...input.slots },
|
|
249
|
+
activation: input.activation ?? existing?.activation ?? .5,
|
|
250
|
+
createdAt: existing?.createdAt ?? now(),
|
|
251
|
+
updatedAt: now(),
|
|
252
|
+
scope: input.scope,
|
|
253
|
+
sessionId: input.sessionId,
|
|
254
|
+
pinned: input.pinned ?? existing?.pinned
|
|
255
|
+
};
|
|
256
|
+
this.chunks.set(chunk.id, chunk);
|
|
257
|
+
return chunk;
|
|
258
|
+
}
|
|
259
|
+
get(chunkId) {
|
|
260
|
+
return this.chunks.get(chunkId);
|
|
261
|
+
}
|
|
262
|
+
touch(chunkId, boost = .15) {
|
|
263
|
+
const chunk = this.chunks.get(chunkId);
|
|
264
|
+
if (!chunk) return void 0;
|
|
265
|
+
const next = {
|
|
266
|
+
...chunk,
|
|
267
|
+
activation: Math.min(2, chunk.activation + boost),
|
|
268
|
+
updatedAt: now()
|
|
269
|
+
};
|
|
270
|
+
this.chunks.set(chunkId, next);
|
|
271
|
+
return next;
|
|
272
|
+
}
|
|
273
|
+
decay(amount = .02) {
|
|
274
|
+
for (const [key, chunk] of this.chunks) {
|
|
275
|
+
if (chunk.pinned) continue;
|
|
276
|
+
const activation = Math.max(0, chunk.activation - amount);
|
|
277
|
+
if (activation <= .01 && chunk.scope !== "workspace") {
|
|
278
|
+
this.chunks.delete(key);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
this.chunks.set(key, {
|
|
282
|
+
...chunk,
|
|
283
|
+
activation
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
forget(chunkId) {
|
|
288
|
+
return this.chunks.delete(chunkId);
|
|
289
|
+
}
|
|
290
|
+
clear(scope) {
|
|
291
|
+
if (!scope) {
|
|
292
|
+
this.chunks.clear();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
for (const [key, chunk] of this.chunks) if (chunk.scope === scope) this.chunks.delete(key);
|
|
296
|
+
}
|
|
297
|
+
search(query = {}) {
|
|
298
|
+
const min = query.minActivation ?? 0;
|
|
299
|
+
const limit = query.limit ?? 8;
|
|
300
|
+
return [...this.chunks.values()].filter((c) => query.type ? c.type === query.type : true).filter((c) => query.scope ? c.scope === query.scope : true).filter((c) => query.sessionId ? c.sessionId === query.sessionId : true).filter((c) => c.activation >= min || c.pinned).sort((a, b) => b.activation - a.activation || b.updatedAt - a.updatedAt).slice(0, limit);
|
|
301
|
+
}
|
|
302
|
+
/** Spreading-style boost: chunks whose slots overlap context terms rise. */
|
|
303
|
+
retrieveForContext(terms, limit = 5) {
|
|
304
|
+
const lower = terms.map((t) => t.toLowerCase()).filter(Boolean);
|
|
305
|
+
if (lower.length === 0) return this.search({ limit });
|
|
306
|
+
return [...this.chunks.values()].map((chunk) => {
|
|
307
|
+
const hay = `${chunk.type} ${JSON.stringify(chunk.slots)}`.toLowerCase();
|
|
308
|
+
let hits = 0;
|
|
309
|
+
for (const term of lower) if (hay.includes(term)) hits += 1;
|
|
310
|
+
return {
|
|
311
|
+
chunk,
|
|
312
|
+
score: chunk.activation + hits * .35 + (chunk.pinned ? .5 : 0)
|
|
313
|
+
};
|
|
314
|
+
}).filter((s) => s.score > .2).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => {
|
|
315
|
+
this.touch(s.chunk.id, .05);
|
|
316
|
+
return this.chunks.get(s.chunk.id);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
snapshot() {
|
|
320
|
+
return [...this.chunks.values()].map((c) => ({
|
|
321
|
+
...c,
|
|
322
|
+
slots: { ...c.slots }
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
restore(chunks) {
|
|
326
|
+
this.chunks.clear();
|
|
327
|
+
for (const chunk of chunks) this.chunks.set(chunk.id, {
|
|
328
|
+
...chunk,
|
|
329
|
+
slots: { ...chunk.slots }
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/runtime/models/provider-registry.ts
|
|
335
|
+
var ProviderRegistry = class {
|
|
336
|
+
constructor() {
|
|
337
|
+
this.settingsProviders = /* @__PURE__ */ new Map();
|
|
338
|
+
this.externalProviders = /* @__PURE__ */ new Map();
|
|
339
|
+
}
|
|
340
|
+
/** Settings-backed providers — cleared on each settings rebuild. */
|
|
341
|
+
register(provider) {
|
|
342
|
+
this.settingsProviders.set(provider.id, provider);
|
|
343
|
+
}
|
|
344
|
+
/** Plugin providers (e.g. WebLLM) that survive settings rebuilds. */
|
|
345
|
+
registerExternal(provider) {
|
|
346
|
+
this.externalProviders.set(provider.id, provider);
|
|
347
|
+
}
|
|
348
|
+
unregisterExternal(id) {
|
|
349
|
+
this.externalProviders.delete(id);
|
|
350
|
+
}
|
|
351
|
+
clear() {
|
|
352
|
+
this.settingsProviders.clear();
|
|
353
|
+
}
|
|
354
|
+
get(id) {
|
|
355
|
+
return this.settingsProviders.get(id) ?? this.externalProviders.get(id);
|
|
356
|
+
}
|
|
357
|
+
list() {
|
|
358
|
+
const byId = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const provider of this.externalProviders.values()) byId.set(provider.id, provider);
|
|
360
|
+
for (const provider of this.settingsProviders.values()) byId.set(provider.id, provider);
|
|
361
|
+
return [...byId.values()];
|
|
362
|
+
}
|
|
363
|
+
listExternal() {
|
|
364
|
+
return [...this.externalProviders.values()];
|
|
365
|
+
}
|
|
366
|
+
resolveProvider(modelId) {
|
|
367
|
+
const slash = modelId.indexOf("/");
|
|
368
|
+
if (slash === -1) return this.list()[0];
|
|
369
|
+
return this.get(modelId.slice(0, slash));
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/runtime/models/effort-support.ts
|
|
374
|
+
var NO_EFFORT = {
|
|
375
|
+
supported: false,
|
|
376
|
+
withTools: false,
|
|
377
|
+
levels: ["none"],
|
|
378
|
+
default: "none"
|
|
379
|
+
};
|
|
380
|
+
var EFFORT_ORDER$1 = [
|
|
381
|
+
"none",
|
|
382
|
+
"minimal",
|
|
383
|
+
"low",
|
|
384
|
+
"medium",
|
|
385
|
+
"high",
|
|
386
|
+
"xhigh",
|
|
387
|
+
"max"
|
|
388
|
+
];
|
|
389
|
+
var KNOWN_EFFORTS = new Set(EFFORT_ORDER$1);
|
|
390
|
+
var providersCache;
|
|
391
|
+
var loadPromise;
|
|
392
|
+
/** Load Models.dev provider catalog (network, then offline snapshot fallback). */
|
|
393
|
+
async function loadModelsCatalog(force = false) {
|
|
394
|
+
if (providersCache && !force) return;
|
|
395
|
+
if (!loadPromise || force) loadPromise = (async () => {
|
|
396
|
+
try {
|
|
397
|
+
providersCache = await Models.make().providers({ signal: AbortSignal.timeout(1e4) });
|
|
398
|
+
} catch {
|
|
399
|
+
providersCache = (await import("@opencode-ai/models/snapshot")).providers;
|
|
400
|
+
}
|
|
401
|
+
})();
|
|
402
|
+
await loadPromise;
|
|
403
|
+
}
|
|
404
|
+
function splitModelRef(modelRef) {
|
|
405
|
+
const trimmed = modelRef.trim();
|
|
406
|
+
const slash = trimmed.indexOf("/");
|
|
407
|
+
if (slash === -1) return { modelId: trimmed };
|
|
408
|
+
return {
|
|
409
|
+
providerId: trimmed.slice(0, slash),
|
|
410
|
+
modelId: trimmed.slice(slash + 1)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function findCatalogModel(modelRef) {
|
|
414
|
+
if (!providersCache) return void 0;
|
|
415
|
+
const { providerId, modelId } = splitModelRef(modelRef);
|
|
416
|
+
if (!modelId) return void 0;
|
|
417
|
+
if (providerId) {
|
|
418
|
+
const exact = providersCache[providerId]?.models?.[modelId];
|
|
419
|
+
if (exact) return exact;
|
|
420
|
+
}
|
|
421
|
+
const lower = modelId.toLowerCase();
|
|
422
|
+
for (const provider of Object.values(providersCache)) {
|
|
423
|
+
const models = provider?.models;
|
|
424
|
+
if (!models) continue;
|
|
425
|
+
if (models[modelId]) return models[modelId];
|
|
426
|
+
for (const [id, meta] of Object.entries(models)) if (id.toLowerCase() === lower) return meta;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
var DEFAULT_CONTEXT_LIMIT = 128e3;
|
|
430
|
+
/** Context window size from Models.dev (`limit.context`), with a safe default. */
|
|
431
|
+
function getModelContextLimit(modelRef) {
|
|
432
|
+
const limit = findCatalogModel(modelRef)?.limit?.context;
|
|
433
|
+
return typeof limit === "number" && limit > 0 ? limit : DEFAULT_CONTEXT_LIMIT;
|
|
434
|
+
}
|
|
435
|
+
function mapEffortValue(value) {
|
|
436
|
+
if (value === null || value === "none" || value === "default") return "none";
|
|
437
|
+
if (typeof value === "string" && KNOWN_EFFORTS.has(value)) return value;
|
|
438
|
+
}
|
|
439
|
+
function levelsFromOptions(options) {
|
|
440
|
+
const found = /* @__PURE__ */ new Set();
|
|
441
|
+
for (const opt of options ?? []) {
|
|
442
|
+
if (opt.type === "toggle") {
|
|
443
|
+
found.add("none");
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (opt.type === "effort") for (const value of opt.values) {
|
|
447
|
+
const mapped = mapEffortValue(value);
|
|
448
|
+
if (mapped) found.add(mapped);
|
|
449
|
+
}
|
|
450
|
+
if (opt.type === "budget_tokens") {
|
|
451
|
+
found.add("low");
|
|
452
|
+
found.add("medium");
|
|
453
|
+
found.add("high");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!found.size) return [
|
|
457
|
+
"none",
|
|
458
|
+
"low",
|
|
459
|
+
"medium",
|
|
460
|
+
"high"
|
|
461
|
+
];
|
|
462
|
+
if (![...found].some((l) => l !== "none")) found.add("medium");
|
|
463
|
+
return EFFORT_ORDER$1.filter((level) => found.has(level));
|
|
464
|
+
}
|
|
465
|
+
function pickDefault(levels) {
|
|
466
|
+
if (levels.includes("medium")) return "medium";
|
|
467
|
+
if (levels.includes("low")) return "low";
|
|
468
|
+
return levels.find((l) => l !== "none") ?? "none";
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Resolve effort capability from Models.dev catalog.
|
|
472
|
+
* Unknown / unloaded models → no effort (safe for chat/completions).
|
|
473
|
+
*/
|
|
474
|
+
function detectEffortCapability(modelRef) {
|
|
475
|
+
const meta = findCatalogModel(modelRef);
|
|
476
|
+
if (!meta?.reasoning) return NO_EFFORT;
|
|
477
|
+
const levels = levelsFromOptions(meta.reasoning_options);
|
|
478
|
+
return {
|
|
479
|
+
supported: true,
|
|
480
|
+
withTools: false,
|
|
481
|
+
levels,
|
|
482
|
+
default: pickDefault(levels)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/runtime/models/provider-config.ts
|
|
487
|
+
var PROVIDERS_PERSIST_KEY = "cognitive/v1/providers";
|
|
488
|
+
function defaultProvidersSettings() {
|
|
489
|
+
return {
|
|
490
|
+
defaultProviderId: "openai",
|
|
491
|
+
providers: [{
|
|
492
|
+
id: "openai",
|
|
493
|
+
baseUrl: "https://api.openai.com/v1",
|
|
494
|
+
apiKey: "",
|
|
495
|
+
models: "gpt-4o-mini,gpt-4o",
|
|
496
|
+
enabled: true
|
|
497
|
+
}, {
|
|
498
|
+
id: "ollama",
|
|
499
|
+
baseUrl: "http://127.0.0.1:11434/v1",
|
|
500
|
+
apiKey: "",
|
|
501
|
+
models: "llama3.2",
|
|
502
|
+
enabled: true
|
|
503
|
+
}]
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function parseModelIds(models) {
|
|
507
|
+
return models.split(",").map((m) => m.trim()).filter(Boolean);
|
|
508
|
+
}
|
|
509
|
+
/** Model refs (`providerId/modelId`) the user enabled in AI Settings. */
|
|
510
|
+
function listConfiguredModelIds(settings) {
|
|
511
|
+
const ids = [];
|
|
512
|
+
for (const provider of settings.providers) {
|
|
513
|
+
if (!provider.enabled || !provider.id.trim()) continue;
|
|
514
|
+
for (const modelId of parseModelIds(provider.models)) ids.push(`${provider.id}/${modelId}`);
|
|
515
|
+
}
|
|
516
|
+
return ids;
|
|
517
|
+
}
|
|
518
|
+
function catalogFromSettings(settings) {
|
|
519
|
+
const entries = [];
|
|
520
|
+
for (const provider of settings.providers) {
|
|
521
|
+
if (!provider.enabled || !provider.id.trim()) continue;
|
|
522
|
+
const modelIds = parseModelIds(provider.models);
|
|
523
|
+
modelIds.forEach((modelId, index) => {
|
|
524
|
+
const tier = modelIds.length === 1 ? "standard" : index === 0 ? "economy" : index === modelIds.length - 1 ? "frontier" : "standard";
|
|
525
|
+
entries.push({
|
|
526
|
+
id: `${provider.id}/${modelId}`,
|
|
527
|
+
providerId: provider.id,
|
|
528
|
+
tier,
|
|
529
|
+
strengths: ["code", "fast"],
|
|
530
|
+
effort: detectEffortCapability(`${provider.id}/${modelId}`)
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return entries;
|
|
535
|
+
}
|
|
536
|
+
function normalizeProvidersSettings(raw) {
|
|
537
|
+
const providers = raw.providers.map((p) => ({
|
|
538
|
+
id: p.id.trim(),
|
|
539
|
+
baseUrl: p.baseUrl.trim(),
|
|
540
|
+
apiKey: p.apiKey ?? "",
|
|
541
|
+
models: p.models ?? "",
|
|
542
|
+
enabled: p.enabled !== false,
|
|
543
|
+
...p.extra && Object.keys(p.extra).length ? { extra: { ...p.extra } } : {}
|
|
544
|
+
}));
|
|
545
|
+
return {
|
|
546
|
+
defaultProviderId: providers.find((p) => p.id === raw.defaultProviderId)?.id ?? providers.find((p) => p.enabled)?.id ?? "",
|
|
547
|
+
providers
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/runtime/models/openai-compatible.ts
|
|
552
|
+
function wireContent(content) {
|
|
553
|
+
if (typeof content === "string") return content;
|
|
554
|
+
if (Array.isArray(content)) return content;
|
|
555
|
+
return content ?? "";
|
|
556
|
+
}
|
|
557
|
+
/** Map internal messages to OpenAI chat/completions wire shape. */
|
|
558
|
+
function toOpenAIMessages(messages) {
|
|
559
|
+
return messages.map((m) => {
|
|
560
|
+
if (m.role === "tool") return {
|
|
561
|
+
role: "tool",
|
|
562
|
+
tool_call_id: m.toolCallId,
|
|
563
|
+
content: typeof m.content === "string" ? m.content || "" : "",
|
|
564
|
+
...m.name ? { name: m.name } : {}
|
|
565
|
+
};
|
|
566
|
+
if (m.role === "assistant" && m.toolCalls?.length) return {
|
|
567
|
+
role: "assistant",
|
|
568
|
+
content: typeof m.content === "string" ? m.content : "",
|
|
569
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
570
|
+
id: tc.id,
|
|
571
|
+
type: "function",
|
|
572
|
+
function: {
|
|
573
|
+
name: tc.name,
|
|
574
|
+
arguments: tc.arguments || "{}"
|
|
575
|
+
}
|
|
576
|
+
}))
|
|
577
|
+
};
|
|
578
|
+
return {
|
|
579
|
+
role: m.role,
|
|
580
|
+
content: wireContent(m.content)
|
|
581
|
+
};
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/** GET `{baseUrl}/models` — baseUrl should already include `/v1` when required. */
|
|
585
|
+
async function fetchOpenAICompatibleModels(baseUrl, apiKey, signal) {
|
|
586
|
+
const root = baseUrl.trim().replace(/\/$/, "");
|
|
587
|
+
if (!root) return [];
|
|
588
|
+
const headers = { Accept: "application/json" };
|
|
589
|
+
if (apiKey?.trim()) headers.Authorization = `Bearer ${apiKey.trim()}`;
|
|
590
|
+
const response = await fetch(`${root}/models`, {
|
|
591
|
+
method: "GET",
|
|
592
|
+
headers,
|
|
593
|
+
signal
|
|
594
|
+
});
|
|
595
|
+
if (!response.ok) throw new Error(`Failed to list models (HTTP ${response.status})`);
|
|
596
|
+
const data = await response.json();
|
|
597
|
+
return (Array.isArray(data.data) ? data.data : []).map((m) => {
|
|
598
|
+
const id = (m.id ?? "").trim();
|
|
599
|
+
if (!id) return null;
|
|
600
|
+
return {
|
|
601
|
+
id,
|
|
602
|
+
name: (m.name ?? id).trim() || id
|
|
603
|
+
};
|
|
604
|
+
}).filter((m) => m != null).sort((a, b) => a.id.localeCompare(b.id));
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Build a chat/completions request body.
|
|
608
|
+
*
|
|
609
|
+
* Reasoning models default to a non-none effort when the field is omitted.
|
|
610
|
+
* With function tools, chat/completions requires an explicit
|
|
611
|
+
* `reasoning_effort: "none"` — omitting still 400s.
|
|
612
|
+
*/
|
|
613
|
+
function buildChatCompletionBody(params) {
|
|
614
|
+
const model = params.model.includes("/") ? params.model.split("/").slice(1).join("/") : params.model;
|
|
615
|
+
const { reasoning_effort: _a, reasoningEffort: _b, reasoning: _c, ...safeOptions } = params.providerOptions ?? {};
|
|
616
|
+
const body = {
|
|
617
|
+
model,
|
|
618
|
+
messages: toOpenAIMessages(params.messages),
|
|
619
|
+
stream: true,
|
|
620
|
+
...safeOptions
|
|
621
|
+
};
|
|
622
|
+
const requestedEffort = params.providerOptions?.reasoning_effort ?? params.providerOptions?.reasoningEffort;
|
|
623
|
+
const reasoningModel = detectEffortCapability(params.model).supported || requestedEffort != null;
|
|
624
|
+
if (params.tools?.length) {
|
|
625
|
+
body.tools = params.tools.map((t) => ({
|
|
626
|
+
type: "function",
|
|
627
|
+
function: {
|
|
628
|
+
name: t.name,
|
|
629
|
+
description: t.description,
|
|
630
|
+
parameters: t.parameters
|
|
631
|
+
}
|
|
632
|
+
}));
|
|
633
|
+
if (reasoningModel) body.reasoning_effort = "none";
|
|
634
|
+
} else if (requestedEffort != null && requestedEffort !== "none") body.reasoning_effort = requestedEffort;
|
|
635
|
+
return body;
|
|
636
|
+
}
|
|
637
|
+
/** Minimal OpenAI-compatible streaming provider (chat/completions SSE). */
|
|
638
|
+
var OpenAICompatibleProvider = class {
|
|
639
|
+
constructor(config) {
|
|
640
|
+
this.config = config;
|
|
641
|
+
}
|
|
642
|
+
get id() {
|
|
643
|
+
return this.config.id;
|
|
644
|
+
}
|
|
645
|
+
listModels() {
|
|
646
|
+
return this.config.defaultModels ?? [{
|
|
647
|
+
id: "default",
|
|
648
|
+
name: "Default"
|
|
649
|
+
}];
|
|
650
|
+
}
|
|
651
|
+
async fetchRemoteModels(signal) {
|
|
652
|
+
return fetchOpenAICompatibleModels(this.config.baseUrl, this.config.apiKey, signal);
|
|
653
|
+
}
|
|
654
|
+
async *stream(params) {
|
|
655
|
+
const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
656
|
+
const body = buildChatCompletionBody(params);
|
|
657
|
+
const headers = { "Content-Type": "application/json" };
|
|
658
|
+
if (this.config.apiKey) headers.Authorization = `Bearer ${this.config.apiKey}`;
|
|
659
|
+
let response;
|
|
660
|
+
try {
|
|
661
|
+
response = await fetch(url, {
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers,
|
|
664
|
+
body: JSON.stringify(body),
|
|
665
|
+
signal: params.signal
|
|
666
|
+
});
|
|
667
|
+
} catch (err) {
|
|
668
|
+
yield {
|
|
669
|
+
type: "error",
|
|
670
|
+
error: err instanceof Error ? err.message : String(err)
|
|
671
|
+
};
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (!response.ok || !response.body) {
|
|
675
|
+
yield {
|
|
676
|
+
type: "error",
|
|
677
|
+
error: `HTTP ${response.status}: ${await response.text()}`
|
|
678
|
+
};
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const reader = response.body.getReader();
|
|
682
|
+
const decoder = new TextDecoder();
|
|
683
|
+
let buffer = "";
|
|
684
|
+
const toolAcc = /* @__PURE__ */ new Map();
|
|
685
|
+
while (true) {
|
|
686
|
+
const { done, value } = await reader.read();
|
|
687
|
+
if (done) break;
|
|
688
|
+
buffer += decoder.decode(value, { stream: true });
|
|
689
|
+
const lines = buffer.split("\n");
|
|
690
|
+
buffer = lines.pop() ?? "";
|
|
691
|
+
for (const line of lines) {
|
|
692
|
+
const trimmed = line.trim();
|
|
693
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
694
|
+
const data = trimmed.slice(5).trim();
|
|
695
|
+
if (data === "[DONE]") {
|
|
696
|
+
yield { type: "done" };
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
const delta = JSON.parse(data).choices?.[0]?.delta;
|
|
701
|
+
if (!delta) continue;
|
|
702
|
+
if (delta.content) yield {
|
|
703
|
+
type: "text",
|
|
704
|
+
text: delta.content
|
|
705
|
+
};
|
|
706
|
+
if (delta.reasoning_content || delta.reasoning) yield {
|
|
707
|
+
type: "reasoning",
|
|
708
|
+
text: String(delta.reasoning_content ?? delta.reasoning)
|
|
709
|
+
};
|
|
710
|
+
if (Array.isArray(delta.tool_calls)) for (const tc of delta.tool_calls) {
|
|
711
|
+
const idx = tc.index ?? 0;
|
|
712
|
+
const cur = toolAcc.get(idx) ?? {
|
|
713
|
+
id: tc.id ?? `call-${idx}`,
|
|
714
|
+
name: "",
|
|
715
|
+
arguments: ""
|
|
716
|
+
};
|
|
717
|
+
if (tc.id) cur.id = tc.id;
|
|
718
|
+
if (tc.function?.name) cur.name += tc.function.name;
|
|
719
|
+
if (tc.function?.arguments) cur.arguments += tc.function.arguments;
|
|
720
|
+
toolAcc.set(idx, cur);
|
|
721
|
+
yield {
|
|
722
|
+
type: "tool_call_delta",
|
|
723
|
+
toolCall: { ...cur }
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
} catch {}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
yield { type: "done" };
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region src/runtime/models/router.ts
|
|
734
|
+
var EFFORT_ORDER = [
|
|
735
|
+
"none",
|
|
736
|
+
"minimal",
|
|
737
|
+
"low",
|
|
738
|
+
"medium",
|
|
739
|
+
"high",
|
|
740
|
+
"xhigh",
|
|
741
|
+
"max"
|
|
742
|
+
];
|
|
743
|
+
var ROLE_TIER = {
|
|
744
|
+
compact: "economy",
|
|
745
|
+
title: "economy",
|
|
746
|
+
summary: "economy",
|
|
747
|
+
explore: "economy",
|
|
748
|
+
plan: "standard",
|
|
749
|
+
build: "standard",
|
|
750
|
+
general: "standard"
|
|
751
|
+
};
|
|
752
|
+
var ROLE_EFFORT = {
|
|
753
|
+
compact: "none",
|
|
754
|
+
title: "none",
|
|
755
|
+
summary: "low",
|
|
756
|
+
explore: "low",
|
|
757
|
+
plan: "medium",
|
|
758
|
+
build: "medium",
|
|
759
|
+
general: "medium"
|
|
760
|
+
};
|
|
761
|
+
function effortIndex(level) {
|
|
762
|
+
return EFFORT_ORDER.indexOf(level);
|
|
763
|
+
}
|
|
764
|
+
function clampEffort(level, max) {
|
|
765
|
+
if (!max) return level;
|
|
766
|
+
return effortIndex(level) > effortIndex(max) ? max : level;
|
|
767
|
+
}
|
|
768
|
+
function resolveEffort(entry, desired, requireTools) {
|
|
769
|
+
if (!entry.effort.supported) return "none";
|
|
770
|
+
if (requireTools) return "none";
|
|
771
|
+
if (!entry.effort.levels.includes(desired)) return entry.effort.default;
|
|
772
|
+
return desired;
|
|
773
|
+
}
|
|
774
|
+
function mapEffortOptions(entry, effort, requireTools) {
|
|
775
|
+
if (!entry.effort.supported) return {};
|
|
776
|
+
if (requireTools) return { reasoning_effort: "none" };
|
|
777
|
+
if (effort === "none") return { reasoning_effort: "none" };
|
|
778
|
+
return { reasoning_effort: effort };
|
|
779
|
+
}
|
|
780
|
+
var ModelRouter = class {
|
|
781
|
+
constructor() {
|
|
782
|
+
this.catalog = [];
|
|
783
|
+
this.utility = /* @__PURE__ */ new Map();
|
|
784
|
+
}
|
|
785
|
+
setCatalog(entries) {
|
|
786
|
+
this.catalog = [...entries];
|
|
787
|
+
}
|
|
788
|
+
recordOutcome(role, model, effort, success, cost = 1) {
|
|
789
|
+
const key = `${role}|${model}|${effort}`;
|
|
790
|
+
const prev = this.utility.get(key) ?? 0;
|
|
791
|
+
this.utility.set(key, prev + (success ? 1 : -.5) - cost * .05);
|
|
792
|
+
}
|
|
793
|
+
resolve(request, policy) {
|
|
794
|
+
if (policy.mode === "manual") {
|
|
795
|
+
const model = policy.model;
|
|
796
|
+
const entry = this.catalog.find((c) => c.id === model) ?? this.fallbackEntry(model);
|
|
797
|
+
const effort = resolveEffort(entry, policy.effort && policy.effort !== "auto" ? policy.effort : this.pickEffort(request, policy.maxEffort ?? entry.effort.default, "balance"), request.requireTools);
|
|
798
|
+
return {
|
|
799
|
+
model: entry.id,
|
|
800
|
+
providerId: entry.providerId,
|
|
801
|
+
effort,
|
|
802
|
+
providerOptions: mapEffortOptions(entry, effort, request.requireTools),
|
|
803
|
+
reason: `manual:${entry.id}+effort:${effort}`
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const optimizeFor = request.optimizeFor ?? policy.optimizeFor;
|
|
807
|
+
const complexity = request.complexity ?? .3;
|
|
808
|
+
let tier = ROLE_TIER[request.role];
|
|
809
|
+
if (optimizeFor === "intelligence" || complexity > .7) tier = "frontier";
|
|
810
|
+
if (optimizeFor === "cost" && complexity < .5) tier = "economy";
|
|
811
|
+
const candidates = this.catalog.filter((c) => c.tier === tier);
|
|
812
|
+
const entry = [...candidates.length ? candidates : this.catalog].sort((a, b) => {
|
|
813
|
+
const ua = this.utility.get(`${request.role}|${a.id}|${ROLE_EFFORT[request.role]}`) ?? 0;
|
|
814
|
+
return (this.utility.get(`${request.role}|${b.id}|${ROLE_EFFORT[request.role]}`) ?? 0) - ua;
|
|
815
|
+
})[0] ?? this.fallbackEntry(`${this.catalog[0]?.providerId ?? "openai"}/default`);
|
|
816
|
+
const effort = resolveEffort(entry, this.pickEffort(request, policy.maxEffort, optimizeFor), request.requireTools);
|
|
817
|
+
return {
|
|
818
|
+
model: entry.id,
|
|
819
|
+
providerId: entry.providerId,
|
|
820
|
+
effort,
|
|
821
|
+
providerOptions: mapEffortOptions(entry, effort, request.requireTools),
|
|
822
|
+
reason: `auto:${optimizeFor}+role:${request.role}+tier:${entry.tier}+effort:${effort}`
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
pickEffort(request, maxEffort, optimizeFor) {
|
|
826
|
+
if (request.manualEffort && request.manualEffort !== "auto") return clampEffort(request.manualEffort, maxEffort);
|
|
827
|
+
let effort = ROLE_EFFORT[request.role];
|
|
828
|
+
const complexity = request.complexity ?? .3;
|
|
829
|
+
if (optimizeFor === "intelligence" || complexity > .6) effort = EFFORT_ORDER[Math.min(EFFORT_ORDER.length - 1, effortIndex(effort) + 1)];
|
|
830
|
+
if (optimizeFor === "cost") effort = EFFORT_ORDER[Math.max(0, effortIndex(effort) - 1)];
|
|
831
|
+
return clampEffort(effort, maxEffort);
|
|
832
|
+
}
|
|
833
|
+
fallbackEntry(model) {
|
|
834
|
+
const slash = model.indexOf("/");
|
|
835
|
+
const providerId = slash === -1 ? "openai" : model.slice(0, slash);
|
|
836
|
+
const id = slash === -1 ? `${providerId}/${model}` : model;
|
|
837
|
+
return {
|
|
838
|
+
id,
|
|
839
|
+
providerId,
|
|
840
|
+
tier: "standard",
|
|
841
|
+
strengths: ["code"],
|
|
842
|
+
effort: detectEffortCapability(id)
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/runtime/tools/broker.ts
|
|
848
|
+
function matchPattern(pattern, name) {
|
|
849
|
+
if (pattern === "*" || pattern === name) return true;
|
|
850
|
+
if (pattern.endsWith("*")) return name.startsWith(pattern.slice(0, -1));
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
/** Evaluates allow/ask/deny and dispatches tool executors. */
|
|
854
|
+
var ToolBroker = class {
|
|
855
|
+
constructor() {
|
|
856
|
+
this.approvalMode = "auto_safe";
|
|
857
|
+
this.rules = [];
|
|
858
|
+
this.executors = /* @__PURE__ */ new Map();
|
|
859
|
+
this.definitions = [];
|
|
860
|
+
this.permissionHandler = null;
|
|
861
|
+
this.effectLog = [];
|
|
862
|
+
}
|
|
863
|
+
setApprovalMode(mode) {
|
|
864
|
+
this.approvalMode = mode;
|
|
865
|
+
}
|
|
866
|
+
getApprovalMode() {
|
|
867
|
+
return this.approvalMode;
|
|
868
|
+
}
|
|
869
|
+
setRules(rules) {
|
|
870
|
+
this.rules = rules;
|
|
871
|
+
}
|
|
872
|
+
setPermissionHandler(handler) {
|
|
873
|
+
this.permissionHandler = handler;
|
|
874
|
+
}
|
|
875
|
+
register(definition, executor) {
|
|
876
|
+
this.definitions = this.definitions.filter((d) => d.name !== definition.name).concat(definition);
|
|
877
|
+
this.executors.set(definition.name, executor);
|
|
878
|
+
}
|
|
879
|
+
unregister(name) {
|
|
880
|
+
this.definitions = this.definitions.filter((d) => d.name !== name);
|
|
881
|
+
this.executors.delete(name);
|
|
882
|
+
}
|
|
883
|
+
clearDefinitions() {
|
|
884
|
+
this.definitions = [];
|
|
885
|
+
this.executors.clear();
|
|
886
|
+
}
|
|
887
|
+
listDefinitions(role) {
|
|
888
|
+
if (role === "plan" || role === "explore") return this.definitions.filter((d) => !isMutatingName(d.name));
|
|
889
|
+
return [...this.definitions];
|
|
890
|
+
}
|
|
891
|
+
resolveAction(toolName, risk) {
|
|
892
|
+
for (const rule of this.rules) if (matchPattern(rule.pattern, toolName) && rule.action === "deny") return "deny";
|
|
893
|
+
let action;
|
|
894
|
+
switch (this.approvalMode) {
|
|
895
|
+
case "auto_all":
|
|
896
|
+
action = "allow";
|
|
897
|
+
break;
|
|
898
|
+
case "manual":
|
|
899
|
+
action = "ask";
|
|
900
|
+
break;
|
|
901
|
+
default: action = risk === "safe" ? "allow" : "ask";
|
|
902
|
+
}
|
|
903
|
+
for (const rule of this.rules) if (matchPattern(rule.pattern, toolName) && rule.action === "allow" && action === "ask") action = "allow";
|
|
904
|
+
return action;
|
|
905
|
+
}
|
|
906
|
+
async execute(call, sessionId, role, opts = {}) {
|
|
907
|
+
if ((role === "plan" || role === "explore") && isMutatingName(call.name)) return {
|
|
908
|
+
id: call.id,
|
|
909
|
+
name: call.name,
|
|
910
|
+
ok: false,
|
|
911
|
+
output: null,
|
|
912
|
+
error: "Tool denied in plan/explore mode"
|
|
913
|
+
};
|
|
914
|
+
let action = this.resolveAction(call.name, opts.risk);
|
|
915
|
+
if (action === "ask" && this.permissionHandler) action = await this.permissionHandler({
|
|
916
|
+
toolName: call.name,
|
|
917
|
+
args: call.arguments,
|
|
918
|
+
sessionId
|
|
919
|
+
});
|
|
920
|
+
if (action === "deny") return {
|
|
921
|
+
id: call.id,
|
|
922
|
+
name: call.name,
|
|
923
|
+
ok: false,
|
|
924
|
+
output: null,
|
|
925
|
+
error: "Permission denied"
|
|
926
|
+
};
|
|
927
|
+
if (action === "ask") return {
|
|
928
|
+
id: call.id,
|
|
929
|
+
name: call.name,
|
|
930
|
+
ok: false,
|
|
931
|
+
output: null,
|
|
932
|
+
error: "Permission ask unresolved"
|
|
933
|
+
};
|
|
934
|
+
const executor = this.executors.get(call.name);
|
|
935
|
+
if (!executor) return {
|
|
936
|
+
id: call.id,
|
|
937
|
+
name: call.name,
|
|
938
|
+
ok: false,
|
|
939
|
+
output: null,
|
|
940
|
+
error: `Unknown tool: ${call.name}`
|
|
941
|
+
};
|
|
942
|
+
const result = await executor(call, sessionId);
|
|
943
|
+
if (result.mutating || isMutatingName(call.name)) this.effectLog.push({
|
|
944
|
+
sessionId,
|
|
945
|
+
tool: call.name,
|
|
946
|
+
at: Date.now()
|
|
947
|
+
});
|
|
948
|
+
return result;
|
|
949
|
+
}
|
|
950
|
+
effectCount(sessionId, since) {
|
|
951
|
+
return this.effectLog.filter((e) => e.sessionId === sessionId && (!since || e.at >= since)).length;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
function isMutatingName(name) {
|
|
955
|
+
return /^(write|edit|delete|mkdir|rm|mv|save|apply)/i.test(name) || name.includes("write") || name.includes("delete");
|
|
956
|
+
}
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/runtime/tools/workspace-adapter.ts
|
|
959
|
+
function asRecord(value) {
|
|
960
|
+
return value && typeof value === "object" ? value : {};
|
|
961
|
+
}
|
|
962
|
+
/** Registers built-in workspace file tools on the broker. */
|
|
963
|
+
function registerWorkspaceTools(broker) {
|
|
964
|
+
broker.register({
|
|
965
|
+
name: "workspace_list",
|
|
966
|
+
description: "List children of a workspace directory path",
|
|
967
|
+
parameters: {
|
|
968
|
+
type: "object",
|
|
969
|
+
properties: { path: {
|
|
970
|
+
type: "string",
|
|
971
|
+
description: "Directory path relative to workspace"
|
|
972
|
+
} },
|
|
973
|
+
required: ["path"]
|
|
974
|
+
}
|
|
975
|
+
}, async (call) => {
|
|
976
|
+
try {
|
|
977
|
+
const path = String(call.arguments.path ?? "");
|
|
978
|
+
const root = await workspaceService.getWorkspace();
|
|
979
|
+
if (!root) return {
|
|
980
|
+
id: call.id,
|
|
981
|
+
name: call.name,
|
|
982
|
+
ok: false,
|
|
983
|
+
output: null,
|
|
984
|
+
error: "No workspace"
|
|
985
|
+
};
|
|
986
|
+
const resource = await root.getResource(path.endsWith("/") ? path : `${path}/`);
|
|
987
|
+
if (!(resource instanceof Directory)) return {
|
|
988
|
+
id: call.id,
|
|
989
|
+
name: call.name,
|
|
990
|
+
ok: false,
|
|
991
|
+
output: null,
|
|
992
|
+
error: "Not a directory"
|
|
993
|
+
};
|
|
994
|
+
const children = await resource.listChildren(true);
|
|
995
|
+
return {
|
|
996
|
+
id: call.id,
|
|
997
|
+
name: call.name,
|
|
998
|
+
ok: true,
|
|
999
|
+
output: children.map((c) => ({
|
|
1000
|
+
name: c.getName(),
|
|
1001
|
+
path: c.getWorkspacePath()
|
|
1002
|
+
}))
|
|
1003
|
+
};
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
return {
|
|
1006
|
+
id: call.id,
|
|
1007
|
+
name: call.name,
|
|
1008
|
+
ok: false,
|
|
1009
|
+
output: null,
|
|
1010
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
broker.register({
|
|
1015
|
+
name: "workspace_read",
|
|
1016
|
+
description: "Read a workspace file as text",
|
|
1017
|
+
parameters: {
|
|
1018
|
+
type: "object",
|
|
1019
|
+
properties: { path: { type: "string" } },
|
|
1020
|
+
required: ["path"]
|
|
1021
|
+
}
|
|
1022
|
+
}, async (call) => {
|
|
1023
|
+
try {
|
|
1024
|
+
const path = String(call.arguments.path ?? "");
|
|
1025
|
+
const root = await workspaceService.getWorkspace();
|
|
1026
|
+
if (!root) return {
|
|
1027
|
+
id: call.id,
|
|
1028
|
+
name: call.name,
|
|
1029
|
+
ok: false,
|
|
1030
|
+
output: null,
|
|
1031
|
+
error: "No workspace"
|
|
1032
|
+
};
|
|
1033
|
+
const resource = await root.getResource(path);
|
|
1034
|
+
if (!(resource instanceof File)) return {
|
|
1035
|
+
id: call.id,
|
|
1036
|
+
name: call.name,
|
|
1037
|
+
ok: false,
|
|
1038
|
+
output: null,
|
|
1039
|
+
error: "Not a file"
|
|
1040
|
+
};
|
|
1041
|
+
const contents = await resource.getContents();
|
|
1042
|
+
return {
|
|
1043
|
+
id: call.id,
|
|
1044
|
+
name: call.name,
|
|
1045
|
+
ok: true,
|
|
1046
|
+
output: contents
|
|
1047
|
+
};
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
return {
|
|
1050
|
+
id: call.id,
|
|
1051
|
+
name: call.name,
|
|
1052
|
+
ok: false,
|
|
1053
|
+
output: null,
|
|
1054
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
broker.register({
|
|
1059
|
+
name: "workspace_write",
|
|
1060
|
+
description: "Write text contents to a workspace file (creates parents as needed)",
|
|
1061
|
+
parameters: {
|
|
1062
|
+
type: "object",
|
|
1063
|
+
properties: {
|
|
1064
|
+
path: { type: "string" },
|
|
1065
|
+
contents: { type: "string" }
|
|
1066
|
+
},
|
|
1067
|
+
required: ["path", "contents"]
|
|
1068
|
+
}
|
|
1069
|
+
}, async (call) => {
|
|
1070
|
+
try {
|
|
1071
|
+
const args = asRecord(call.arguments);
|
|
1072
|
+
const path = String(args.path ?? "");
|
|
1073
|
+
const contents = String(args.contents ?? "");
|
|
1074
|
+
const root = await workspaceService.getWorkspace();
|
|
1075
|
+
if (!root) return {
|
|
1076
|
+
id: call.id,
|
|
1077
|
+
name: call.name,
|
|
1078
|
+
ok: false,
|
|
1079
|
+
output: null,
|
|
1080
|
+
error: "No workspace",
|
|
1081
|
+
mutating: true
|
|
1082
|
+
};
|
|
1083
|
+
const resource = await root.getResource(path, { create: true });
|
|
1084
|
+
if (!(resource instanceof File)) return {
|
|
1085
|
+
id: call.id,
|
|
1086
|
+
name: call.name,
|
|
1087
|
+
ok: false,
|
|
1088
|
+
output: null,
|
|
1089
|
+
error: "Not a file",
|
|
1090
|
+
mutating: true
|
|
1091
|
+
};
|
|
1092
|
+
await resource.saveContents(contents);
|
|
1093
|
+
return {
|
|
1094
|
+
id: call.id,
|
|
1095
|
+
name: call.name,
|
|
1096
|
+
ok: true,
|
|
1097
|
+
output: { path },
|
|
1098
|
+
mutating: true
|
|
1099
|
+
};
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
return {
|
|
1102
|
+
id: call.id,
|
|
1103
|
+
name: call.name,
|
|
1104
|
+
ok: false,
|
|
1105
|
+
output: null,
|
|
1106
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1107
|
+
mutating: true
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
//#endregion
|
|
1113
|
+
//#region src/runtime/tools/command-adapter.ts
|
|
1114
|
+
var SKIP = /* @__PURE__ */ new Set(["open_cognitive_ai_settings", "open_ai_config"]);
|
|
1115
|
+
/** Built-in tools that must survive command refresh (not command-backed). */
|
|
1116
|
+
var KEEP_TOOLS = /* @__PURE__ */ new Set([
|
|
1117
|
+
"workspace_list",
|
|
1118
|
+
"workspace_read",
|
|
1119
|
+
"workspace_write",
|
|
1120
|
+
"attachment_list",
|
|
1121
|
+
"attachment_read"
|
|
1122
|
+
]);
|
|
1123
|
+
function sanitizeName(id) {
|
|
1124
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
1125
|
+
}
|
|
1126
|
+
/** Registers Docks commands as first-class LLM tools (tool name = command id). */
|
|
1127
|
+
function refreshCommandTools(broker, role) {
|
|
1128
|
+
for (const def of broker.listDefinitions("build")) if (!KEEP_TOOLS.has(def.name)) broker.unregister(def.name);
|
|
1129
|
+
const commands = commandRegistry.listCommands(commandRegistry.createExecutionContext());
|
|
1130
|
+
const entries = Object.values(commands);
|
|
1131
|
+
for (const command of entries) {
|
|
1132
|
+
if (!command?.id || SKIP.has(command.id)) continue;
|
|
1133
|
+
if (!command.description) continue;
|
|
1134
|
+
if ((role === "plan" || role === "explore") && /delete|write|save|rm|mv/i.test(command.id)) continue;
|
|
1135
|
+
const toolName = sanitizeName(command.id);
|
|
1136
|
+
if (KEEP_TOOLS.has(toolName)) continue;
|
|
1137
|
+
const properties = {};
|
|
1138
|
+
const required = [];
|
|
1139
|
+
for (const p of command.parameters ?? []) {
|
|
1140
|
+
properties[p.name] = {
|
|
1141
|
+
type: p.type ?? "string",
|
|
1142
|
+
description: p.description,
|
|
1143
|
+
...p.allowedValues ? { enum: p.allowedValues } : {}
|
|
1144
|
+
};
|
|
1145
|
+
if (p.required) required.push(p.name);
|
|
1146
|
+
}
|
|
1147
|
+
broker.register({
|
|
1148
|
+
name: toolName,
|
|
1149
|
+
description: `${command.name}: ${command.description}`,
|
|
1150
|
+
parameters: {
|
|
1151
|
+
type: "object",
|
|
1152
|
+
properties,
|
|
1153
|
+
required
|
|
1154
|
+
}
|
|
1155
|
+
}, async (call, _sessionId) => {
|
|
1156
|
+
try {
|
|
1157
|
+
const result = await commandRegistry.execute(command.id, { params: call.arguments });
|
|
1158
|
+
return {
|
|
1159
|
+
id: call.id,
|
|
1160
|
+
name: call.name,
|
|
1161
|
+
ok: true,
|
|
1162
|
+
output: result
|
|
1163
|
+
};
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
return {
|
|
1166
|
+
id: call.id,
|
|
1167
|
+
name: call.name,
|
|
1168
|
+
ok: false,
|
|
1169
|
+
output: null,
|
|
1170
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
//#endregion
|
|
1177
|
+
//#region src/runtime/attachments.ts
|
|
1178
|
+
/** Soft cap per attached text file when stored / returned by attachment_read. */
|
|
1179
|
+
var MAX_ATTACHMENT_CHARS = 1e5;
|
|
1180
|
+
/** Soft cap for visual media attachments (bytes). */
|
|
1181
|
+
var MAX_IMAGE_BYTES = 4e6;
|
|
1182
|
+
/** Well-known visual media types (raster icons/images + SVG). */
|
|
1183
|
+
var MEDIA_BY_MIME = /* @__PURE__ */ new Map([
|
|
1184
|
+
["image/jpeg", {
|
|
1185
|
+
mime: "image/jpeg",
|
|
1186
|
+
mode: "raster",
|
|
1187
|
+
vision: true
|
|
1188
|
+
}],
|
|
1189
|
+
["image/jpg", {
|
|
1190
|
+
mime: "image/jpeg",
|
|
1191
|
+
mode: "raster",
|
|
1192
|
+
vision: true
|
|
1193
|
+
}],
|
|
1194
|
+
["image/pjpeg", {
|
|
1195
|
+
mime: "image/jpeg",
|
|
1196
|
+
mode: "raster",
|
|
1197
|
+
vision: true
|
|
1198
|
+
}],
|
|
1199
|
+
["image/png", {
|
|
1200
|
+
mime: "image/png",
|
|
1201
|
+
mode: "raster",
|
|
1202
|
+
vision: true
|
|
1203
|
+
}],
|
|
1204
|
+
["image/x-png", {
|
|
1205
|
+
mime: "image/png",
|
|
1206
|
+
mode: "raster",
|
|
1207
|
+
vision: true
|
|
1208
|
+
}],
|
|
1209
|
+
["image/gif", {
|
|
1210
|
+
mime: "image/gif",
|
|
1211
|
+
mode: "raster",
|
|
1212
|
+
vision: true
|
|
1213
|
+
}],
|
|
1214
|
+
["image/webp", {
|
|
1215
|
+
mime: "image/webp",
|
|
1216
|
+
mode: "raster",
|
|
1217
|
+
vision: true
|
|
1218
|
+
}],
|
|
1219
|
+
["image/bmp", {
|
|
1220
|
+
mime: "image/bmp",
|
|
1221
|
+
mode: "raster",
|
|
1222
|
+
vision: true
|
|
1223
|
+
}],
|
|
1224
|
+
["image/x-ms-bmp", {
|
|
1225
|
+
mime: "image/bmp",
|
|
1226
|
+
mode: "raster",
|
|
1227
|
+
vision: true
|
|
1228
|
+
}],
|
|
1229
|
+
["image/avif", {
|
|
1230
|
+
mime: "image/avif",
|
|
1231
|
+
mode: "raster",
|
|
1232
|
+
vision: true
|
|
1233
|
+
}],
|
|
1234
|
+
["image/tiff", {
|
|
1235
|
+
mime: "image/tiff",
|
|
1236
|
+
mode: "raster",
|
|
1237
|
+
vision: false
|
|
1238
|
+
}],
|
|
1239
|
+
["image/tif", {
|
|
1240
|
+
mime: "image/tiff",
|
|
1241
|
+
mode: "raster",
|
|
1242
|
+
vision: false
|
|
1243
|
+
}],
|
|
1244
|
+
["image/x-icon", {
|
|
1245
|
+
mime: "image/x-icon",
|
|
1246
|
+
mode: "raster",
|
|
1247
|
+
vision: true
|
|
1248
|
+
}],
|
|
1249
|
+
["image/vnd.microsoft.icon", {
|
|
1250
|
+
mime: "image/x-icon",
|
|
1251
|
+
mode: "raster",
|
|
1252
|
+
vision: true
|
|
1253
|
+
}],
|
|
1254
|
+
["image/ico", {
|
|
1255
|
+
mime: "image/x-icon",
|
|
1256
|
+
mode: "raster",
|
|
1257
|
+
vision: true
|
|
1258
|
+
}],
|
|
1259
|
+
["image/svg+xml", {
|
|
1260
|
+
mime: "image/svg+xml",
|
|
1261
|
+
mode: "svg",
|
|
1262
|
+
vision: false
|
|
1263
|
+
}],
|
|
1264
|
+
["image/svg", {
|
|
1265
|
+
mime: "image/svg+xml",
|
|
1266
|
+
mode: "svg",
|
|
1267
|
+
vision: false
|
|
1268
|
+
}]
|
|
1269
|
+
]);
|
|
1270
|
+
var MEDIA_BY_EXT = /* @__PURE__ */ new Map([
|
|
1271
|
+
["jpg", MEDIA_BY_MIME.get("image/jpeg")],
|
|
1272
|
+
["jpeg", MEDIA_BY_MIME.get("image/jpeg")],
|
|
1273
|
+
["jpe", MEDIA_BY_MIME.get("image/jpeg")],
|
|
1274
|
+
["jfif", MEDIA_BY_MIME.get("image/jpeg")],
|
|
1275
|
+
["png", MEDIA_BY_MIME.get("image/png")],
|
|
1276
|
+
["gif", MEDIA_BY_MIME.get("image/gif")],
|
|
1277
|
+
["webp", MEDIA_BY_MIME.get("image/webp")],
|
|
1278
|
+
["bmp", MEDIA_BY_MIME.get("image/bmp")],
|
|
1279
|
+
["avif", MEDIA_BY_MIME.get("image/avif")],
|
|
1280
|
+
["tif", MEDIA_BY_MIME.get("image/tiff")],
|
|
1281
|
+
["tiff", MEDIA_BY_MIME.get("image/tiff")],
|
|
1282
|
+
["ico", MEDIA_BY_MIME.get("image/x-icon")],
|
|
1283
|
+
["cur", MEDIA_BY_MIME.get("image/x-icon")],
|
|
1284
|
+
["svg", MEDIA_BY_MIME.get("image/svg+xml")],
|
|
1285
|
+
["svgz", MEDIA_BY_MIME.get("image/svg+xml")]
|
|
1286
|
+
]);
|
|
1287
|
+
/** File picker `accept` list for visual media + common text sources. */
|
|
1288
|
+
var ATTACHMENT_FILE_ACCEPT = [
|
|
1289
|
+
"image/*",
|
|
1290
|
+
".svg",
|
|
1291
|
+
".svgz",
|
|
1292
|
+
".ico",
|
|
1293
|
+
".cur",
|
|
1294
|
+
".bmp",
|
|
1295
|
+
".tif",
|
|
1296
|
+
".tiff",
|
|
1297
|
+
".avif",
|
|
1298
|
+
"text/*",
|
|
1299
|
+
".ts",
|
|
1300
|
+
".tsx",
|
|
1301
|
+
".js",
|
|
1302
|
+
".jsx",
|
|
1303
|
+
".json",
|
|
1304
|
+
".md",
|
|
1305
|
+
".css",
|
|
1306
|
+
".html",
|
|
1307
|
+
".py",
|
|
1308
|
+
".rs",
|
|
1309
|
+
".go",
|
|
1310
|
+
".sql",
|
|
1311
|
+
".yml",
|
|
1312
|
+
".yaml",
|
|
1313
|
+
".toml",
|
|
1314
|
+
".txt",
|
|
1315
|
+
".csv"
|
|
1316
|
+
].join(",");
|
|
1317
|
+
function fileExtension(name) {
|
|
1318
|
+
const base = name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
|
|
1319
|
+
const dot = base.lastIndexOf(".");
|
|
1320
|
+
return dot >= 0 ? base.slice(dot + 1).toLowerCase() : "";
|
|
1321
|
+
}
|
|
1322
|
+
function resolveMediaType(input) {
|
|
1323
|
+
const mime = (input.mime || "").toLowerCase().trim();
|
|
1324
|
+
if (mime && MEDIA_BY_MIME.has(mime)) return MEDIA_BY_MIME.get(mime);
|
|
1325
|
+
const ext = fileExtension(input.name);
|
|
1326
|
+
return MEDIA_BY_EXT.get(ext) ?? null;
|
|
1327
|
+
}
|
|
1328
|
+
function isImageAttachment(part) {
|
|
1329
|
+
if (part.kind === "image") return true;
|
|
1330
|
+
if (part.dataUrl?.startsWith("data:image/")) return true;
|
|
1331
|
+
return resolveMediaType({
|
|
1332
|
+
name: part.name,
|
|
1333
|
+
mime: part.mime
|
|
1334
|
+
}) != null;
|
|
1335
|
+
}
|
|
1336
|
+
function isSvgAttachment(part) {
|
|
1337
|
+
return resolveMediaType({
|
|
1338
|
+
name: part.name,
|
|
1339
|
+
mime: part.mime
|
|
1340
|
+
})?.mode === "svg" || Boolean(part.text && part.mime?.includes("svg"));
|
|
1341
|
+
}
|
|
1342
|
+
function newAttachmentId() {
|
|
1343
|
+
return `att-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1344
|
+
}
|
|
1345
|
+
/** Persistable session entry holding the full payload (not sent in chat history). */
|
|
1346
|
+
function toSessionAttachment(attachment, opts = {}) {
|
|
1347
|
+
if ((isImageAttachment(attachment) ? "image" : "text") === "image") {
|
|
1348
|
+
let text = attachment.text;
|
|
1349
|
+
let truncated = Boolean(attachment.truncated);
|
|
1350
|
+
if (text && text.length > 1e5) {
|
|
1351
|
+
text = text.slice(0, MAX_ATTACHMENT_CHARS);
|
|
1352
|
+
truncated = true;
|
|
1353
|
+
}
|
|
1354
|
+
return {
|
|
1355
|
+
id: opts.id ?? newAttachmentId(),
|
|
1356
|
+
name: attachment.name,
|
|
1357
|
+
path: attachment.path,
|
|
1358
|
+
mime: attachment.mime,
|
|
1359
|
+
kind: "image",
|
|
1360
|
+
text,
|
|
1361
|
+
dataUrl: attachment.dataUrl,
|
|
1362
|
+
byteLength: attachment.byteLength,
|
|
1363
|
+
truncated: text ? truncated : attachment.truncated,
|
|
1364
|
+
createdAt: Date.now(),
|
|
1365
|
+
messageId: opts.messageId
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
let text = attachment.text ?? "";
|
|
1369
|
+
let truncated = Boolean(attachment.truncated);
|
|
1370
|
+
if (text.length > 1e5) {
|
|
1371
|
+
text = text.slice(0, MAX_ATTACHMENT_CHARS);
|
|
1372
|
+
truncated = true;
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
id: opts.id ?? newAttachmentId(),
|
|
1376
|
+
name: attachment.name,
|
|
1377
|
+
path: attachment.path,
|
|
1378
|
+
mime: attachment.mime,
|
|
1379
|
+
kind: "text",
|
|
1380
|
+
text,
|
|
1381
|
+
byteLength: attachment.byteLength,
|
|
1382
|
+
truncated,
|
|
1383
|
+
createdAt: Date.now(),
|
|
1384
|
+
messageId: opts.messageId
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
/** Lightweight history/UI reference — no payload. */
|
|
1388
|
+
function toFileRefPart(stored) {
|
|
1389
|
+
return {
|
|
1390
|
+
type: "file",
|
|
1391
|
+
id: stored.id,
|
|
1392
|
+
name: stored.name,
|
|
1393
|
+
path: stored.path,
|
|
1394
|
+
mime: stored.mime,
|
|
1395
|
+
kind: stored.kind,
|
|
1396
|
+
byteLength: stored.byteLength
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
/** One-line catalog entry published into the user turn for the model. */
|
|
1400
|
+
function formatAttachmentRefForModel(part) {
|
|
1401
|
+
const label = part.path?.trim() || part.name;
|
|
1402
|
+
const kind = part.kind ?? "file";
|
|
1403
|
+
const bytes = part.byteLength != null ? ` bytes=${part.byteLength}` : "";
|
|
1404
|
+
return `[Attachment published: id=${part.id} name="${label}" kind=${kind}${part.mime ? ` mime=${part.mime}` : ""}${bytes}] → use attachment_read with id="${part.id}"`;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Build provider user content from history parts.
|
|
1408
|
+
* File parts are references only — payloads stay in the session store.
|
|
1409
|
+
*/
|
|
1410
|
+
function userContentFromParts(parts) {
|
|
1411
|
+
const texts = [];
|
|
1412
|
+
const refs = [];
|
|
1413
|
+
for (const part of parts) {
|
|
1414
|
+
if (part.type === "text" && part.text.trim()) texts.push(part.text);
|
|
1415
|
+
if (part.type === "file") refs.push(formatAttachmentRefForModel(part));
|
|
1416
|
+
}
|
|
1417
|
+
const body = [...texts, ...refs].join("\n").trim();
|
|
1418
|
+
if (!refs.length) return body;
|
|
1419
|
+
return [body, "Attachments live in this session store only. Call attachment_read (or attachment_list) before answering about them. Do not use canvas/workspace/editor commands to locate attachments."].filter(Boolean).join("\n");
|
|
1420
|
+
}
|
|
1421
|
+
function attachmentMeta(stored) {
|
|
1422
|
+
return {
|
|
1423
|
+
id: stored.id,
|
|
1424
|
+
name: stored.name,
|
|
1425
|
+
path: stored.path,
|
|
1426
|
+
mime: stored.mime,
|
|
1427
|
+
kind: stored.kind,
|
|
1428
|
+
byteLength: stored.byteLength,
|
|
1429
|
+
truncated: stored.truncated
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function readTextAttachment(stored, opts = {}) {
|
|
1433
|
+
if (stored.kind !== "text" && !(stored.kind === "image" && stored.text)) return {
|
|
1434
|
+
ok: false,
|
|
1435
|
+
error: `${stored.name} is not a text attachment`
|
|
1436
|
+
};
|
|
1437
|
+
const full = stored.text ?? "";
|
|
1438
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
1439
|
+
const limit = Math.max(1, opts.limit ?? 4e4);
|
|
1440
|
+
const slice = full.slice(offset, offset + limit);
|
|
1441
|
+
return {
|
|
1442
|
+
ok: true,
|
|
1443
|
+
output: {
|
|
1444
|
+
...attachmentMeta(stored),
|
|
1445
|
+
offset,
|
|
1446
|
+
length: slice.length,
|
|
1447
|
+
totalLength: full.length,
|
|
1448
|
+
truncated: offset + slice.length < full.length,
|
|
1449
|
+
content: slice
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
/** Marker field: specialist loop presents image bytes in a follow-up multimodal message. */
|
|
1454
|
+
var IMAGE_PRESENT_KEY = "_presentImage";
|
|
1455
|
+
function readImageAttachment(stored, opts = {}) {
|
|
1456
|
+
if (stored.kind !== "image") return {
|
|
1457
|
+
ok: false,
|
|
1458
|
+
error: `${stored.name} is not an image attachment`
|
|
1459
|
+
};
|
|
1460
|
+
if (stored.text != null && isSvgAttachment(stored)) return readTextAttachment(stored, opts);
|
|
1461
|
+
if (!stored.dataUrl) return {
|
|
1462
|
+
ok: false,
|
|
1463
|
+
error: `${stored.name} has no image payload`
|
|
1464
|
+
};
|
|
1465
|
+
const present = resolveMediaType({
|
|
1466
|
+
name: stored.name,
|
|
1467
|
+
mime: stored.mime
|
|
1468
|
+
})?.vision !== false;
|
|
1469
|
+
return {
|
|
1470
|
+
ok: true,
|
|
1471
|
+
output: {
|
|
1472
|
+
...attachmentMeta(stored),
|
|
1473
|
+
...present ? {
|
|
1474
|
+
[IMAGE_PRESENT_KEY]: true,
|
|
1475
|
+
dataUrl: stored.dataUrl,
|
|
1476
|
+
note: "Image bytes will be provided to the model in the next message for vision inspection."
|
|
1477
|
+
} : {
|
|
1478
|
+
dataUrlOmitted: true,
|
|
1479
|
+
note: "This media type is stored for preview; vision presentation is skipped. Ask for a raster export if needed."
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function stripAttachmentToolOutput(output) {
|
|
1485
|
+
if (!output || typeof output !== "object") return output;
|
|
1486
|
+
const copy = { ...output };
|
|
1487
|
+
delete copy.dataUrl;
|
|
1488
|
+
delete copy[IMAGE_PRESENT_KEY];
|
|
1489
|
+
return copy;
|
|
1490
|
+
}
|
|
1491
|
+
function looksBinaryText(sample) {
|
|
1492
|
+
return sample.includes("\0");
|
|
1493
|
+
}
|
|
1494
|
+
function bytesToBase64(bytes) {
|
|
1495
|
+
const chunk = 32768;
|
|
1496
|
+
let binary = "";
|
|
1497
|
+
for (let i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
1498
|
+
return btoa(binary);
|
|
1499
|
+
}
|
|
1500
|
+
function textToBase64(text) {
|
|
1501
|
+
return bytesToBase64(new TextEncoder().encode(text));
|
|
1502
|
+
}
|
|
1503
|
+
async function readFileAsDataUrl(file, mime) {
|
|
1504
|
+
const buffer = await file.arrayBuffer();
|
|
1505
|
+
return `data:${mime};base64,${bytesToBase64(new Uint8Array(buffer))}`;
|
|
1506
|
+
}
|
|
1507
|
+
function svgDataUrl(text) {
|
|
1508
|
+
return `data:image/svg+xml;base64,${textToBase64(text)}`;
|
|
1509
|
+
}
|
|
1510
|
+
/** Read a browser `File` into a prompt attachment (payload kept until publish). */
|
|
1511
|
+
async function readPromptAttachmentFromFile(file) {
|
|
1512
|
+
const mime = file.type || "application/octet-stream";
|
|
1513
|
+
const media = resolveMediaType({
|
|
1514
|
+
name: file.name,
|
|
1515
|
+
mime
|
|
1516
|
+
});
|
|
1517
|
+
if (media) {
|
|
1518
|
+
if (file.size > 4e6) return {
|
|
1519
|
+
ok: false,
|
|
1520
|
+
error: `${file.name}: media is larger than ${Math.round(MAX_IMAGE_BYTES / 1e6)}MB`
|
|
1521
|
+
};
|
|
1522
|
+
if (media.mode === "svg") try {
|
|
1523
|
+
const text = await file.text();
|
|
1524
|
+
if (!text.trim()) return {
|
|
1525
|
+
ok: false,
|
|
1526
|
+
error: `${file.name}: empty SVG`
|
|
1527
|
+
};
|
|
1528
|
+
let storedText = text;
|
|
1529
|
+
let truncated = false;
|
|
1530
|
+
if (storedText.length > 1e5) {
|
|
1531
|
+
storedText = storedText.slice(0, MAX_ATTACHMENT_CHARS);
|
|
1532
|
+
truncated = true;
|
|
1533
|
+
}
|
|
1534
|
+
return {
|
|
1535
|
+
ok: true,
|
|
1536
|
+
attachment: {
|
|
1537
|
+
name: file.name,
|
|
1538
|
+
mime: media.mime,
|
|
1539
|
+
kind: "image",
|
|
1540
|
+
text: storedText,
|
|
1541
|
+
dataUrl: svgDataUrl(storedText),
|
|
1542
|
+
byteLength: file.size,
|
|
1543
|
+
truncated
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
return {
|
|
1548
|
+
ok: false,
|
|
1549
|
+
error: `${file.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
try {
|
|
1553
|
+
const dataUrl = await readFileAsDataUrl(file, media.mime);
|
|
1554
|
+
return {
|
|
1555
|
+
ok: true,
|
|
1556
|
+
attachment: {
|
|
1557
|
+
name: file.name,
|
|
1558
|
+
mime: media.mime,
|
|
1559
|
+
kind: "image",
|
|
1560
|
+
dataUrl,
|
|
1561
|
+
byteLength: file.size
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
return {
|
|
1566
|
+
ok: false,
|
|
1567
|
+
error: `${file.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (mime.startsWith("audio/") || mime.startsWith("video/")) return {
|
|
1572
|
+
ok: false,
|
|
1573
|
+
error: `${file.name}: audio/video attachments are not supported`
|
|
1574
|
+
};
|
|
1575
|
+
if (mime.startsWith("image/")) return {
|
|
1576
|
+
ok: false,
|
|
1577
|
+
error: `${file.name}: unsupported image type (try jpeg, png, gif, webp, svg, ico, bmp, avif)`
|
|
1578
|
+
};
|
|
1579
|
+
if (file.size > 2e6) return {
|
|
1580
|
+
ok: false,
|
|
1581
|
+
error: `${file.name}: file is larger than 2MB`
|
|
1582
|
+
};
|
|
1583
|
+
let text;
|
|
1584
|
+
try {
|
|
1585
|
+
text = await file.text();
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
return {
|
|
1588
|
+
ok: false,
|
|
1589
|
+
error: `${file.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
if (looksBinaryText(text.slice(0, 2048))) return {
|
|
1593
|
+
ok: false,
|
|
1594
|
+
error: `${file.name}: binary files are not supported`
|
|
1595
|
+
};
|
|
1596
|
+
let truncated = false;
|
|
1597
|
+
if (text.length > 1e5) {
|
|
1598
|
+
text = text.slice(0, MAX_ATTACHMENT_CHARS);
|
|
1599
|
+
truncated = true;
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
ok: true,
|
|
1603
|
+
attachment: {
|
|
1604
|
+
name: file.name,
|
|
1605
|
+
mime,
|
|
1606
|
+
kind: "text",
|
|
1607
|
+
text,
|
|
1608
|
+
byteLength: file.size,
|
|
1609
|
+
truncated
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
//#endregion
|
|
1614
|
+
//#region src/runtime/tools/attachment-adapter.ts
|
|
1615
|
+
/** Registers read-only session attachment tools. */
|
|
1616
|
+
function registerAttachmentTools(broker, lookup) {
|
|
1617
|
+
broker.register({
|
|
1618
|
+
name: "attachment_list",
|
|
1619
|
+
description: "List chat-session attachments (id, name, kind, mime, size). Use this for files the user attached in chat — not workspace/canvas files. Payloads are omitted; call attachment_read to inspect one.",
|
|
1620
|
+
parameters: {
|
|
1621
|
+
type: "object",
|
|
1622
|
+
properties: {}
|
|
1623
|
+
}
|
|
1624
|
+
}, async (call, sessionId) => {
|
|
1625
|
+
const items = lookup(sessionId).map(attachmentMeta);
|
|
1626
|
+
return {
|
|
1627
|
+
id: call.id,
|
|
1628
|
+
name: call.name,
|
|
1629
|
+
ok: true,
|
|
1630
|
+
output: {
|
|
1631
|
+
count: items.length,
|
|
1632
|
+
attachments: items
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
});
|
|
1636
|
+
broker.register({
|
|
1637
|
+
name: "attachment_read",
|
|
1638
|
+
description: "Inspect a chat-session attachment by id from the user message or attachment_list. Prefer this whenever the user asks about an attached screenshot/file. Text returns content (optional offset/limit). Images are presented for vision inspection.",
|
|
1639
|
+
parameters: {
|
|
1640
|
+
type: "object",
|
|
1641
|
+
properties: {
|
|
1642
|
+
id: {
|
|
1643
|
+
type: "string",
|
|
1644
|
+
description: "Attachment id from attachment_list or the user message"
|
|
1645
|
+
},
|
|
1646
|
+
offset: {
|
|
1647
|
+
type: "number",
|
|
1648
|
+
description: "Text start offset (characters)"
|
|
1649
|
+
},
|
|
1650
|
+
limit: {
|
|
1651
|
+
type: "number",
|
|
1652
|
+
description: "Max text characters to return"
|
|
1653
|
+
}
|
|
1654
|
+
},
|
|
1655
|
+
required: ["id"]
|
|
1656
|
+
}
|
|
1657
|
+
}, async (call, sessionId) => {
|
|
1658
|
+
const id = String(call.arguments.id ?? "").trim();
|
|
1659
|
+
if (!id) return {
|
|
1660
|
+
id: call.id,
|
|
1661
|
+
name: call.name,
|
|
1662
|
+
ok: false,
|
|
1663
|
+
output: null,
|
|
1664
|
+
error: "id is required"
|
|
1665
|
+
};
|
|
1666
|
+
const stored = lookup(sessionId, id)[0];
|
|
1667
|
+
if (!stored) return {
|
|
1668
|
+
id: call.id,
|
|
1669
|
+
name: call.name,
|
|
1670
|
+
ok: false,
|
|
1671
|
+
output: null,
|
|
1672
|
+
error: `Unknown attachment: ${id}`
|
|
1673
|
+
};
|
|
1674
|
+
if (stored.kind === "image") {
|
|
1675
|
+
const result = readImageAttachment(stored, {
|
|
1676
|
+
offset: typeof call.arguments.offset === "number" ? call.arguments.offset : void 0,
|
|
1677
|
+
limit: typeof call.arguments.limit === "number" ? call.arguments.limit : void 0
|
|
1678
|
+
});
|
|
1679
|
+
if (!result.ok) return {
|
|
1680
|
+
id: call.id,
|
|
1681
|
+
name: call.name,
|
|
1682
|
+
ok: false,
|
|
1683
|
+
output: null,
|
|
1684
|
+
error: result.error
|
|
1685
|
+
};
|
|
1686
|
+
return {
|
|
1687
|
+
id: call.id,
|
|
1688
|
+
name: call.name,
|
|
1689
|
+
ok: true,
|
|
1690
|
+
output: result.output
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
const result = readTextAttachment(stored, {
|
|
1694
|
+
offset: typeof call.arguments.offset === "number" ? call.arguments.offset : void 0,
|
|
1695
|
+
limit: typeof call.arguments.limit === "number" ? call.arguments.limit : void 0
|
|
1696
|
+
});
|
|
1697
|
+
if (!result.ok) return {
|
|
1698
|
+
id: call.id,
|
|
1699
|
+
name: call.name,
|
|
1700
|
+
ok: false,
|
|
1701
|
+
output: null,
|
|
1702
|
+
error: result.error
|
|
1703
|
+
};
|
|
1704
|
+
return {
|
|
1705
|
+
id: call.id,
|
|
1706
|
+
name: call.name,
|
|
1707
|
+
ok: true,
|
|
1708
|
+
output: result.output
|
|
1709
|
+
};
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
//#endregion
|
|
1713
|
+
//#region src/runtime/compact/compaction.ts
|
|
1714
|
+
var COMPACT_SYSTEM = `You compress older chat turns into a durable summary for future assistant turns.
|
|
1715
|
+
Preserve: user goals, decisions, constraints, important file/paths, tool outcomes that still matter, and open questions.
|
|
1716
|
+
Omit chit-chat, repeated content, and raw tool dumps.
|
|
1717
|
+
Reply with concise markdown only (short sections or bullets). No preamble.`;
|
|
1718
|
+
/** Format one message for extractive / LLM compaction input. */
|
|
1719
|
+
function formatMessageForCompaction(msg, maxChars = 280) {
|
|
1720
|
+
const bits = [];
|
|
1721
|
+
for (const part of msg.parts) {
|
|
1722
|
+
const piece = formatPart(part);
|
|
1723
|
+
if (piece) bits.push(piece);
|
|
1724
|
+
}
|
|
1725
|
+
const body = bits.join(" · ").replace(/\s+/g, " ").trim();
|
|
1726
|
+
if (!body) return "";
|
|
1727
|
+
const clipped = body.length <= maxChars ? body : `${body.slice(0, maxChars)}…`;
|
|
1728
|
+
return `${msg.role}: ${clipped}`;
|
|
1729
|
+
}
|
|
1730
|
+
function formatPart(part) {
|
|
1731
|
+
if (part.type === "text") return part.text;
|
|
1732
|
+
if (part.type === "file") {
|
|
1733
|
+
const label = part.path || part.name;
|
|
1734
|
+
return `${part.kind === "image" ? "image" : "file"} ${label} (${part.id})`;
|
|
1735
|
+
}
|
|
1736
|
+
if (part.type === "reasoning") return `(thinking) ${part.text}`;
|
|
1737
|
+
if (part.type === "tool") {
|
|
1738
|
+
const status = part.status === "error" ? "error" : part.status === "done" ? "ok" : part.status;
|
|
1739
|
+
const result = part.result === void 0 ? "" : ` → ${JSON.stringify(part.result).slice(0, 120)}`;
|
|
1740
|
+
return `tool ${part.name} [${status}]${result}`;
|
|
1741
|
+
}
|
|
1742
|
+
if (part.type === "step") return `step ${part.title}`;
|
|
1743
|
+
return "";
|
|
1744
|
+
}
|
|
1745
|
+
/** Anchored summary compaction: keep recent messages, summarize the rest. */
|
|
1746
|
+
function compactHistory(history, options = {}) {
|
|
1747
|
+
const keepCount = options.keepCount ?? 12;
|
|
1748
|
+
if (history.length <= keepCount) return {
|
|
1749
|
+
summary: options.previousSummary ?? "",
|
|
1750
|
+
kept: [...history],
|
|
1751
|
+
dropped: [],
|
|
1752
|
+
didCompact: false
|
|
1753
|
+
};
|
|
1754
|
+
const kept = history.slice(-keepCount);
|
|
1755
|
+
const dropped = history.slice(0, -keepCount);
|
|
1756
|
+
return {
|
|
1757
|
+
summary: buildExtractiveSummary(dropped, options.previousSummary),
|
|
1758
|
+
kept,
|
|
1759
|
+
dropped,
|
|
1760
|
+
didCompact: true
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
function buildExtractiveSummary(dropped, previousSummary) {
|
|
1764
|
+
const lines = [];
|
|
1765
|
+
if (previousSummary?.trim()) lines.push(`Prior: ${previousSummary.trim()}`);
|
|
1766
|
+
for (const msg of dropped) {
|
|
1767
|
+
const line = formatMessageForCompaction(msg);
|
|
1768
|
+
if (line) lines.push(line);
|
|
1769
|
+
}
|
|
1770
|
+
return [
|
|
1771
|
+
"## Objective",
|
|
1772
|
+
"Continue prior work from compacted history.",
|
|
1773
|
+
"## Notes",
|
|
1774
|
+
...lines.slice(-24)
|
|
1775
|
+
].join("\n");
|
|
1776
|
+
}
|
|
1777
|
+
/** Ask the model to summarize dropped turns; returns null on empty/failure. */
|
|
1778
|
+
async function summarizeDroppedHistory(input) {
|
|
1779
|
+
if (!input.dropped.length) return null;
|
|
1780
|
+
const transcript = input.dropped.map((msg) => formatMessageForCompaction(msg, 600)).filter(Boolean).join("\n");
|
|
1781
|
+
if (!transcript.trim()) return null;
|
|
1782
|
+
const userParts = [input.previousSummary?.trim() ? `## Existing summary\n${input.previousSummary.trim()}` : "", `## Older turns to fold in\n${transcript}`].filter(Boolean);
|
|
1783
|
+
const messages = [{
|
|
1784
|
+
role: "system",
|
|
1785
|
+
content: COMPACT_SYSTEM
|
|
1786
|
+
}, {
|
|
1787
|
+
role: "user",
|
|
1788
|
+
content: userParts.join("\n\n")
|
|
1789
|
+
}];
|
|
1790
|
+
let text = "";
|
|
1791
|
+
try {
|
|
1792
|
+
for await (const chunk of input.provider.stream({
|
|
1793
|
+
model: input.model,
|
|
1794
|
+
messages,
|
|
1795
|
+
providerOptions: input.providerOptions ?? { reasoning_effort: "none" },
|
|
1796
|
+
signal: input.signal
|
|
1797
|
+
})) {
|
|
1798
|
+
if (chunk.type === "error") return null;
|
|
1799
|
+
if (chunk.type === "text" && chunk.text) text += chunk.text;
|
|
1800
|
+
}
|
|
1801
|
+
} catch {
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
return text.trim() || null;
|
|
1805
|
+
}
|
|
1806
|
+
/** System message body injected ahead of live history when a summary exists. */
|
|
1807
|
+
function compactionContextMessage(summary) {
|
|
1808
|
+
return `## Compacted prior context\nThe following summarizes earlier turns that were removed from the live transcript. Treat it as authoritative background.\n\n${summary.trim()}`;
|
|
1809
|
+
}
|
|
1810
|
+
//#endregion
|
|
1811
|
+
//#region src/runtime/session/session-runtime.ts
|
|
1812
|
+
function sid() {
|
|
1813
|
+
return `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1814
|
+
}
|
|
1815
|
+
function cloneBuffers(buffers) {
|
|
1816
|
+
return {
|
|
1817
|
+
goal: buffers.goal ? {
|
|
1818
|
+
...buffers.goal,
|
|
1819
|
+
slots: { ...buffers.goal.slots }
|
|
1820
|
+
} : null,
|
|
1821
|
+
retrieval: buffers.retrieval ? {
|
|
1822
|
+
...buffers.retrieval,
|
|
1823
|
+
slots: { ...buffers.retrieval.slots }
|
|
1824
|
+
} : null,
|
|
1825
|
+
imaginal: buffers.imaginal ? {
|
|
1826
|
+
...buffers.imaginal,
|
|
1827
|
+
slots: { ...buffers.imaginal.slots }
|
|
1828
|
+
} : null,
|
|
1829
|
+
emotion: buffers.emotion ? {
|
|
1830
|
+
...buffers.emotion,
|
|
1831
|
+
slots: { ...buffers.emotion.slots }
|
|
1832
|
+
} : null,
|
|
1833
|
+
motor: buffers.motor ? {
|
|
1834
|
+
...buffers.motor,
|
|
1835
|
+
slots: { ...buffers.motor.slots }
|
|
1836
|
+
} : null
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
var SessionRuntime = class {
|
|
1840
|
+
constructor() {
|
|
1841
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
1842
|
+
this.checkpoints = /* @__PURE__ */ new Map();
|
|
1843
|
+
this.focusedId = null;
|
|
1844
|
+
}
|
|
1845
|
+
createSession(input = {}) {
|
|
1846
|
+
const session = {
|
|
1847
|
+
id: sid(),
|
|
1848
|
+
title: input.title ?? "New session",
|
|
1849
|
+
status: "idle",
|
|
1850
|
+
history: [],
|
|
1851
|
+
attachments: [],
|
|
1852
|
+
buffers: emptyBuffers(),
|
|
1853
|
+
specialistMode: input.specialistMode ?? "build",
|
|
1854
|
+
parentSessionId: input.parentSessionId,
|
|
1855
|
+
createdAt: Date.now(),
|
|
1856
|
+
updatedAt: Date.now()
|
|
1857
|
+
};
|
|
1858
|
+
this.sessions.set(session.id, session);
|
|
1859
|
+
this.checkpoints.set(session.id, []);
|
|
1860
|
+
this.focusedId = session.id;
|
|
1861
|
+
return session;
|
|
1862
|
+
}
|
|
1863
|
+
get(sessionId) {
|
|
1864
|
+
return this.sessions.get(sessionId);
|
|
1865
|
+
}
|
|
1866
|
+
list() {
|
|
1867
|
+
return [...this.sessions.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1868
|
+
}
|
|
1869
|
+
focus(sessionId) {
|
|
1870
|
+
if (!this.sessions.has(sessionId)) return false;
|
|
1871
|
+
this.focusedId = sessionId;
|
|
1872
|
+
return true;
|
|
1873
|
+
}
|
|
1874
|
+
focused() {
|
|
1875
|
+
return this.focusedId ? this.sessions.get(this.focusedId) : void 0;
|
|
1876
|
+
}
|
|
1877
|
+
remove(sessionId) {
|
|
1878
|
+
if (!this.sessions.has(sessionId)) return false;
|
|
1879
|
+
this.sessions.delete(sessionId);
|
|
1880
|
+
this.checkpoints.delete(sessionId);
|
|
1881
|
+
if (this.focusedId === sessionId) this.focusedId = this.list()[0]?.id ?? null;
|
|
1882
|
+
return true;
|
|
1883
|
+
}
|
|
1884
|
+
update(sessionId, patch) {
|
|
1885
|
+
const session = this.sessions.get(sessionId);
|
|
1886
|
+
if (!session) return void 0;
|
|
1887
|
+
const next = {
|
|
1888
|
+
...session,
|
|
1889
|
+
...patch,
|
|
1890
|
+
updatedAt: Date.now()
|
|
1891
|
+
};
|
|
1892
|
+
this.sessions.set(sessionId, next);
|
|
1893
|
+
return next;
|
|
1894
|
+
}
|
|
1895
|
+
addMessage(sessionId, message) {
|
|
1896
|
+
const session = this.sessions.get(sessionId);
|
|
1897
|
+
if (!session) return;
|
|
1898
|
+
session.history = [...session.history, message];
|
|
1899
|
+
session.updatedAt = Date.now();
|
|
1900
|
+
this.sessions.set(sessionId, session);
|
|
1901
|
+
}
|
|
1902
|
+
checkpoint(sessionId, label, toolEffectCount = 0) {
|
|
1903
|
+
const session = this.sessions.get(sessionId);
|
|
1904
|
+
if (!session) return void 0;
|
|
1905
|
+
const cp = {
|
|
1906
|
+
id: `cp-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
1907
|
+
sessionId,
|
|
1908
|
+
label: label ?? `Checkpoint ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
|
|
1909
|
+
createdAt: Date.now(),
|
|
1910
|
+
messageSeq: session.history.length,
|
|
1911
|
+
buffers: cloneBuffers(session.buffers),
|
|
1912
|
+
compactionSummary: session.compactionSummary,
|
|
1913
|
+
specialistMode: session.specialistMode,
|
|
1914
|
+
toolEffectCount
|
|
1915
|
+
};
|
|
1916
|
+
const list = this.checkpoints.get(sessionId) ?? [];
|
|
1917
|
+
list.push(cp);
|
|
1918
|
+
this.checkpoints.set(sessionId, list);
|
|
1919
|
+
return cp;
|
|
1920
|
+
}
|
|
1921
|
+
listCheckpoints(sessionId) {
|
|
1922
|
+
return [...this.checkpoints.get(sessionId) ?? []];
|
|
1923
|
+
}
|
|
1924
|
+
restore(input) {
|
|
1925
|
+
const session = this.sessions.get(input.sessionId);
|
|
1926
|
+
const cp = (this.checkpoints.get(input.sessionId) ?? []).find((c) => c.id === input.checkpointId);
|
|
1927
|
+
if (!session || !cp) return void 0;
|
|
1928
|
+
const history = session.history.slice(0, cp.messageSeq);
|
|
1929
|
+
const buffers = cloneBuffers(cp.buffers);
|
|
1930
|
+
if (input.mode === "fork") {
|
|
1931
|
+
const forked = this.createSession({
|
|
1932
|
+
title: `${session.title} (restored)`,
|
|
1933
|
+
specialistMode: cp.specialistMode
|
|
1934
|
+
});
|
|
1935
|
+
this.update(forked.id, {
|
|
1936
|
+
history,
|
|
1937
|
+
buffers,
|
|
1938
|
+
compactionSummary: cp.compactionSummary,
|
|
1939
|
+
status: "idle"
|
|
1940
|
+
});
|
|
1941
|
+
return { sessionId: forked.id };
|
|
1942
|
+
}
|
|
1943
|
+
this.update(input.sessionId, {
|
|
1944
|
+
history,
|
|
1945
|
+
buffers,
|
|
1946
|
+
compactionSummary: cp.compactionSummary,
|
|
1947
|
+
specialistMode: cp.specialistMode,
|
|
1948
|
+
status: "idle"
|
|
1949
|
+
});
|
|
1950
|
+
return { sessionId: input.sessionId };
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Truncate history and store a compaction summary.
|
|
1954
|
+
* Pass `summary` to override the extractive default (e.g. LLM summary).
|
|
1955
|
+
* Returns whether history was shortened.
|
|
1956
|
+
*/
|
|
1957
|
+
compact(sessionId, keepCount = 12, summary) {
|
|
1958
|
+
const session = this.sessions.get(sessionId);
|
|
1959
|
+
if (!session) return {
|
|
1960
|
+
didCompact: false,
|
|
1961
|
+
summary: ""
|
|
1962
|
+
};
|
|
1963
|
+
const result = compactHistory(session.history, {
|
|
1964
|
+
keepCount,
|
|
1965
|
+
previousSummary: session.compactionSummary
|
|
1966
|
+
});
|
|
1967
|
+
if (!result.didCompact) return {
|
|
1968
|
+
didCompact: false,
|
|
1969
|
+
summary: session.compactionSummary ?? ""
|
|
1970
|
+
};
|
|
1971
|
+
const nextSummary = (summary?.trim() || result.summary || session.compactionSummary || "").trim();
|
|
1972
|
+
this.update(sessionId, {
|
|
1973
|
+
history: result.kept,
|
|
1974
|
+
compactionSummary: nextSummary
|
|
1975
|
+
});
|
|
1976
|
+
return {
|
|
1977
|
+
didCompact: true,
|
|
1978
|
+
summary: nextSummary
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
/** Preview a compact without mutating (for LLM summarization of dropped turns). */
|
|
1982
|
+
prepareCompact(sessionId, keepCount = 12) {
|
|
1983
|
+
const session = this.sessions.get(sessionId);
|
|
1984
|
+
if (!session) return null;
|
|
1985
|
+
return compactHistory(session.history, {
|
|
1986
|
+
keepCount,
|
|
1987
|
+
previousSummary: session.compactionSummary
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
snapshot() {
|
|
1991
|
+
const checkpoints = {};
|
|
1992
|
+
for (const [id, list] of this.checkpoints) checkpoints[id] = list.map((c) => ({
|
|
1993
|
+
...c,
|
|
1994
|
+
buffers: cloneBuffers(c.buffers)
|
|
1995
|
+
}));
|
|
1996
|
+
return {
|
|
1997
|
+
sessions: this.list().map((s) => ({
|
|
1998
|
+
...s,
|
|
1999
|
+
attachments: [...s.attachments ?? []],
|
|
2000
|
+
history: s.history.map((m) => ({
|
|
2001
|
+
...m,
|
|
2002
|
+
parts: [...m.parts]
|
|
2003
|
+
})),
|
|
2004
|
+
buffers: cloneBuffers(s.buffers)
|
|
2005
|
+
})),
|
|
2006
|
+
checkpoints
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
loadSnapshot(data) {
|
|
2010
|
+
this.sessions.clear();
|
|
2011
|
+
this.checkpoints.clear();
|
|
2012
|
+
for (const session of data.sessions) this.sessions.set(session.id, {
|
|
2013
|
+
...session,
|
|
2014
|
+
attachments: [...session.attachments ?? []],
|
|
2015
|
+
buffers: cloneBuffers(session.buffers),
|
|
2016
|
+
history: [...session.history]
|
|
2017
|
+
});
|
|
2018
|
+
for (const [id, list] of Object.entries(data.checkpoints ?? {})) this.checkpoints.set(id, list.map((c) => ({
|
|
2019
|
+
...c,
|
|
2020
|
+
buffers: cloneBuffers(c.buffers)
|
|
2021
|
+
})));
|
|
2022
|
+
this.focusedId = this.list()[0]?.id ?? null;
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
//#endregion
|
|
2026
|
+
//#region src/runtime/specialists/classify-risk.ts
|
|
2027
|
+
var CLASSIFY_SYSTEM = `You classify tool calls for user approval.
|
|
2028
|
+
Mark risk "critical" if the call can change state: write/edit/delete files, connect or load a workspace, run shell/commands with side effects, install, deploy, publish, push, or otherwise mutate the environment.
|
|
2029
|
+
Mark risk "safe" only for read-only inspection (list, read, search, describe).
|
|
2030
|
+
When unsure, choose "critical".
|
|
2031
|
+
Reply with ONLY a JSON array: [{"id":"<tool call id>","risk":"safe"|"critical"}]`;
|
|
2032
|
+
/** Ask the model whether pending tool calls are safe or critical. Unknown → critical. */
|
|
2033
|
+
async function classifyToolRisks(input) {
|
|
2034
|
+
const risks = /* @__PURE__ */ new Map();
|
|
2035
|
+
if (!input.tools.length) return risks;
|
|
2036
|
+
for (const tool of input.tools) risks.set(tool.id, "critical");
|
|
2037
|
+
const catalog = input.tools.map((t) => ({
|
|
2038
|
+
id: t.id,
|
|
2039
|
+
name: t.name,
|
|
2040
|
+
arguments: truncate(t.arguments, 800)
|
|
2041
|
+
}));
|
|
2042
|
+
const messages = [{
|
|
2043
|
+
role: "system",
|
|
2044
|
+
content: CLASSIFY_SYSTEM
|
|
2045
|
+
}, {
|
|
2046
|
+
role: "user",
|
|
2047
|
+
content: JSON.stringify(catalog)
|
|
2048
|
+
}];
|
|
2049
|
+
let text = "";
|
|
2050
|
+
for await (const chunk of input.provider.stream({
|
|
2051
|
+
model: input.model,
|
|
2052
|
+
messages,
|
|
2053
|
+
providerOptions: { reasoning_effort: "none" },
|
|
2054
|
+
signal: input.signal
|
|
2055
|
+
})) {
|
|
2056
|
+
if (chunk.type === "error") return risks;
|
|
2057
|
+
if (chunk.type === "text" && chunk.text) text += chunk.text;
|
|
2058
|
+
}
|
|
2059
|
+
for (const row of parseRiskRows(text)) {
|
|
2060
|
+
if (!risks.has(row.id)) continue;
|
|
2061
|
+
risks.set(row.id, row.risk === "safe" ? "safe" : "critical");
|
|
2062
|
+
}
|
|
2063
|
+
return risks;
|
|
2064
|
+
}
|
|
2065
|
+
function truncate(value, max) {
|
|
2066
|
+
return value.length <= max ? value : `${value.slice(0, max)}…`;
|
|
2067
|
+
}
|
|
2068
|
+
function parseRiskRows(text) {
|
|
2069
|
+
const json = extractJsonArray(text);
|
|
2070
|
+
if (!json) return [];
|
|
2071
|
+
try {
|
|
2072
|
+
const parsed = JSON.parse(json);
|
|
2073
|
+
if (!Array.isArray(parsed)) return [];
|
|
2074
|
+
return parsed.map((row) => {
|
|
2075
|
+
if (!row || typeof row !== "object") return null;
|
|
2076
|
+
const id = String(row.id ?? "");
|
|
2077
|
+
const risk = String(row.risk ?? "");
|
|
2078
|
+
if (!id) return null;
|
|
2079
|
+
return {
|
|
2080
|
+
id,
|
|
2081
|
+
risk
|
|
2082
|
+
};
|
|
2083
|
+
}).filter((row) => row != null);
|
|
2084
|
+
} catch {
|
|
2085
|
+
return [];
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
function extractJsonArray(text) {
|
|
2089
|
+
const trimmed = text.trim();
|
|
2090
|
+
if (trimmed.startsWith("[")) {
|
|
2091
|
+
const end = trimmed.lastIndexOf("]");
|
|
2092
|
+
return end >= 0 ? trimmed.slice(0, end + 1) : null;
|
|
2093
|
+
}
|
|
2094
|
+
const start = trimmed.indexOf("[");
|
|
2095
|
+
const end = trimmed.lastIndexOf("]");
|
|
2096
|
+
if (start >= 0 && end > start) return trimmed.slice(start, end + 1);
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
//#endregion
|
|
2100
|
+
//#region src/runtime/specialists/loop.ts
|
|
2101
|
+
var MAX_TOOL_ITERATIONS = 8;
|
|
2102
|
+
function textFromParts(parts) {
|
|
2103
|
+
return parts.filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
2104
|
+
}
|
|
2105
|
+
function historyToProviderMessages(systemPrompt, history, compactionSummary) {
|
|
2106
|
+
const messages = [{
|
|
2107
|
+
role: "system",
|
|
2108
|
+
content: systemPrompt
|
|
2109
|
+
}];
|
|
2110
|
+
if (compactionSummary?.trim()) messages.push({
|
|
2111
|
+
role: "system",
|
|
2112
|
+
content: compactionContextMessage(compactionSummary)
|
|
2113
|
+
});
|
|
2114
|
+
for (const msg of history) {
|
|
2115
|
+
if (msg.role === "user") {
|
|
2116
|
+
const content = userContentFromParts(msg.parts);
|
|
2117
|
+
if (!(typeof content === "string" ? !content.trim() : !content.length)) messages.push({
|
|
2118
|
+
role: "user",
|
|
2119
|
+
content
|
|
2120
|
+
});
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
if (msg.role === "system") {
|
|
2124
|
+
const text = textFromParts(msg.parts);
|
|
2125
|
+
if (text) messages.push({
|
|
2126
|
+
role: "system",
|
|
2127
|
+
content: text
|
|
2128
|
+
});
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
const text = textFromParts(msg.parts);
|
|
2132
|
+
const tools = msg.parts.filter((p) => p.type === "tool");
|
|
2133
|
+
if (tools.length) {
|
|
2134
|
+
messages.push({
|
|
2135
|
+
role: "assistant",
|
|
2136
|
+
content: text,
|
|
2137
|
+
toolCalls: tools.map((t) => ({
|
|
2138
|
+
id: t.id,
|
|
2139
|
+
name: t.name,
|
|
2140
|
+
arguments: JSON.stringify(t.args ?? {})
|
|
2141
|
+
}))
|
|
2142
|
+
});
|
|
2143
|
+
for (const tool of tools) messages.push({
|
|
2144
|
+
role: "tool",
|
|
2145
|
+
toolCallId: tool.id,
|
|
2146
|
+
name: tool.name,
|
|
2147
|
+
content: JSON.stringify(tool.result ?? {})
|
|
2148
|
+
});
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
if (text) messages.push({
|
|
2152
|
+
role: "assistant",
|
|
2153
|
+
content: text
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
return messages;
|
|
2157
|
+
}
|
|
2158
|
+
async function runSpecialistLoop(input) {
|
|
2159
|
+
const parts = [];
|
|
2160
|
+
const emit = (part) => {
|
|
2161
|
+
parts.push(part);
|
|
2162
|
+
input.onPart?.(part);
|
|
2163
|
+
};
|
|
2164
|
+
const messages = historyToProviderMessages(input.systemPrompt, input.history, input.compactionSummary);
|
|
2165
|
+
let success = true;
|
|
2166
|
+
for (let iter = 0; iter < MAX_TOOL_ITERATIONS; iter++) {
|
|
2167
|
+
input.refreshTools?.();
|
|
2168
|
+
const tools = input.broker.listDefinitions(input.role);
|
|
2169
|
+
let pendingTools = [];
|
|
2170
|
+
let assistantText = "";
|
|
2171
|
+
for await (const chunk of input.provider.stream({
|
|
2172
|
+
model: input.resolved.model,
|
|
2173
|
+
messages,
|
|
2174
|
+
tools: tools.length ? tools : void 0,
|
|
2175
|
+
providerOptions: input.resolved.providerOptions,
|
|
2176
|
+
signal: input.signal
|
|
2177
|
+
})) {
|
|
2178
|
+
if (chunk.type === "error") {
|
|
2179
|
+
emit({
|
|
2180
|
+
type: "text",
|
|
2181
|
+
text: `Error: ${chunk.error}`
|
|
2182
|
+
});
|
|
2183
|
+
success = false;
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
2186
|
+
if (chunk.type === "reasoning" && chunk.text) emit({
|
|
2187
|
+
type: "reasoning",
|
|
2188
|
+
text: chunk.text,
|
|
2189
|
+
status: "streaming"
|
|
2190
|
+
});
|
|
2191
|
+
if (chunk.type === "text" && chunk.text) {
|
|
2192
|
+
assistantText += chunk.text;
|
|
2193
|
+
emit({
|
|
2194
|
+
type: "text",
|
|
2195
|
+
text: chunk.text
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
if (chunk.type === "tool_call_delta" && chunk.toolCall) {
|
|
2199
|
+
const existing = pendingTools.find((t) => t.id === chunk.toolCall.id);
|
|
2200
|
+
if (existing) {
|
|
2201
|
+
existing.name = chunk.toolCall.name || existing.name;
|
|
2202
|
+
existing.arguments = chunk.toolCall.arguments;
|
|
2203
|
+
} else pendingTools.push({ ...chunk.toolCall });
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (!pendingTools.length) {
|
|
2207
|
+
if (assistantText && !parts.some((p) => p.type === "text")) emit({
|
|
2208
|
+
type: "text",
|
|
2209
|
+
text: assistantText
|
|
2210
|
+
});
|
|
2211
|
+
break;
|
|
2212
|
+
}
|
|
2213
|
+
messages.push({
|
|
2214
|
+
role: "assistant",
|
|
2215
|
+
content: assistantText,
|
|
2216
|
+
toolCalls: pendingTools.map((t) => ({
|
|
2217
|
+
id: t.id,
|
|
2218
|
+
name: t.name,
|
|
2219
|
+
arguments: t.arguments || "{}"
|
|
2220
|
+
}))
|
|
2221
|
+
});
|
|
2222
|
+
const risks = input.broker.getApprovalMode() === "auto_safe" ? await classifyToolRisks({
|
|
2223
|
+
provider: input.provider,
|
|
2224
|
+
model: input.resolved.model,
|
|
2225
|
+
tools: pendingTools.map((t) => ({
|
|
2226
|
+
id: t.id,
|
|
2227
|
+
name: t.name,
|
|
2228
|
+
arguments: t.arguments || "{}"
|
|
2229
|
+
})),
|
|
2230
|
+
signal: input.signal
|
|
2231
|
+
}) : null;
|
|
2232
|
+
for (const tool of pendingTools) {
|
|
2233
|
+
let args = {};
|
|
2234
|
+
try {
|
|
2235
|
+
args = tool.arguments ? JSON.parse(tool.arguments) : {};
|
|
2236
|
+
} catch {
|
|
2237
|
+
args = { raw: tool.arguments };
|
|
2238
|
+
}
|
|
2239
|
+
emit({
|
|
2240
|
+
type: "tool",
|
|
2241
|
+
id: tool.id,
|
|
2242
|
+
name: tool.name,
|
|
2243
|
+
args,
|
|
2244
|
+
status: "running"
|
|
2245
|
+
});
|
|
2246
|
+
const started = Date.now();
|
|
2247
|
+
const result = await input.broker.execute({
|
|
2248
|
+
id: tool.id,
|
|
2249
|
+
name: tool.name,
|
|
2250
|
+
arguments: args
|
|
2251
|
+
}, input.sessionId, input.role, { risk: risks?.get(tool.id) });
|
|
2252
|
+
const rawOutput = result.ok ? result.output : { error: result.error };
|
|
2253
|
+
const presentImage = result.ok && rawOutput && typeof rawOutput === "object" && rawOutput["_presentImage"] === true && typeof rawOutput.dataUrl === "string";
|
|
2254
|
+
const safeOutput = result.ok ? stripAttachmentToolOutput(rawOutput) : rawOutput;
|
|
2255
|
+
emit({
|
|
2256
|
+
type: "tool",
|
|
2257
|
+
id: tool.id,
|
|
2258
|
+
name: tool.name,
|
|
2259
|
+
args,
|
|
2260
|
+
result: safeOutput,
|
|
2261
|
+
status: result.ok ? "done" : "error",
|
|
2262
|
+
durationMs: Date.now() - started
|
|
2263
|
+
});
|
|
2264
|
+
messages.push({
|
|
2265
|
+
role: "tool",
|
|
2266
|
+
toolCallId: tool.id,
|
|
2267
|
+
name: tool.name,
|
|
2268
|
+
content: JSON.stringify(presentImage ? {
|
|
2269
|
+
...safeOutput ?? {},
|
|
2270
|
+
note: "Image bytes follow in the next user message for vision inspection."
|
|
2271
|
+
} : safeOutput)
|
|
2272
|
+
});
|
|
2273
|
+
if (presentImage) {
|
|
2274
|
+
const dataUrl = String(rawOutput.dataUrl);
|
|
2275
|
+
const name = String(rawOutput.name ?? "image");
|
|
2276
|
+
const id = String(rawOutput.id ?? "");
|
|
2277
|
+
messages.push({
|
|
2278
|
+
role: "user",
|
|
2279
|
+
content: [{
|
|
2280
|
+
type: "text",
|
|
2281
|
+
text: `Attachment image for inspection: id=${id} name="${name}"`
|
|
2282
|
+
}, {
|
|
2283
|
+
type: "image_url",
|
|
2284
|
+
image_url: { url: dataUrl }
|
|
2285
|
+
}]
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
if (!result.ok) success = false;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return {
|
|
2292
|
+
message: {
|
|
2293
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
2294
|
+
role: "assistant",
|
|
2295
|
+
parts: coalesceParts(parts),
|
|
2296
|
+
createdAt: Date.now(),
|
|
2297
|
+
routedModel: input.resolved.model,
|
|
2298
|
+
effort: input.resolved.effort
|
|
2299
|
+
},
|
|
2300
|
+
success
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
function coalesceParts(parts) {
|
|
2304
|
+
const out = [];
|
|
2305
|
+
for (const part of parts) {
|
|
2306
|
+
const last = out[out.length - 1];
|
|
2307
|
+
if (part.type === "text" && last?.type === "text") {
|
|
2308
|
+
last.text += part.text;
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
if (part.type === "reasoning" && last?.type === "reasoning") {
|
|
2312
|
+
last.text += part.text;
|
|
2313
|
+
last.status = part.status;
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
if (part.type === "tool" && last?.type === "tool" && last.id === part.id) {
|
|
2317
|
+
out[out.length - 1] = part;
|
|
2318
|
+
continue;
|
|
2319
|
+
}
|
|
2320
|
+
out.push(part);
|
|
2321
|
+
}
|
|
2322
|
+
for (const part of out) if (part.type === "reasoning") part.status = "done";
|
|
2323
|
+
return out;
|
|
2324
|
+
}
|
|
2325
|
+
//#endregion
|
|
2326
|
+
//#region src/runtime/specialists/index.ts
|
|
2327
|
+
function systemPromptFor(role, goal, opts = {}) {
|
|
2328
|
+
const preamble = `You are a specialist in the Eclipse Docks AI runtime. Prefer tools for file and command actions. Be concise. ${opts.hasAttachments ? "This turn publishes session attachments. To inspect them, call attachment_read with the attachment id from the user message (attachment_list if you need ids). Do not open canvases, editors, workspaces, or unrelated commands to find attachment contents — they are not workspace files." : "When the user publishes attachments, use attachment_list / attachment_read to inspect them — do not assume their contents are already in the transcript."}`;
|
|
2329
|
+
switch (role) {
|
|
2330
|
+
case "plan": return `${preamble}\nMode: PLAN (read-only). Analyze and propose a plan for: ${goal}`;
|
|
2331
|
+
case "explore": return `${preamble}\nMode: EXPLORE (read-only). Search and report findings for: ${goal}`;
|
|
2332
|
+
case "build": return `${preamble}\nMode: BUILD. Implement the goal using tools when needed: ${goal}`;
|
|
2333
|
+
default: return `${preamble}\nGoal: ${goal}`;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
//#endregion
|
|
2337
|
+
//#region src/runtime/context/usage.ts
|
|
2338
|
+
/** Rough token estimate (~4 chars / token) with a small structural overhead. */
|
|
2339
|
+
function estimateTokens(text) {
|
|
2340
|
+
if (!text) return 0;
|
|
2341
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
2342
|
+
}
|
|
2343
|
+
function estimatePartTokens(part) {
|
|
2344
|
+
if (part.type === "text" || part.type === "reasoning") return estimateTokens(part.text);
|
|
2345
|
+
if (part.type === "file") return estimateTokens(part.name) + estimateTokens(part.id) + 8;
|
|
2346
|
+
if (part.type === "tool") return estimateTokens(part.name) + estimateTokens(JSON.stringify(part.args ?? {})) + estimateTokens(JSON.stringify(part.result ?? {})) + 8;
|
|
2347
|
+
if (part.type === "step") return estimateTokens(part.title) + 4;
|
|
2348
|
+
return 0;
|
|
2349
|
+
}
|
|
2350
|
+
function estimateMessageTokens(message) {
|
|
2351
|
+
let messages = 4;
|
|
2352
|
+
let tools = 0;
|
|
2353
|
+
let reasoning = 0;
|
|
2354
|
+
let toolCount = 0;
|
|
2355
|
+
let reasoningCount = 0;
|
|
2356
|
+
for (const part of message.parts) if (part.type === "tool") {
|
|
2357
|
+
tools += estimatePartTokens(part);
|
|
2358
|
+
toolCount += 1;
|
|
2359
|
+
} else if (part.type === "reasoning") {
|
|
2360
|
+
reasoning += estimatePartTokens(part);
|
|
2361
|
+
reasoningCount += 1;
|
|
2362
|
+
} else messages += estimatePartTokens(part);
|
|
2363
|
+
return {
|
|
2364
|
+
messages,
|
|
2365
|
+
tools,
|
|
2366
|
+
reasoning,
|
|
2367
|
+
toolCount,
|
|
2368
|
+
reasoningCount
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
function contextUsageLevel(percent) {
|
|
2372
|
+
if (percent > 85) return "critical";
|
|
2373
|
+
if (percent > 60) return "warn";
|
|
2374
|
+
return "ok";
|
|
2375
|
+
}
|
|
2376
|
+
/** Estimate how much of the model context window the session currently occupies. */
|
|
2377
|
+
function estimateSessionContextUsage(session, modelRef) {
|
|
2378
|
+
const limit = getModelContextLimit(modelRef);
|
|
2379
|
+
const buckets = [
|
|
2380
|
+
{
|
|
2381
|
+
id: "summary",
|
|
2382
|
+
label: "Summary",
|
|
2383
|
+
tokens: 0,
|
|
2384
|
+
count: 0
|
|
2385
|
+
},
|
|
2386
|
+
{
|
|
2387
|
+
id: "messages",
|
|
2388
|
+
label: "Chat text",
|
|
2389
|
+
tokens: 0,
|
|
2390
|
+
count: 0
|
|
2391
|
+
},
|
|
2392
|
+
{
|
|
2393
|
+
id: "tools",
|
|
2394
|
+
label: "Tools",
|
|
2395
|
+
tokens: 0,
|
|
2396
|
+
count: 0
|
|
2397
|
+
},
|
|
2398
|
+
{
|
|
2399
|
+
id: "reasoning",
|
|
2400
|
+
label: "Thinking",
|
|
2401
|
+
tokens: 0,
|
|
2402
|
+
count: 0
|
|
2403
|
+
}
|
|
2404
|
+
];
|
|
2405
|
+
if (session?.compactionSummary) {
|
|
2406
|
+
buckets[0].tokens = estimateTokens(session.compactionSummary);
|
|
2407
|
+
buckets[0].count = 1;
|
|
2408
|
+
}
|
|
2409
|
+
for (const message of session?.history ?? []) {
|
|
2410
|
+
const parts = estimateMessageTokens(message);
|
|
2411
|
+
buckets[1].tokens += parts.messages;
|
|
2412
|
+
buckets[1].count += 1;
|
|
2413
|
+
buckets[2].tokens += parts.tools;
|
|
2414
|
+
buckets[2].count += parts.toolCount;
|
|
2415
|
+
buckets[3].tokens += parts.reasoning;
|
|
2416
|
+
buckets[3].count += parts.reasoningCount;
|
|
2417
|
+
}
|
|
2418
|
+
const used = buckets.reduce((sum, b) => sum + b.tokens, 0);
|
|
2419
|
+
const percent = limit > 0 ? Math.min(100, used / limit * 100) : 0;
|
|
2420
|
+
return {
|
|
2421
|
+
used,
|
|
2422
|
+
limit,
|
|
2423
|
+
percent,
|
|
2424
|
+
remaining: Math.max(0, limit - used),
|
|
2425
|
+
level: contextUsageLevel(percent),
|
|
2426
|
+
buckets: buckets.filter((b) => b.tokens > 0),
|
|
2427
|
+
modelRef
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
function formatTokenCount(n) {
|
|
2431
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2432
|
+
if (n >= 1e4) return `${Math.round(n / 1e3)}k`;
|
|
2433
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
2434
|
+
return String(Math.round(n));
|
|
2435
|
+
}
|
|
2436
|
+
function formatContextPercent(percent) {
|
|
2437
|
+
if (percent <= 0) return "0%";
|
|
2438
|
+
if (percent < 1) return "<1%";
|
|
2439
|
+
if (percent < 10) return `${percent.toFixed(1)}%`;
|
|
2440
|
+
return `${Math.round(percent)}%`;
|
|
2441
|
+
}
|
|
2442
|
+
function formatBucketDetail(bucket) {
|
|
2443
|
+
const tokens = `${formatTokenCount(bucket.tokens)} tok`;
|
|
2444
|
+
if (bucket.id === "summary") return tokens;
|
|
2445
|
+
if (bucket.id === "messages") {
|
|
2446
|
+
const n = bucket.count;
|
|
2447
|
+
return `${n} msg${n === 1 ? "" : "s"} · ${tokens}`;
|
|
2448
|
+
}
|
|
2449
|
+
if (bucket.id === "tools") {
|
|
2450
|
+
const n = bucket.count;
|
|
2451
|
+
return `${n} call${n === 1 ? "" : "s"} · ${tokens}`;
|
|
2452
|
+
}
|
|
2453
|
+
return `${bucket.count} · ${tokens}`;
|
|
2454
|
+
}
|
|
2455
|
+
//#endregion
|
|
2456
|
+
//#region src/runtime/cognitive-runtime.ts
|
|
2457
|
+
var PERSIST_KEY = "cognitive/v1/state";
|
|
2458
|
+
/** Auto-compact after a turn when live history exceeds this many messages. */
|
|
2459
|
+
var AUTO_COMPACT_AFTER = 40;
|
|
2460
|
+
/** Messages retained in the live transcript after compaction. */
|
|
2461
|
+
var COMPACT_KEEP_COUNT = 12;
|
|
2462
|
+
var CognitiveRuntime = class {
|
|
2463
|
+
constructor() {
|
|
2464
|
+
this.events = new RuntimeEvents();
|
|
2465
|
+
this.sessions = new SessionRuntime();
|
|
2466
|
+
this.memory = new CognitiveMemory();
|
|
2467
|
+
this.providers = new ProviderRegistry();
|
|
2468
|
+
this.models = new ModelRouter();
|
|
2469
|
+
this.tools = new ToolBroker();
|
|
2470
|
+
this.core = createCoreState(defaultNeeds());
|
|
2471
|
+
this.policy = {
|
|
2472
|
+
mode: "auto",
|
|
2473
|
+
optimizeFor: "balance"
|
|
2474
|
+
};
|
|
2475
|
+
this.providersSettings = defaultProvidersSettings();
|
|
2476
|
+
this.approvalMode = "auto_safe";
|
|
2477
|
+
this.promptEnhancers = [];
|
|
2478
|
+
this.permissionResolvers = /* @__PURE__ */ new Map();
|
|
2479
|
+
this.started = false;
|
|
2480
|
+
this.ready = Promise.resolve();
|
|
2481
|
+
}
|
|
2482
|
+
start() {
|
|
2483
|
+
if (this.started) return;
|
|
2484
|
+
this.started = true;
|
|
2485
|
+
this.applyProvidersSettings(defaultProvidersSettings(), {
|
|
2486
|
+
persist: false,
|
|
2487
|
+
emit: false
|
|
2488
|
+
});
|
|
2489
|
+
registerWorkspaceTools(this.tools);
|
|
2490
|
+
registerAttachmentTools(this.tools, (sessionId, attachmentId) => {
|
|
2491
|
+
const list = this.sessions.get(sessionId)?.attachments ?? [];
|
|
2492
|
+
if (!attachmentId) return list;
|
|
2493
|
+
const found = list.find((a) => a.id === attachmentId);
|
|
2494
|
+
return found ? [found] : [];
|
|
2495
|
+
});
|
|
2496
|
+
this.tools.setApprovalMode(this.approvalMode);
|
|
2497
|
+
this.tools.setPermissionHandler(async (req) => this.requestPermission(req));
|
|
2498
|
+
this.ready = this.hydrateAll();
|
|
2499
|
+
}
|
|
2500
|
+
/** Resolves after initial provider/settings hydrate. */
|
|
2501
|
+
whenReady() {
|
|
2502
|
+
this.start();
|
|
2503
|
+
return this.ready;
|
|
2504
|
+
}
|
|
2505
|
+
getProvidersSettings() {
|
|
2506
|
+
return structuredClone(this.providersSettings);
|
|
2507
|
+
}
|
|
2508
|
+
/** Models available for routing (`providerId/modelId`), including external plugins. */
|
|
2509
|
+
listConfiguredModels() {
|
|
2510
|
+
const ids = listConfiguredModelIds(this.providersSettings);
|
|
2511
|
+
const seen = new Set(ids);
|
|
2512
|
+
for (const provider of this.providers.listExternal()) {
|
|
2513
|
+
const models = provider.listModels();
|
|
2514
|
+
if (!Array.isArray(models)) continue;
|
|
2515
|
+
for (const model of models) {
|
|
2516
|
+
const id = `${provider.id}/${model.id}`;
|
|
2517
|
+
if (seen.has(id)) continue;
|
|
2518
|
+
seen.add(id);
|
|
2519
|
+
ids.push(id);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
return ids;
|
|
2523
|
+
}
|
|
2524
|
+
async setProvidersSettings(settings) {
|
|
2525
|
+
this.applyProvidersSettings(settings, {
|
|
2526
|
+
persist: true,
|
|
2527
|
+
emit: true
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Register a plugin ModelProvider (e.g. WebLLM) that survives settings rebuilds.
|
|
2532
|
+
* Models from listModels() are merged into the router catalog as `providerId/modelId`.
|
|
2533
|
+
*/
|
|
2534
|
+
registerExternalProvider(provider) {
|
|
2535
|
+
this.providers.registerExternal(provider);
|
|
2536
|
+
this.rebuildCatalog();
|
|
2537
|
+
this.events.publish(TOPIC.providersChanged, this.providersSettings);
|
|
2538
|
+
}
|
|
2539
|
+
unregisterExternalProvider(id) {
|
|
2540
|
+
this.providers.unregisterExternal(id);
|
|
2541
|
+
this.rebuildCatalog();
|
|
2542
|
+
this.events.publish(TOPIC.providersChanged, this.providersSettings);
|
|
2543
|
+
}
|
|
2544
|
+
/** Append instructions to the specialist system prompt (e.g. RAG guidance). */
|
|
2545
|
+
addSystemPromptEnhancer(enhancer) {
|
|
2546
|
+
this.promptEnhancers.push(enhancer);
|
|
2547
|
+
return () => {
|
|
2548
|
+
this.promptEnhancers = this.promptEnhancers.filter((e) => e !== enhancer);
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
/** First catalog model for the default provider — used by Manual routing. */
|
|
2552
|
+
defaultModelId() {
|
|
2553
|
+
const catalog = catalogFromSettings(this.providersSettings);
|
|
2554
|
+
return catalog.find((e) => e.providerId === this.providersSettings.defaultProviderId)?.id ?? catalog[0]?.id ?? "openai/gpt-4o-mini";
|
|
2555
|
+
}
|
|
2556
|
+
setModelPolicy(policy) {
|
|
2557
|
+
this.policy = policy;
|
|
2558
|
+
}
|
|
2559
|
+
getModelPolicy() {
|
|
2560
|
+
return this.policy;
|
|
2561
|
+
}
|
|
2562
|
+
getApprovalMode() {
|
|
2563
|
+
return this.approvalMode;
|
|
2564
|
+
}
|
|
2565
|
+
setApprovalMode(mode) {
|
|
2566
|
+
const changed = this.approvalMode !== mode;
|
|
2567
|
+
this.approvalMode = mode;
|
|
2568
|
+
this.tools.setRules([]);
|
|
2569
|
+
this.tools.setApprovalMode(mode);
|
|
2570
|
+
this.tools.setPermissionHandler(async (req) => this.requestPermission(req));
|
|
2571
|
+
if (!changed) return;
|
|
2572
|
+
this.events.publish(TOPIC.approvalModeChanged, mode);
|
|
2573
|
+
this.persist();
|
|
2574
|
+
}
|
|
2575
|
+
/** Low-level override; prefer {@link setApprovalMode} for the convenience modes. */
|
|
2576
|
+
setPermissionPolicy(policy) {
|
|
2577
|
+
if (typeof policy === "function") {
|
|
2578
|
+
this.tools.setPermissionHandler(policy);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (policy === "allow") this.setApprovalMode("auto_all");
|
|
2582
|
+
else if (policy === "ask") this.setApprovalMode("manual");
|
|
2583
|
+
else {
|
|
2584
|
+
this.setApprovalMode("manual");
|
|
2585
|
+
this.tools.setRules([{
|
|
2586
|
+
pattern: "*",
|
|
2587
|
+
action: "deny"
|
|
2588
|
+
}]);
|
|
2589
|
+
}
|
|
2590
|
+
this.tools.setPermissionHandler(policy === "allow" ? null : async (req) => this.requestPermission(req));
|
|
2591
|
+
}
|
|
2592
|
+
resolvePermission(requestId, action) {
|
|
2593
|
+
const resolve = this.permissionResolvers.get(requestId);
|
|
2594
|
+
if (!resolve) return;
|
|
2595
|
+
this.permissionResolvers.delete(requestId);
|
|
2596
|
+
const focused = this.sessions.focused();
|
|
2597
|
+
if (focused?.status === "waiting_approval") {
|
|
2598
|
+
this.sessions.update(focused.id, { status: "running" });
|
|
2599
|
+
this.emitSessions();
|
|
2600
|
+
}
|
|
2601
|
+
resolve(action);
|
|
2602
|
+
}
|
|
2603
|
+
/** Emit session list changes and persist them. */
|
|
2604
|
+
commitSessions() {
|
|
2605
|
+
this.emitSessions();
|
|
2606
|
+
this.persist();
|
|
2607
|
+
}
|
|
2608
|
+
/** Ensure a focused session exists and return its id. */
|
|
2609
|
+
ensureSessionId(sessionId) {
|
|
2610
|
+
this.start();
|
|
2611
|
+
if (sessionId && this.sessions.get(sessionId)) {
|
|
2612
|
+
this.sessions.focus(sessionId);
|
|
2613
|
+
return sessionId;
|
|
2614
|
+
}
|
|
2615
|
+
const focused = this.sessions.focused();
|
|
2616
|
+
if (focused) return focused.id;
|
|
2617
|
+
return this.sessions.createSession().id;
|
|
2618
|
+
}
|
|
2619
|
+
/** Store attachment payloads on the session (not inlined into history until prompt). */
|
|
2620
|
+
addAttachments(sessionId, attachments) {
|
|
2621
|
+
const id = this.ensureSessionId(sessionId);
|
|
2622
|
+
const session = this.sessions.get(id);
|
|
2623
|
+
if (!session || !attachments.length) return [];
|
|
2624
|
+
const room = Math.max(0, 8 - (session.attachments?.length ?? 0));
|
|
2625
|
+
const accepted = attachments.slice(0, room).map((attachment) => toSessionAttachment(attachment));
|
|
2626
|
+
if (!accepted.length) return [];
|
|
2627
|
+
this.sessions.update(id, { attachments: [...session.attachments ?? [], ...accepted] });
|
|
2628
|
+
this.commitSessions();
|
|
2629
|
+
return accepted;
|
|
2630
|
+
}
|
|
2631
|
+
/** Remove an attachment payload from the session store. */
|
|
2632
|
+
removeAttachment(sessionId, attachmentId) {
|
|
2633
|
+
const session = this.sessions.get(sessionId);
|
|
2634
|
+
if (!session) return false;
|
|
2635
|
+
const next = (session.attachments ?? []).filter((a) => a.id !== attachmentId);
|
|
2636
|
+
if (next.length === (session.attachments?.length ?? 0)) return false;
|
|
2637
|
+
this.sessions.update(sessionId, { attachments: next });
|
|
2638
|
+
this.commitSessions();
|
|
2639
|
+
return true;
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* Compact a session: drop older turns, store a summary (LLM when possible),
|
|
2643
|
+
* and persist. The summary is injected into later model calls.
|
|
2644
|
+
*/
|
|
2645
|
+
async compactSession(sessionId, keepCount = COMPACT_KEEP_COUNT, signal) {
|
|
2646
|
+
this.start();
|
|
2647
|
+
await this.ready;
|
|
2648
|
+
const prepared = this.sessions.prepareCompact(sessionId, keepCount);
|
|
2649
|
+
if (!prepared?.didCompact) return { didCompact: false };
|
|
2650
|
+
this.sessions.update(sessionId, { status: "compact" });
|
|
2651
|
+
this.emitSessions();
|
|
2652
|
+
let summary = prepared.summary;
|
|
2653
|
+
const resolved = this.models.resolve({
|
|
2654
|
+
role: "compact",
|
|
2655
|
+
complexity: .2,
|
|
2656
|
+
requireTools: false
|
|
2657
|
+
}, this.policy);
|
|
2658
|
+
const provider = this.providers.resolveProvider(resolved.model) ?? this.providers.list()[0];
|
|
2659
|
+
if (provider) {
|
|
2660
|
+
const llmSummary = await summarizeDroppedHistory({
|
|
2661
|
+
provider,
|
|
2662
|
+
model: resolved.model,
|
|
2663
|
+
dropped: prepared.dropped,
|
|
2664
|
+
previousSummary: this.sessions.get(sessionId)?.compactionSummary,
|
|
2665
|
+
providerOptions: resolved.providerOptions,
|
|
2666
|
+
signal
|
|
2667
|
+
});
|
|
2668
|
+
if (llmSummary) summary = llmSummary;
|
|
2669
|
+
}
|
|
2670
|
+
this.sessions.update(sessionId, {
|
|
2671
|
+
history: prepared.kept,
|
|
2672
|
+
compactionSummary: summary,
|
|
2673
|
+
status: "idle"
|
|
2674
|
+
});
|
|
2675
|
+
this.commitSessions();
|
|
2676
|
+
return { didCompact: true };
|
|
2677
|
+
}
|
|
2678
|
+
async prompt(input) {
|
|
2679
|
+
this.start();
|
|
2680
|
+
await loadModelsCatalog().catch(() => void 0);
|
|
2681
|
+
const session = (input.sessionId ? this.sessions.get(input.sessionId) : this.sessions.focused()) ?? this.sessions.createSession();
|
|
2682
|
+
const text = input.text.trim();
|
|
2683
|
+
const messageId = `msg-${Date.now()}-u`;
|
|
2684
|
+
const fresh = (input.attachments ?? []).slice(0, 8).map((attachment) => toSessionAttachment(attachment, { messageId }));
|
|
2685
|
+
if (fresh.length) this.sessions.update(session.id, { attachments: [...this.sessions.get(session.id)?.attachments ?? [], ...fresh] });
|
|
2686
|
+
const existingIds = (input.attachmentIds ?? []).filter(Boolean);
|
|
2687
|
+
const stored = this.sessions.get(session.id)?.attachments ?? [];
|
|
2688
|
+
const byId = new Map(stored.map((a) => [a.id, a]));
|
|
2689
|
+
const published = [];
|
|
2690
|
+
for (const item of fresh) published.push(item);
|
|
2691
|
+
for (const id of existingIds) {
|
|
2692
|
+
const found = byId.get(id);
|
|
2693
|
+
if (found && !published.some((p) => p.id === found.id)) published.push(found);
|
|
2694
|
+
}
|
|
2695
|
+
if (!text && !published.length) throw new Error("Prompt requires text or at least one attachment");
|
|
2696
|
+
const parts = [];
|
|
2697
|
+
if (text) parts.push({
|
|
2698
|
+
type: "text",
|
|
2699
|
+
text
|
|
2700
|
+
});
|
|
2701
|
+
for (const item of published) parts.push(toFileRefPart(item));
|
|
2702
|
+
const promptSummary = summarizePrompt(text, published.map((a) => ({
|
|
2703
|
+
name: a.name,
|
|
2704
|
+
kind: a.kind
|
|
2705
|
+
})));
|
|
2706
|
+
const userMessage = {
|
|
2707
|
+
id: messageId,
|
|
2708
|
+
role: "user",
|
|
2709
|
+
parts,
|
|
2710
|
+
createdAt: Date.now()
|
|
2711
|
+
};
|
|
2712
|
+
this.sessions.addMessage(session.id, userMessage);
|
|
2713
|
+
if (isDefaultChatTitle(session.title)) this.sessions.update(session.id, { title: titleFromPrompt(promptSummary) });
|
|
2714
|
+
const contextChunk = this.memory.upsert({
|
|
2715
|
+
type: "context",
|
|
2716
|
+
slots: {
|
|
2717
|
+
kind: "user",
|
|
2718
|
+
summary: promptSummary
|
|
2719
|
+
},
|
|
2720
|
+
scope: "session",
|
|
2721
|
+
sessionId: session.id,
|
|
2722
|
+
activation: .8
|
|
2723
|
+
});
|
|
2724
|
+
const retrieved = this.memory.retrieveForContext(promptSummary.split(/\s+/).slice(0, 12));
|
|
2725
|
+
const buffers = {
|
|
2726
|
+
...session.buffers,
|
|
2727
|
+
retrieval: retrieved[0] ?? contextChunk
|
|
2728
|
+
};
|
|
2729
|
+
const tick = coreTick({
|
|
2730
|
+
state: this.core,
|
|
2731
|
+
buffers,
|
|
2732
|
+
context: contextChunk
|
|
2733
|
+
});
|
|
2734
|
+
this.core = tick.state;
|
|
2735
|
+
this.sessions.update(session.id, {
|
|
2736
|
+
buffers: tick.buffers,
|
|
2737
|
+
status: "running"
|
|
2738
|
+
});
|
|
2739
|
+
this.events.publish(TOPIC.coreTick, {
|
|
2740
|
+
valenceDelta: tick.valenceDelta,
|
|
2741
|
+
intent: tick.intent,
|
|
2742
|
+
conscious: tick.state.conscious
|
|
2743
|
+
});
|
|
2744
|
+
const role = input.role ?? session.specialistMode ?? "build";
|
|
2745
|
+
refreshCommandTools(this.tools, role);
|
|
2746
|
+
const routeReq = {
|
|
2747
|
+
role,
|
|
2748
|
+
complexity: Math.min(1, text.length / 800),
|
|
2749
|
+
requireTools: true
|
|
2750
|
+
};
|
|
2751
|
+
const resolved = this.models.resolve(routeReq, this.policy);
|
|
2752
|
+
this.events.publish(TOPIC.modelRouted, resolved);
|
|
2753
|
+
const provider = this.providers.resolveProvider(resolved.model) ?? this.providers.list()[0];
|
|
2754
|
+
if (!provider) {
|
|
2755
|
+
const err = {
|
|
2756
|
+
id: `msg-${Date.now()}-e`,
|
|
2757
|
+
role: "assistant",
|
|
2758
|
+
parts: [{
|
|
2759
|
+
type: "text",
|
|
2760
|
+
text: "No model provider configured. Open AI Settings to add one."
|
|
2761
|
+
}],
|
|
2762
|
+
createdAt: Date.now()
|
|
2763
|
+
};
|
|
2764
|
+
this.sessions.addMessage(session.id, err);
|
|
2765
|
+
this.sessions.update(session.id, { status: "idle" });
|
|
2766
|
+
this.emitSessions();
|
|
2767
|
+
return err;
|
|
2768
|
+
}
|
|
2769
|
+
const goal = tick.intent?.summary ?? promptSummary;
|
|
2770
|
+
let systemPrompt = systemPromptFor(role, goal, { hasAttachments: published.length > 0 || (this.sessions.get(session.id)?.attachments?.length ?? 0) > 0 });
|
|
2771
|
+
for (const enhance of this.promptEnhancers) systemPrompt = await enhance(systemPrompt, {
|
|
2772
|
+
role,
|
|
2773
|
+
goal,
|
|
2774
|
+
sessionId: session.id
|
|
2775
|
+
});
|
|
2776
|
+
const liveSession = this.sessions.get(session.id);
|
|
2777
|
+
const result = await runSpecialistLoop({
|
|
2778
|
+
role,
|
|
2779
|
+
systemPrompt,
|
|
2780
|
+
history: liveSession.history,
|
|
2781
|
+
compactionSummary: liveSession.compactionSummary,
|
|
2782
|
+
resolved,
|
|
2783
|
+
provider,
|
|
2784
|
+
broker: this.tools,
|
|
2785
|
+
refreshTools: () => refreshCommandTools(this.tools, role),
|
|
2786
|
+
sessionId: session.id,
|
|
2787
|
+
signal: input.signal,
|
|
2788
|
+
onPart: (part) => this.events.publish(TOPIC.messagePart, {
|
|
2789
|
+
sessionId: session.id,
|
|
2790
|
+
part
|
|
2791
|
+
})
|
|
2792
|
+
});
|
|
2793
|
+
this.models.recordOutcome(role, resolved.model, resolved.effort, result.success);
|
|
2794
|
+
this.sessions.addMessage(session.id, result.message);
|
|
2795
|
+
this.memory.upsert({
|
|
2796
|
+
type: "episodic",
|
|
2797
|
+
slots: {
|
|
2798
|
+
kind: result.success ? "progress" : "error",
|
|
2799
|
+
summary: goal
|
|
2800
|
+
},
|
|
2801
|
+
scope: "workspace",
|
|
2802
|
+
sessionId: session.id,
|
|
2803
|
+
activation: .7
|
|
2804
|
+
});
|
|
2805
|
+
if ((this.sessions.get(session.id)?.history.length ?? 0) > AUTO_COMPACT_AFTER) await this.compactSession(session.id, COMPACT_KEEP_COUNT, input.signal);
|
|
2806
|
+
this.sessions.update(session.id, { status: "idle" });
|
|
2807
|
+
this.events.publish(TOPIC.messageDone, {
|
|
2808
|
+
sessionId: session.id,
|
|
2809
|
+
message: result.message
|
|
2810
|
+
});
|
|
2811
|
+
this.emitSessions();
|
|
2812
|
+
this.persist();
|
|
2813
|
+
return result.message;
|
|
2814
|
+
}
|
|
2815
|
+
applyProvidersSettings(settings, opts) {
|
|
2816
|
+
const normalized = normalizeProvidersSettings(settings);
|
|
2817
|
+
this.providersSettings = normalized;
|
|
2818
|
+
this.providers.clear();
|
|
2819
|
+
const externalIds = new Set(this.providers.listExternal().map((p) => p.id));
|
|
2820
|
+
for (const entry of normalized.providers) {
|
|
2821
|
+
if (!entry.enabled || !entry.id) continue;
|
|
2822
|
+
if (externalIds.has(entry.id)) continue;
|
|
2823
|
+
const modelIds = parseModelIds(entry.models);
|
|
2824
|
+
this.providers.register(new OpenAICompatibleProvider({
|
|
2825
|
+
id: entry.id,
|
|
2826
|
+
baseUrl: entry.baseUrl || "http://127.0.0.1",
|
|
2827
|
+
apiKey: entry.apiKey || void 0,
|
|
2828
|
+
defaultModels: modelIds.map((id) => ({
|
|
2829
|
+
id,
|
|
2830
|
+
name: id
|
|
2831
|
+
}))
|
|
2832
|
+
}));
|
|
2833
|
+
}
|
|
2834
|
+
this.rebuildCatalog();
|
|
2835
|
+
if (opts.emit) this.events.publish(TOPIC.providersChanged, normalized);
|
|
2836
|
+
if (opts.persist) this.persistProviders();
|
|
2837
|
+
}
|
|
2838
|
+
rebuildCatalog() {
|
|
2839
|
+
const catalog = catalogFromSettings(this.providersSettings);
|
|
2840
|
+
const seen = new Set(catalog.map((e) => e.id));
|
|
2841
|
+
for (const provider of this.providers.listExternal()) {
|
|
2842
|
+
const models = provider.listModels();
|
|
2843
|
+
const list = Array.isArray(models) ? models : [];
|
|
2844
|
+
for (const model of list) {
|
|
2845
|
+
const id = `${provider.id}/${model.id}`;
|
|
2846
|
+
if (seen.has(id)) continue;
|
|
2847
|
+
seen.add(id);
|
|
2848
|
+
catalog.push(externalCatalogEntry(id, provider.id));
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
this.models.setCatalog(catalog);
|
|
2852
|
+
}
|
|
2853
|
+
async requestPermission(req) {
|
|
2854
|
+
const requestId = `perm-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
2855
|
+
this.sessions.update(req.sessionId, { status: "waiting_approval" });
|
|
2856
|
+
this.emitSessions();
|
|
2857
|
+
return new Promise((resolve) => {
|
|
2858
|
+
this.permissionResolvers.set(requestId, resolve);
|
|
2859
|
+
this.events.publish(TOPIC.permissionAsked, {
|
|
2860
|
+
requestId,
|
|
2861
|
+
...req
|
|
2862
|
+
});
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
emitSessions() {
|
|
2866
|
+
this.events.publish(TOPIC.sessionChanged, {
|
|
2867
|
+
sessions: this.sessions.list(),
|
|
2868
|
+
focusedId: this.sessions.focused()?.id ?? null
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
async hydrateAll() {
|
|
2872
|
+
await loadModelsCatalog().catch(() => void 0);
|
|
2873
|
+
await this.hydrateProviders();
|
|
2874
|
+
this.applyProvidersSettings(this.providersSettings, {
|
|
2875
|
+
persist: false,
|
|
2876
|
+
emit: true
|
|
2877
|
+
});
|
|
2878
|
+
await this.hydrate();
|
|
2879
|
+
if (!this.sessions.list().length) this.sessions.createSession({ title: "Chat" });
|
|
2880
|
+
const mostRecent = this.sessions.list()[0];
|
|
2881
|
+
if (mostRecent) this.sessions.focus(mostRecent.id);
|
|
2882
|
+
this.emitSessions();
|
|
2883
|
+
}
|
|
2884
|
+
async hydrateProviders() {
|
|
2885
|
+
try {
|
|
2886
|
+
const saved = await persistenceService.getObject(PROVIDERS_PERSIST_KEY);
|
|
2887
|
+
if (!saved?.providers?.length) return;
|
|
2888
|
+
this.applyProvidersSettings(saved, {
|
|
2889
|
+
persist: false,
|
|
2890
|
+
emit: true
|
|
2891
|
+
});
|
|
2892
|
+
} catch {}
|
|
2893
|
+
}
|
|
2894
|
+
async persistProviders() {
|
|
2895
|
+
try {
|
|
2896
|
+
await persistenceService.persistObject(PROVIDERS_PERSIST_KEY, this.providersSettings);
|
|
2897
|
+
} catch {}
|
|
2898
|
+
}
|
|
2899
|
+
async hydrate() {
|
|
2900
|
+
try {
|
|
2901
|
+
const saved = await persistenceService.getObject(PERSIST_KEY);
|
|
2902
|
+
if (!saved) return;
|
|
2903
|
+
if (saved.sessions) this.sessions.loadSnapshot(saved.sessions);
|
|
2904
|
+
if (saved.memory) this.memory.restore(saved.memory);
|
|
2905
|
+
if (saved.approvalMode && isToolApprovalMode(saved.approvalMode)) {
|
|
2906
|
+
this.approvalMode = saved.approvalMode;
|
|
2907
|
+
this.tools.setApprovalMode(saved.approvalMode);
|
|
2908
|
+
}
|
|
2909
|
+
} catch {}
|
|
2910
|
+
}
|
|
2911
|
+
async persist() {
|
|
2912
|
+
try {
|
|
2913
|
+
await persistenceService.persistObject(PERSIST_KEY, {
|
|
2914
|
+
sessions: this.sessions.snapshot(),
|
|
2915
|
+
memory: this.memory.snapshot(),
|
|
2916
|
+
approvalMode: this.approvalMode
|
|
2917
|
+
});
|
|
2918
|
+
} catch {}
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
function isToolApprovalMode(value) {
|
|
2922
|
+
return value === "manual" || value === "auto_safe" || value === "auto_all";
|
|
2923
|
+
}
|
|
2924
|
+
function externalCatalogEntry(id, providerId) {
|
|
2925
|
+
return {
|
|
2926
|
+
id,
|
|
2927
|
+
providerId,
|
|
2928
|
+
tier: "standard",
|
|
2929
|
+
strengths: ["fast"],
|
|
2930
|
+
effort: detectEffortCapability(id)
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
var cognitiveRuntime = new CognitiveRuntime();
|
|
2934
|
+
function isDefaultChatTitle(title) {
|
|
2935
|
+
const t = title.trim().toLowerCase();
|
|
2936
|
+
return !t || t === "chat" || t === "new chat" || t === "new session";
|
|
2937
|
+
}
|
|
2938
|
+
function titleFromPrompt(text) {
|
|
2939
|
+
const trimmed = text.trim().replace(/\s+/g, " ");
|
|
2940
|
+
if (!trimmed) return "Chat";
|
|
2941
|
+
return trimmed.length <= 42 ? trimmed : `${trimmed.slice(0, 42).trimEnd()}…`;
|
|
2942
|
+
}
|
|
2943
|
+
function summarizePrompt(text, attachments) {
|
|
2944
|
+
if (text) return text;
|
|
2945
|
+
if (!attachments.length) return "";
|
|
2946
|
+
if (attachments.length === 1) return `Attached ${attachments[0].name}`;
|
|
2947
|
+
return `Attached ${attachments.map((a) => a.name).join(", ")}`;
|
|
2948
|
+
}
|
|
2949
|
+
//#endregion
|
|
2950
|
+
export { AI_SETTINGS_KEY as _, formatContextPercent as a, TOPIC as b, readPromptAttachmentFromFile as c, defaultProvidersSettings as d, listConfiguredModelIds as f, AI_SETTINGS_EDITOR_ID as g, loadModelsCatalog as h, formatBucketDetail as i, OpenAICompatibleProvider as l, detectEffortCapability as m, cognitiveRuntime as n, formatTokenCount as o, parseModelIds as p, estimateSessionContextUsage as r, ATTACHMENT_FILE_ACCEPT as s, CognitiveRuntime as t, fetchOpenAICompatibleModels as u, AI_VIEW_NAME as v, OPEN_AI_SETTINGS_COMMAND as y };
|
|
2951
|
+
|
|
2952
|
+
//# sourceMappingURL=runtime-DfNGzEjU.js.map
|