@lasso-ai/cli 1.5.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/agent.d.ts +1 -0
- package/dist/cli/backends.d.ts +53 -0
- package/dist/cli/backends.js +267 -0
- package/dist/cli/bridge.d.ts +8 -5
- package/dist/cli/bridge.js +357 -151
- package/dist/cli/host/install.js +12 -1
- package/dist/cli/local-runtime.d.ts +28 -0
- package/dist/cli/local-runtime.js +294 -0
- package/dist/cli/oneshot.js +28 -10
- package/dist/cli/runtime-client.d.ts +91 -10
- package/dist/cli/runtime-client.js +38 -3
- package/dist/overlay.js +44 -28
- package/dist/runtime/__tests__/adapter-normalization.test.d.ts +1 -0
- package/dist/runtime/__tests__/adapter-normalization.test.js +392 -0
- package/dist/runtime/__tests__/state-machine.test.d.ts +1 -0
- package/dist/runtime/__tests__/state-machine.test.js +107 -0
- package/dist/runtime/__tests__/verification-engine.test.d.ts +1 -0
- package/dist/runtime/__tests__/verification-engine.test.js +91 -0
- package/dist/runtime/__tests__/workspace-guard.test.d.ts +1 -0
- package/dist/runtime/__tests__/workspace-guard.test.js +104 -0
- package/dist/runtime/adapters/claude-code-adapter.d.ts +46 -0
- package/dist/runtime/adapters/claude-code-adapter.js +230 -0
- package/dist/runtime/adapters/codex-cli-adapter.d.ts +44 -0
- package/dist/runtime/adapters/codex-cli-adapter.js +202 -0
- package/dist/runtime/adapters/cursor-adapter.d.ts +47 -0
- package/dist/runtime/adapters/cursor-adapter.js +214 -0
- package/dist/runtime/adapters/factory.d.ts +19 -0
- package/dist/runtime/adapters/factory.js +48 -0
- package/dist/runtime/adapters/local-agent-adapter.d.ts +143 -0
- package/dist/runtime/adapters/local-agent-adapter.js +408 -0
- package/dist/runtime/adapters/opencode-adapter.d.ts +43 -0
- package/dist/runtime/adapters/opencode-adapter.js +210 -0
- package/dist/runtime/agent-runtime.d.ts +286 -0
- package/dist/runtime/agent-runtime.js +8 -0
- package/dist/runtime/agent-state-machine.d.ts +67 -0
- package/dist/runtime/agent-state-machine.js +234 -0
- package/dist/runtime/backend-interface.d.ts +272 -0
- package/dist/runtime/backend-interface.js +38 -0
- package/dist/runtime/event-mapper.d.ts +11 -0
- package/dist/runtime/event-mapper.js +40 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +43 -0
- package/dist/runtime/local-session-runner.d.ts +60 -0
- package/dist/runtime/local-session-runner.js +185 -0
- package/dist/runtime/review.d.ts +24 -0
- package/dist/runtime/review.js +30 -0
- package/dist/runtime/verification-engine.d.ts +71 -0
- package/dist/runtime/verification-engine.js +374 -0
- package/dist/runtime/workspace-guard.d.ts +70 -0
- package/dist/runtime/workspace-guard.js +321 -0
- package/docs/local-coding-agents.md +183 -0
- package/package.json +22 -3
package/dist/cli/host/install.js
CHANGED
|
@@ -27,7 +27,18 @@ const AGENT_LABEL = "com.lasso.host";
|
|
|
27
27
|
const LAUNCH_AGENT_FILE = node_path_1.default.join(node_os_1.default.homedir(), "Library", "LaunchAgents", `${AGENT_LABEL}.plist`);
|
|
28
28
|
function cliEntry() {
|
|
29
29
|
const entry = process.argv[1];
|
|
30
|
-
|
|
30
|
+
if (!entry)
|
|
31
|
+
return null;
|
|
32
|
+
// Resolve symlinks (e.g. pnpm's global bin is extensionless) so the
|
|
33
|
+
// spawned child gets a real .js path.
|
|
34
|
+
let resolved = entry;
|
|
35
|
+
try {
|
|
36
|
+
resolved = node_fs_1.default.realpathSync(entry);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// keep the original if realpath fails
|
|
40
|
+
}
|
|
41
|
+
return resolved.endsWith(".js") ? resolved : null;
|
|
31
42
|
}
|
|
32
43
|
function spawnDaemon(env) {
|
|
33
44
|
const port = (0, paths_1.hostProxyPort)(env);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalRuntime — local coding agent execution on the user's machine.
|
|
3
|
+
*
|
|
4
|
+
* Implements the same session surface the CLI bridge uses for hosted
|
|
5
|
+
* sessions (RuntimeClient), but runs Claude Code / OpenCode / Codex CLI /
|
|
6
|
+
* Cursor Agent in THIS process: prompts, project context, and source code
|
|
7
|
+
* never travel through Lasso's server. Session state is in-memory (per CLI
|
|
8
|
+
* process) and the review-first workflow comes from the shared runtime core
|
|
9
|
+
* (LocalSessionRunner + WorkspaceGuard + VerificationEngine).
|
|
10
|
+
*/
|
|
11
|
+
import type { RuntimeSession, RuntimeSessionInput, RuntimeEventHandler, SessionVerificationResult, RuntimeFacade } from "./runtime-client.js";
|
|
12
|
+
export declare class LocalRuntime implements RuntimeFacade {
|
|
13
|
+
private sessions;
|
|
14
|
+
private abortControllers;
|
|
15
|
+
createSession(input: RuntimeSessionInput): Promise<RuntimeSession>;
|
|
16
|
+
getSession(sessionId: string): Promise<RuntimeSession>;
|
|
17
|
+
runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal): Promise<void>;
|
|
18
|
+
verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
|
|
19
|
+
switchModel(sessionId: string, model: {
|
|
20
|
+
provider: string;
|
|
21
|
+
id: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
}, _apiKey?: string, backendConfig?: RuntimeSessionInput["backendConfig"]): Promise<void>;
|
|
24
|
+
cancelSession(sessionId: string): Promise<void>;
|
|
25
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
26
|
+
private toSession;
|
|
27
|
+
}
|
|
28
|
+
export declare function getLocalRuntime(): LocalRuntime;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LocalRuntime — local coding agent execution on the user's machine.
|
|
4
|
+
*
|
|
5
|
+
* Implements the same session surface the CLI bridge uses for hosted
|
|
6
|
+
* sessions (RuntimeClient), but runs Claude Code / OpenCode / Codex CLI /
|
|
7
|
+
* Cursor Agent in THIS process: prompts, project context, and source code
|
|
8
|
+
* never travel through Lasso's server. Session state is in-memory (per CLI
|
|
9
|
+
* process) and the review-first workflow comes from the shared runtime core
|
|
10
|
+
* (LocalSessionRunner + WorkspaceGuard + VerificationEngine).
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.LocalRuntime = void 0;
|
|
17
|
+
exports.getLocalRuntime = getLocalRuntime;
|
|
18
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
19
|
+
const index_js_1 = require("../runtime/index.js");
|
|
20
|
+
class LocalRuntime {
|
|
21
|
+
sessions = new Map();
|
|
22
|
+
abortControllers = new Map();
|
|
23
|
+
async createSession(input) {
|
|
24
|
+
const backendConfig = input.backendConfig;
|
|
25
|
+
if (!(0, index_js_1.isLocalAgentConfig)(backendConfig)) {
|
|
26
|
+
throw new Error("LocalRuntime only accepts local_agent backend configs");
|
|
27
|
+
}
|
|
28
|
+
if (!backendConfig.projectPath) {
|
|
29
|
+
throw new Error("LocalRuntime requires backendConfig.projectPath");
|
|
30
|
+
}
|
|
31
|
+
const id = node_crypto_1.default.randomUUID();
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const record = {
|
|
34
|
+
id,
|
|
35
|
+
phase: "execute",
|
|
36
|
+
status: "running",
|
|
37
|
+
iteration: 0,
|
|
38
|
+
userPrompt: input.userPrompt,
|
|
39
|
+
conversation: [
|
|
40
|
+
{ id: node_crypto_1.default.randomUUID(), role: "user", content: input.userPrompt, timestamp: now.getTime() },
|
|
41
|
+
],
|
|
42
|
+
observations: [],
|
|
43
|
+
actions: [],
|
|
44
|
+
completedActions: [],
|
|
45
|
+
errors: [],
|
|
46
|
+
selection: input.selection,
|
|
47
|
+
project: input.project,
|
|
48
|
+
model: input.model,
|
|
49
|
+
createdAt: now,
|
|
50
|
+
updatedAt: now,
|
|
51
|
+
metadata: { backendConfig },
|
|
52
|
+
backendConfig: { ...backendConfig, type: "local_agent" },
|
|
53
|
+
projectPath: backendConfig.projectPath,
|
|
54
|
+
input,
|
|
55
|
+
running: false,
|
|
56
|
+
};
|
|
57
|
+
this.sessions.set(id, record);
|
|
58
|
+
return this.toSession(record);
|
|
59
|
+
}
|
|
60
|
+
async getSession(sessionId) {
|
|
61
|
+
const record = this.sessions.get(sessionId);
|
|
62
|
+
if (!record)
|
|
63
|
+
throw new Error("Session not found");
|
|
64
|
+
return this.toSession(record);
|
|
65
|
+
}
|
|
66
|
+
async runSession(sessionId, projectPath, onEvent, signal) {
|
|
67
|
+
const record = this.sessions.get(sessionId);
|
|
68
|
+
if (!record)
|
|
69
|
+
throw new Error("Session not found");
|
|
70
|
+
if (record.running)
|
|
71
|
+
throw new Error("Session is already running");
|
|
72
|
+
record.running = true;
|
|
73
|
+
record.status = "running";
|
|
74
|
+
record.phase = "execute";
|
|
75
|
+
record.updatedAt = new Date();
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
this.abortControllers.set(sessionId, controller);
|
|
78
|
+
const onOuterAbort = () => controller.abort();
|
|
79
|
+
signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
80
|
+
const emit = (event) => {
|
|
81
|
+
void onEvent(event);
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
// Fresh capture per run — corrective re-runs capture the post-apply state.
|
|
85
|
+
const guard = await index_js_1.WorkspaceGuard.capture(projectPath);
|
|
86
|
+
record.backend =
|
|
87
|
+
record.backend ||
|
|
88
|
+
(await (0, index_js_1.createLocalAgentBackend)({ ...record.backendConfig, type: "local_agent", projectPath }));
|
|
89
|
+
const previousErrors = (record.errors || []);
|
|
90
|
+
const externalSessionId = typeof record.metadata.backendSessionId === "string" ? record.metadata.backendSessionId : undefined;
|
|
91
|
+
const runner = new index_js_1.LocalSessionRunner({
|
|
92
|
+
sessionId: record.id,
|
|
93
|
+
backend: record.backend,
|
|
94
|
+
guard,
|
|
95
|
+
emit,
|
|
96
|
+
phase: "execute",
|
|
97
|
+
});
|
|
98
|
+
const outcome = await runner.run({
|
|
99
|
+
taskId: record.id,
|
|
100
|
+
userPrompt: record.userPrompt,
|
|
101
|
+
project: {
|
|
102
|
+
projectId: record.project.projectId || "local",
|
|
103
|
+
framework: record.project.framework,
|
|
104
|
+
hasTypeScript: Boolean(record.project.hasTypeScript),
|
|
105
|
+
hasTailwind: false,
|
|
106
|
+
hasReact: Boolean(record.project.hasReact),
|
|
107
|
+
hasNext: Boolean(record.project.hasNext),
|
|
108
|
+
hasVite: Boolean(record.project.hasVite),
|
|
109
|
+
files: record.project.files || [],
|
|
110
|
+
},
|
|
111
|
+
selection: record.selection,
|
|
112
|
+
conversation: record.conversation.map((msg) => ({
|
|
113
|
+
role: msg.role,
|
|
114
|
+
content: msg.content,
|
|
115
|
+
timestamp: msg.timestamp,
|
|
116
|
+
})),
|
|
117
|
+
previousObservations: [],
|
|
118
|
+
previousErrors,
|
|
119
|
+
currentPhase: "execute",
|
|
120
|
+
constraints: { safetyLevel: "standard" },
|
|
121
|
+
sessionRef: externalSessionId ? { id: externalSessionId } : undefined,
|
|
122
|
+
}, controller.signal);
|
|
123
|
+
// Persist review payload + normalized actions for the review flow.
|
|
124
|
+
record.actions = outcome.actions;
|
|
125
|
+
record.observations.push(...outcome.observations.map((obs) => ({
|
|
126
|
+
id: obs.id,
|
|
127
|
+
type: String(obs.type),
|
|
128
|
+
summary: obs.summary,
|
|
129
|
+
data: obs.data,
|
|
130
|
+
timestamp: obs.timestamp,
|
|
131
|
+
phase: String(obs.phase),
|
|
132
|
+
})));
|
|
133
|
+
record.errors = outcome.errors.map((err) => ({
|
|
134
|
+
id: err.id,
|
|
135
|
+
type: String(err.type),
|
|
136
|
+
message: err.message,
|
|
137
|
+
phase: String(err.phase),
|
|
138
|
+
timestamp: err.timestamp,
|
|
139
|
+
recoverable: err.recoverable,
|
|
140
|
+
}));
|
|
141
|
+
if (outcome.conversation.length > 0) {
|
|
142
|
+
record.conversation.push(...outcome.conversation.map((msg) => ({
|
|
143
|
+
id: node_crypto_1.default.randomUUID(),
|
|
144
|
+
role: msg.role,
|
|
145
|
+
content: msg.content,
|
|
146
|
+
timestamp: msg.timestamp,
|
|
147
|
+
})));
|
|
148
|
+
}
|
|
149
|
+
record.metadata = {
|
|
150
|
+
...record.metadata,
|
|
151
|
+
...outcome.metadata,
|
|
152
|
+
pendingChanges: outcome.changes,
|
|
153
|
+
binaryChanges: outcome.binaryPaths,
|
|
154
|
+
};
|
|
155
|
+
record.status =
|
|
156
|
+
outcome.status === "waiting" ? "waiting" : outcome.status === "completed" ? "completed" : "failed";
|
|
157
|
+
record.phase = "complete";
|
|
158
|
+
record.updatedAt = new Date();
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
record.status = "failed";
|
|
162
|
+
record.errors = [
|
|
163
|
+
...record.errors,
|
|
164
|
+
{
|
|
165
|
+
id: node_crypto_1.default.randomUUID(),
|
|
166
|
+
type: "execution",
|
|
167
|
+
message: error instanceof Error ? error.message : "Local agent run failed",
|
|
168
|
+
phase: "execute",
|
|
169
|
+
timestamp: Date.now(),
|
|
170
|
+
recoverable: false,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
record.updatedAt = new Date();
|
|
174
|
+
emit({
|
|
175
|
+
type: "error",
|
|
176
|
+
sessionId: record.id,
|
|
177
|
+
timestamp: Date.now(),
|
|
178
|
+
data: { error: error instanceof Error ? error.message : "Local agent run failed" },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
signal?.removeEventListener("abort", onOuterAbort);
|
|
183
|
+
this.abortControllers.delete(sessionId);
|
|
184
|
+
record.running = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async verifySession(sessionId, projectPath) {
|
|
188
|
+
const record = this.sessions.get(sessionId);
|
|
189
|
+
if (!record)
|
|
190
|
+
throw new Error("Session not found");
|
|
191
|
+
const engine = new index_js_1.VerificationEngine(projectPath);
|
|
192
|
+
const results = await engine.verifyAll("verify");
|
|
193
|
+
const observations = engine.getObservations(results);
|
|
194
|
+
const errors = engine.getErrors(results);
|
|
195
|
+
const passed = engine.allPassed(results);
|
|
196
|
+
// Keep verification errors so a corrective re-run receives them as context.
|
|
197
|
+
if (!passed) {
|
|
198
|
+
record.errors = [
|
|
199
|
+
...record.errors,
|
|
200
|
+
...errors.map((err) => ({
|
|
201
|
+
id: err.id,
|
|
202
|
+
type: String(err.type),
|
|
203
|
+
message: err.message,
|
|
204
|
+
phase: String(err.phase),
|
|
205
|
+
timestamp: err.timestamp,
|
|
206
|
+
recoverable: err.recoverable,
|
|
207
|
+
})),
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
const command = results.map((r) => r.command).find((c) => typeof c === "string");
|
|
211
|
+
return { passed, errors, observations, command, requiresFix: !passed };
|
|
212
|
+
}
|
|
213
|
+
async switchModel(sessionId, model, _apiKey, backendConfig) {
|
|
214
|
+
const record = this.sessions.get(sessionId);
|
|
215
|
+
if (!record)
|
|
216
|
+
throw new Error("Session not found");
|
|
217
|
+
if (!(0, index_js_1.isLocalAgentConfig)(backendConfig)) {
|
|
218
|
+
throw new Error("LocalRuntime.switchModel requires a local_agent backendConfig");
|
|
219
|
+
}
|
|
220
|
+
const nextBackend = await (0, index_js_1.createLocalAgentBackend)({ ...backendConfig, type: "local_agent" });
|
|
221
|
+
if (record.backend) {
|
|
222
|
+
await record.backend.switchBackend(nextBackend, {
|
|
223
|
+
taskId: record.id,
|
|
224
|
+
userPrompt: record.userPrompt,
|
|
225
|
+
project: {
|
|
226
|
+
projectId: record.project.projectId || "local",
|
|
227
|
+
hasTypeScript: Boolean(record.project.hasTypeScript),
|
|
228
|
+
hasTailwind: false,
|
|
229
|
+
hasReact: Boolean(record.project.hasReact),
|
|
230
|
+
hasNext: Boolean(record.project.hasNext),
|
|
231
|
+
hasVite: Boolean(record.project.hasVite),
|
|
232
|
+
files: record.project.files || [],
|
|
233
|
+
},
|
|
234
|
+
conversation: [],
|
|
235
|
+
previousObservations: [],
|
|
236
|
+
previousErrors: [],
|
|
237
|
+
currentPhase: "execute",
|
|
238
|
+
constraints: {},
|
|
239
|
+
sessionRef: typeof record.metadata.backendSessionId === "string"
|
|
240
|
+
? { id: record.metadata.backendSessionId }
|
|
241
|
+
: undefined,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
record.backend = nextBackend;
|
|
245
|
+
record.backendConfig = { ...backendConfig, type: "local_agent" };
|
|
246
|
+
record.model = { provider: model.provider, id: model.id, name: model.name };
|
|
247
|
+
record.metadata = {
|
|
248
|
+
...record.metadata,
|
|
249
|
+
backendConfig: record.backendConfig,
|
|
250
|
+
backendModel: model.id,
|
|
251
|
+
};
|
|
252
|
+
// Switching agents drops the previous agent's external session: context
|
|
253
|
+
// cannot be transferred across CLI agents.
|
|
254
|
+
delete record.metadata.backendSessionId;
|
|
255
|
+
record.updatedAt = new Date();
|
|
256
|
+
}
|
|
257
|
+
async cancelSession(sessionId) {
|
|
258
|
+
const controller = this.abortControllers.get(sessionId);
|
|
259
|
+
if (controller)
|
|
260
|
+
controller.abort();
|
|
261
|
+
}
|
|
262
|
+
async deleteSession(sessionId) {
|
|
263
|
+
this.sessions.delete(sessionId);
|
|
264
|
+
this.abortControllers.delete(sessionId);
|
|
265
|
+
}
|
|
266
|
+
toSession(record) {
|
|
267
|
+
return {
|
|
268
|
+
id: record.id,
|
|
269
|
+
phase: record.phase,
|
|
270
|
+
status: record.status,
|
|
271
|
+
iteration: record.iteration,
|
|
272
|
+
userPrompt: record.userPrompt,
|
|
273
|
+
conversation: record.conversation,
|
|
274
|
+
observations: record.observations,
|
|
275
|
+
actions: record.actions,
|
|
276
|
+
completedActions: record.completedActions,
|
|
277
|
+
errors: record.errors,
|
|
278
|
+
selection: record.selection,
|
|
279
|
+
project: record.project,
|
|
280
|
+
model: record.model,
|
|
281
|
+
createdAt: record.createdAt,
|
|
282
|
+
updatedAt: record.updatedAt,
|
|
283
|
+
metadata: record.metadata,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
exports.LocalRuntime = LocalRuntime;
|
|
288
|
+
/** Process-wide local runtime (sessions live for the CLI bridge lifetime). */
|
|
289
|
+
let sharedLocalRuntime;
|
|
290
|
+
function getLocalRuntime() {
|
|
291
|
+
if (!sharedLocalRuntime)
|
|
292
|
+
sharedLocalRuntime = new LocalRuntime();
|
|
293
|
+
return sharedLocalRuntime;
|
|
294
|
+
}
|
package/dist/cli/oneshot.js
CHANGED
|
@@ -138,7 +138,7 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
|
|
|
138
138
|
onProgress("review", "Verifying changes...");
|
|
139
139
|
break;
|
|
140
140
|
case "verification_completed":
|
|
141
|
-
const passed = event.data.passed;
|
|
141
|
+
const passed = event.data.success ?? event.data.passed;
|
|
142
142
|
onProgress(passed ? "working" : "error", passed ? "Verification passed" : "Verification failed");
|
|
143
143
|
break;
|
|
144
144
|
case "waiting_for_user":
|
|
@@ -173,27 +173,45 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
|
|
|
173
173
|
function extractChangesFromSession(session) {
|
|
174
174
|
if (!session)
|
|
175
175
|
return [];
|
|
176
|
+
const pending = session.metadata?.pendingChanges;
|
|
177
|
+
if (Array.isArray(pending) && pending.length > 0) {
|
|
178
|
+
return pending.map((change) => ({
|
|
179
|
+
filePath: change.filePath ?? change.path,
|
|
180
|
+
oldString: change.oldString ?? "",
|
|
181
|
+
newString: change.newString ?? "",
|
|
182
|
+
operation: change.operation ||
|
|
183
|
+
(change.oldString === "" && change.newString === "" ? "deleted" : change.oldString === "" ? "created" : "modified"),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
176
186
|
const changes = [];
|
|
177
187
|
for (const action of session.actions || []) {
|
|
178
|
-
if (action.
|
|
188
|
+
if (!action.data)
|
|
189
|
+
continue;
|
|
190
|
+
const filePath = action.data.filePath ?? action.data.path;
|
|
191
|
+
if (!filePath)
|
|
192
|
+
continue;
|
|
193
|
+
if (action.type === "edit_file") {
|
|
179
194
|
changes.push({
|
|
180
|
-
filePath
|
|
181
|
-
oldString: action.data.oldString,
|
|
182
|
-
newString: action.data.newString,
|
|
195
|
+
filePath,
|
|
196
|
+
oldString: action.data.oldString ?? "",
|
|
197
|
+
newString: action.data.newString ?? "",
|
|
198
|
+
operation: "modified",
|
|
183
199
|
});
|
|
184
200
|
}
|
|
185
|
-
else if (action.type === "create_file"
|
|
201
|
+
else if (action.type === "create_file") {
|
|
186
202
|
changes.push({
|
|
187
|
-
filePath
|
|
203
|
+
filePath,
|
|
188
204
|
oldString: "",
|
|
189
205
|
newString: action.data.content || "",
|
|
206
|
+
operation: "created",
|
|
190
207
|
});
|
|
191
208
|
}
|
|
192
|
-
else if (action.type === "delete_file"
|
|
209
|
+
else if (action.type === "delete_file") {
|
|
193
210
|
changes.push({
|
|
194
|
-
filePath
|
|
195
|
-
oldString: "(deleted)",
|
|
211
|
+
filePath,
|
|
212
|
+
oldString: action.data.oldContent ?? "(deleted)",
|
|
196
213
|
newString: "",
|
|
214
|
+
operation: "deleted",
|
|
197
215
|
});
|
|
198
216
|
}
|
|
199
217
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Client for the new Lasso Agentic Runtime
|
|
5
5
|
* Uses SSE for real-time progress events
|
|
6
6
|
*/
|
|
7
|
+
export type RuntimeProvider = "anthropic" | "openai" | "google" | "nvidia" | "ollama" | "claude_code" | "opencode" | "codex_cli" | "cursor";
|
|
7
8
|
export interface RuntimeSessionInput {
|
|
8
9
|
userPrompt: string;
|
|
9
10
|
project: {
|
|
@@ -17,7 +18,7 @@ export interface RuntimeSessionInput {
|
|
|
17
18
|
currentRoute?: string;
|
|
18
19
|
};
|
|
19
20
|
model: {
|
|
20
|
-
provider:
|
|
21
|
+
provider: RuntimeProvider;
|
|
21
22
|
id: string;
|
|
22
23
|
name?: string;
|
|
23
24
|
};
|
|
@@ -38,6 +39,12 @@ export interface RuntimeSessionInput {
|
|
|
38
39
|
screenshot?: string;
|
|
39
40
|
};
|
|
40
41
|
conversationUid?: string;
|
|
42
|
+
backendConfig?: {
|
|
43
|
+
type: "hosted_model" | "local_agent";
|
|
44
|
+
provider: "claude_code" | "opencode" | "codex_cli" | "cursor";
|
|
45
|
+
modelId?: string;
|
|
46
|
+
projectPath?: string;
|
|
47
|
+
};
|
|
41
48
|
}
|
|
42
49
|
export interface RuntimeSession {
|
|
43
50
|
id: string;
|
|
@@ -97,6 +104,44 @@ export interface RuntimeSession {
|
|
|
97
104
|
conversationUid?: string;
|
|
98
105
|
createdAt: Date;
|
|
99
106
|
updatedAt: Date;
|
|
107
|
+
/** Runtime-owned metadata: pendingChanges, backendConfig, backendSessionId, … */
|
|
108
|
+
metadata?: Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
/** Result of post-apply verification (typecheck/build via VerificationEngine). */
|
|
111
|
+
export interface SessionVerificationResult {
|
|
112
|
+
passed: boolean;
|
|
113
|
+
errors: Array<{
|
|
114
|
+
id: string;
|
|
115
|
+
type: string;
|
|
116
|
+
message: string;
|
|
117
|
+
phase: string;
|
|
118
|
+
timestamp: number;
|
|
119
|
+
recoverable: boolean;
|
|
120
|
+
}>;
|
|
121
|
+
observations: Array<{
|
|
122
|
+
id: string;
|
|
123
|
+
type: string;
|
|
124
|
+
summary: string;
|
|
125
|
+
timestamp: number;
|
|
126
|
+
}>;
|
|
127
|
+
command?: string;
|
|
128
|
+
requiresFix: boolean;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The session surface shared by RuntimeClient (hosted, via Lasso server) and
|
|
132
|
+
* LocalRuntime (local coding agents, executed in this process).
|
|
133
|
+
*/
|
|
134
|
+
export interface RuntimeFacade {
|
|
135
|
+
createSession(input: RuntimeSessionInput): Promise<RuntimeSession>;
|
|
136
|
+
runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal): Promise<void>;
|
|
137
|
+
getSession(sessionId: string): Promise<RuntimeSession>;
|
|
138
|
+
verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
|
|
139
|
+
switchModel(sessionId: string, model: {
|
|
140
|
+
provider: string;
|
|
141
|
+
id: string;
|
|
142
|
+
name?: string;
|
|
143
|
+
}, apiKey?: string, backendConfig?: RuntimeSessionInput["backendConfig"]): Promise<void>;
|
|
144
|
+
cancelSession(sessionId: string): Promise<void>;
|
|
100
145
|
}
|
|
101
146
|
export type RuntimeEvent = {
|
|
102
147
|
type: "agent_started";
|
|
@@ -166,12 +211,7 @@ export type RuntimeEvent = {
|
|
|
166
211
|
} | {
|
|
167
212
|
type: "error";
|
|
168
213
|
sessionId: string;
|
|
169
|
-
data:
|
|
170
|
-
errorType: string;
|
|
171
|
-
message: string;
|
|
172
|
-
phase: string;
|
|
173
|
-
recoverable: boolean;
|
|
174
|
-
};
|
|
214
|
+
data: Record<string, unknown>;
|
|
175
215
|
timestamp: number;
|
|
176
216
|
} | {
|
|
177
217
|
type: "model_changed";
|
|
@@ -184,7 +224,22 @@ export type RuntimeEvent = {
|
|
|
184
224
|
} | {
|
|
185
225
|
type: "agent_completed";
|
|
186
226
|
sessionId: string;
|
|
187
|
-
data:
|
|
227
|
+
data: Record<string, unknown>;
|
|
228
|
+
timestamp: number;
|
|
229
|
+
} | {
|
|
230
|
+
type: "agent_cancelled";
|
|
231
|
+
sessionId: string;
|
|
232
|
+
data: Record<string, unknown>;
|
|
233
|
+
timestamp: number;
|
|
234
|
+
} | {
|
|
235
|
+
type: "agent_failed";
|
|
236
|
+
sessionId: string;
|
|
237
|
+
data: Record<string, unknown>;
|
|
238
|
+
timestamp: number;
|
|
239
|
+
} | {
|
|
240
|
+
type: "file_changed";
|
|
241
|
+
sessionId: string;
|
|
242
|
+
data: Record<string, unknown>;
|
|
188
243
|
timestamp: number;
|
|
189
244
|
} | {
|
|
190
245
|
type: "started";
|
|
@@ -215,13 +270,39 @@ export declare class RuntimeClient {
|
|
|
215
270
|
*/
|
|
216
271
|
cancelSession(sessionId: string): Promise<void>;
|
|
217
272
|
/**
|
|
218
|
-
* Switch model for a session
|
|
273
|
+
* Switch model/backend for a session
|
|
219
274
|
*/
|
|
220
275
|
switchModel(sessionId: string, model: {
|
|
221
276
|
provider: string;
|
|
222
277
|
id: string;
|
|
223
278
|
name?: string;
|
|
224
|
-
}, apiKey?: string): Promise<void>;
|
|
279
|
+
}, apiKey?: string, backendConfig?: RuntimeSessionInput["backendConfig"]): Promise<void>;
|
|
280
|
+
/**
|
|
281
|
+
* Verify the workspace for a session (after reviewed changes were applied).
|
|
282
|
+
*
|
|
283
|
+
* Returns whether verification passed; on failure the session keeps the
|
|
284
|
+
* verification errors so a corrective re-run can fix them.
|
|
285
|
+
*/
|
|
286
|
+
verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
|
|
287
|
+
/**
|
|
288
|
+
* List available backends from the server (hosted providers + local agents).
|
|
289
|
+
*/
|
|
290
|
+
listBackends(projectPath?: string): Promise<{
|
|
291
|
+
backends: Array<{
|
|
292
|
+
provider: string;
|
|
293
|
+
type: string;
|
|
294
|
+
available: boolean;
|
|
295
|
+
authenticated: boolean;
|
|
296
|
+
installed?: boolean;
|
|
297
|
+
version?: string;
|
|
298
|
+
error?: string;
|
|
299
|
+
fixInstructions?: string;
|
|
300
|
+
models?: Array<{
|
|
301
|
+
id: string;
|
|
302
|
+
label: string;
|
|
303
|
+
}>;
|
|
304
|
+
}>;
|
|
305
|
+
}>;
|
|
225
306
|
/**
|
|
226
307
|
* Delete a session
|
|
227
308
|
*/
|
|
@@ -110,21 +110,56 @@ class RuntimeClient {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
|
-
* Switch model for a session
|
|
113
|
+
* Switch model/backend for a session
|
|
114
114
|
*/
|
|
115
|
-
async switchModel(sessionId, model, apiKey) {
|
|
115
|
+
async switchModel(sessionId, model, apiKey, backendConfig) {
|
|
116
116
|
const response = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/model`, {
|
|
117
117
|
method: "POST",
|
|
118
118
|
headers: {
|
|
119
119
|
"Content-Type": "application/json",
|
|
120
120
|
...(this.apiKey && { "Authorization": `Bearer ${this.apiKey}` }),
|
|
121
121
|
},
|
|
122
|
-
body: JSON.stringify({ ...model, apiKey }),
|
|
122
|
+
body: JSON.stringify({ ...model, apiKey, backendConfig }),
|
|
123
123
|
});
|
|
124
124
|
if (!response.ok) {
|
|
125
125
|
throw new Error(`Failed to switch model: ${response.statusText}`);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Verify the workspace for a session (after reviewed changes were applied).
|
|
130
|
+
*
|
|
131
|
+
* Returns whether verification passed; on failure the session keeps the
|
|
132
|
+
* verification errors so a corrective re-run can fix them.
|
|
133
|
+
*/
|
|
134
|
+
async verifySession(sessionId, projectPath) {
|
|
135
|
+
const response = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/verify`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: {
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
...(this.apiKey && { "Authorization": `Bearer ${this.apiKey}` }),
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify({ projectPath }),
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new Error(`Failed to verify session: ${response.statusText}`);
|
|
145
|
+
}
|
|
146
|
+
return (await response.json());
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* List available backends from the server (hosted providers + local agents).
|
|
150
|
+
*/
|
|
151
|
+
async listBackends(projectPath) {
|
|
152
|
+
const url = `${this.serverUrl}/api/v1/agent-runtime/backends${projectPath ? `?projectPath=${encodeURIComponent(projectPath)}` : ""}`;
|
|
153
|
+
const response = await fetch(url, {
|
|
154
|
+
headers: {
|
|
155
|
+
...(this.apiKey && { "Authorization": `Bearer ${this.apiKey}` }),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error(`Failed to list backends: ${response.statusText}`);
|
|
160
|
+
}
|
|
161
|
+
return (await response.json());
|
|
162
|
+
}
|
|
128
163
|
/**
|
|
129
164
|
* Delete a session
|
|
130
165
|
*/
|