@lapis-notes/ai 0.1.2 → 0.1.5

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE.md +3 -7
  3. package/README.md +1 -1
  4. package/dist/ai-plugin.d.ts +2 -2
  5. package/dist/ai-plugin.js +28 -11
  6. package/dist/catalog/ai-catalog-panel.svelte +20 -3
  7. package/dist/chat/ai-chat-panel.svelte +187 -87
  8. package/dist/chat/ai-chat-panel.svelte.d.ts +2 -0
  9. package/dist/chat/ai-view-bootstrap.d.ts +1 -0
  10. package/dist/chat/ai-view-bootstrap.js +32 -12
  11. package/dist/chat/ai-view-panel.svelte +14 -0
  12. package/dist/chat/ai-view.d.ts +1 -1
  13. package/dist/chat/chat-controller.svelte.d.ts +1 -1
  14. package/dist/chat/chat-controller.svelte.js +94 -29
  15. package/dist/chat/chat-tool-display.d.ts +2 -12
  16. package/dist/chat/chat-tool-display.js +2 -206
  17. package/dist/chat/composer-model-catalog.d.ts +20 -0
  18. package/dist/chat/composer-model-catalog.js +44 -0
  19. package/dist/commands/agent.d.ts +1 -1
  20. package/dist/commands/agent.js +6 -1
  21. package/dist/conversations/context-handoff.d.ts +1 -25
  22. package/dist/conversations/context-handoff.js +1 -207
  23. package/dist/conversations/hashes.d.ts +1 -5
  24. package/dist/conversations/hashes.js +1 -40
  25. package/dist/core/types.d.ts +1 -241
  26. package/dist/core/types.js +1 -50
  27. package/dist/history/ai-history-panel.svelte +4 -1
  28. package/dist/host/controller-connection.d.ts +14 -0
  29. package/dist/host/controller-connection.js +62 -0
  30. package/dist/host/controller-runtime.d.ts +13 -0
  31. package/dist/host/controller-runtime.js +112 -0
  32. package/dist/host/create-host-runtimes.d.ts +2 -1
  33. package/dist/host/create-host-runtimes.js +5 -7
  34. package/dist/index.d.ts +1 -1
  35. package/dist/memory/candidate-extractor.d.ts +2 -25
  36. package/dist/memory/candidate-extractor.js +1 -191
  37. package/dist/memory/hashes.d.ts +1 -5
  38. package/dist/memory/hashes.js +1 -37
  39. package/dist/memory/memory-record-store.d.ts +3 -48
  40. package/dist/memory/memory-record-store.js +16 -220
  41. package/dist/memory/memory-scheduler.d.ts +1 -21
  42. package/dist/memory/memory-scheduler.js +1 -143
  43. package/dist/memory/native-memory-service.d.ts +1 -72
  44. package/dist/memory/native-memory-service.js +1 -1256
  45. package/dist/memory/paths.d.ts +1 -13
  46. package/dist/memory/paths.js +1 -60
  47. package/dist/memory/runtime-consolidation-provider.d.ts +2 -18
  48. package/dist/memory/runtime-consolidation-provider.js +1 -154
  49. package/dist/memory/types.d.ts +1 -139
  50. package/dist/providers/acp-model-provider.d.ts +8 -3
  51. package/dist/providers/acp-model-provider.js +21 -49
  52. package/dist/providers/model-provider.d.ts +3 -2
  53. package/dist/providers/model-provider.js +15 -2
  54. package/dist/runtime-adapters.d.ts +3 -0
  55. package/dist/runtime-adapters.js +2 -0
  56. package/dist/settings/acp-agents.d.ts +4 -1
  57. package/dist/settings/acp-agents.js +5 -1
  58. package/dist/settings/ai-settings-tab.js +8 -6
  59. package/dist/settings/ai-settings.d.ts +4 -4
  60. package/dist/settings/ai-settings.js +5 -3
  61. package/dist/settings/register-ai-settings.js +27 -14
  62. package/dist/tools/app-tool-host.d.ts +22 -2
  63. package/dist/tools/app-tool-host.js +51 -4
  64. package/dist/tools/desktop-app-tool-bridge.d.ts +4 -1
  65. package/dist/tools/desktop-app-tool-bridge.js +94 -157
  66. package/manifest.json +20 -2
  67. package/package.json +17 -7
  68. package/registry-content/overview.md +10 -0
  69. package/registry.json +211 -2
@@ -1,13 +1 @@
1
- import type { MemoryScope } from "./types";
2
- export declare const AI_MEMORY_EPISODIC_PROVIDER_ID = "ai-memory-episodic";
3
- export declare const AI_MEMORY_CURATED_PROVIDER_ID = "ai-memory-curated";
4
- export declare const AI_MEMORY_PATH_PREFIX = "ai-memory";
5
- export declare function decodeMemoryPathSegment(value: string): string;
6
- export declare function conversationMemoryScope(scopeDir: string): MemoryScope;
7
- export declare function memoryScopeKey(scope: MemoryScope): string;
8
- export declare function episodicScopePrefix(scope: MemoryScope): string;
9
- export declare function episodicMemoryPath(scope: MemoryScope, conversationId: string, entryId: string): string;
10
- export declare function curatedScopePrefix(scope: MemoryScope): string;
11
- export declare function curatedMemoryPath(scope: MemoryScope, memoryId: string): string;
12
- export declare function currentMemoryScopes(scopeDir: string): MemoryScope[];
13
- export declare function scopeProximity(scope: MemoryScope, currentScopeDir: string): number;
1
+ export { AI_MEMORY_EPISODIC_PROVIDER_ID, AI_MEMORY_CURATED_PROVIDER_ID, AI_MEMORY_PATH_PREFIX, decodeMemoryPathSegment, conversationMemoryScope, memoryScopeKey, episodicScopePrefix, episodicMemoryPath, curatedScopePrefix, curatedMemoryPath, currentMemoryScopes, scopeProximity, } from "@lapismd/ai-controller/memory/lapis";
@@ -1,60 +1 @@
1
- export const AI_MEMORY_EPISODIC_PROVIDER_ID = "ai-memory-episodic";
2
- export const AI_MEMORY_CURATED_PROVIDER_ID = "ai-memory-curated";
3
- export const AI_MEMORY_PATH_PREFIX = "ai-memory";
4
- function encodeSegment(value) {
5
- return encodeURIComponent(value).replaceAll("%", "~");
6
- }
7
- export function decodeMemoryPathSegment(value) {
8
- return decodeURIComponent(value.replaceAll("~", "%"));
9
- }
10
- export function conversationMemoryScope(scopeDir) {
11
- return scopeDir
12
- ? { kind: "project", projectDir: scopeDir }
13
- : { kind: "workspace" };
14
- }
15
- export function memoryScopeKey(scope) {
16
- return scope.kind === "project"
17
- ? `project:${scope.projectDir}`
18
- : `${scope.kind}:`;
19
- }
20
- export function episodicScopePrefix(scope) {
21
- if (scope.kind === "project") {
22
- return `${AI_MEMORY_PATH_PREFIX}/episodic/project/${encodeSegment(scope.projectDir)}`;
23
- }
24
- return `${AI_MEMORY_PATH_PREFIX}/episodic/${scope.kind}/_`;
25
- }
26
- export function episodicMemoryPath(scope, conversationId, entryId) {
27
- return `${episodicScopePrefix(scope)}/${encodeSegment(conversationId)}/${encodeSegment(entryId)}`;
28
- }
29
- export function curatedScopePrefix(scope) {
30
- if (scope.kind === "project") {
31
- return `${AI_MEMORY_PATH_PREFIX}/curated/project/${encodeSegment(scope.projectDir)}`;
32
- }
33
- return `${AI_MEMORY_PATH_PREFIX}/curated/${scope.kind}/_`;
34
- }
35
- export function curatedMemoryPath(scope, memoryId) {
36
- return `${curatedScopePrefix(scope)}/${encodeSegment(memoryId)}`;
37
- }
38
- export function currentMemoryScopes(scopeDir) {
39
- const scopes = [{ kind: "user" }, { kind: "workspace" }];
40
- if (!scopeDir)
41
- return scopes;
42
- const segments = scopeDir.split("/");
43
- const projects = [];
44
- for (let index = segments.length; index > 0 && projects.length < 8; index -= 1) {
45
- projects.push({ kind: "project", projectDir: segments.slice(0, index).join("/") });
46
- }
47
- return [...scopes, ...projects];
48
- }
49
- export function scopeProximity(scope, currentScopeDir) {
50
- if (scope.kind === "user")
51
- return 0.75;
52
- if (scope.kind === "workspace")
53
- return currentScopeDir ? 0.8 : 1;
54
- if (scope.projectDir === currentScopeDir)
55
- return 1;
56
- if (!currentScopeDir.startsWith(`${scope.projectDir}/`))
57
- return 0.35;
58
- const distance = currentScopeDir.split("/").length - scope.projectDir.split("/").length;
59
- return Math.max(0.45, 0.9 - distance * 0.08);
60
- }
1
+ export { AI_MEMORY_EPISODIC_PROVIDER_ID, AI_MEMORY_CURATED_PROVIDER_ID, AI_MEMORY_PATH_PREFIX, decodeMemoryPathSegment, conversationMemoryScope, memoryScopeKey, episodicScopePrefix, episodicMemoryPath, curatedScopePrefix, curatedMemoryPath, currentMemoryScopes, scopeProximity, } from "@lapismd/ai-controller/memory/lapis";
@@ -1,18 +1,2 @@
1
- import type { AgentRequest, AgentRuntime } from "../core/types";
2
- import type { ConsolidationProposal, GroundedConsolidationInput, MemoryConsolidationProvider } from "./types";
3
- export type RuntimeConsolidationConfiguration = {
4
- runtimeId: "acp" | "codex-native";
5
- agent: "codex" | "cursor";
6
- model: string;
7
- };
8
- export type RuntimeMemoryConsolidationProviderOptions = {
9
- configuration: () => RuntimeConsolidationConfiguration;
10
- resolveRuntime: (request: AgentRequest) => Promise<AgentRuntime>;
11
- timeoutMs?: number;
12
- };
13
- export declare class RuntimeMemoryConsolidationProvider implements MemoryConsolidationProvider {
14
- #private;
15
- private readonly options;
16
- constructor(options: RuntimeMemoryConsolidationProviderOptions);
17
- propose(input: GroundedConsolidationInput, signal?: AbortSignal): Promise<ConsolidationProposal>;
18
- }
1
+ export { RuntimeMemoryConsolidationProvider } from "@lapismd/ai-controller/memory/lapis";
2
+ export type { RuntimeConsolidationConfiguration, RuntimeMemoryConsolidationProviderOptions, } from "@lapismd/ai-controller/memory/lapis";
@@ -1,154 +1 @@
1
- const MAX_INPUT_CHARS = 32_000;
2
- const MAX_OUTPUT_CHARS = 64_000;
3
- const DEFAULT_TIMEOUT_MS = 60_000;
4
- export class RuntimeMemoryConsolidationProvider {
5
- options;
6
- #timeoutMs;
7
- constructor(options) {
8
- this.options = options;
9
- this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10
- }
11
- async propose(input, signal) {
12
- const configuration = this.options.configuration();
13
- if (!configuration.model.trim()) {
14
- throw new Error("Memory consolidation requires a pinned model.");
15
- }
16
- const prompt = consolidationPrompt(input);
17
- const request = {
18
- prompt: "",
19
- agent: configuration.agent,
20
- model: {
21
- provider: configuration.agent,
22
- model: configuration.model.trim(),
23
- },
24
- thinking: "low",
25
- metadata: {
26
- runtime: configuration.runtimeId,
27
- purpose: "memory-consolidation",
28
- },
29
- mcpServers: [],
30
- restricted: true,
31
- requireApprovals: false,
32
- requirePolicyAmendments: false,
33
- };
34
- const runtime = await this.options.resolveRuntime(request);
35
- const session = await runtime.start(request);
36
- const deadline = new AbortController();
37
- const timer = setTimeout(() => deadline.abort(new Error("Memory consolidation timed out.")), this.#timeoutMs);
38
- const abort = () => deadline.abort(signal?.reason);
39
- signal?.addEventListener("abort", abort, { once: true });
40
- try {
41
- return await collectProposal(session, prompt, deadline.signal);
42
- }
43
- finally {
44
- clearTimeout(timer);
45
- signal?.removeEventListener("abort", abort);
46
- await session.close().catch(() => undefined);
47
- }
48
- }
49
- }
50
- async function collectProposal(session, prompt, signal) {
51
- let output = "";
52
- let completed = false;
53
- const cancelOnAbort = () => void session.cancel?.().catch(() => undefined);
54
- signal.addEventListener("abort", cancelOnAbort, { once: true });
55
- const send = session.send(prompt);
56
- const events = session.events()[Symbol.asyncIterator]();
57
- try {
58
- while (true) {
59
- const next = await nextEvent(events, signal);
60
- if (next.done)
61
- break;
62
- const event = next.value;
63
- assertReadOnlyEvent(event);
64
- if (event.type === "text") {
65
- output += event.text;
66
- if (output.length > MAX_OUTPUT_CHARS) {
67
- await session.cancel?.().catch(() => undefined);
68
- throw new Error("Memory consolidation output exceeded its limit.");
69
- }
70
- }
71
- if (event.type === "error")
72
- throw event.error;
73
- if (event.type === "completed") {
74
- completed = true;
75
- break;
76
- }
77
- }
78
- await send;
79
- if (!completed)
80
- throw new Error("Memory consolidation ended without completion.");
81
- return parseProposal(output);
82
- }
83
- catch (error) {
84
- await session.cancel?.().catch(() => undefined);
85
- await send.catch(() => undefined);
86
- throw error;
87
- }
88
- finally {
89
- signal.removeEventListener("abort", cancelOnAbort);
90
- await events.return?.().catch(() => undefined);
91
- }
92
- }
93
- function nextEvent(events, signal) {
94
- if (signal.aborted)
95
- return Promise.reject(signal.reason);
96
- return new Promise((resolve, reject) => {
97
- const onAbort = () => reject(signal.reason);
98
- signal.addEventListener("abort", onAbort, { once: true });
99
- void events.next().then((next) => {
100
- signal.removeEventListener("abort", onAbort);
101
- resolve(next);
102
- }, (error) => {
103
- signal.removeEventListener("abort", onAbort);
104
- reject(error);
105
- });
106
- });
107
- }
108
- function assertReadOnlyEvent(event) {
109
- if (event.type === "tool.start" ||
110
- event.type === "tool.end" ||
111
- event.type === "command.start" ||
112
- event.type === "command.end" ||
113
- event.type === "file.changed" ||
114
- event.type === "permission.request" ||
115
- event.type === "question.request") {
116
- throw new Error(`Memory consolidation aborted on forbidden runtime event: ${event.type}`);
117
- }
118
- }
119
- function consolidationPrompt(input) {
120
- const payload = JSON.stringify({
121
- scope: input.scope,
122
- candidates: input.candidates.slice(0, 10).map((candidate) => ({
123
- id: candidate.id,
124
- claim: candidate.claim.slice(0, 2_000),
125
- evidence: candidate.evidence.slice(0, 20).map((evidence) => ({
126
- conversationId: evidence.conversationId,
127
- entryId: evidence.entryId,
128
- entryHash: evidence.entryHash,
129
- observedAt: evidence.observedAt,
130
- })),
131
- })),
132
- });
133
- if (payload.length > MAX_INPUT_CHARS) {
134
- throw new Error("Memory consolidation input exceeded its limit.");
135
- }
136
- return [
137
- "You are a restricted memory consolidation processor.",
138
- "Return exactly one JSON object and no markdown or commentary.",
139
- "Use only the supplied candidates. Do not invent candidate IDs or evidence.",
140
- "The object must be {\"memories\":[{\"candidateIds\":[string],\"kind\":\"preference\"|\"fact\"|\"decision\"|\"procedure\"|\"constraint\",\"scope\":MemoryScope,\"importance\":1|2|3|4|5,\"triggers\":[string],\"summary\":string,\"supersessionKey\"?:string}]}.",
141
- "Input:",
142
- payload,
143
- ].join("\n");
144
- }
145
- function parseProposal(output) {
146
- const parsed = JSON.parse(output.trim());
147
- if (!isObject(parsed) || !Array.isArray(parsed.memories)) {
148
- throw new Error("Memory consolidation returned an invalid JSON shape.");
149
- }
150
- return parsed;
151
- }
152
- function isObject(value) {
153
- return typeof value === "object" && value !== null && !Array.isArray(value);
154
- }
1
+ export { RuntimeMemoryConsolidationProvider } from "@lapismd/ai-controller/memory/lapis";
@@ -1,139 +1 @@
1
- import type { ConversationLocation } from "../conversations/types";
2
- import type { AgentContextBlock } from "../core/types";
3
- export type MemoryScope = {
4
- kind: "user";
5
- } | {
6
- kind: "workspace";
7
- } | {
8
- kind: "project";
9
- projectDir: string;
10
- };
11
- export type MemoryOriginClass = "owner" | "agent" | "untrusted" | "system";
12
- export type MemoryEvidenceRef = {
13
- conversationId: string;
14
- entryId: string;
15
- entryHash: string;
16
- observedAt: string;
17
- scopeDirAtObservation: string;
18
- };
19
- export type DurableMemoryKind = "preference" | "fact" | "decision" | "procedure" | "constraint";
20
- export type DurableMemoryRecord = {
21
- schemaVersion: 1;
22
- id: string;
23
- revision: number;
24
- kind: DurableMemoryKind;
25
- scope: MemoryScope;
26
- status: "active" | "retracted";
27
- supersessionKey?: string;
28
- importance: 1 | 2 | 3 | 4 | 5;
29
- triggers: string[];
30
- summary: string;
31
- evidence: MemoryEvidenceRef[];
32
- createdBy: "owner-ui" | "deterministic-promotion" | "consolidator";
33
- createdAt: string;
34
- updatedAt: string;
35
- previousRevisionHash?: string;
36
- };
37
- export type MemoryContext = {
38
- scopeDir: string;
39
- conversationId?: string;
40
- agentBindingId?: string;
41
- runId?: string;
42
- };
43
- export type MemorySearchQuery = {
44
- query: string;
45
- scope?: "current" | "workspace";
46
- corpus?: "curated" | "episodic" | "all";
47
- limit?: number;
48
- since?: string;
49
- before?: string;
50
- includeSuperseded?: boolean;
51
- };
52
- export type MemoryCorpus = "curated" | "episodic";
53
- export type MemorySearchResult = {
54
- ref: string;
55
- corpus: MemoryCorpus;
56
- snippet: string;
57
- kind: DurableMemoryKind | "episode";
58
- scope: MemoryScope;
59
- observedAt: string;
60
- effectiveTrust: MemoryOriginClass;
61
- status: "active" | "retracted";
62
- score: number;
63
- evidence: MemoryEvidenceRef[];
64
- };
65
- export type MemoryEvidenceMessage = {
66
- id: string;
67
- type: "message" | "tool";
68
- role?: "user" | "assistant";
69
- text: string;
70
- observedAt: string;
71
- originClass: MemoryOriginClass;
72
- hash: string;
73
- verification: "matching" | "modified";
74
- };
75
- export type MemoryDetail = {
76
- ref: string;
77
- corpus: MemoryCorpus;
78
- status: "available" | "moved" | "missing" | "ambiguous" | "modified";
79
- result?: MemorySearchResult;
80
- currentLocation?: ConversationLocation;
81
- evidenceMessages: MemoryEvidenceMessage[];
82
- history?: DurableMemoryRecord[];
83
- };
84
- export type MemoryRebuildResult = {
85
- conversations: number;
86
- episodes: number;
87
- skipped: number;
88
- inconsistent: number;
89
- };
90
- export type ConsolidationPreview = {
91
- scope: MemoryScope;
92
- candidateIds: string[];
93
- proposals: number;
94
- };
95
- export type ConsolidationResult = ConsolidationPreview & {
96
- written: number;
97
- needsReview: number;
98
- };
99
- export type MemoryForgetPreview = {
100
- conversationId: string;
101
- episodeRefs: string[];
102
- candidateIds: string[];
103
- memoryIds: string[];
104
- };
105
- export type MemoryForgetResult = MemoryForgetPreview & {
106
- exclusionPath: string;
107
- retracted: number;
108
- needsReview: number;
109
- };
110
- export type GroundedConsolidationInput = {
111
- scope: MemoryScope;
112
- candidates: Array<{
113
- id: string;
114
- claim: string;
115
- evidence: MemoryEvidenceRef[];
116
- }>;
117
- };
118
- export type ConsolidationProposal = {
119
- memories: Array<Pick<DurableMemoryRecord, "kind" | "scope" | "importance" | "triggers" | "summary"> & {
120
- candidateIds: string[];
121
- supersessionKey?: string;
122
- }>;
123
- };
124
- export interface MemoryConsolidationProvider {
125
- propose(input: GroundedConsolidationInput, signal?: AbortSignal): Promise<ConsolidationProposal>;
126
- }
127
- export interface MemoryService {
128
- search(query: MemorySearchQuery, context: MemoryContext, signal?: AbortSignal): Promise<MemorySearchResult[]>;
129
- get(ref: string, context: MemoryContext, signal?: AbortSignal): Promise<MemoryDetail>;
130
- ingestConversation(location: ConversationLocation): Promise<void>;
131
- previewConsolidation(scope: MemoryScope): Promise<ConsolidationPreview>;
132
- consolidate(scope: MemoryScope): Promise<ConsolidationResult>;
133
- rebuild(scope?: MemoryScope): Promise<MemoryRebuildResult>;
134
- previewForgetConversation(location: ConversationLocation): Promise<MemoryForgetPreview>;
135
- forgetConversation(location: ConversationLocation): Promise<MemoryForgetResult>;
136
- }
137
- export interface AutomaticMemoryRecall {
138
- recall(query: string, context: MemoryContext, signal?: AbortSignal): Promise<AgentContextBlock[]>;
139
- }
1
+ export type { MemoryScope, MemoryOriginClass, MemoryEvidenceRef, DurableMemoryKind, DurableMemoryRecord, MemoryContext, MemorySearchQuery, MemoryCorpus, MemorySearchResult, MemoryEvidenceMessage, MemoryDetail, MemoryRebuildResult, ConsolidationPreview, ConsolidationResult, MemoryForgetPreview, MemoryForgetResult, GroundedConsolidationInput, ConsolidationProposal, MemoryConsolidationProvider, MemoryService, AutomaticMemoryRecall, } from "@lapismd/ai-controller/memory/lapis";
@@ -1,10 +1,15 @@
1
+ import type { ControllerConnection } from "../host/controller-connection";
1
2
  import type { ModelRef } from "../core/types";
2
3
  import type { ModelProvider, ProviderAuthStatus } from "./model-provider";
3
4
  export declare class AcpModelProvider implements ModelProvider {
4
- #private;
5
+ readonly options: {
6
+ connection(): Promise<ControllerConnection>;
7
+ providerIds?: string[];
8
+ };
5
9
  readonly id: string;
6
- constructor(id: string, options?: {
7
- workspace?: string;
10
+ constructor(id: string, options: {
11
+ connection(): Promise<ControllerConnection>;
12
+ providerIds?: string[];
8
13
  });
9
14
  listModels(): Promise<ModelRef[]>;
10
15
  authStatus(): Promise<ProviderAuthStatus>;
@@ -1,22 +1,31 @@
1
- import { getNativeDesktopBridge, getNativeDesktopCapability, hasNativeDesktopCapability, } from "@lapis-notes/api/desktop-native";
1
+ import { hasNativeDesktopCapability } from "@lapis-notes/api/desktop-native";
2
2
  export class AcpModelProvider {
3
+ options;
3
4
  id;
4
- #workspace;
5
- constructor(id, options = {}) {
5
+ constructor(id, options) {
6
+ this.options = options;
6
7
  this.id = id;
7
- this.#workspace = options.workspace;
8
8
  }
9
9
  async listModels() {
10
10
  if (!hasNativeDesktopCapability("agent-runtime"))
11
11
  return [];
12
- const bridge = getNativeDesktopBridge();
13
- if (!bridge)
14
- return [];
15
- const payload = { agent: this.id, workspace: this.#workspace };
16
- const catalog = getNativeDesktopCapability("agent-runtime")?.details?.deferredModels ===
17
- true
18
- ? await deferredModelCatalog(bridge, payload)
19
- : await bridge.invoke("desktop_agent_acp_models", payload);
12
+ const connection = await this.options.connection();
13
+ let catalog;
14
+ let firstError;
15
+ for (const provider of this.options.providerIds ?? [this.id]) {
16
+ try {
17
+ const current = await connection.client.request("providers.models", { provider, workspace: connection.workspace });
18
+ catalog = current;
19
+ if (current.models?.length)
20
+ break;
21
+ }
22
+ catch (error) {
23
+ firstError ??= error;
24
+ }
25
+ }
26
+ if (!catalog && firstError)
27
+ throw firstError;
28
+ catalog ??= { agent: this.id, models: [] };
20
29
  const current = catalog.currentModel?.trim();
21
30
  const entries = new Map((catalog.entries ?? []).map((entry) => [entry.id, entry]));
22
31
  return [...new Set(catalog.models ?? [])]
@@ -60,40 +69,3 @@ export class AcpModelProvider {
60
69
  }
61
70
  }
62
71
  }
63
- async function deferredModelCatalog(bridge, payload) {
64
- if (!bridge.onAgentRuntimeEvent) {
65
- throw new Error("The desktop agent host cannot deliver model catalogs.");
66
- }
67
- const requestId = crypto.randomUUID();
68
- let unsubscribe = () => { };
69
- let timeoutId;
70
- const result = new Promise((resolve, reject) => {
71
- unsubscribe = bridge.onAgentRuntimeEvent((event) => {
72
- if (event.sessionId !== requestId ||
73
- event.runId !== "model-catalog" ||
74
- event.event.type !== "event") {
75
- return;
76
- }
77
- const detail = event.event.event;
78
- if (detail?.type === "model_catalog") {
79
- resolve(detail.catalog);
80
- }
81
- else if (detail?.type === "model_catalog_error") {
82
- reject(new Error(String(detail.message ?? "Model discovery failed.")));
83
- }
84
- });
85
- timeoutId = setTimeout(() => reject(new Error("Desktop ACP model discovery timed out.")), 60_000);
86
- });
87
- try {
88
- const started = await bridge.invoke("desktop_agent_acp_models", { ...payload, requestId });
89
- if (started.requestId !== requestId) {
90
- throw new Error("The desktop agent host returned a different model request id.");
91
- }
92
- return await result;
93
- }
94
- finally {
95
- if (timeoutId)
96
- clearTimeout(timeoutId);
97
- unsubscribe();
98
- }
99
- }
@@ -10,12 +10,13 @@ export interface ModelProvider {
10
10
  authStatus(): Promise<ProviderAuthStatus>;
11
11
  }
12
12
  export interface AgentModelCatalog {
13
- listModels(provider: string): Promise<ModelRef[]>;
13
+ listModels(provider: string, runtime?: string): Promise<ModelRef[]>;
14
14
  }
15
+ export declare function modelCatalogProviderId(provider: string, runtime?: string): string;
15
16
  export declare class ModelProviderRegistry implements AgentModelCatalog {
16
17
  #private;
17
18
  constructor(providers?: ModelProvider[]);
18
- listModels(provider: string): Promise<ModelRef[]>;
19
+ listModels(provider: string, runtime?: string): Promise<ModelRef[]>;
19
20
  }
20
21
  export declare class StaticModelProvider implements ModelProvider {
21
22
  #private;
@@ -1,11 +1,24 @@
1
+ export function modelCatalogProviderId(provider, runtime) {
2
+ if (provider === "codex" && runtime === "codex-native")
3
+ return "codex-native";
4
+ if (provider === "opencode" && runtime === "opencode-native")
5
+ return "opencode-native";
6
+ return provider;
7
+ }
1
8
  export class ModelProviderRegistry {
2
9
  #providers = new Map();
3
10
  constructor(providers = []) {
4
11
  for (const provider of providers)
5
12
  this.#providers.set(provider.id, provider);
6
13
  }
7
- async listModels(provider) {
8
- return (await this.#providers.get(provider)?.listModels()) ?? [];
14
+ async listModels(provider, runtime) {
15
+ if (runtime === "fake")
16
+ return [];
17
+ const id = modelCatalogProviderId(provider, runtime);
18
+ const models = (await this.#providers.get(id)?.listModels()) ?? [];
19
+ return id === provider
20
+ ? models
21
+ : models.map((model) => ({ ...model, provider }));
9
22
  }
10
23
  }
11
24
  export class StaticModelProvider {
@@ -1,3 +1,6 @@
1
+ export { ControllerConnectionPool } from "./host/controller-connection";
2
+ export type { ControllerConnection } from "./host/controller-connection";
3
+ export { ControllerAgentRuntime } from "./host/controller-runtime";
1
4
  export { createAgentProcessHost } from "./host/desktop-process-host";
2
5
  export { UnavailableAgentProcessHost } from "./host/process-host";
3
6
  export type { AgentProcessHandle, AgentProcessHost, AgentProcessMessage, } from "./host/process-host";
@@ -1,3 +1,5 @@
1
+ export { ControllerConnectionPool } from "./host/controller-connection";
2
+ export { ControllerAgentRuntime } from "./host/controller-runtime";
1
3
  export { createAgentProcessHost } from "./host/desktop-process-host";
2
4
  export { UnavailableAgentProcessHost } from "./host/process-host";
3
5
  export { AcpAgentRuntime } from "./runtimes/acp/acp-runtime";
@@ -1,8 +1,11 @@
1
- export declare const ACP_AGENT_IDS: readonly ["codex", "cursor"];
1
+ export declare const ACP_AGENT_IDS: readonly ["codex", "cursor", "opencode"];
2
2
  export type AcpAgentId = (typeof ACP_AGENT_IDS)[number];
3
+ export declare const PROCESSOR_AGENT_IDS: readonly ["codex", "cursor"];
4
+ export type ProcessorAgentId = (typeof PROCESSOR_AGENT_IDS)[number];
3
5
  export declare const DEFAULT_ACP_AGENT: AcpAgentId;
4
6
  export declare function isAcpAgentId(value: unknown): value is AcpAgentId;
5
7
  export declare function normalizeAcpAgent(value: unknown): AcpAgentId;
8
+ export declare function normalizeProcessorAgent(value: unknown): ProcessorAgentId;
6
9
  export declare function catalogModelsForAgent<T extends {
7
10
  provider?: string;
8
11
  }>(agent: AcpAgentId, models: T[]): T[];
@@ -1,4 +1,5 @@
1
- export const ACP_AGENT_IDS = ["codex", "cursor"];
1
+ export const ACP_AGENT_IDS = ["codex", "cursor", "opencode"];
2
+ export const PROCESSOR_AGENT_IDS = ["codex", "cursor"];
2
3
  export const DEFAULT_ACP_AGENT = "codex";
3
4
  export function isAcpAgentId(value) {
4
5
  return ACP_AGENT_IDS.some((id) => id === value);
@@ -6,6 +7,9 @@ export function isAcpAgentId(value) {
6
7
  export function normalizeAcpAgent(value) {
7
8
  return isAcpAgentId(value) ? value : DEFAULT_ACP_AGENT;
8
9
  }
10
+ export function normalizeProcessorAgent(value) {
11
+ return PROCESSOR_AGENT_IDS.find((id) => id === value) ?? "codex";
12
+ }
9
13
  export function catalogModelsForAgent(agent, models) {
10
14
  return models.filter((model) => model.provider === agent || !model.provider);
11
15
  }
@@ -1,10 +1,11 @@
1
1
  import { PluginSettingTab, Setting, } from "@lapis-notes/api";
2
- import { ACP_AGENT_IDS } from "./acp-agents";
2
+ import { ACP_AGENT_IDS, PROCESSOR_AGENT_IDS, normalizeProcessorAgent, } from "./acp-agents";
3
3
  import { applyAppToolEnablement } from "./ai-settings";
4
4
  import { listAppToolSettingRows, registeredAppToolRefs, } from "./app-tool-setting-rows";
5
5
  const ACP_AGENT_LABELS = {
6
6
  codex: "Codex",
7
7
  cursor: "Cursor",
8
+ opencode: "OpenCode",
8
9
  };
9
10
  const THINKING_OPTIONS = [
10
11
  { id: "off", label: "Off" },
@@ -34,6 +35,7 @@ export class AiSettingsTab extends PluginSettingTab {
34
35
  .addOption("auto", "Automatic")
35
36
  .addOption("acp", "ACP")
36
37
  .addOption("codex-native", "Codex native")
38
+ .addOption("opencode-native", "OpenCode native")
37
39
  .addOption("fake", "Fake (tests)")
38
40
  .setValue(settings.defaultRuntime)
39
41
  .onChange((value) => {
@@ -122,14 +124,14 @@ export class AiSettingsTab extends PluginSettingTab {
122
124
  .setName("Memory processor agent")
123
125
  .setDesc("Pinned independently from the interactive chat agent.")
124
126
  .addDropdown((dropdown) => {
125
- for (const id of ACP_AGENT_IDS) {
127
+ for (const id of PROCESSOR_AGENT_IDS) {
126
128
  dropdown.addOption(id, ACP_AGENT_LABELS[id]);
127
129
  }
128
130
  dropdown
129
131
  .setValue(settings.memoryConsolidationAgent)
130
132
  .onChange((value) => {
131
133
  void this.aiPlugin.updateSettings({
132
- memoryConsolidationAgent: value,
134
+ memoryConsolidationAgent: normalizeProcessorAgent(value),
133
135
  });
134
136
  });
135
137
  });
@@ -171,12 +173,12 @@ export class AiSettingsTab extends PluginSettingTab {
171
173
  .setName("Handoff summary agent")
172
174
  .setDesc("Pinned independently from the interactive chat agent.")
173
175
  .addDropdown((dropdown) => {
174
- for (const id of ACP_AGENT_IDS) {
176
+ for (const id of PROCESSOR_AGENT_IDS) {
175
177
  dropdown.addOption(id, ACP_AGENT_LABELS[id]);
176
178
  }
177
179
  dropdown.setValue(settings.handoffSummaryAgent).onChange((value) => {
178
180
  void this.aiPlugin.updateSettings({
179
- handoffSummaryAgent: value,
181
+ handoffSummaryAgent: normalizeProcessorAgent(value),
180
182
  });
181
183
  });
182
184
  });
@@ -214,7 +216,7 @@ export class AiSettingsTab extends PluginSettingTab {
214
216
  }
215
217
  async loadModels(dropdown, settings, setting) {
216
218
  try {
217
- const models = await this.aiPlugin.models.listModels(settings.acpAgent);
219
+ const models = await this.aiPlugin.models.listModels(settings.acpAgent, settings.defaultRuntime);
218
220
  if (models.length === 0) {
219
221
  setting.setDesc(`The ${settings.acpAgent} provider returned no model catalog; keeping the saved selection.`);
220
222
  dropdown.setDisabled(false);