om-memory-system 5.10.1 → 6.1.0-next.237
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 +1 -1
- package/dist/adapters/opencode/auto-capture-host.d.ts +2 -2
- package/dist/adapters/opencode/auto-capture-host.js +5 -15
- package/dist/adapters/opencode/auto-capture-summary.js +9 -6
- package/dist/adapters/opencode/backfill-models.js +1 -1
- package/dist/adapters/opencode/backfill-startup.js +5 -4
- package/dist/adapters/opencode/conversation.d.ts +5 -1
- package/dist/adapters/opencode/conversation.js +23 -23
- package/dist/adapters/opencode/import-command.d.ts +0 -12
- package/dist/adapters/opencode/import-command.js +1 -17
- package/dist/adapters/opencode/internal-capture-sessions.d.ts +0 -3
- package/dist/adapters/opencode/internal-capture-sessions.js +0 -26
- package/dist/adapters/opencode/memory-tool.d.ts +3 -4
- package/dist/adapters/opencode/memory-tool.js +8 -50
- package/dist/adapters/opencode/opencode-host.d.ts +27 -0
- package/dist/adapters/opencode/opencode-host.js +30 -0
- package/dist/adapters/opencode/opencode-import-models.d.ts +2 -2
- package/dist/adapters/opencode/opencode-import-models.js +8 -10
- package/dist/adapters/opencode/opencode-provider.d.ts +23 -62
- package/dist/adapters/opencode/opencode-provider.js +75 -378
- package/dist/adapters/opencode/package-skills.d.ts +1 -6
- package/dist/adapters/opencode/package-skills.js +2 -17
- package/dist/adapters/opencode/profile-learning.d.ts +1 -2
- package/dist/adapters/opencode/profile-learning.js +15 -19
- package/dist/adapters/opencode/profile-llm-client.d.ts +3 -2
- package/dist/adapters/opencode/profile-llm-client.js +8 -17
- package/dist/adapters/opencode/profile-model.d.ts +2 -18
- package/dist/adapters/opencode/profile-model.js +18 -69
- package/dist/adapters/opencode/prompt-model.d.ts +9 -0
- package/dist/adapters/opencode/prompt-model.js +21 -0
- package/dist/adapters/opencode/skills-tool.d.ts +3 -3
- package/dist/adapters/opencode/skills-tool.js +4 -30
- package/dist/adapters/opencode/toast.d.ts +11 -0
- package/dist/adapters/opencode/toast.js +11 -0
- package/dist/adapters/opencode/user-prompt.d.ts +2 -2
- package/dist/adapters/opencode/user-prompt.js +4 -5
- package/dist/config-template.d.ts +1 -1
- package/dist/config-template.js +2 -6
- package/dist/config.d.ts +0 -9
- package/dist/config.js +0 -9
- package/dist/core/retrieval.d.ts +0 -21
- package/dist/core/retrieval.js +0 -29
- package/dist/importer/handled-lookup.d.ts +17 -0
- package/dist/importer/handled-lookup.js +13 -0
- package/dist/importer/importer.js +3 -3
- package/dist/importer/ledger.d.ts +9 -0
- package/dist/importer/ledger.js +21 -0
- package/dist/importer/opencode-reader.d.ts +1 -1
- package/dist/importer/opencode-reader.js +88 -34
- package/dist/importer/opencode-v2-units.d.ts +16 -0
- package/dist/importer/opencode-v2-units.js +93 -0
- package/dist/importer/settings-models.d.ts +11 -6
- package/dist/importer/settings-models.js +15 -11
- package/dist/plugin.d.ts +0 -3
- package/dist/plugin.js +1 -7
- package/dist/services/global-config-writer.js +2 -3
- package/dist/services/settings-snapshot.js +6 -6
- package/dist/services/turso/vector-search.js +4 -2
- package/dist/shared/api/schemas.d.ts +1 -1
- package/dist/utils/memory-limits.d.ts +0 -1
- package/dist/utils/memory-limits.js +0 -1
- package/dist/v2/adapter.d.ts +16 -84
- package/dist/v2/adapter.js +17 -88
- package/dist/v2/memory-bridge.js +1 -1
- package/dist/v2/plugin.js +27 -16
- package/dist/v2/session-events.d.ts +30 -0
- package/dist/v2/session-events.js +130 -0
- package/dist/v2/startup.d.ts +14 -0
- package/dist/v2/startup.js +210 -0
- package/dist/v2/tools.d.ts +106 -0
- package/dist/v2/tools.js +89 -0
- package/dist/web/assets/index-DBSeQOs6.js +171 -0
- package/dist/web/index.html +1 -1
- package/package.json +2 -14
- package/dist/adapters/opencode/in-process-client.d.ts +0 -2
- package/dist/adapters/opencode/in-process-client.js +0 -16
- package/dist/adapters/opencode/opencode-diagnostics.d.ts +0 -7
- package/dist/adapters/opencode/opencode-diagnostics.js +0 -37
- package/dist/adapters/opencode/opencode-host-config.d.ts +0 -13
- package/dist/adapters/opencode/opencode-host-config.js +0 -53
- package/dist/adapters/opencode/opencode-sdk-client.d.ts +0 -6
- package/dist/adapters/opencode/opencode-sdk-client.js +0 -38
- package/dist/index.d.ts +0 -28
- package/dist/index.js +0 -620
- package/dist/v2/legacy-client.d.ts +0 -15
- package/dist/v2/legacy-client.js +0 -275
- package/dist/web/assets/index-DQgI670D.js +0 -171
|
@@ -1,150 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structured output
|
|
2
|
+
* Structured output through the OpenCode V2 plugin context.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Per call we create a transient session, prompt it with a JSON schema,
|
|
11
|
-
* then delete the session so it does not pollute the user's TUI session
|
|
12
|
-
* list.
|
|
13
|
-
*
|
|
14
|
-
* Internal capture sessions are least-privilege (issue #189): ordinary
|
|
15
|
-
* agent tools are denied, only StructuredOutput is allowed, a dedicated
|
|
16
|
-
* agent caps steps, and a hard timeout fails closed.
|
|
17
|
-
*
|
|
18
|
-
* The primary transport is the authenticated v2 SDK client initialized from
|
|
19
|
-
* the plugin host's client configuration. A raw fetch fallback remains for
|
|
20
|
-
* older SDK builds that do not expose the v2 session methods.
|
|
4
|
+
* OpenCode owns the user's provider sign-in, token refresh and routing, so OMMS asks it
|
|
5
|
+
* for one sessionless text generation (`ctx.generate.text`) per call. The generation
|
|
6
|
+
* has no session, agent or tools, so it cannot touch the user's session list or files.
|
|
7
|
+
* The reply must be one JSON object; OMMS parses it and validates it with the caller's
|
|
8
|
+
* schema. A hard timeout fails closed.
|
|
21
9
|
*/
|
|
22
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
23
11
|
import { homedir } from "node:os";
|
|
24
12
|
import { join } from "node:path";
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/** Hard ceiling for a single internal structured-output prompt. */
|
|
13
|
+
import { latestOpencodeSessionModel } from "./opencode-host.js";
|
|
14
|
+
export { getOpencodeHost } from "./opencode-host.js";
|
|
15
|
+
/** Error texts for a reply that holds no usable JSON; capture diagnostics classify them. */
|
|
16
|
+
export const OPENCODE_EMPTY_REPLY = "omms: opencode returned an empty reply";
|
|
17
|
+
export const OPENCODE_NO_JSON_REPLY = "omms: the model did not return a JSON object";
|
|
18
|
+
/** Hard ceiling for a single internal structured-output request. */
|
|
32
19
|
export const STRUCTURED_OUTPUT_TIMEOUT_MS = 90_000;
|
|
33
20
|
let _structuredOutputTimeoutMs = STRUCTURED_OUTPUT_TIMEOUT_MS;
|
|
34
|
-
/** Test helper: override the structured-output
|
|
21
|
+
/** Test helper: override the structured-output timeout. Pass undefined to reset. */
|
|
35
22
|
export function setStructuredOutputTimeoutMsForTests(ms) {
|
|
36
23
|
_structuredOutputTimeoutMs = ms ?? STRUCTURED_OUTPUT_TIMEOUT_MS;
|
|
37
24
|
}
|
|
38
|
-
export const STRUCTURED_OUTPUT_PERMISSIONS = [
|
|
39
|
-
{ permission: "*", pattern: "*", action: "deny" },
|
|
40
|
-
{ permission: "StructuredOutput", pattern: "*", action: "allow" },
|
|
41
|
-
];
|
|
42
|
-
export const STRUCTURED_OUTPUT_TOOLS = {
|
|
43
|
-
"*": false,
|
|
44
|
-
StructuredOutput: true,
|
|
45
|
-
};
|
|
46
|
-
export const STRUCTURED_OUTPUT_METADATA = {
|
|
47
|
-
omms: {
|
|
48
|
-
internal: true,
|
|
49
|
-
purpose: "structured-output",
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
const _internalSessions = new Set();
|
|
53
25
|
let _connectedProviders = new Set();
|
|
54
|
-
let _v2Client;
|
|
55
|
-
let _v2BaseUrl;
|
|
56
|
-
let _hostFetch;
|
|
57
|
-
let _useSdkTransport = false;
|
|
58
|
-
export function setHostFetch(customFetch) {
|
|
59
|
-
_hostFetch = customFetch;
|
|
60
|
-
}
|
|
61
|
-
export function resetHostFetch() {
|
|
62
|
-
_hostFetch = undefined;
|
|
63
|
-
}
|
|
64
|
-
/** Test helper: forget the server URL, as in the native V2 plugin. */
|
|
65
|
-
export function resetV2BaseUrlForTests() {
|
|
66
|
-
_v2BaseUrl = undefined;
|
|
67
|
-
_useSdkTransport = false;
|
|
68
|
-
}
|
|
69
26
|
export function setConnectedProviders(providers) {
|
|
70
27
|
_connectedProviders = new Set(providers);
|
|
71
28
|
}
|
|
72
29
|
export function isProviderConnected(providerName) {
|
|
73
30
|
return _connectedProviders.has(providerName);
|
|
74
31
|
}
|
|
75
|
-
export function setV2Client(client) {
|
|
76
|
-
_v2Client = client;
|
|
77
|
-
}
|
|
78
|
-
export function getV2Client() {
|
|
79
|
-
return _v2Client;
|
|
80
|
-
}
|
|
81
|
-
export function createV2Client(serverUrl, transport) {
|
|
82
|
-
const baseUrl = typeof serverUrl === "string" ? serverUrl : serverUrl.toString();
|
|
83
|
-
const activeTransport = transport ?? (_hostFetch ? { fetch: _hostFetch } : undefined);
|
|
84
|
-
_v2BaseUrl = baseUrl;
|
|
85
|
-
_useSdkTransport = Boolean(activeTransport?.fetch || activeTransport?.headers);
|
|
86
|
-
return createLazyV2Client(baseUrl, activeTransport);
|
|
87
|
-
}
|
|
88
|
-
/** True while an internal structured-output session is live (create → delete). */
|
|
89
|
-
export function isInternalStructuredSession(sessionID) {
|
|
90
|
-
return _internalSessions.has(sessionID);
|
|
91
|
-
}
|
|
92
|
-
/** Test helper: clear tracked internal session IDs. */
|
|
93
|
-
export function resetInternalStructuredSessions() {
|
|
94
|
-
_internalSessions.clear();
|
|
95
|
-
}
|
|
96
|
-
function markInternalSession(sessionID) {
|
|
97
|
-
_internalSessions.add(sessionID);
|
|
98
|
-
}
|
|
99
|
-
function unmarkInternalSession(sessionID) {
|
|
100
|
-
_internalSessions.delete(sessionID);
|
|
101
|
-
}
|
|
102
|
-
function sessionCreateBody() {
|
|
103
|
-
return {
|
|
104
|
-
title: INTERNAL_CAPTURE_SESSION_TITLE,
|
|
105
|
-
permission: STRUCTURED_OUTPUT_PERMISSIONS,
|
|
106
|
-
metadata: STRUCTURED_OUTPUT_METADATA,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function sessionPromptFields(args) {
|
|
110
|
-
return {
|
|
111
|
-
model: { providerID: args.providerID, modelID: args.modelID },
|
|
112
|
-
agent: STRUCTURED_OUTPUT_AGENT,
|
|
113
|
-
system: args.systemPrompt,
|
|
114
|
-
parts: [{ type: "text", text: args.userPrompt }],
|
|
115
|
-
tools: STRUCTURED_OUTPUT_TOOLS,
|
|
116
|
-
// `noReply` suppresses assistant generation in current OpenCode builds,
|
|
117
|
-
// which also suppresses `info.structured_output`; structured capture needs
|
|
118
|
-
// the assistant run even though the temporary session is deleted afterward.
|
|
119
|
-
format: {
|
|
120
|
-
type: "json_schema",
|
|
121
|
-
schema: args.jsonSchema,
|
|
122
|
-
...(args.retryCount !== undefined ? { retryCount: args.retryCount } : {}),
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
function reportReply(onReply, data) {
|
|
127
|
-
if (!onReply)
|
|
128
|
-
return;
|
|
129
|
-
try {
|
|
130
|
-
onReply({
|
|
131
|
-
...(typeof data.info.finish === "string" ? { finish: data.info.finish } : {}),
|
|
132
|
-
partTypes: (Array.isArray(data.parts) ? data.parts : [])
|
|
133
|
-
.map((part) => typeof part === "object" && part !== null ? part.type : undefined)
|
|
134
|
-
.filter((type) => typeof type === "string"),
|
|
135
|
-
structuredOutput: data.info.structured_output ?? data.info.structured,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
// Diagnostics must never change the call's result.
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
32
|
/**
|
|
143
33
|
* Resolve `opencodeModel: "inherit"` to a concrete provider/model.
|
|
144
34
|
*
|
|
145
|
-
* Prefer an explicit prompt-recorded model (auto-capture path)
|
|
146
|
-
*
|
|
147
|
-
* paths don't send the literal model id
|
|
35
|
+
* Prefer an explicit prompt-recorded model (auto-capture path), then the latest
|
|
36
|
+
* model request this plugin saw, then OpenCode's recent model list, so
|
|
37
|
+
* profile-learning / conflict / dedup paths don't send the literal model id
|
|
38
|
+
* "inherit" (ProviderModelNotFoundError).
|
|
148
39
|
*/
|
|
149
40
|
export function resolveOpencodeModelRef(opts) {
|
|
150
41
|
if (opts.modelID !== "inherit") {
|
|
@@ -153,6 +44,10 @@ export function resolveOpencodeModelRef(opts) {
|
|
|
153
44
|
if (opts.prompt?.providerId && opts.prompt?.modelId) {
|
|
154
45
|
return { providerID: opts.prompt.providerId, modelID: opts.prompt.modelId };
|
|
155
46
|
}
|
|
47
|
+
// The latest request's model is the session model; headless OpenCode has no model.json.
|
|
48
|
+
const latest = latestOpencodeSessionModel();
|
|
49
|
+
if (latest)
|
|
50
|
+
return latest;
|
|
156
51
|
const recent = readRecentOpencodeModel(opts.providerID);
|
|
157
52
|
if (recent)
|
|
158
53
|
return recent;
|
|
@@ -178,279 +73,81 @@ function readRecentOpencodeModel(preferredProvider) {
|
|
|
178
73
|
return undefined;
|
|
179
74
|
}
|
|
180
75
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
providerID,
|
|
200
|
-
modelID,
|
|
201
|
-
systemPrompt,
|
|
202
|
-
userPrompt,
|
|
203
|
-
directory,
|
|
204
|
-
retryCount,
|
|
205
|
-
jsonSchema,
|
|
206
|
-
schema,
|
|
207
|
-
onReply,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
const baseUrl = _v2BaseUrl;
|
|
211
|
-
if (!baseUrl) {
|
|
212
|
-
throw new Error("omms: v2 server base URL not initialized; call createV2Client(serverUrl) first");
|
|
213
|
-
}
|
|
214
|
-
const base = stripTrailingSlash(baseUrl);
|
|
215
|
-
const sessionID = await createSession(base, directory);
|
|
216
|
-
markInternalSession(sessionID);
|
|
217
|
-
try {
|
|
218
|
-
const reply = await withStructuredOutputTimeout(() => promptSession(base, {
|
|
219
|
-
sessionID,
|
|
220
|
-
directory,
|
|
221
|
-
providerID,
|
|
222
|
-
modelID,
|
|
223
|
-
systemPrompt,
|
|
224
|
-
userPrompt,
|
|
225
|
-
jsonSchema,
|
|
226
|
-
retryCount,
|
|
227
|
-
}), () => abortSession(base, sessionID, directory));
|
|
228
|
-
reportReply(onReply, reply);
|
|
229
|
-
const info = reply.info;
|
|
230
|
-
if (info.error) {
|
|
231
|
-
throw assistantError(info.error);
|
|
232
|
-
}
|
|
233
|
-
const structuredOutput = info.structured_output ?? info.structured;
|
|
234
|
-
if (structuredOutput === undefined || structuredOutput === null) {
|
|
235
|
-
throw new Error("omms: opencode returned no structured output (info.structured_output/info.structured were empty)");
|
|
236
|
-
}
|
|
237
|
-
return schema.parse(structuredOutput);
|
|
238
|
-
}
|
|
239
|
-
finally {
|
|
240
|
-
unmarkInternalSession(sessionID);
|
|
241
|
-
// Best-effort: leaving a transient session behind is cosmetic, not
|
|
242
|
-
// worth failing a successful capture if cleanup itself errors.
|
|
243
|
-
try {
|
|
244
|
-
await deleteSession(base, sessionID, directory);
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
// intentionally swallowed
|
|
248
|
-
}
|
|
249
|
-
finally {
|
|
250
|
-
untrackInternalCaptureSession(sessionID);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
function hasV2SessionClient(client) {
|
|
255
|
-
const session = client.session;
|
|
256
|
-
if (typeof session !== "object" || session === null)
|
|
257
|
-
return false;
|
|
258
|
-
const candidate = session;
|
|
259
|
-
return (typeof candidate.create === "function" &&
|
|
260
|
-
typeof candidate.prompt === "function" &&
|
|
261
|
-
typeof candidate.delete === "function");
|
|
262
|
-
}
|
|
263
|
-
async function generateViaSdkClient(client, args) {
|
|
264
|
-
const createdResponse = await client.session.create({
|
|
265
|
-
...sessionCreateBody(),
|
|
266
|
-
...(args.directory ? { directory: args.directory } : {}),
|
|
267
|
-
});
|
|
268
|
-
const created = readSdkData(createdResponse, "POST /session");
|
|
269
|
-
if (!created.id) {
|
|
270
|
-
throw new Error("omms: session.create returned no session id; cannot generate structured output");
|
|
271
|
-
}
|
|
272
|
-
const sessionID = created.id;
|
|
273
|
-
trackInternalCaptureSession(sessionID);
|
|
274
|
-
markInternalSession(sessionID);
|
|
76
|
+
function structuredPrompt(systemPrompt, userPrompt, jsonSchema) {
|
|
77
|
+
return [
|
|
78
|
+
systemPrompt,
|
|
79
|
+
userPrompt,
|
|
80
|
+
[
|
|
81
|
+
"Return only one JSON object matching this JSON Schema.",
|
|
82
|
+
"Do not wrap the JSON in Markdown.",
|
|
83
|
+
JSON.stringify(jsonSchema),
|
|
84
|
+
].join("\n"),
|
|
85
|
+
]
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.join("\n\n");
|
|
88
|
+
}
|
|
89
|
+
/** The JSON object in a model reply: the whole reply, a fenced block, or the outer braces. */
|
|
90
|
+
export function parseJsonReply(text) {
|
|
91
|
+
const trimmed = text.trim();
|
|
92
|
+
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1];
|
|
93
|
+
const candidate = fenced ?? trimmed;
|
|
275
94
|
try {
|
|
276
|
-
|
|
277
|
-
sessionID,
|
|
278
|
-
...(args.directory ? { directory: args.directory } : {}),
|
|
279
|
-
...sessionPromptFields(args),
|
|
280
|
-
}), () => client.session.abort?.({
|
|
281
|
-
sessionID,
|
|
282
|
-
...(args.directory ? { directory: args.directory } : {}),
|
|
283
|
-
}));
|
|
284
|
-
const data = readSdkData(promptResponse, "POST /session/{id}/message");
|
|
285
|
-
if (!data.info) {
|
|
286
|
-
throw new Error("omms: prompt response missing `info`");
|
|
287
|
-
}
|
|
288
|
-
reportReply(args.onReply, data);
|
|
289
|
-
if (data.info.error) {
|
|
290
|
-
throw assistantError(data.info.error);
|
|
291
|
-
}
|
|
292
|
-
const structuredOutput = data.info.structured_output ?? data.info.structured;
|
|
293
|
-
if (structuredOutput === undefined || structuredOutput === null) {
|
|
294
|
-
throw new Error("omms: opencode returned no structured output (info.structured_output/info.structured were empty)");
|
|
295
|
-
}
|
|
296
|
-
return args.schema.parse(structuredOutput);
|
|
95
|
+
return JSON.parse(candidate);
|
|
297
96
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
finally {
|
|
310
|
-
untrackInternalCaptureSession(sessionID);
|
|
97
|
+
catch {
|
|
98
|
+
const start = candidate.indexOf("{");
|
|
99
|
+
const end = candidate.lastIndexOf("}");
|
|
100
|
+
if (start >= 0 && end > start) {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(candidate.slice(start, end + 1));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// The parser's own message can quote the reply, which must not reach the log.
|
|
106
|
+
}
|
|
311
107
|
}
|
|
108
|
+
throw new Error(OPENCODE_NO_JSON_REPLY);
|
|
312
109
|
}
|
|
313
110
|
}
|
|
314
|
-
async function withStructuredOutputTimeout(run
|
|
111
|
+
async function withStructuredOutputTimeout(run) {
|
|
315
112
|
let timer;
|
|
316
113
|
const timeoutMs = _structuredOutputTimeoutMs;
|
|
317
|
-
const
|
|
114
|
+
const timeout = new Promise((_, reject) => {
|
|
318
115
|
timer = setTimeout(() => {
|
|
319
116
|
reject(new Error(`omms: structured-output timed out after ${timeoutMs}ms`));
|
|
320
117
|
}, timeoutMs);
|
|
321
118
|
});
|
|
322
119
|
try {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
catch (error) {
|
|
326
|
-
if (error instanceof Error && error.message.includes("structured-output timed out after")) {
|
|
327
|
-
try {
|
|
328
|
-
await onTimeout();
|
|
329
|
-
}
|
|
330
|
-
catch {
|
|
331
|
-
// best-effort abort
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
throw error;
|
|
120
|
+
// On a timeout the pending generation's result is dropped.
|
|
121
|
+
return await Promise.race([run(), timeout]);
|
|
335
122
|
}
|
|
336
123
|
finally {
|
|
337
124
|
if (timer !== undefined)
|
|
338
125
|
clearTimeout(timer);
|
|
339
126
|
}
|
|
340
127
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
throw new Error(message);
|
|
350
|
-
throw httpStatusError(message, result.response.status, parseRetryAfter(result.response.headers?.get?.("retry-after")));
|
|
351
|
-
}
|
|
352
|
-
if (result?.data === undefined) {
|
|
353
|
-
throw new Error(`omms: opencode ${label} returned no response data`);
|
|
354
|
-
}
|
|
355
|
-
return result.data;
|
|
356
|
-
}
|
|
357
|
-
function stripTrailingSlash(url) {
|
|
358
|
-
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
359
|
-
}
|
|
360
|
-
function buildQuery(directory) {
|
|
361
|
-
if (!directory)
|
|
362
|
-
return "";
|
|
363
|
-
return `?directory=${encodeURIComponent(directory)}`;
|
|
364
|
-
}
|
|
365
|
-
async function fetchJson(endpoint, init) {
|
|
366
|
-
let res;
|
|
367
|
-
try {
|
|
368
|
-
res = await activeFetch()(new Request(endpoint.url, init));
|
|
369
|
-
}
|
|
370
|
-
catch (error) {
|
|
371
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
372
|
-
throw new Error(`omms: failed to fetch ${endpoint.label} at ${diagnosticUrl(endpoint.url)}: ${message}`, { cause: error });
|
|
373
|
-
}
|
|
374
|
-
return readJson(res, endpoint);
|
|
375
|
-
}
|
|
376
|
-
async function createSession(base, directory) {
|
|
377
|
-
const url = `${base}/session${buildQuery(directory)}`;
|
|
378
|
-
const body = await fetchJson({ label: "POST /session", url }, {
|
|
379
|
-
method: "POST",
|
|
380
|
-
headers: { "Content-Type": "application/json" },
|
|
381
|
-
body: JSON.stringify(sessionCreateBody()),
|
|
382
|
-
});
|
|
383
|
-
if (!body.id) {
|
|
384
|
-
throw new Error("omms: session.create returned no session id; cannot generate structured output");
|
|
385
|
-
}
|
|
386
|
-
trackInternalCaptureSession(body.id);
|
|
387
|
-
return body.id;
|
|
388
|
-
}
|
|
389
|
-
/** The model's error reply, with its HTTP status when OpenCode reports one. */
|
|
390
|
-
function assistantError(error) {
|
|
391
|
-
const message = `omms: opencode reported ${error.name}: ${formatAssistantError(error)}`;
|
|
392
|
-
const status = error.data?.statusCode;
|
|
393
|
-
return typeof status === "number" ? httpStatusError(message, status) : new Error(message);
|
|
394
|
-
}
|
|
395
|
-
function formatAssistantError(error) {
|
|
396
|
-
if (!error.data)
|
|
397
|
-
return error.name;
|
|
398
|
-
const details = safeAssistantErrorDetails(error.data);
|
|
399
|
-
if (!error.data.message)
|
|
400
|
-
return details;
|
|
401
|
-
return details ? `${error.data.message}; ${details}` : error.data.message;
|
|
402
|
-
}
|
|
403
|
-
function safeAssistantErrorDetails(data) {
|
|
404
|
-
const safeFields = {};
|
|
405
|
-
for (const key of ["statusCode", "providerID", "modelID"]) {
|
|
406
|
-
const value = data[key];
|
|
407
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
408
|
-
safeFields[key] = value;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
const entries = Object.entries(safeFields);
|
|
412
|
-
if (entries.length === 0)
|
|
413
|
-
return "";
|
|
414
|
-
return `details=${JSON.stringify(Object.fromEntries(entries))}`;
|
|
415
|
-
}
|
|
416
|
-
async function promptSession(base, args) {
|
|
417
|
-
const url = `${base}/session/${encodeURIComponent(args.sessionID)}/message${buildQuery(args.directory)}`;
|
|
418
|
-
const body = sessionPromptFields(args);
|
|
419
|
-
const data = await fetchJson({ label: "POST /session/{id}/message", url }, {
|
|
420
|
-
method: "POST",
|
|
421
|
-
headers: { "Content-Type": "application/json" },
|
|
422
|
-
body: JSON.stringify(body),
|
|
128
|
+
/**
|
|
129
|
+
* Generate one structured-output reply with one sessionless `ctx.generate.text` request.
|
|
130
|
+
* Throws on a failed request, a reply with no JSON object, a timeout, or a schema mismatch.
|
|
131
|
+
*/
|
|
132
|
+
export async function generateStructuredOutput(opts) {
|
|
133
|
+
const { providerID, modelID } = resolveOpencodeModelRef({
|
|
134
|
+
providerID: opts.providerID,
|
|
135
|
+
modelID: opts.modelID,
|
|
423
136
|
});
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
137
|
+
const jsonSchema = opts.schema.toJSONSchema?.() ?? (await import("zod")).z.toJSONSchema(opts.schema);
|
|
138
|
+
const generated = await withStructuredOutputTimeout(() => opts.host.generate.text({
|
|
139
|
+
prompt: structuredPrompt(opts.systemPrompt, opts.userPrompt, jsonSchema),
|
|
140
|
+
model: { providerID, id: modelID },
|
|
141
|
+
}));
|
|
142
|
+
const text = generated?.text ?? "";
|
|
143
|
+
if (!text.trim())
|
|
144
|
+
throw new Error(OPENCODE_EMPTY_REPLY);
|
|
145
|
+
const structuredOutput = parseJsonReply(text);
|
|
431
146
|
try {
|
|
432
|
-
|
|
147
|
+
opts.onReply?.({ partTypes: ["text"], structuredOutput });
|
|
433
148
|
}
|
|
434
149
|
catch {
|
|
435
|
-
//
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
async function deleteSession(base, sessionID, directory) {
|
|
439
|
-
const url = `${base}/session/${encodeURIComponent(sessionID)}${buildQuery(directory)}`;
|
|
440
|
-
let res;
|
|
441
|
-
try {
|
|
442
|
-
res = await activeFetch()(new Request(url, { method: "DELETE" }));
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
446
|
-
throw new Error(`omms: failed to fetch DELETE /session/{id} at ${diagnosticUrl(url)}: ${message}`, { cause: error });
|
|
447
|
-
}
|
|
448
|
-
// DELETE /session/:id returns boolean. We only care that it ran; failures
|
|
449
|
-
// are swallowed at the call site.
|
|
450
|
-
if (!res.ok) {
|
|
451
|
-
throw new Error(`omms: opencode DELETE /session/{id} failed at ${diagnosticUrl(url)} (${responseStatus(res)})`);
|
|
150
|
+
// Diagnostics must never change the call's result.
|
|
452
151
|
}
|
|
453
|
-
|
|
454
|
-
function activeFetch() {
|
|
455
|
-
return _hostFetch ?? globalThis.fetch;
|
|
152
|
+
return opts.schema.parse(structuredOutput);
|
|
456
153
|
}
|
|
@@ -4,12 +4,7 @@
|
|
|
4
4
|
* bundles the plugin into one file at another depth.
|
|
5
5
|
*/
|
|
6
6
|
export declare function packageSkillsFolder(moduleUrl?: string): string;
|
|
7
|
-
/**
|
|
8
|
-
* V1: add the package's skills folder to OpenCode's skill paths once. The V1
|
|
9
|
-
* SDK `Config` type has no `skills` key, although OpenCode reads it.
|
|
10
|
-
*/
|
|
11
|
-
export declare function applySkillsPathConfig(config: object, folder?: string): void;
|
|
12
|
-
/** V2: add the package's skills to the plugin's skill list once. */
|
|
7
|
+
/** Add the package's skills to the plugin's skill list once. */
|
|
13
8
|
export declare function addPackageSkill(editor: {
|
|
14
9
|
get(id: string): unknown;
|
|
15
10
|
add(skill: any): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { log } from "../../services/logger.js";
|
|
@@ -27,22 +27,7 @@ export function packageSkillsFolder(moduleUrl = import.meta.url) {
|
|
|
27
27
|
return join(dirname(fileURLToPath(moduleUrl)), "..", "..", "..", "skills");
|
|
28
28
|
}
|
|
29
29
|
const defaultFolder = packageSkillsFolder();
|
|
30
|
-
/**
|
|
31
|
-
* V1: add the package's skills folder to OpenCode's skill paths once. The V1
|
|
32
|
-
* SDK `Config` type has no `skills` key, although OpenCode reads it.
|
|
33
|
-
*/
|
|
34
|
-
export function applySkillsPathConfig(config, folder = defaultFolder) {
|
|
35
|
-
const cfg = config;
|
|
36
|
-
if (!existsSync(folder)) {
|
|
37
|
-
log("OMMS skills folder not found; the packaged skills are not loaded", { folder });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const paths = cfg.skills?.paths ?? [];
|
|
41
|
-
if (paths.includes(folder))
|
|
42
|
-
return;
|
|
43
|
-
cfg.skills = { ...cfg.skills, paths: [...paths, folder] };
|
|
44
|
-
}
|
|
45
|
-
/** V2: add the package's skills to the plugin's skill list once. */
|
|
30
|
+
/** Add the package's skills to the plugin's skill list once. */
|
|
46
31
|
export function addPackageSkill(editor, folder = defaultFolder) {
|
|
47
32
|
for (const name of SKILL_NAMES) {
|
|
48
33
|
if (editor.get(name))
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function performUserProfileLearning(ctx: PluginInput, directory: string): Promise<void>;
|
|
1
|
+
export declare function performUserProfileLearning(directory: string): Promise<void>;
|
|
3
2
|
import { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX } from "../../core/extraction.js";
|
|
4
3
|
export { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX };
|
|
5
4
|
export declare function createUserProfileToolSchema(existingProfile: boolean): {
|
|
@@ -10,8 +10,9 @@ import { shouldStartAutoCleanup } from "../../services/user-profile/auto-cleanup
|
|
|
10
10
|
import { sortProfileItems } from "../../utils/profile.js";
|
|
11
11
|
import { truncateToMaxBytes } from "../../utils/context-limit.js";
|
|
12
12
|
import { loadOpencodeProvider } from "./opencode-provider-loader.js";
|
|
13
|
+
import { showOpencodeNotice } from "./toast.js";
|
|
13
14
|
let isLearningRunning = false;
|
|
14
|
-
export async function performUserProfileLearning(
|
|
15
|
+
export async function performUserProfileLearning(directory) {
|
|
15
16
|
if (isLearningRunning)
|
|
16
17
|
return;
|
|
17
18
|
if (!profileBackoff.canRun())
|
|
@@ -182,16 +183,11 @@ Rules:
|
|
|
182
183
|
.catch(() => { });
|
|
183
184
|
}
|
|
184
185
|
if (CONFIG.showUserProfileToasts) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
variant: "success",
|
|
191
|
-
duration: 3000,
|
|
192
|
-
},
|
|
193
|
-
})
|
|
194
|
-
.catch(() => { });
|
|
186
|
+
showOpencodeNotice({
|
|
187
|
+
title: "User Profile Updated",
|
|
188
|
+
message: `Analyzed ${prompts.length} of ${count} pending prompts and updated your profile`,
|
|
189
|
+
variant: "success",
|
|
190
|
+
});
|
|
195
191
|
}
|
|
196
192
|
}
|
|
197
193
|
catch (error) {
|
|
@@ -461,12 +457,12 @@ async function analyzeUserProfile(context, existingProfile) {
|
|
|
461
457
|
log("user-profile-learning: trying opencode provider");
|
|
462
458
|
try {
|
|
463
459
|
const { generateStructuredOutput } = await loadOpencodeProvider();
|
|
464
|
-
const {
|
|
460
|
+
const { getOpencodeProfileHost } = await import("./profile-llm-client.js");
|
|
465
461
|
log("user-profile-learning: opencode provider diag", {
|
|
466
462
|
provider: resolveOpencodeHostModel(CONFIG)?.providerID,
|
|
467
463
|
model: resolveOpencodeHostModel(CONFIG)?.modelID,
|
|
468
464
|
});
|
|
469
|
-
const
|
|
465
|
+
const host = await getOpencodeProfileHost();
|
|
470
466
|
const systemPrompt = `You are a user behavior analyst for a coding assistant.
|
|
471
467
|
|
|
472
468
|
Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
|
|
@@ -483,7 +479,7 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne
|
|
|
483
479
|
log("user-profile-learning: calling LLM", { contextLen: context.length });
|
|
484
480
|
const result = await Promise.race([
|
|
485
481
|
generateStructuredOutput({
|
|
486
|
-
|
|
482
|
+
host,
|
|
487
483
|
providerID: resolveOpencodeHostModel(CONFIG).providerID,
|
|
488
484
|
modelID: resolveOpencodeHostModel(CONFIG).modelID,
|
|
489
485
|
systemPrompt,
|
|
@@ -565,18 +561,18 @@ If no clear chains, return { "paths": [] }.`;
|
|
|
565
561
|
try {
|
|
566
562
|
const { z } = await import("zod");
|
|
567
563
|
const { generateStructuredOutput } = await loadOpencodeProvider();
|
|
568
|
-
const {
|
|
569
|
-
let
|
|
564
|
+
const { getOpencodeProfileHost } = await import("./profile-llm-client.js");
|
|
565
|
+
let host;
|
|
570
566
|
try {
|
|
571
|
-
|
|
567
|
+
host = await getOpencodeProfileHost();
|
|
572
568
|
}
|
|
573
569
|
catch {
|
|
574
570
|
// provider not available, skip learning paths
|
|
575
571
|
}
|
|
576
|
-
if (
|
|
572
|
+
if (host) {
|
|
577
573
|
result = (await Promise.race([
|
|
578
574
|
generateStructuredOutput({
|
|
579
|
-
|
|
575
|
+
host,
|
|
580
576
|
providerID: resolveOpencodeHostModel(CONFIG).providerID,
|
|
581
577
|
modelID: resolveOpencodeHostModel(CONFIG).modelID,
|
|
582
578
|
systemPrompt,
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { OpencodeHost } from "./opencode-host.js";
|
|
2
|
+
/** The OpenCode plugin context for profile calls, once the host model's provider is signed in. */
|
|
3
|
+
export declare function getOpencodeProfileHost(): Promise<OpencodeHost>;
|