@eclipse-docks/extension-ai-system 0.8.0 → 0.8.2
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 +2 -2
- package/dist/api.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +87 -2
- package/dist/api.js.map +1 -0
- package/dist/{cognitive-extension-CG_d39K5.js → cognitive-extension-BKGxwLYj.js} +524 -102
- package/dist/cognitive-extension-BKGxwLYj.js.map +1 -0
- package/dist/cognitive-extension.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/abort.d.ts +5 -0
- package/dist/runtime/abort.d.ts.map +1 -0
- package/dist/runtime/attachment-paths.d.ts +17 -0
- package/dist/runtime/attachment-paths.d.ts.map +1 -0
- package/dist/runtime/attachments.d.ts +5 -5
- package/dist/runtime/attachments.d.ts.map +1 -1
- package/dist/runtime/cognitive-runtime.d.ts +12 -3
- package/dist/runtime/cognitive-runtime.d.ts.map +1 -1
- package/dist/runtime/context/usage.d.ts +3 -0
- package/dist/runtime/context/usage.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/models/openai-compatible.d.ts.map +1 -1
- package/dist/runtime/models/types.d.ts +6 -1
- package/dist/runtime/models/types.d.ts.map +1 -1
- package/dist/runtime/session-workspace.d.ts +20 -0
- package/dist/runtime/session-workspace.d.ts.map +1 -0
- package/dist/runtime/skills-commands.d.ts +3 -0
- package/dist/runtime/skills-commands.d.ts.map +1 -0
- package/dist/runtime/skills-parse.d.ts +14 -0
- package/dist/runtime/skills-parse.d.ts.map +1 -0
- package/dist/runtime/skills-resolve.d.ts +9 -0
- package/dist/runtime/skills-resolve.d.ts.map +1 -0
- package/dist/runtime/skills-select.d.ts +9 -0
- package/dist/runtime/skills-select.d.ts.map +1 -0
- package/dist/runtime/skills-types.d.ts +47 -0
- package/dist/runtime/skills-types.d.ts.map +1 -0
- package/dist/runtime/skills.d.ts +13 -0
- package/dist/runtime/skills.d.ts.map +1 -0
- package/dist/runtime/specialists/index.d.ts.map +1 -1
- package/dist/runtime/specialists/loop.d.ts.map +1 -1
- package/dist/runtime/tools/command-adapter.d.ts.map +1 -1
- package/dist/runtime/tools/fetch-adapter.d.ts +4 -0
- package/dist/runtime/tools/fetch-adapter.d.ts.map +1 -0
- package/dist/runtime/tools/index.d.ts +1 -1
- package/dist/runtime/tools/index.d.ts.map +1 -1
- package/dist/runtime/tools/workspace-adapter.d.ts +1 -1
- package/dist/runtime/tools/workspace-adapter.d.ts.map +1 -1
- package/dist/runtime/tools/workspace-path-qualify.d.ts +22 -0
- package/dist/runtime/tools/workspace-path-qualify.d.ts.map +1 -0
- package/dist/runtime/tools/workspace-path.d.ts +17 -0
- package/dist/runtime/tools/workspace-path.d.ts.map +1 -0
- package/dist/runtime/types.d.ts +7 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/{runtime-DfNGzEjU.js → runtime-B0IHjzQO.js} +1042 -401
- package/dist/runtime-B0IHjzQO.js.map +1 -0
- package/dist/view/busy-status.d.ts +12 -0
- package/dist/view/busy-status.d.ts.map +1 -0
- package/dist/view/cognitive-settings-editor.d.ts +0 -1
- package/dist/view/cognitive-settings-editor.d.ts.map +1 -1
- package/dist/view/cognitive-view.d.ts +22 -0
- package/dist/view/cognitive-view.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/cognitive-extension-CG_d39K5.js.map +0 -1
- package/dist/runtime/tools/attachment-adapter.d.ts +0 -6
- package/dist/runtime/tools/attachment-adapter.d.ts.map +0 -1
- package/dist/runtime-DfNGzEjU.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Directory, File, commandRegistry, persistenceService, workspaceService } from "@eclipse-docks/core";
|
|
1
|
+
import { Directory, File, IN_MEMORY_ROOT_NAME, commandRegistry, contributionRegistry, filebrowserDialog, getInMemoryRoot, persistenceService, registerAll, setInMemoryMountProjection, workspaceService } from "@eclipse-docks/core";
|
|
2
2
|
import { Models } from "@opencode-ai/models";
|
|
3
3
|
//#region src/runtime/events.ts
|
|
4
4
|
/** Tiny in-process bus so runtime stays UI-agnostic. */
|
|
@@ -548,6 +548,17 @@ function normalizeProvidersSettings(raw) {
|
|
|
548
548
|
};
|
|
549
549
|
}
|
|
550
550
|
//#endregion
|
|
551
|
+
//#region src/runtime/abort.ts
|
|
552
|
+
/** True when an AbortSignal has already been aborted. */
|
|
553
|
+
function isAborted(signal) {
|
|
554
|
+
return Boolean(signal?.aborted);
|
|
555
|
+
}
|
|
556
|
+
/** True for DOMException / Error abort failures from fetch and friends. */
|
|
557
|
+
function isAbortError(err) {
|
|
558
|
+
if (!err || typeof err !== "object") return false;
|
|
559
|
+
return err.name === "AbortError";
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
551
562
|
//#region src/runtime/models/openai-compatible.ts
|
|
552
563
|
function wireContent(content) {
|
|
553
564
|
if (typeof content === "string") return content;
|
|
@@ -613,11 +624,16 @@ async function fetchOpenAICompatibleModels(baseUrl, apiKey, signal) {
|
|
|
613
624
|
function buildChatCompletionBody(params) {
|
|
614
625
|
const model = params.model.includes("/") ? params.model.split("/").slice(1).join("/") : params.model;
|
|
615
626
|
const { reasoning_effort: _a, reasoningEffort: _b, reasoning: _c, ...safeOptions } = params.providerOptions ?? {};
|
|
627
|
+
const { stream_options: userStreamOptions, ...restSafeOptions } = safeOptions;
|
|
616
628
|
const body = {
|
|
617
629
|
model,
|
|
618
630
|
messages: toOpenAIMessages(params.messages),
|
|
619
631
|
stream: true,
|
|
620
|
-
...
|
|
632
|
+
...restSafeOptions,
|
|
633
|
+
stream_options: {
|
|
634
|
+
...userStreamOptions && typeof userStreamOptions === "object" ? userStreamOptions : {},
|
|
635
|
+
include_usage: true
|
|
636
|
+
}
|
|
621
637
|
};
|
|
622
638
|
const requestedEffort = params.providerOptions?.reasoning_effort ?? params.providerOptions?.reasoningEffort;
|
|
623
639
|
const reasoningModel = detectEffortCapability(params.model).supported || requestedEffort != null;
|
|
@@ -665,6 +681,10 @@ var OpenAICompatibleProvider = class {
|
|
|
665
681
|
signal: params.signal
|
|
666
682
|
});
|
|
667
683
|
} catch (err) {
|
|
684
|
+
if (params.signal?.aborted || isAbortError(err)) {
|
|
685
|
+
yield { type: "done" };
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
668
688
|
yield {
|
|
669
689
|
type: "error",
|
|
670
690
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -683,7 +703,21 @@ var OpenAICompatibleProvider = class {
|
|
|
683
703
|
let buffer = "";
|
|
684
704
|
const toolAcc = /* @__PURE__ */ new Map();
|
|
685
705
|
while (true) {
|
|
686
|
-
|
|
706
|
+
let done;
|
|
707
|
+
let value;
|
|
708
|
+
try {
|
|
709
|
+
({done, value} = await reader.read());
|
|
710
|
+
} catch (err) {
|
|
711
|
+
if (params.signal?.aborted || isAbortError(err)) {
|
|
712
|
+
yield { type: "done" };
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
yield {
|
|
716
|
+
type: "error",
|
|
717
|
+
error: err instanceof Error ? err.message : String(err)
|
|
718
|
+
};
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
687
721
|
if (done) break;
|
|
688
722
|
buffer += decoder.decode(value, { stream: true });
|
|
689
723
|
const lines = buffer.split("\n");
|
|
@@ -697,15 +731,22 @@ var OpenAICompatibleProvider = class {
|
|
|
697
731
|
return;
|
|
698
732
|
}
|
|
699
733
|
try {
|
|
700
|
-
const
|
|
734
|
+
const json = JSON.parse(data);
|
|
735
|
+
const usage = parseStreamUsage(json.usage);
|
|
736
|
+
if (usage) yield {
|
|
737
|
+
type: "usage",
|
|
738
|
+
usage
|
|
739
|
+
};
|
|
740
|
+
const delta = json.choices?.[0]?.delta;
|
|
701
741
|
if (!delta) continue;
|
|
702
|
-
if (delta.content) yield {
|
|
742
|
+
if (typeof delta.content === "string" && delta.content) yield {
|
|
703
743
|
type: "text",
|
|
704
744
|
text: delta.content
|
|
705
745
|
};
|
|
706
|
-
|
|
746
|
+
const reasoning = delta.reasoning_content ?? delta.reasoning;
|
|
747
|
+
if (reasoning) yield {
|
|
707
748
|
type: "reasoning",
|
|
708
|
-
text: String(
|
|
749
|
+
text: String(reasoning)
|
|
709
750
|
};
|
|
710
751
|
if (Array.isArray(delta.tool_calls)) for (const tc of delta.tool_calls) {
|
|
711
752
|
const idx = tc.index ?? 0;
|
|
@@ -729,6 +770,17 @@ var OpenAICompatibleProvider = class {
|
|
|
729
770
|
yield { type: "done" };
|
|
730
771
|
}
|
|
731
772
|
};
|
|
773
|
+
function parseStreamUsage(usage) {
|
|
774
|
+
if (!usage) return null;
|
|
775
|
+
const inputTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
|
|
776
|
+
const outputTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
|
|
777
|
+
if (!Number.isFinite(inputTokens) && !Number.isFinite(outputTokens)) return null;
|
|
778
|
+
if (inputTokens <= 0 && outputTokens <= 0) return null;
|
|
779
|
+
return {
|
|
780
|
+
inputTokens: Math.max(0, Math.round(inputTokens) || 0),
|
|
781
|
+
outputTokens: Math.max(0, Math.round(outputTokens) || 0)
|
|
782
|
+
};
|
|
783
|
+
}
|
|
732
784
|
//#endregion
|
|
733
785
|
//#region src/runtime/models/router.ts
|
|
734
786
|
var EFFORT_ORDER = [
|
|
@@ -955,227 +1007,40 @@ function isMutatingName(name) {
|
|
|
955
1007
|
return /^(write|edit|delete|mkdir|rm|mv|save|apply)/i.test(name) || name.includes("write") || name.includes("delete");
|
|
956
1008
|
}
|
|
957
1009
|
//#endregion
|
|
958
|
-
//#region src/runtime/
|
|
959
|
-
|
|
960
|
-
|
|
1010
|
+
//#region src/runtime/attachment-paths.ts
|
|
1011
|
+
/** Mirrors core `IN_MEMORY_ROOT_NAME` without importing the filesys graph. */
|
|
1012
|
+
var IN_MEMORY_ROOT_NAME$1 = "in-memory";
|
|
1013
|
+
/** URI scheme for UI references (`in-memory://<sessionId>/<file>`). */
|
|
1014
|
+
var IN_MEMORY_URI_SCHEME = "in-memory://";
|
|
1015
|
+
/** Sanitize a single path segment (no slashes). Prefer id-prefixed names for uniqueness. */
|
|
1016
|
+
function safeAttachmentFileName(name, id) {
|
|
1017
|
+
return `${id}-${(name || "attachment").replace(/[/\\]/g, "_").trim() || "attachment"}`;
|
|
1018
|
+
}
|
|
1019
|
+
/** Workspace path used by workspace_* tools (no protocol). */
|
|
1020
|
+
function attachmentWorkspacePath(sessionId, stored) {
|
|
1021
|
+
return `${IN_MEMORY_ROOT_NAME$1}/${sessionId}/${safeAttachmentFileName(stored.name, stored.id)}`;
|
|
1022
|
+
}
|
|
1023
|
+
/** UI / catalog URI form (`in-memory://<sessionId>/<file>`). */
|
|
1024
|
+
function attachmentUri(sessionId, stored) {
|
|
1025
|
+
return `${IN_MEMORY_URI_SCHEME}${sessionId}/${safeAttachmentFileName(stored.name, stored.id)}`;
|
|
961
1026
|
}
|
|
962
|
-
/**
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
});
|
|
1027
|
+
/**
|
|
1028
|
+
* Map `in-memory://…` references to composite workspace paths.
|
|
1029
|
+
* Real workspace paths are returned unchanged.
|
|
1030
|
+
*/
|
|
1031
|
+
function toWorkspaceToolPath(path) {
|
|
1032
|
+
const trimmed = path.trim();
|
|
1033
|
+
const lower = trimmed.toLowerCase();
|
|
1034
|
+
if (lower === "in-memory:" || lower === "in-memory://" || lower === "in-memory") return IN_MEMORY_ROOT_NAME$1;
|
|
1035
|
+
if (lower.startsWith("in-memory://")) {
|
|
1036
|
+
const rest = trimmed.slice(12).replace(/^\/+/, "");
|
|
1037
|
+
return rest ? `${IN_MEMORY_ROOT_NAME$1}/${rest}` : IN_MEMORY_ROOT_NAME$1;
|
|
1174
1038
|
}
|
|
1039
|
+
return trimmed;
|
|
1175
1040
|
}
|
|
1176
1041
|
//#endregion
|
|
1177
1042
|
//#region src/runtime/attachments.ts
|
|
1178
|
-
/** Soft cap per attached text file when stored / returned
|
|
1043
|
+
/** Soft cap per attached text file when stored / returned via workspace_read. */
|
|
1179
1044
|
var MAX_ATTACHMENT_CHARS = 1e5;
|
|
1180
1045
|
/** Soft cap for visual media attachments (bytes). */
|
|
1181
1046
|
var MAX_IMAGE_BYTES = 4e6;
|
|
@@ -1384,7 +1249,7 @@ function toSessionAttachment(attachment, opts = {}) {
|
|
|
1384
1249
|
messageId: opts.messageId
|
|
1385
1250
|
};
|
|
1386
1251
|
}
|
|
1387
|
-
/** Lightweight history/UI reference — no payload. */
|
|
1252
|
+
/** Lightweight history/UI reference — no payload (path is an in-memory workspace path/URI). */
|
|
1388
1253
|
function toFileRefPart(stored) {
|
|
1389
1254
|
return {
|
|
1390
1255
|
type: "file",
|
|
@@ -1398,14 +1263,15 @@ function toFileRefPart(stored) {
|
|
|
1398
1263
|
}
|
|
1399
1264
|
/** One-line catalog entry published into the user turn for the model. */
|
|
1400
1265
|
function formatAttachmentRefForModel(part) {
|
|
1401
|
-
const
|
|
1266
|
+
const display = part.path || part.name;
|
|
1267
|
+
const toolPath = toWorkspaceToolPath(display);
|
|
1402
1268
|
const kind = part.kind ?? "file";
|
|
1403
1269
|
const bytes = part.byteLength != null ? ` bytes=${part.byteLength}` : "";
|
|
1404
|
-
return `[Attachment published:
|
|
1270
|
+
return `[Attachment published: ${display} kind=${kind}${part.mime ? ` mime=${part.mime}` : ""}${bytes}] → workspace_read path="${toolPath}"`;
|
|
1405
1271
|
}
|
|
1406
1272
|
/**
|
|
1407
1273
|
* Build provider user content from history parts.
|
|
1408
|
-
* File parts are references only — payloads
|
|
1274
|
+
* File parts are references only — payloads live under the in-memory workspace root.
|
|
1409
1275
|
*/
|
|
1410
1276
|
function userContentFromParts(parts) {
|
|
1411
1277
|
const texts = [];
|
|
@@ -1416,71 +1282,10 @@ function userContentFromParts(parts) {
|
|
|
1416
1282
|
}
|
|
1417
1283
|
const body = [...texts, ...refs].join("\n").trim();
|
|
1418
1284
|
if (!refs.length) return body;
|
|
1419
|
-
return [body, "
|
|
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
|
-
};
|
|
1285
|
+
return [body, "Session attachments are files under the in-memory workspace root. List with workspace_list, read with workspace_read. Do not connect storage backends to find them."].filter(Boolean).join("\n");
|
|
1452
1286
|
}
|
|
1453
1287
|
/** Marker field: specialist loop presents image bytes in a follow-up multimodal message. */
|
|
1454
1288
|
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
1289
|
function stripAttachmentToolOutput(output) {
|
|
1485
1290
|
if (!output || typeof output !== "object") return output;
|
|
1486
1291
|
const copy = { ...output };
|
|
@@ -1608,107 +1413,579 @@ async function readPromptAttachmentFromFile(file) {
|
|
|
1608
1413
|
byteLength: file.size,
|
|
1609
1414
|
truncated
|
|
1610
1415
|
}
|
|
1611
|
-
};
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
//#endregion
|
|
1419
|
+
//#region src/runtime/tools/workspace-path-qualify.ts
|
|
1420
|
+
function isInMemoryToolPath(path) {
|
|
1421
|
+
return path === "in-memory" || path.startsWith(`in-memory/`);
|
|
1422
|
+
}
|
|
1423
|
+
function isWorkspaceRootPath(path) {
|
|
1424
|
+
const trimmed = path.trim();
|
|
1425
|
+
return !trimmed || trimmed === "." || trimmed === "/";
|
|
1426
|
+
}
|
|
1427
|
+
/** First path segment — composite roots are keyed by this name. */
|
|
1428
|
+
function firstPathSegment(path) {
|
|
1429
|
+
return path.split("/").filter(Boolean)[0] ?? "";
|
|
1430
|
+
}
|
|
1431
|
+
function pathHasKnownRoot(path, rootNames) {
|
|
1432
|
+
const first = firstPathSegment(path);
|
|
1433
|
+
if (!first) return false;
|
|
1434
|
+
return (rootNames instanceof Set ? rootNames : new Set(rootNames)).has(first);
|
|
1435
|
+
}
|
|
1436
|
+
function joinUnderRoot(rootPath, relativePath) {
|
|
1437
|
+
const root = rootPath.replace(/\/+$/, "");
|
|
1438
|
+
const relative = relativePath.replace(/^\/+/, "");
|
|
1439
|
+
if (!root) return relative;
|
|
1440
|
+
if (!relative) return root;
|
|
1441
|
+
return `${root}/${relative}`;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Decide how to qualify a workspace tool path that is missing a connected-root prefix.
|
|
1445
|
+
*/
|
|
1446
|
+
function decideRootQualification(path, durableRoots, mode) {
|
|
1447
|
+
if (isInMemoryToolPath(path) || isWorkspaceRootPath(path)) return { action: "keep" };
|
|
1448
|
+
if (pathHasKnownRoot(path, durableRoots)) return { action: "keep" };
|
|
1449
|
+
if (durableRoots.length === 1) return {
|
|
1450
|
+
action: "prefix",
|
|
1451
|
+
root: durableRoots[0]
|
|
1452
|
+
};
|
|
1453
|
+
if (durableRoots.length === 0) return {
|
|
1454
|
+
action: "error",
|
|
1455
|
+
error: mode === "write" ? "No durable workspace folder connected. Connect a folder before writing, or use an in-memory/… path." : "No workspace folder connected."
|
|
1456
|
+
};
|
|
1457
|
+
if (mode === "write") return { action: "pick" };
|
|
1458
|
+
return {
|
|
1459
|
+
action: "error",
|
|
1460
|
+
error: `Path must start with a workspace root (${durableRoots.join(", ")}). Example: ${durableRoots[0]}/${path}`
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
//#endregion
|
|
1464
|
+
//#region src/runtime/tools/workspace-path.ts
|
|
1465
|
+
/**
|
|
1466
|
+
* Ensure tool paths are under a composite workspace root.
|
|
1467
|
+
* - Already rooted / in-memory / empty → unchanged
|
|
1468
|
+
* - One durable root → auto-prefix
|
|
1469
|
+
* - Several durable roots (writes) → folder picker dialog
|
|
1470
|
+
*/
|
|
1471
|
+
async function qualifyWorkspaceToolPath(rawPath, mode = "write") {
|
|
1472
|
+
const path = toWorkspaceToolPath(String(rawPath ?? ""));
|
|
1473
|
+
if (isInMemoryToolPath(path) || isWorkspaceRootPath(path)) return {
|
|
1474
|
+
ok: true,
|
|
1475
|
+
path
|
|
1476
|
+
};
|
|
1477
|
+
const folders = await workspaceService.getFolders();
|
|
1478
|
+
const allNames = folders.map((f) => f.name);
|
|
1479
|
+
if (pathHasKnownRoot(path, allNames)) return {
|
|
1480
|
+
ok: true,
|
|
1481
|
+
path
|
|
1482
|
+
};
|
|
1483
|
+
const durableRoots = folders.filter((f) => f.type !== "in-memory").map((f) => f.name);
|
|
1484
|
+
const decision = decideRootQualification(path, mode === "write" ? durableRoots : allNames.length ? allNames : durableRoots, mode);
|
|
1485
|
+
if (decision.action === "keep") return {
|
|
1486
|
+
ok: true,
|
|
1487
|
+
path
|
|
1488
|
+
};
|
|
1489
|
+
if (decision.action === "prefix") return {
|
|
1490
|
+
ok: true,
|
|
1491
|
+
path: joinUnderRoot(decision.root, path),
|
|
1492
|
+
resolvedFrom: path
|
|
1493
|
+
};
|
|
1494
|
+
if (decision.action === "error") return {
|
|
1495
|
+
ok: false,
|
|
1496
|
+
error: decision.error
|
|
1497
|
+
};
|
|
1498
|
+
const chosen = await filebrowserDialog("directory");
|
|
1499
|
+
if (!chosen) return {
|
|
1500
|
+
ok: false,
|
|
1501
|
+
error: "Write cancelled: no workspace folder selected."
|
|
1502
|
+
};
|
|
1503
|
+
return {
|
|
1504
|
+
ok: true,
|
|
1505
|
+
path: joinUnderRoot(chosen, path),
|
|
1506
|
+
resolvedFrom: path
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
//#endregion
|
|
1510
|
+
//#region src/runtime/tools/workspace-adapter.ts
|
|
1511
|
+
function asRecord$1(value) {
|
|
1512
|
+
return value && typeof value === "object" ? value : {};
|
|
1513
|
+
}
|
|
1514
|
+
/** Some File backends (e.g. in-memory) expose mime; the abstract File does not. */
|
|
1515
|
+
function optionalMimeType(resource) {
|
|
1516
|
+
const getMimeType = resource.getMimeType;
|
|
1517
|
+
return typeof getMimeType === "function" ? getMimeType.call(resource) : void 0;
|
|
1518
|
+
}
|
|
1519
|
+
function fileNameFromPath(path) {
|
|
1520
|
+
const parts = path.split("/").filter(Boolean);
|
|
1521
|
+
return parts[parts.length - 1] || path;
|
|
1522
|
+
}
|
|
1523
|
+
/** Resolve a tool path against the shared in-memory root or the mounted workspace. */
|
|
1524
|
+
async function resolveResource(path, options) {
|
|
1525
|
+
if (isInMemoryToolPath(path)) {
|
|
1526
|
+
const root = getInMemoryRoot();
|
|
1527
|
+
const relative = path === IN_MEMORY_ROOT_NAME ? "" : path.slice(`${IN_MEMORY_ROOT_NAME}/`.length);
|
|
1528
|
+
if (!relative) return {
|
|
1529
|
+
resource: root,
|
|
1530
|
+
path: IN_MEMORY_ROOT_NAME
|
|
1531
|
+
};
|
|
1532
|
+
const lookup = options?.directory && !relative.endsWith("/") ? `${relative}/` : relative;
|
|
1533
|
+
return {
|
|
1534
|
+
resource: await root.getResource(lookup, options?.create ? { create: true } : void 0),
|
|
1535
|
+
path
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
const workspace = await workspaceService.getWorkspace();
|
|
1539
|
+
if (!workspace) return {
|
|
1540
|
+
resource: null,
|
|
1541
|
+
path
|
|
1542
|
+
};
|
|
1543
|
+
if (isWorkspaceRootPath(path)) return {
|
|
1544
|
+
resource: workspace,
|
|
1545
|
+
path: ""
|
|
1546
|
+
};
|
|
1547
|
+
const lookup = options?.directory && !path.endsWith("/") ? `${path}/` : path;
|
|
1548
|
+
return {
|
|
1549
|
+
resource: await workspace.getResource(lookup, options?.create ? { create: true } : void 0),
|
|
1550
|
+
path
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Present image-like workspace files for vision (data URLs or known image extensions).
|
|
1555
|
+
* Returns null when the file should be treated as ordinary text.
|
|
1556
|
+
*/
|
|
1557
|
+
function visionToolOutput(path, contents, mimeHint) {
|
|
1558
|
+
const name = fileNameFromPath(path);
|
|
1559
|
+
const dataUrl = typeof contents === "string" && contents.startsWith("data:image/") ? contents : void 0;
|
|
1560
|
+
const mime = mimeHint || (dataUrl ? dataUrl.slice(5, dataUrl.indexOf(";")) : void 0) || resolveMediaType({
|
|
1561
|
+
name,
|
|
1562
|
+
mime: mimeHint
|
|
1563
|
+
})?.mime;
|
|
1564
|
+
if (isSvgAttachment({
|
|
1565
|
+
name,
|
|
1566
|
+
mime,
|
|
1567
|
+
text: dataUrl ? void 0 : contents
|
|
1568
|
+
})) return null;
|
|
1569
|
+
const media = resolveMediaType({
|
|
1570
|
+
name,
|
|
1571
|
+
mime
|
|
1572
|
+
});
|
|
1573
|
+
if (!dataUrl && media?.mode !== "raster") return null;
|
|
1574
|
+
if (!dataUrl) return null;
|
|
1575
|
+
return {
|
|
1576
|
+
path,
|
|
1577
|
+
name,
|
|
1578
|
+
mime,
|
|
1579
|
+
kind: "image",
|
|
1580
|
+
...media?.vision !== false ? {
|
|
1581
|
+
[IMAGE_PRESENT_KEY]: true,
|
|
1582
|
+
dataUrl,
|
|
1583
|
+
note: "Image bytes will be provided to the model in the next message for vision inspection."
|
|
1584
|
+
} : {
|
|
1585
|
+
dataUrlOmitted: true,
|
|
1586
|
+
note: "This media type is stored for preview; vision presentation is skipped."
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
/** Registers built-in workspace file tools (paths may use in-memory:// URI form). */
|
|
1591
|
+
function registerWorkspaceTools(broker) {
|
|
1592
|
+
broker.register({
|
|
1593
|
+
name: "workspace_list",
|
|
1594
|
+
description: "List children of a workspace directory. Omit path (or use \"\" / \"/\") for connected roots (use those path prefixes for writes). in-memory/… works without mounting.",
|
|
1595
|
+
parameters: {
|
|
1596
|
+
type: "object",
|
|
1597
|
+
properties: { path: {
|
|
1598
|
+
type: "string",
|
|
1599
|
+
description: "Directory path under a connected root (empty = list roots). in-memory://… URIs are accepted."
|
|
1600
|
+
} },
|
|
1601
|
+
required: []
|
|
1602
|
+
}
|
|
1603
|
+
}, async (call) => {
|
|
1604
|
+
try {
|
|
1605
|
+
const path = toWorkspaceToolPath(String(call.arguments.path ?? ""));
|
|
1606
|
+
const { resource } = await resolveResource(path, { directory: true });
|
|
1607
|
+
if (!(resource instanceof Directory)) return {
|
|
1608
|
+
id: call.id,
|
|
1609
|
+
name: call.name,
|
|
1610
|
+
ok: false,
|
|
1611
|
+
output: null,
|
|
1612
|
+
error: isInMemoryToolPath(path) ? "Not a directory" : "No workspace / not a directory"
|
|
1613
|
+
};
|
|
1614
|
+
const children = await resource.listChildren(true);
|
|
1615
|
+
return {
|
|
1616
|
+
id: call.id,
|
|
1617
|
+
name: call.name,
|
|
1618
|
+
ok: true,
|
|
1619
|
+
output: children.map((c) => ({
|
|
1620
|
+
name: c.getName(),
|
|
1621
|
+
path: c.getWorkspacePath(),
|
|
1622
|
+
kind: c instanceof Directory ? "directory" : "file"
|
|
1623
|
+
}))
|
|
1624
|
+
};
|
|
1625
|
+
} catch (err) {
|
|
1626
|
+
return {
|
|
1627
|
+
id: call.id,
|
|
1628
|
+
name: call.name,
|
|
1629
|
+
ok: false,
|
|
1630
|
+
output: null,
|
|
1631
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
broker.register({
|
|
1636
|
+
name: "workspace_read",
|
|
1637
|
+
description: "Read a workspace file as text. Paths should be root/… from workspace_list; a bare name is auto-rooted when only one folder is connected. Image data-URL files are presented for vision.",
|
|
1638
|
+
parameters: {
|
|
1639
|
+
type: "object",
|
|
1640
|
+
properties: {
|
|
1641
|
+
path: {
|
|
1642
|
+
type: "string",
|
|
1643
|
+
description: "File path under a connected root (e.g. tests/readme.md)."
|
|
1644
|
+
},
|
|
1645
|
+
offset: {
|
|
1646
|
+
type: "number",
|
|
1647
|
+
description: "Text start offset"
|
|
1648
|
+
},
|
|
1649
|
+
limit: {
|
|
1650
|
+
type: "number",
|
|
1651
|
+
description: "Max text characters to return"
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
required: ["path"]
|
|
1655
|
+
}
|
|
1656
|
+
}, async (call) => {
|
|
1657
|
+
try {
|
|
1658
|
+
const qualified = await qualifyWorkspaceToolPath(String(call.arguments.path ?? ""), "read");
|
|
1659
|
+
if (!qualified.ok) return {
|
|
1660
|
+
id: call.id,
|
|
1661
|
+
name: call.name,
|
|
1662
|
+
ok: false,
|
|
1663
|
+
output: null,
|
|
1664
|
+
error: qualified.error
|
|
1665
|
+
};
|
|
1666
|
+
const path = qualified.path;
|
|
1667
|
+
const { resource } = await resolveResource(path);
|
|
1668
|
+
if (!(resource instanceof File)) return {
|
|
1669
|
+
id: call.id,
|
|
1670
|
+
name: call.name,
|
|
1671
|
+
ok: false,
|
|
1672
|
+
output: null,
|
|
1673
|
+
error: isInMemoryToolPath(path) ? "Not a file" : "No workspace / not a file"
|
|
1674
|
+
};
|
|
1675
|
+
const contents = await resource.getContents();
|
|
1676
|
+
const text = typeof contents === "string" ? contents : String(contents ?? "");
|
|
1677
|
+
const vision = visionToolOutput(path, text, optionalMimeType(resource));
|
|
1678
|
+
if (vision) return {
|
|
1679
|
+
id: call.id,
|
|
1680
|
+
name: call.name,
|
|
1681
|
+
ok: true,
|
|
1682
|
+
output: vision
|
|
1683
|
+
};
|
|
1684
|
+
const offset = typeof call.arguments.offset === "number" ? Math.max(0, call.arguments.offset) : 0;
|
|
1685
|
+
const limit = typeof call.arguments.limit === "number" ? Math.max(1, call.arguments.limit) : void 0;
|
|
1686
|
+
if (offset || limit != null) {
|
|
1687
|
+
const slice = text.slice(offset, limit != null ? offset + limit : void 0);
|
|
1688
|
+
return {
|
|
1689
|
+
id: call.id,
|
|
1690
|
+
name: call.name,
|
|
1691
|
+
ok: true,
|
|
1692
|
+
output: {
|
|
1693
|
+
path,
|
|
1694
|
+
offset,
|
|
1695
|
+
length: slice.length,
|
|
1696
|
+
totalLength: text.length,
|
|
1697
|
+
truncated: offset + slice.length < text.length,
|
|
1698
|
+
content: slice
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
id: call.id,
|
|
1704
|
+
name: call.name,
|
|
1705
|
+
ok: true,
|
|
1706
|
+
output: text
|
|
1707
|
+
};
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
return {
|
|
1710
|
+
id: call.id,
|
|
1711
|
+
name: call.name,
|
|
1712
|
+
ok: false,
|
|
1713
|
+
output: null,
|
|
1714
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
broker.register({
|
|
1719
|
+
name: "workspace_write",
|
|
1720
|
+
description: "Write text to a workspace file (creates parents as needed). Prefer root/… paths from workspace_list. A bare filename uses the only durable root, or opens a folder picker when several roots are connected.",
|
|
1721
|
+
parameters: {
|
|
1722
|
+
type: "object",
|
|
1723
|
+
properties: {
|
|
1724
|
+
path: {
|
|
1725
|
+
type: "string",
|
|
1726
|
+
description: "File path under a connected root (e.g. tests/demo.canvas.js)."
|
|
1727
|
+
},
|
|
1728
|
+
contents: { type: "string" }
|
|
1729
|
+
},
|
|
1730
|
+
required: ["path", "contents"]
|
|
1731
|
+
}
|
|
1732
|
+
}, async (call) => {
|
|
1733
|
+
try {
|
|
1734
|
+
const args = asRecord$1(call.arguments);
|
|
1735
|
+
const qualified = await qualifyWorkspaceToolPath(String(args.path ?? ""), "write");
|
|
1736
|
+
if (!qualified.ok) return {
|
|
1737
|
+
id: call.id,
|
|
1738
|
+
name: call.name,
|
|
1739
|
+
ok: false,
|
|
1740
|
+
output: null,
|
|
1741
|
+
error: qualified.error,
|
|
1742
|
+
mutating: true
|
|
1743
|
+
};
|
|
1744
|
+
const path = qualified.path;
|
|
1745
|
+
const contents = String(args.contents ?? "");
|
|
1746
|
+
const { resource } = await resolveResource(path, { create: true });
|
|
1747
|
+
if (!(resource instanceof File)) return {
|
|
1748
|
+
id: call.id,
|
|
1749
|
+
name: call.name,
|
|
1750
|
+
ok: false,
|
|
1751
|
+
output: null,
|
|
1752
|
+
error: isInMemoryToolPath(path) ? "Not a file" : "No workspace / not a file",
|
|
1753
|
+
mutating: true
|
|
1754
|
+
};
|
|
1755
|
+
await resource.saveContents(contents);
|
|
1756
|
+
return {
|
|
1757
|
+
id: call.id,
|
|
1758
|
+
name: call.name,
|
|
1759
|
+
ok: true,
|
|
1760
|
+
output: {
|
|
1761
|
+
path,
|
|
1762
|
+
...qualified.resolvedFrom ? { resolvedFrom: qualified.resolvedFrom } : {}
|
|
1763
|
+
},
|
|
1764
|
+
mutating: true
|
|
1765
|
+
};
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
return {
|
|
1768
|
+
id: call.id,
|
|
1769
|
+
name: call.name,
|
|
1770
|
+
ok: false,
|
|
1771
|
+
output: null,
|
|
1772
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1773
|
+
mutating: true
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1612
1777
|
}
|
|
1613
1778
|
//#endregion
|
|
1614
|
-
//#region src/runtime/tools/
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1779
|
+
//#region src/runtime/tools/fetch-adapter.ts
|
|
1780
|
+
var DEFAULT_MAX_BYTES = 512e3;
|
|
1781
|
+
var DEFAULT_MAX_CHARS = 12e4;
|
|
1782
|
+
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
1783
|
+
function asRecord(value) {
|
|
1784
|
+
return value && typeof value === "object" ? value : {};
|
|
1785
|
+
}
|
|
1786
|
+
function parseUrl(raw) {
|
|
1787
|
+
const trimmed = raw.trim();
|
|
1788
|
+
if (!trimmed) return { error: "Missing url" };
|
|
1789
|
+
let parsed;
|
|
1790
|
+
try {
|
|
1791
|
+
parsed = new URL(trimmed);
|
|
1792
|
+
} catch {
|
|
1793
|
+
return { error: `Invalid url: ${trimmed}` };
|
|
1794
|
+
}
|
|
1795
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return { error: `Only http(s) URLs are allowed (got ${parsed.protocol})` };
|
|
1796
|
+
return parsed;
|
|
1797
|
+
}
|
|
1798
|
+
function isTextualContentType(contentType) {
|
|
1799
|
+
const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
1800
|
+
if (!type) return true;
|
|
1801
|
+
if (type.startsWith("text/")) return true;
|
|
1802
|
+
return type.includes("json") || type.includes("xml") || type.includes("javascript") || type.includes("svg") || type.includes("markdown") || type.includes("yaml") || type.includes("csv");
|
|
1803
|
+
}
|
|
1804
|
+
/** Registers a read-only HTTP(S) fetch tool for remote documentation and text content. */
|
|
1805
|
+
function registerFetchTools(broker) {
|
|
1636
1806
|
broker.register({
|
|
1637
|
-
name: "
|
|
1638
|
-
description: "
|
|
1807
|
+
name: "fetch",
|
|
1808
|
+
description: "Fetch a remote http(s) URL and return text content (HTML, Markdown, JSON, etc.). Use for docs and references (e.g. Web Awesome agent skills on jsDelivr). Binary responses return metadata only.",
|
|
1639
1809
|
parameters: {
|
|
1640
1810
|
type: "object",
|
|
1641
1811
|
properties: {
|
|
1642
|
-
|
|
1812
|
+
url: {
|
|
1643
1813
|
type: "string",
|
|
1644
|
-
description: "
|
|
1645
|
-
},
|
|
1646
|
-
offset: {
|
|
1647
|
-
type: "number",
|
|
1648
|
-
description: "Text start offset (characters)"
|
|
1814
|
+
description: "Absolute http(s) URL to fetch"
|
|
1649
1815
|
},
|
|
1650
|
-
|
|
1816
|
+
max_chars: {
|
|
1651
1817
|
type: "number",
|
|
1652
|
-
description:
|
|
1818
|
+
description: `Max characters of body text to return (default ${DEFAULT_MAX_CHARS})`
|
|
1653
1819
|
}
|
|
1654
1820
|
},
|
|
1655
|
-
required: ["
|
|
1821
|
+
required: ["url"]
|
|
1656
1822
|
}
|
|
1657
|
-
}, async (call
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
|
|
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 {
|
|
1823
|
+
}, async (call) => {
|
|
1824
|
+
const args = asRecord(call.arguments);
|
|
1825
|
+
const parsed = parseUrl(String(args.url ?? ""));
|
|
1826
|
+
if ("error" in parsed) return {
|
|
1668
1827
|
id: call.id,
|
|
1669
1828
|
name: call.name,
|
|
1670
1829
|
ok: false,
|
|
1671
1830
|
output: null,
|
|
1672
|
-
error:
|
|
1831
|
+
error: parsed.error
|
|
1673
1832
|
};
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1833
|
+
const maxCharsRaw = Number(args.max_chars);
|
|
1834
|
+
const maxChars = Number.isFinite(maxCharsRaw) && maxCharsRaw > 0 ? Math.min(Math.floor(maxCharsRaw), DEFAULT_MAX_CHARS * 2) : DEFAULT_MAX_CHARS;
|
|
1835
|
+
const controller = new AbortController();
|
|
1836
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
1837
|
+
try {
|
|
1838
|
+
const response = await fetch(parsed.href, {
|
|
1839
|
+
method: "GET",
|
|
1840
|
+
redirect: "follow",
|
|
1841
|
+
signal: controller.signal,
|
|
1842
|
+
headers: { Accept: "text/*, application/json, application/xml, */*;q=0.1" }
|
|
1678
1843
|
});
|
|
1679
|
-
|
|
1844
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1845
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
1846
|
+
const truncatedBytes = buffer.byteLength > DEFAULT_MAX_BYTES;
|
|
1847
|
+
const slice = truncatedBytes ? buffer.subarray(0, DEFAULT_MAX_BYTES) : buffer;
|
|
1848
|
+
if (!isTextualContentType(contentType) && !looksLikeUtf8Text(slice)) return {
|
|
1680
1849
|
id: call.id,
|
|
1681
1850
|
name: call.name,
|
|
1682
|
-
ok:
|
|
1683
|
-
output:
|
|
1684
|
-
|
|
1851
|
+
ok: response.ok,
|
|
1852
|
+
output: {
|
|
1853
|
+
url: response.url || parsed.href,
|
|
1854
|
+
status: response.status,
|
|
1855
|
+
contentType,
|
|
1856
|
+
byteLength: buffer.byteLength,
|
|
1857
|
+
binary: true,
|
|
1858
|
+
note: "Response is not text; body omitted. Prefer documentation or raw text URLs."
|
|
1859
|
+
},
|
|
1860
|
+
error: response.ok ? void 0 : `HTTP ${response.status}`
|
|
1685
1861
|
};
|
|
1862
|
+
const decoded = new TextDecoder("utf-8", { fatal: false }).decode(slice);
|
|
1863
|
+
const truncatedChars = decoded.length > maxChars;
|
|
1864
|
+
const body = truncatedChars ? decoded.slice(0, maxChars) : decoded;
|
|
1686
1865
|
return {
|
|
1687
1866
|
id: call.id,
|
|
1688
1867
|
name: call.name,
|
|
1689
|
-
ok:
|
|
1690
|
-
output:
|
|
1868
|
+
ok: response.ok,
|
|
1869
|
+
output: {
|
|
1870
|
+
url: response.url || parsed.href,
|
|
1871
|
+
status: response.status,
|
|
1872
|
+
contentType,
|
|
1873
|
+
byteLength: buffer.byteLength,
|
|
1874
|
+
truncated: truncatedBytes || truncatedChars,
|
|
1875
|
+
body
|
|
1876
|
+
},
|
|
1877
|
+
error: response.ok ? void 0 : `HTTP ${response.status}`
|
|
1878
|
+
};
|
|
1879
|
+
} catch (err) {
|
|
1880
|
+
const message = err instanceof Error && err.name === "AbortError" ? `Timed out after ${DEFAULT_TIMEOUT_MS}ms` : err instanceof Error ? err.message : String(err);
|
|
1881
|
+
return {
|
|
1882
|
+
id: call.id,
|
|
1883
|
+
name: call.name,
|
|
1884
|
+
ok: false,
|
|
1885
|
+
output: null,
|
|
1886
|
+
error: message
|
|
1691
1887
|
};
|
|
1888
|
+
} finally {
|
|
1889
|
+
clearTimeout(timer);
|
|
1692
1890
|
}
|
|
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
1891
|
});
|
|
1711
1892
|
}
|
|
1893
|
+
function looksLikeUtf8Text(bytes) {
|
|
1894
|
+
if (!bytes.length) return true;
|
|
1895
|
+
const sample = bytes.subarray(0, Math.min(bytes.length, 512));
|
|
1896
|
+
let odd = 0;
|
|
1897
|
+
for (const b of sample) {
|
|
1898
|
+
if (b === 0) return false;
|
|
1899
|
+
if (b < 7 || b > 13 && b < 32) odd += 1;
|
|
1900
|
+
}
|
|
1901
|
+
return odd / sample.length < .1;
|
|
1902
|
+
}
|
|
1903
|
+
//#endregion
|
|
1904
|
+
//#region src/runtime/tools/command-adapter.ts
|
|
1905
|
+
/** Commands that must never be exposed as LLM tools (pickers, storage boots, settings). */
|
|
1906
|
+
var SKIP = /* @__PURE__ */ new Set([
|
|
1907
|
+
"open_cognitive_ai_settings",
|
|
1908
|
+
"open_ai_config",
|
|
1909
|
+
"load_workspace",
|
|
1910
|
+
"connect_opfs",
|
|
1911
|
+
"connect_indexeddb",
|
|
1912
|
+
"disconnect_workspace",
|
|
1913
|
+
"disconnect_folder",
|
|
1914
|
+
"open_editor",
|
|
1915
|
+
"list_extensions",
|
|
1916
|
+
"enable_extension",
|
|
1917
|
+
"disable_extension"
|
|
1918
|
+
]);
|
|
1919
|
+
var SKIP_PREFIXES = [
|
|
1920
|
+
"connect_",
|
|
1921
|
+
"disconnect_",
|
|
1922
|
+
"install_",
|
|
1923
|
+
"uninstall_"
|
|
1924
|
+
];
|
|
1925
|
+
/** Built-in tools that must survive command refresh (not command-backed). */
|
|
1926
|
+
var KEEP_TOOLS = /* @__PURE__ */ new Set([
|
|
1927
|
+
"workspace_list",
|
|
1928
|
+
"workspace_read",
|
|
1929
|
+
"workspace_write",
|
|
1930
|
+
"fetch"
|
|
1931
|
+
]);
|
|
1932
|
+
function sanitizeName(id) {
|
|
1933
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
1934
|
+
}
|
|
1935
|
+
function shouldSkipCommand(id) {
|
|
1936
|
+
if (SKIP.has(id)) return true;
|
|
1937
|
+
return SKIP_PREFIXES.some((prefix) => id.startsWith(prefix));
|
|
1938
|
+
}
|
|
1939
|
+
/** Registers Docks commands as first-class LLM tools (tool name = command id). */
|
|
1940
|
+
function refreshCommandTools(broker, role) {
|
|
1941
|
+
for (const def of broker.listDefinitions("build")) if (!KEEP_TOOLS.has(def.name)) broker.unregister(def.name);
|
|
1942
|
+
const commands = commandRegistry.listCommands(commandRegistry.createExecutionContext());
|
|
1943
|
+
const entries = Object.values(commands);
|
|
1944
|
+
for (const command of entries) {
|
|
1945
|
+
if (!command?.id || shouldSkipCommand(command.id)) continue;
|
|
1946
|
+
if (!command.description) continue;
|
|
1947
|
+
if ((role === "plan" || role === "explore") && /delete|write|save|rm|mv/i.test(command.id)) continue;
|
|
1948
|
+
const toolName = sanitizeName(command.id);
|
|
1949
|
+
if (KEEP_TOOLS.has(toolName)) continue;
|
|
1950
|
+
const properties = {};
|
|
1951
|
+
const required = [];
|
|
1952
|
+
for (const p of command.parameters ?? []) {
|
|
1953
|
+
properties[p.name] = {
|
|
1954
|
+
type: p.type ?? "string",
|
|
1955
|
+
description: p.description,
|
|
1956
|
+
...p.allowedValues ? { enum: p.allowedValues } : {}
|
|
1957
|
+
};
|
|
1958
|
+
if (p.required) required.push(p.name);
|
|
1959
|
+
}
|
|
1960
|
+
broker.register({
|
|
1961
|
+
name: toolName,
|
|
1962
|
+
description: `${command.name}: ${command.description}`,
|
|
1963
|
+
parameters: {
|
|
1964
|
+
type: "object",
|
|
1965
|
+
properties,
|
|
1966
|
+
required
|
|
1967
|
+
}
|
|
1968
|
+
}, async (call, _sessionId) => {
|
|
1969
|
+
try {
|
|
1970
|
+
const result = await commandRegistry.execute(command.id, { params: call.arguments });
|
|
1971
|
+
return {
|
|
1972
|
+
id: call.id,
|
|
1973
|
+
name: call.name,
|
|
1974
|
+
ok: true,
|
|
1975
|
+
output: result
|
|
1976
|
+
};
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
return {
|
|
1979
|
+
id: call.id,
|
|
1980
|
+
name: call.name,
|
|
1981
|
+
ok: false,
|
|
1982
|
+
output: null,
|
|
1983
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1712
1989
|
//#endregion
|
|
1713
1990
|
//#region src/runtime/compact/compaction.ts
|
|
1714
1991
|
var COMPACT_SYSTEM = `You compress older chat turns into a durable summary for future assistant turns.
|
|
@@ -2026,7 +2303,7 @@ var SessionRuntime = class {
|
|
|
2026
2303
|
//#region src/runtime/specialists/classify-risk.ts
|
|
2027
2304
|
var CLASSIFY_SYSTEM = `You classify tool calls for user approval.
|
|
2028
2305
|
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).
|
|
2306
|
+
Mark risk "safe" only for read-only inspection (list, read, search, describe, fetch).
|
|
2030
2307
|
When unsure, choose "critical".
|
|
2031
2308
|
Reply with ONLY a JSON array: [{"id":"<tool call id>","risk":"safe"|"critical"}]`;
|
|
2032
2309
|
/** Ask the model whether pending tool calls are safe or critical. Unknown → critical. */
|
|
@@ -2163,7 +2440,25 @@ async function runSpecialistLoop(input) {
|
|
|
2163
2440
|
};
|
|
2164
2441
|
const messages = historyToProviderMessages(input.systemPrompt, input.history, input.compactionSummary);
|
|
2165
2442
|
let success = true;
|
|
2443
|
+
let cancelled = false;
|
|
2444
|
+
const usage = {
|
|
2445
|
+
inputTokens: 0,
|
|
2446
|
+
outputTokens: 0
|
|
2447
|
+
};
|
|
2448
|
+
const markCancelled = () => {
|
|
2449
|
+
if (cancelled) return;
|
|
2450
|
+
cancelled = true;
|
|
2451
|
+
success = false;
|
|
2452
|
+
if (!parts.some((p) => p.type === "text")) emit({
|
|
2453
|
+
type: "text",
|
|
2454
|
+
text: "Cancelled."
|
|
2455
|
+
});
|
|
2456
|
+
};
|
|
2166
2457
|
for (let iter = 0; iter < MAX_TOOL_ITERATIONS; iter++) {
|
|
2458
|
+
if (isAborted(input.signal)) {
|
|
2459
|
+
markCancelled();
|
|
2460
|
+
break;
|
|
2461
|
+
}
|
|
2167
2462
|
input.refreshTools?.();
|
|
2168
2463
|
const tools = input.broker.listDefinitions(input.role);
|
|
2169
2464
|
let pendingTools = [];
|
|
@@ -2175,6 +2470,7 @@ async function runSpecialistLoop(input) {
|
|
|
2175
2470
|
providerOptions: input.resolved.providerOptions,
|
|
2176
2471
|
signal: input.signal
|
|
2177
2472
|
})) {
|
|
2473
|
+
if (isAborted(input.signal)) break;
|
|
2178
2474
|
if (chunk.type === "error") {
|
|
2179
2475
|
emit({
|
|
2180
2476
|
type: "text",
|
|
@@ -2183,6 +2479,10 @@ async function runSpecialistLoop(input) {
|
|
|
2183
2479
|
success = false;
|
|
2184
2480
|
break;
|
|
2185
2481
|
}
|
|
2482
|
+
if (chunk.type === "usage" && chunk.usage) {
|
|
2483
|
+
usage.inputTokens += chunk.usage.inputTokens;
|
|
2484
|
+
usage.outputTokens += chunk.usage.outputTokens;
|
|
2485
|
+
}
|
|
2186
2486
|
if (chunk.type === "reasoning" && chunk.text) emit({
|
|
2187
2487
|
type: "reasoning",
|
|
2188
2488
|
text: chunk.text,
|
|
@@ -2203,6 +2503,10 @@ async function runSpecialistLoop(input) {
|
|
|
2203
2503
|
} else pendingTools.push({ ...chunk.toolCall });
|
|
2204
2504
|
}
|
|
2205
2505
|
}
|
|
2506
|
+
if (isAborted(input.signal)) {
|
|
2507
|
+
markCancelled();
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2206
2510
|
if (!pendingTools.length) {
|
|
2207
2511
|
if (assistantText && !parts.some((p) => p.type === "text")) emit({
|
|
2208
2512
|
type: "text",
|
|
@@ -2229,7 +2533,12 @@ async function runSpecialistLoop(input) {
|
|
|
2229
2533
|
})),
|
|
2230
2534
|
signal: input.signal
|
|
2231
2535
|
}) : null;
|
|
2536
|
+
const pendingImages = [];
|
|
2232
2537
|
for (const tool of pendingTools) {
|
|
2538
|
+
if (isAborted(input.signal)) {
|
|
2539
|
+
markCancelled();
|
|
2540
|
+
break;
|
|
2541
|
+
}
|
|
2233
2542
|
let args = {};
|
|
2234
2543
|
try {
|
|
2235
2544
|
args = tool.arguments ? JSON.parse(tool.arguments) : {};
|
|
@@ -2267,26 +2576,30 @@ async function runSpecialistLoop(input) {
|
|
|
2267
2576
|
name: tool.name,
|
|
2268
2577
|
content: JSON.stringify(presentImage ? {
|
|
2269
2578
|
...safeOutput ?? {},
|
|
2270
|
-
note: "Image bytes follow
|
|
2579
|
+
note: "Image bytes follow after all tool results for vision inspection."
|
|
2271
2580
|
} : safeOutput)
|
|
2272
2581
|
});
|
|
2273
|
-
if (presentImage) {
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
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
|
-
}
|
|
2582
|
+
if (presentImage) pendingImages.push({
|
|
2583
|
+
id: String(rawOutput.id ?? ""),
|
|
2584
|
+
name: String(rawOutput.name ?? "image"),
|
|
2585
|
+
dataUrl: String(rawOutput.dataUrl)
|
|
2586
|
+
});
|
|
2288
2587
|
if (!result.ok) success = false;
|
|
2289
2588
|
}
|
|
2589
|
+
if (cancelled || isAborted(input.signal)) {
|
|
2590
|
+
markCancelled();
|
|
2591
|
+
break;
|
|
2592
|
+
}
|
|
2593
|
+
if (pendingImages.length) messages.push({
|
|
2594
|
+
role: "user",
|
|
2595
|
+
content: [{
|
|
2596
|
+
type: "text",
|
|
2597
|
+
text: pendingImages.length === 1 ? `Attachment image for inspection: id=${pendingImages[0].id} name="${pendingImages[0].name}"` : `Attachment images for inspection:\n${pendingImages.map((img) => `- id=${img.id} name="${img.name}"`).join("\n")}`
|
|
2598
|
+
}, ...pendingImages.map((img) => ({
|
|
2599
|
+
type: "image_url",
|
|
2600
|
+
image_url: { url: img.dataUrl }
|
|
2601
|
+
}))]
|
|
2602
|
+
});
|
|
2290
2603
|
}
|
|
2291
2604
|
return {
|
|
2292
2605
|
message: {
|
|
@@ -2295,7 +2608,8 @@ async function runSpecialistLoop(input) {
|
|
|
2295
2608
|
parts: coalesceParts(parts),
|
|
2296
2609
|
createdAt: Date.now(),
|
|
2297
2610
|
routedModel: input.resolved.model,
|
|
2298
|
-
effort: input.resolved.effort
|
|
2611
|
+
effort: input.resolved.effort,
|
|
2612
|
+
...usage.inputTokens > 0 || usage.outputTokens > 0 ? { usage } : {}
|
|
2299
2613
|
},
|
|
2300
2614
|
success
|
|
2301
2615
|
};
|
|
@@ -2325,7 +2639,14 @@ function coalesceParts(parts) {
|
|
|
2325
2639
|
//#endregion
|
|
2326
2640
|
//#region src/runtime/specialists/index.ts
|
|
2327
2641
|
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.
|
|
2642
|
+
const preamble = `You are a specialist in the Eclipse Docks AI runtime. Prefer tools for file and command actions. Be concise. After tools finish, write a short human-friendly final reply (what you did and the outcome). Do not paste tool arguments, stdout, JSON, or command logs — those already appear in the UI details panel. ${opts.hasAttachments ? [
|
|
2643
|
+
"This turn publishes session attachments under the shared in-memory root (in-memory/<sessionId>/… or in-memory://<sessionId>/…).",
|
|
2644
|
+
"Read them with workspace_read / workspace_list — in-memory paths work without mounting that root into the workspace tree.",
|
|
2645
|
+
"Do not call load_workspace / connect_opfs / connect_indexeddb to find attachments.",
|
|
2646
|
+
"Canvas from a screenshot: skills.load eclipse-docks-canvas → workspace_read the attachment path → workspace_write a .canvas.js (into a durable workspace folder) → canvas.open / canvas.apply_source.",
|
|
2647
|
+
"Qualify durable writes as <root>/<file> using a root from workspace_list (e.g. tests/demo.canvas.js); bare filenames are auto-rooted when a single durable folder is connected, otherwise a folder picker appears.",
|
|
2648
|
+
"If no durable workspace folder is connected for writing canvases, say so and stop."
|
|
2649
|
+
].join(" ") : "Session attachments live under the shared in-memory root and are readable via workspace_read / workspace_list."} Workspace paths use a connected root prefix from workspace_list (never write bare names at the composite root unless a single durable folder can auto-qualify them). For specialized workflows (e.g. canvas.js), call skills.list to discover registered skills, then skills.load with the matching name before following those instructions.`;
|
|
2329
2650
|
switch (role) {
|
|
2330
2651
|
case "plan": return `${preamble}\nMode: PLAN (read-only). Analyze and propose a plan for: ${goal}`;
|
|
2331
2652
|
case "explore": return `${preamble}\nMode: EXPLORE (read-only). Search and report findings for: ${goal}`;
|
|
@@ -2417,6 +2738,13 @@ function estimateSessionContextUsage(session, modelRef) {
|
|
|
2417
2738
|
}
|
|
2418
2739
|
const used = buckets.reduce((sum, b) => sum + b.tokens, 0);
|
|
2419
2740
|
const percent = limit > 0 ? Math.min(100, used / limit * 100) : 0;
|
|
2741
|
+
let inputTokens = 0;
|
|
2742
|
+
let outputTokens = 0;
|
|
2743
|
+
for (const message of session?.history ?? []) {
|
|
2744
|
+
if (!message.usage) continue;
|
|
2745
|
+
inputTokens += message.usage.inputTokens;
|
|
2746
|
+
outputTokens += message.usage.outputTokens;
|
|
2747
|
+
}
|
|
2420
2748
|
return {
|
|
2421
2749
|
used,
|
|
2422
2750
|
limit,
|
|
@@ -2424,7 +2752,9 @@ function estimateSessionContextUsage(session, modelRef) {
|
|
|
2424
2752
|
remaining: Math.max(0, limit - used),
|
|
2425
2753
|
level: contextUsageLevel(percent),
|
|
2426
2754
|
buckets: buckets.filter((b) => b.tokens > 0),
|
|
2427
|
-
modelRef
|
|
2755
|
+
modelRef,
|
|
2756
|
+
inputTokens,
|
|
2757
|
+
outputTokens
|
|
2428
2758
|
};
|
|
2429
2759
|
}
|
|
2430
2760
|
function formatTokenCount(n) {
|
|
@@ -2453,6 +2783,184 @@ function formatBucketDetail(bucket) {
|
|
|
2453
2783
|
return `${bucket.count} · ${tokens}`;
|
|
2454
2784
|
}
|
|
2455
2785
|
//#endregion
|
|
2786
|
+
//#region src/runtime/session-workspace.ts
|
|
2787
|
+
function attachmentContents(stored) {
|
|
2788
|
+
if (stored.kind === "image") {
|
|
2789
|
+
if (stored.dataUrl) return stored.dataUrl;
|
|
2790
|
+
if (stored.text) return stored.text;
|
|
2791
|
+
}
|
|
2792
|
+
return stored.text ?? "";
|
|
2793
|
+
}
|
|
2794
|
+
/** Build live-file callbacks that always re-read via the caller-provided resolver. */
|
|
2795
|
+
function attachmentLiveContents(resolve) {
|
|
2796
|
+
return {
|
|
2797
|
+
get mimeType() {
|
|
2798
|
+
return resolve()?.mime;
|
|
2799
|
+
},
|
|
2800
|
+
getContents() {
|
|
2801
|
+
const stored = resolve();
|
|
2802
|
+
if (!stored) throw new Error("Attachment payload is no longer available");
|
|
2803
|
+
return attachmentContents(stored);
|
|
2804
|
+
},
|
|
2805
|
+
size() {
|
|
2806
|
+
const stored = resolve();
|
|
2807
|
+
if (!stored) return null;
|
|
2808
|
+
if (stored.byteLength != null) return stored.byteLength;
|
|
2809
|
+
return attachmentContents(stored).length;
|
|
2810
|
+
}
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
/** Relative path under the shared in-memory root (`<sessionId>/<file>`). */
|
|
2814
|
+
function relativeAttachmentPath(sessionId, stored) {
|
|
2815
|
+
return attachmentWorkspacePath(sessionId, stored).slice(`${IN_MEMORY_ROOT_NAME}/`.length);
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Publish a live file node. Payload is resolved only through `resolve` — nothing is copied.
|
|
2819
|
+
*/
|
|
2820
|
+
async function syncAttachmentToWorkspace(sessionId, stored, resolve) {
|
|
2821
|
+
const root = getInMemoryRoot();
|
|
2822
|
+
const relative = relativeAttachmentPath(sessionId, stored);
|
|
2823
|
+
root.writeLiveFile(relative, attachmentLiveContents(resolve));
|
|
2824
|
+
return attachmentWorkspacePath(sessionId, stored);
|
|
2825
|
+
}
|
|
2826
|
+
async function syncAttachmentsToWorkspace(sessionId, attachments, resolveById) {
|
|
2827
|
+
for (const attachment of attachments) await syncAttachmentToWorkspace(sessionId, attachment, () => resolveById(attachment.id));
|
|
2828
|
+
}
|
|
2829
|
+
async function removeAttachmentFromWorkspace(sessionId, stored) {
|
|
2830
|
+
const root = getInMemoryRoot();
|
|
2831
|
+
const relative = relativeAttachmentPath(sessionId, stored);
|
|
2832
|
+
const resource = await root.getResource(relative);
|
|
2833
|
+
if (resource) await resource.delete();
|
|
2834
|
+
}
|
|
2835
|
+
async function removeSessionWorkspaceFolder(sessionId) {
|
|
2836
|
+
const dir = await getInMemoryRoot().getResource(sessionId);
|
|
2837
|
+
if (dir instanceof Directory) await dir.delete();
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Restrict the workspace-mounted in-memory view to a single folder name (active session).
|
|
2841
|
+
* Pass `null` to show the full tree again.
|
|
2842
|
+
*/
|
|
2843
|
+
function projectInMemoryMountToFolder(folderName) {
|
|
2844
|
+
if (!folderName) {
|
|
2845
|
+
setInMemoryMountProjection(null);
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
const name = folderName;
|
|
2849
|
+
setInMemoryMountProjection({
|
|
2850
|
+
async listChildren(root) {
|
|
2851
|
+
return [root.ensureDirectoryEntry(name)];
|
|
2852
|
+
},
|
|
2853
|
+
async getResource(root, path, options) {
|
|
2854
|
+
const segments = path.split("/").filter((s) => s.trim());
|
|
2855
|
+
if (segments[0] !== name) return null;
|
|
2856
|
+
if (segments.length === 1 && (path.endsWith("/") || path === name)) return root.ensureDirectoryEntry(name);
|
|
2857
|
+
return root.getResource(path, options);
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
//#endregion
|
|
2862
|
+
//#region src/runtime/skills-parse.ts
|
|
2863
|
+
/**
|
|
2864
|
+
* Parse a SKILL.md document (YAML frontmatter + markdown body).
|
|
2865
|
+
* Compatible with Cursor Agent Skills layout.
|
|
2866
|
+
*/
|
|
2867
|
+
function parseSkillMarkdown(raw) {
|
|
2868
|
+
const text = raw.replace(/^\uFEFF/, "");
|
|
2869
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
2870
|
+
if (!match) return {
|
|
2871
|
+
name: "",
|
|
2872
|
+
description: "",
|
|
2873
|
+
body: text.trim()
|
|
2874
|
+
};
|
|
2875
|
+
const front = match[1] ?? "";
|
|
2876
|
+
const body = (match[2] ?? "").trim();
|
|
2877
|
+
return {
|
|
2878
|
+
name: readFrontmatterString(front, "name"),
|
|
2879
|
+
description: readFrontmatterString(front, "description"),
|
|
2880
|
+
body
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
/** Short ambient prompt block (uses skill description). */
|
|
2884
|
+
function formatSkillForPrompt(skill) {
|
|
2885
|
+
const description = skill.description.trim();
|
|
2886
|
+
if (!description) return skill.body.trim();
|
|
2887
|
+
return description;
|
|
2888
|
+
}
|
|
2889
|
+
function readFrontmatterString(front, key) {
|
|
2890
|
+
const folded = front.match(new RegExp(`^${key}:\\s*>-\\s*\\r?\\n((?:[ \\t]+.*\\r?\\n?)*)`, "m"));
|
|
2891
|
+
if (folded?.[1]) return folded[1].split(/\r?\n/).map((line) => line.replace(/^[ \t]+/, "").trimEnd()).filter((line, index, all) => line.length > 0 || index < all.length - 1).join(" ").replace(/\s+/g, " ").trim();
|
|
2892
|
+
const quoted = front.match(new RegExp(`^${key}:\\s*[\"']([\\s\\S]*?)[\"']\\s*$`, "m"));
|
|
2893
|
+
if (quoted?.[1]) return quoted[1].replace(/\s+/g, " ").trim();
|
|
2894
|
+
return front.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim() ?? "";
|
|
2895
|
+
}
|
|
2896
|
+
//#endregion
|
|
2897
|
+
//#region src/runtime/skills-types.ts
|
|
2898
|
+
function isSkillContribution(value) {
|
|
2899
|
+
if (!value || typeof value !== "object") return false;
|
|
2900
|
+
const skill = value;
|
|
2901
|
+
const hasBody = typeof skill.body === "string";
|
|
2902
|
+
const hasLoad = typeof skill.load === "function";
|
|
2903
|
+
return typeof skill.name === "string" && skill.name.length > 0 && typeof skill.label === "string" && (hasBody || hasLoad);
|
|
2904
|
+
}
|
|
2905
|
+
//#endregion
|
|
2906
|
+
//#region src/runtime/skills-resolve.ts
|
|
2907
|
+
function rawFromLoadResult(result) {
|
|
2908
|
+
if (typeof result === "string") return result;
|
|
2909
|
+
return typeof result?.default === "string" ? result.default : "";
|
|
2910
|
+
}
|
|
2911
|
+
async function readSkillRaw(skill) {
|
|
2912
|
+
if (typeof skill.body === "string" && skill.body.trim()) return skill.body.replace(/^\uFEFF/, "");
|
|
2913
|
+
if (!skill.load) return "";
|
|
2914
|
+
return rawFromLoadResult(await skill.load()).replace(/^\uFEFF/, "");
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Resolve SKILL.md (eager `body` or lazy `load`) into name/description/body.
|
|
2918
|
+
* Frontmatter wins for description unless the contribution sets an explicit override.
|
|
2919
|
+
*/
|
|
2920
|
+
async function resolveSkill(skill) {
|
|
2921
|
+
const raw = (await readSkillRaw(skill)).trim();
|
|
2922
|
+
const parsed = raw ? parseSkillMarkdown(raw) : {
|
|
2923
|
+
name: "",
|
|
2924
|
+
description: "",
|
|
2925
|
+
body: ""
|
|
2926
|
+
};
|
|
2927
|
+
return {
|
|
2928
|
+
name: skill.name || parsed.name,
|
|
2929
|
+
label: skill.label || parsed.name || skill.name,
|
|
2930
|
+
description: (skill.description ?? parsed.description).trim(),
|
|
2931
|
+
body: parsed.body.trim() || (raw && !parsed.description && !parsed.name ? raw : "")
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
/** @deprecated Prefer {@link resolveSkill}; kept for callers that only need the body. */
|
|
2935
|
+
async function resolveSkillBody(skill) {
|
|
2936
|
+
return (await resolveSkill(skill)).body;
|
|
2937
|
+
}
|
|
2938
|
+
//#endregion
|
|
2939
|
+
//#region src/runtime/skills.ts
|
|
2940
|
+
/** Contribution slot for AI agent skills (Cursor-compatible SKILL.md content). */
|
|
2941
|
+
var SYSTEM_SKILLS = "system.skills";
|
|
2942
|
+
function listSkillContributions() {
|
|
2943
|
+
return contributionRegistry.getContributions(SYSTEM_SKILLS).filter(isSkillContribution);
|
|
2944
|
+
}
|
|
2945
|
+
/** Register a SKILL.md document on {@link SYSTEM_SKILLS}. Returns an unregister function. */
|
|
2946
|
+
function registerSkillMarkdown(raw, opts) {
|
|
2947
|
+
const parsed = parseSkillMarkdown(raw);
|
|
2948
|
+
const name = (opts?.name || parsed.name || `skill-${Date.now()}`).trim();
|
|
2949
|
+
const contribution = {
|
|
2950
|
+
name,
|
|
2951
|
+
label: opts?.label || parsed.name || name,
|
|
2952
|
+
body: (opts?.body || raw).trim(),
|
|
2953
|
+
ranking: opts?.ranking,
|
|
2954
|
+
icon: opts?.icon,
|
|
2955
|
+
relevant: opts?.relevant,
|
|
2956
|
+
load: opts?.load,
|
|
2957
|
+
description: opts?.description
|
|
2958
|
+
};
|
|
2959
|
+
if (!contribution.body && !contribution.load) return () => void 0;
|
|
2960
|
+
contributionRegistry.registerContribution(SYSTEM_SKILLS, contribution);
|
|
2961
|
+
return () => contributionRegistry.unregisterContribution(SYSTEM_SKILLS, name);
|
|
2962
|
+
}
|
|
2963
|
+
//#endregion
|
|
2456
2964
|
//#region src/runtime/cognitive-runtime.ts
|
|
2457
2965
|
var PERSIST_KEY = "cognitive/v1/state";
|
|
2458
2966
|
/** Auto-compact after a turn when live history exceeds this many messages. */
|
|
@@ -2487,12 +2995,7 @@ var CognitiveRuntime = class {
|
|
|
2487
2995
|
emit: false
|
|
2488
2996
|
});
|
|
2489
2997
|
registerWorkspaceTools(this.tools);
|
|
2490
|
-
|
|
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
|
-
});
|
|
2998
|
+
registerFetchTools(this.tools);
|
|
2496
2999
|
this.tools.setApprovalMode(this.approvalMode);
|
|
2497
3000
|
this.tools.setPermissionHandler(async (req) => this.requestPermission(req));
|
|
2498
3001
|
this.ready = this.hydrateAll();
|
|
@@ -2548,13 +3051,23 @@ var CognitiveRuntime = class {
|
|
|
2548
3051
|
this.promptEnhancers = this.promptEnhancers.filter((e) => e !== enhancer);
|
|
2549
3052
|
};
|
|
2550
3053
|
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Register a Cursor-compatible SKILL.md document on {@link SYSTEM_SKILLS}.
|
|
3056
|
+
* Relevant skills are selected into the specialist prompt each turn.
|
|
3057
|
+
*/
|
|
3058
|
+
addSkillMarkdown(raw) {
|
|
3059
|
+
return registerSkillMarkdown(raw);
|
|
3060
|
+
}
|
|
2551
3061
|
/** First catalog model for the default provider — used by Manual routing. */
|
|
2552
3062
|
defaultModelId() {
|
|
2553
3063
|
const catalog = catalogFromSettings(this.providersSettings);
|
|
2554
3064
|
return catalog.find((e) => e.providerId === this.providersSettings.defaultProviderId)?.id ?? catalog[0]?.id ?? "openai/gpt-4o-mini";
|
|
2555
3065
|
}
|
|
2556
3066
|
setModelPolicy(policy) {
|
|
2557
|
-
|
|
3067
|
+
const next = normalizeModelPolicy(policy) ?? this.policy;
|
|
3068
|
+
if (sameModelPolicy(this.policy, next)) return;
|
|
3069
|
+
this.policy = next;
|
|
3070
|
+
this.persist();
|
|
2558
3071
|
}
|
|
2559
3072
|
getModelPolicy() {
|
|
2560
3073
|
return this.policy;
|
|
@@ -2616,28 +3129,46 @@ var CognitiveRuntime = class {
|
|
|
2616
3129
|
if (focused) return focused.id;
|
|
2617
3130
|
return this.sessions.createSession().id;
|
|
2618
3131
|
}
|
|
2619
|
-
/** Store attachment payloads on the session
|
|
2620
|
-
addAttachments(sessionId, attachments) {
|
|
3132
|
+
/** Store attachment payloads on the session and mirror them into in-memory/<sessionId>/. */
|
|
3133
|
+
async addAttachments(sessionId, attachments) {
|
|
2621
3134
|
const id = this.ensureSessionId(sessionId);
|
|
2622
3135
|
const session = this.sessions.get(id);
|
|
2623
3136
|
if (!session || !attachments.length) return [];
|
|
2624
3137
|
const room = Math.max(0, 8 - (session.attachments?.length ?? 0));
|
|
2625
|
-
const accepted = attachments.slice(0, room).map((attachment) =>
|
|
3138
|
+
const accepted = attachments.slice(0, room).map((attachment) => {
|
|
3139
|
+
const stored = toSessionAttachment(attachment);
|
|
3140
|
+
return {
|
|
3141
|
+
...stored,
|
|
3142
|
+
path: attachmentUri(id, stored)
|
|
3143
|
+
};
|
|
3144
|
+
});
|
|
2626
3145
|
if (!accepted.length) return [];
|
|
2627
3146
|
this.sessions.update(id, { attachments: [...session.attachments ?? [], ...accepted] });
|
|
2628
3147
|
this.commitSessions();
|
|
3148
|
+
await this.mirrorAttachments(id, accepted).catch(() => void 0);
|
|
2629
3149
|
return accepted;
|
|
2630
3150
|
}
|
|
2631
|
-
/** Remove an attachment payload from the session store. */
|
|
3151
|
+
/** Remove an attachment payload from the session store and in-memory workspace. */
|
|
2632
3152
|
removeAttachment(sessionId, attachmentId) {
|
|
2633
3153
|
const session = this.sessions.get(sessionId);
|
|
2634
3154
|
if (!session) return false;
|
|
3155
|
+
const removed = (session.attachments ?? []).find((a) => a.id === attachmentId);
|
|
2635
3156
|
const next = (session.attachments ?? []).filter((a) => a.id !== attachmentId);
|
|
2636
3157
|
if (next.length === (session.attachments?.length ?? 0)) return false;
|
|
2637
3158
|
this.sessions.update(sessionId, { attachments: next });
|
|
2638
3159
|
this.commitSessions();
|
|
3160
|
+
if (removed) removeAttachmentFromWorkspace(sessionId, removed);
|
|
2639
3161
|
return true;
|
|
2640
3162
|
}
|
|
3163
|
+
/** Drop a session and its in-memory attachment folder. */
|
|
3164
|
+
removeSession(sessionId) {
|
|
3165
|
+
const ok = this.sessions.remove(sessionId);
|
|
3166
|
+
if (ok) {
|
|
3167
|
+
removeSessionWorkspaceFolder(sessionId);
|
|
3168
|
+
this.commitSessions();
|
|
3169
|
+
}
|
|
3170
|
+
return ok;
|
|
3171
|
+
}
|
|
2641
3172
|
/**
|
|
2642
3173
|
* Compact a session: drop older turns, store a summary (LLM when possible),
|
|
2643
3174
|
* and persist. The summary is injected into later model calls.
|
|
@@ -2681,8 +3212,17 @@ var CognitiveRuntime = class {
|
|
|
2681
3212
|
const session = (input.sessionId ? this.sessions.get(input.sessionId) : this.sessions.focused()) ?? this.sessions.createSession();
|
|
2682
3213
|
const text = input.text.trim();
|
|
2683
3214
|
const messageId = `msg-${Date.now()}-u`;
|
|
2684
|
-
const fresh = (input.attachments ?? []).slice(0, 8).map((attachment) =>
|
|
2685
|
-
|
|
3215
|
+
const fresh = (input.attachments ?? []).slice(0, 8).map((attachment) => {
|
|
3216
|
+
const stored = toSessionAttachment(attachment, { messageId });
|
|
3217
|
+
return {
|
|
3218
|
+
...stored,
|
|
3219
|
+
path: attachmentUri(session.id, stored)
|
|
3220
|
+
};
|
|
3221
|
+
});
|
|
3222
|
+
if (fresh.length) {
|
|
3223
|
+
this.sessions.update(session.id, { attachments: [...this.sessions.get(session.id)?.attachments ?? [], ...fresh] });
|
|
3224
|
+
await this.mirrorAttachments(session.id, fresh);
|
|
3225
|
+
}
|
|
2686
3226
|
const existingIds = (input.attachmentIds ?? []).filter(Boolean);
|
|
2687
3227
|
const stored = this.sessions.get(session.id)?.attachments ?? [];
|
|
2688
3228
|
const byId = new Map(stored.map((a) => [a.id, a]));
|
|
@@ -2693,12 +3233,16 @@ var CognitiveRuntime = class {
|
|
|
2693
3233
|
if (found && !published.some((p) => p.id === found.id)) published.push(found);
|
|
2694
3234
|
}
|
|
2695
3235
|
if (!text && !published.length) throw new Error("Prompt requires text or at least one attachment");
|
|
3236
|
+
if (published.length) await this.mirrorAttachments(session.id, published).catch(() => void 0);
|
|
2696
3237
|
const parts = [];
|
|
2697
3238
|
if (text) parts.push({
|
|
2698
3239
|
type: "text",
|
|
2699
3240
|
text
|
|
2700
3241
|
});
|
|
2701
|
-
for (const item of published) parts.push(toFileRefPart(
|
|
3242
|
+
for (const item of published) parts.push(toFileRefPart({
|
|
3243
|
+
...item,
|
|
3244
|
+
path: item.path || attachmentUri(session.id, item)
|
|
3245
|
+
}));
|
|
2702
3246
|
const promptSummary = summarizePrompt(text, published.map((a) => ({
|
|
2703
3247
|
name: a.name,
|
|
2704
3248
|
kind: a.kind
|
|
@@ -2736,6 +3280,7 @@ var CognitiveRuntime = class {
|
|
|
2736
3280
|
buffers: tick.buffers,
|
|
2737
3281
|
status: "running"
|
|
2738
3282
|
});
|
|
3283
|
+
this.emitSessions();
|
|
2739
3284
|
this.events.publish(TOPIC.coreTick, {
|
|
2740
3285
|
valenceDelta: tick.valenceDelta,
|
|
2741
3286
|
intent: tick.intent,
|
|
@@ -2802,7 +3347,7 @@ var CognitiveRuntime = class {
|
|
|
2802
3347
|
sessionId: session.id,
|
|
2803
3348
|
activation: .7
|
|
2804
3349
|
});
|
|
2805
|
-
if ((this.sessions.get(session.id)?.history.length ?? 0) > AUTO_COMPACT_AFTER) await this.compactSession(session.id, COMPACT_KEEP_COUNT, input.signal);
|
|
3350
|
+
if (!Boolean(input.signal?.aborted) && (this.sessions.get(session.id)?.history.length ?? 0) > AUTO_COMPACT_AFTER) await this.compactSession(session.id, COMPACT_KEEP_COUNT, input.signal);
|
|
2806
3351
|
this.sessions.update(session.id, { status: "idle" });
|
|
2807
3352
|
this.events.publish(TOPIC.messageDone, {
|
|
2808
3353
|
sessionId: session.id,
|
|
@@ -2863,11 +3408,17 @@ var CognitiveRuntime = class {
|
|
|
2863
3408
|
});
|
|
2864
3409
|
}
|
|
2865
3410
|
emitSessions() {
|
|
3411
|
+
const focusedId = this.sessions.focused()?.id ?? null;
|
|
3412
|
+
projectInMemoryMountToFolder(focusedId);
|
|
2866
3413
|
this.events.publish(TOPIC.sessionChanged, {
|
|
2867
3414
|
sessions: this.sessions.list(),
|
|
2868
|
-
focusedId
|
|
3415
|
+
focusedId
|
|
2869
3416
|
});
|
|
2870
3417
|
}
|
|
3418
|
+
/** Live in-memory file refs that resolve payloads from the session store. */
|
|
3419
|
+
mirrorAttachments(sessionId, attachments) {
|
|
3420
|
+
return syncAttachmentsToWorkspace(sessionId, attachments, (attachmentId) => this.sessions.get(sessionId)?.attachments?.find((a) => a.id === attachmentId));
|
|
3421
|
+
}
|
|
2871
3422
|
async hydrateAll() {
|
|
2872
3423
|
await loadModelsCatalog().catch(() => void 0);
|
|
2873
3424
|
await this.hydrateProviders();
|
|
@@ -2879,6 +3430,16 @@ var CognitiveRuntime = class {
|
|
|
2879
3430
|
if (!this.sessions.list().length) this.sessions.createSession({ title: "Chat" });
|
|
2880
3431
|
const mostRecent = this.sessions.list()[0];
|
|
2881
3432
|
if (mostRecent) this.sessions.focus(mostRecent.id);
|
|
3433
|
+
for (const session of this.sessions.list()) {
|
|
3434
|
+
const list = session.attachments ?? [];
|
|
3435
|
+
if (!list.length) continue;
|
|
3436
|
+
const stamped = list.map((a) => a.path ? a : {
|
|
3437
|
+
...a,
|
|
3438
|
+
path: attachmentUri(session.id, a)
|
|
3439
|
+
});
|
|
3440
|
+
if (stamped.some((a, i) => a.path !== list[i]?.path)) this.sessions.update(session.id, { attachments: stamped });
|
|
3441
|
+
await this.mirrorAttachments(session.id, stamped).catch(() => void 0);
|
|
3442
|
+
}
|
|
2882
3443
|
this.emitSessions();
|
|
2883
3444
|
}
|
|
2884
3445
|
async hydrateProviders() {
|
|
@@ -2906,6 +3467,8 @@ var CognitiveRuntime = class {
|
|
|
2906
3467
|
this.approvalMode = saved.approvalMode;
|
|
2907
3468
|
this.tools.setApprovalMode(saved.approvalMode);
|
|
2908
3469
|
}
|
|
3470
|
+
const policy = normalizeModelPolicy(saved.modelPolicy);
|
|
3471
|
+
if (policy) this.policy = policy;
|
|
2909
3472
|
} catch {}
|
|
2910
3473
|
}
|
|
2911
3474
|
async persist() {
|
|
@@ -2913,7 +3476,8 @@ var CognitiveRuntime = class {
|
|
|
2913
3476
|
await persistenceService.persistObject(PERSIST_KEY, {
|
|
2914
3477
|
sessions: this.sessions.snapshot(),
|
|
2915
3478
|
memory: this.memory.snapshot(),
|
|
2916
|
-
approvalMode: this.approvalMode
|
|
3479
|
+
approvalMode: this.approvalMode,
|
|
3480
|
+
modelPolicy: this.policy
|
|
2917
3481
|
});
|
|
2918
3482
|
} catch {}
|
|
2919
3483
|
}
|
|
@@ -2921,6 +3485,32 @@ var CognitiveRuntime = class {
|
|
|
2921
3485
|
function isToolApprovalMode(value) {
|
|
2922
3486
|
return value === "manual" || value === "auto_safe" || value === "auto_all";
|
|
2923
3487
|
}
|
|
3488
|
+
function isOptimizeFor(value) {
|
|
3489
|
+
return value === "cost" || value === "balance" || value === "intelligence";
|
|
3490
|
+
}
|
|
3491
|
+
function isEffortOrAuto(value) {
|
|
3492
|
+
return value === "auto" || value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
|
|
3493
|
+
}
|
|
3494
|
+
function normalizeModelPolicy(value) {
|
|
3495
|
+
if (!value || typeof value !== "object") return null;
|
|
3496
|
+
const raw = value;
|
|
3497
|
+
if (raw.mode === "auto" && isOptimizeFor(raw.optimizeFor)) return {
|
|
3498
|
+
mode: "auto",
|
|
3499
|
+
optimizeFor: raw.optimizeFor
|
|
3500
|
+
};
|
|
3501
|
+
if (raw.mode === "manual" && typeof raw.model === "string" && raw.model.trim()) return {
|
|
3502
|
+
mode: "manual",
|
|
3503
|
+
model: raw.model.trim(),
|
|
3504
|
+
...isEffortOrAuto(raw.effort) ? { effort: raw.effort } : {}
|
|
3505
|
+
};
|
|
3506
|
+
return null;
|
|
3507
|
+
}
|
|
3508
|
+
function sameModelPolicy(a, b) {
|
|
3509
|
+
if (a.mode !== b.mode) return false;
|
|
3510
|
+
if (a.mode === "auto" && b.mode === "auto") return a.optimizeFor === b.optimizeFor;
|
|
3511
|
+
if (a.mode === "manual" && b.mode === "manual") return a.model === b.model && (a.effort ?? "auto") === (b.effort ?? "auto");
|
|
3512
|
+
return false;
|
|
3513
|
+
}
|
|
2924
3514
|
function externalCatalogEntry(id, providerId) {
|
|
2925
3515
|
return {
|
|
2926
3516
|
id,
|
|
@@ -2947,6 +3537,57 @@ function summarizePrompt(text, attachments) {
|
|
|
2947
3537
|
return `Attached ${attachments.map((a) => a.name).join(", ")}`;
|
|
2948
3538
|
}
|
|
2949
3539
|
//#endregion
|
|
2950
|
-
|
|
3540
|
+
//#region src/runtime/skills-commands.ts
|
|
3541
|
+
/** Register skills.list / skills.load as Docks commands (also exposed as LLM tools). */
|
|
3542
|
+
function registerSkillCommands() {
|
|
3543
|
+
registerAll({
|
|
3544
|
+
command: {
|
|
3545
|
+
id: "skills.list",
|
|
3546
|
+
name: "List skills",
|
|
3547
|
+
description: "List registered agent skills (name + description from SKILL.md). Call skills.load with a skill name to read the full instructions.",
|
|
3548
|
+
parameters: []
|
|
3549
|
+
},
|
|
3550
|
+
handler: { execute: async () => Promise.all(listSkillContributions().map(async (skill) => {
|
|
3551
|
+
const resolved = await resolveSkill(skill);
|
|
3552
|
+
return {
|
|
3553
|
+
name: resolved.name,
|
|
3554
|
+
label: resolved.label,
|
|
3555
|
+
description: resolved.description
|
|
3556
|
+
};
|
|
3557
|
+
})) }
|
|
3558
|
+
});
|
|
3559
|
+
registerAll({
|
|
3560
|
+
command: {
|
|
3561
|
+
id: "skills.load",
|
|
3562
|
+
name: "Load skill",
|
|
3563
|
+
description: "Load the full body of a registered agent skill by name. Prefer skills returned by skills.list.",
|
|
3564
|
+
parameters: [{
|
|
3565
|
+
name: "name",
|
|
3566
|
+
description: "Skill name (e.g. eclipse-docks-canvas)",
|
|
3567
|
+
required: true
|
|
3568
|
+
}]
|
|
3569
|
+
},
|
|
3570
|
+
handler: { execute: async (context) => {
|
|
3571
|
+
const name = String(context.params?.name ?? "").trim();
|
|
3572
|
+
if (!name) return {
|
|
3573
|
+
error: "Missing skill name",
|
|
3574
|
+
available: listSkillContributions().map((skill) => skill.name)
|
|
3575
|
+
};
|
|
3576
|
+
const skill = listSkillContributions().find((entry) => entry.name === name);
|
|
3577
|
+
if (!skill) return {
|
|
3578
|
+
error: `Unknown skill: ${name}`,
|
|
3579
|
+
available: listSkillContributions().map((entry) => entry.name)
|
|
3580
|
+
};
|
|
3581
|
+
const resolved = await resolveSkill(skill);
|
|
3582
|
+
if (!resolved.body) return {
|
|
3583
|
+
error: `Skill ${name} has no body`,
|
|
3584
|
+
name: resolved.name
|
|
3585
|
+
};
|
|
3586
|
+
return resolved;
|
|
3587
|
+
} }
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
//#endregion
|
|
3591
|
+
export { detectEffortCapability as C, AI_VIEW_NAME as D, AI_SETTINGS_KEY as E, OPEN_AI_SETTINGS_COMMAND as O, parseModelIds as S, AI_SETTINGS_EDITOR_ID as T, readPromptAttachmentFromFile as _, listSkillContributions as a, defaultProvidersSettings as b, resolveSkillBody as c, parseSkillMarkdown as d, estimateSessionContextUsage as f, ATTACHMENT_FILE_ACCEPT as g, formatTokenCount as h, SYSTEM_SKILLS as i, TOPIC as k, isSkillContribution as l, formatContextPercent as m, CognitiveRuntime as n, registerSkillMarkdown as o, formatBucketDetail as p, cognitiveRuntime as r, resolveSkill as s, registerSkillCommands as t, formatSkillForPrompt as u, OpenAICompatibleProvider as v, loadModelsCatalog as w, listConfiguredModelIds as x, fetchOpenAICompatibleModels as y };
|
|
2951
3592
|
|
|
2952
|
-
//# sourceMappingURL=runtime-
|
|
3593
|
+
//# sourceMappingURL=runtime-B0IHjzQO.js.map
|