@alisio/core 0.1.0-alpha.17 → 0.1.0-alpha.18
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/agents/active.d.ts +66 -0
- package/dist/agents/active.js +113 -0
- package/dist/application.d.ts +9 -3
- package/dist/application.js +22 -5
- package/dist/commands/builtins.d.ts +10 -0
- package/dist/commands/builtins.js +115 -0
- package/dist/commands/catalog.d.ts +83 -0
- package/dist/commands/catalog.js +268 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +27 -0
- package/dist/core/contracts.d.ts +109 -2
- package/dist/core/runner.d.ts +4 -0
- package/dist/core/runner.js +49 -12
- package/dist/index.d.ts +8 -4
- package/dist/index.js +7 -3
- package/dist/mcp/rich.js +68 -0
- package/dist/plugins/host.d.ts +4 -0
- package/dist/plugins/host.js +22 -8
- package/dist/providers/settings.d.ts +15 -0
- package/dist/providers/settings.js +38 -0
- package/dist/runtime/blobs.d.ts +29 -0
- package/dist/runtime/blobs.js +94 -0
- package/dist/runtime/diff.d.ts +23 -0
- package/dist/runtime/diff.js +154 -0
- package/dist/runtime/store.d.ts +69 -3
- package/dist/runtime/store.js +292 -11
- package/dist/tools/standard.js +59 -9
- package/package.json +2 -2
package/dist/runtime/store.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { textResult, } from "@alisio/sdk";
|
|
2
2
|
import { openDatabase } from "./sqlite.js";
|
|
3
|
+
/** Whether a process id belongs to a live process (EPERM still means alive). */
|
|
4
|
+
function alive(pid) {
|
|
5
|
+
try {
|
|
6
|
+
process.kill(pid, 0);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch (e) {
|
|
10
|
+
return e.code !== "ESRCH";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const TERMINAL_RUN = "('completed','turns_exceeded','failed','cancelled','interrupted')";
|
|
3
14
|
export class SQLiteStore {
|
|
4
15
|
db;
|
|
5
16
|
constructor(path) {
|
|
@@ -45,12 +56,61 @@ export class SQLiteStore {
|
|
|
45
56
|
this.db.exec("CREATE INDEX IF NOT EXISTS sessions_parent ON sessions(parent_id)");
|
|
46
57
|
this.db.exec("INSERT OR IGNORE INTO schema_migrations VALUES(3)");
|
|
47
58
|
});
|
|
59
|
+
if (!has(4))
|
|
60
|
+
this.db.transaction(() => {
|
|
61
|
+
// v4 (additive): run journal, workspace UI metadata, blob index and nullable columns.
|
|
62
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS runs(
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
session TEXT NOT NULL REFERENCES sessions(id),
|
|
65
|
+
status TEXT NOT NULL,
|
|
66
|
+
request_id TEXT,
|
|
67
|
+
correlation_id TEXT,
|
|
68
|
+
owner_pid INTEGER,
|
|
69
|
+
model TEXT,
|
|
70
|
+
created_at INTEGER NOT NULL,
|
|
71
|
+
started_at INTEGER,
|
|
72
|
+
ended_at INTEGER,
|
|
73
|
+
error TEXT,
|
|
74
|
+
usage TEXT);
|
|
75
|
+
CREATE UNIQUE INDEX IF NOT EXISTS runs_request ON runs(session, request_id) WHERE request_id IS NOT NULL;
|
|
76
|
+
CREATE INDEX IF NOT EXISTS runs_session ON runs(session, created_at);
|
|
77
|
+
CREATE TABLE IF NOT EXISTS workspaces(
|
|
78
|
+
path TEXT PRIMARY KEY,
|
|
79
|
+
label TEXT,
|
|
80
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
81
|
+
last_opened_at INTEGER);
|
|
82
|
+
CREATE TABLE IF NOT EXISTS blobs(
|
|
83
|
+
hash TEXT PRIMARY KEY,
|
|
84
|
+
mime TEXT NOT NULL,
|
|
85
|
+
size INTEGER NOT NULL,
|
|
86
|
+
width INTEGER,
|
|
87
|
+
height INTEGER,
|
|
88
|
+
created_at INTEGER NOT NULL);`);
|
|
89
|
+
const add = (table, name, type) => {
|
|
90
|
+
const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
91
|
+
if (!columns.some((c) => c.name === name))
|
|
92
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${type}`);
|
|
93
|
+
};
|
|
94
|
+
add("sessions", "pinned", "INTEGER NOT NULL DEFAULT 0");
|
|
95
|
+
add("sessions", "archived_at", "INTEGER");
|
|
96
|
+
add("events", "created_at", "INTEGER");
|
|
97
|
+
add("events", "correlation_id", "TEXT");
|
|
98
|
+
add("tool_calls", "run_id", "TEXT");
|
|
99
|
+
add("tool_calls", "name", "TEXT");
|
|
100
|
+
add("tool_calls", "effect", "TEXT");
|
|
101
|
+
add("tool_calls", "started_at", "INTEGER");
|
|
102
|
+
add("tool_calls", "ended_at", "INTEGER");
|
|
103
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS events_session ON events(session, seq);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS messages_session ON messages(session, seq);
|
|
105
|
+
INSERT OR IGNORE INTO schema_migrations VALUES(4);`);
|
|
106
|
+
});
|
|
48
107
|
}
|
|
49
108
|
create(workspace, provider, model) {
|
|
50
109
|
const id = crypto.randomUUID();
|
|
110
|
+
const now = Date.now();
|
|
51
111
|
this.db
|
|
52
|
-
.prepare("INSERT INTO sessions(id,workspace,provider,model) VALUES(
|
|
53
|
-
.run(id, workspace, provider, model);
|
|
112
|
+
.prepare("INSERT INTO sessions(id,workspace,provider,model,created_at,updated_at) VALUES(?,?,?,?,?,?)")
|
|
113
|
+
.run(id, workspace, provider, model, now, now);
|
|
54
114
|
return { id, workspace, provider, model };
|
|
55
115
|
}
|
|
56
116
|
row(r) {
|
|
@@ -68,6 +128,8 @@ export class SQLiteStore {
|
|
|
68
128
|
...(r.options ? { options: JSON.parse(String(r.options)) } : {}),
|
|
69
129
|
...(r.created_at ? { createdAt: Number(r.created_at) } : {}),
|
|
70
130
|
...(r.updated_at ? { updatedAt: Number(r.updated_at) } : {}),
|
|
131
|
+
...(r.pinned ? { pinned: true } : {}),
|
|
132
|
+
...(r.archived_at ? { archivedAt: Number(r.archived_at) } : {}),
|
|
71
133
|
};
|
|
72
134
|
}
|
|
73
135
|
get(id) {
|
|
@@ -147,6 +209,10 @@ export class SQLiteStore {
|
|
|
147
209
|
this.db
|
|
148
210
|
.prepare("INSERT INTO messages(session,body) VALUES(?,?)")
|
|
149
211
|
.run(id, JSON.stringify(message));
|
|
212
|
+
this.touch(id);
|
|
213
|
+
}
|
|
214
|
+
touch(id) {
|
|
215
|
+
this.db.prepare("UPDATE sessions SET updated_at=? WHERE id=?").run(Date.now(), id);
|
|
150
216
|
}
|
|
151
217
|
setModel(id, model) {
|
|
152
218
|
if (!model.trim())
|
|
@@ -201,15 +267,30 @@ export class SQLiteStore {
|
|
|
201
267
|
.prepare("UPDATE sessions SET locked_pid=NULL WHERE id=? AND locked_pid=?")
|
|
202
268
|
.run(id, process.pid);
|
|
203
269
|
}
|
|
204
|
-
beginCall(id, call) {
|
|
270
|
+
beginCall(id, call, meta = {}) {
|
|
205
271
|
this.db
|
|
206
|
-
.prepare("INSERT INTO tool_calls(session,call_id,status) VALUES(?,?,'pending')")
|
|
207
|
-
.run(id, call.id);
|
|
272
|
+
.prepare("INSERT INTO tool_calls(session,call_id,status,run_id,name,effect,started_at) VALUES(?,?,'pending',?,?,?,?)")
|
|
273
|
+
.run(id, call.id, meta.runId ?? null, call.name, meta.effect ?? null, Date.now());
|
|
208
274
|
}
|
|
209
275
|
endCall(id, call, result) {
|
|
210
276
|
this.db
|
|
211
|
-
.prepare("INSERT INTO tool_calls(session,call_id,status,result) VALUES(?,?,'completed'
|
|
212
|
-
.run(id, call.id, JSON.stringify(result));
|
|
277
|
+
.prepare("INSERT INTO tool_calls(session,call_id,status,result,name,ended_at) VALUES(?,?,'completed',?,?,?) ON CONFLICT(session,call_id) DO UPDATE SET status='completed',result=excluded.result,ended_at=excluded.ended_at,name=COALESCE(tool_calls.name,excluded.name)")
|
|
278
|
+
.run(id, call.id, JSON.stringify(result), call.name, Date.now());
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Calls of a session with one effect (for example `write`), oldest first. Needs the v4
|
|
282
|
+
* `effect`/`run_id`/`started_at` columns; rows written by older versions have no effect.
|
|
283
|
+
*/
|
|
284
|
+
effectCalls(session, effect) {
|
|
285
|
+
const rows = this.db
|
|
286
|
+
.prepare("SELECT call_id,name,run_id,started_at FROM tool_calls WHERE session=? AND effect=? ORDER BY started_at, rowid")
|
|
287
|
+
.all(session, effect);
|
|
288
|
+
return rows.map((row) => ({
|
|
289
|
+
callId: row.call_id,
|
|
290
|
+
name: row.name ?? "",
|
|
291
|
+
...(row.run_id ? { runId: row.run_id } : {}),
|
|
292
|
+
...(row.started_at != null ? { startedAt: Number(row.started_at) } : {}),
|
|
293
|
+
}));
|
|
213
294
|
}
|
|
214
295
|
/** Rich (ui/image) parts persist as part of the serialized result and replay verbatim. */
|
|
215
296
|
callResult(id, callId) {
|
|
@@ -244,10 +325,210 @@ export class SQLiteStore {
|
|
|
244
325
|
}
|
|
245
326
|
});
|
|
246
327
|
}
|
|
247
|
-
event
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.
|
|
328
|
+
/** Persists a durable event and returns its global `events.seq`. */
|
|
329
|
+
event(id, runId, type, data, meta = {}) {
|
|
330
|
+
const { lastInsertRowid } = this.db
|
|
331
|
+
.prepare("INSERT INTO events(session,run_id,type,body,created_at,correlation_id) VALUES(?,?,?,?,?,?)")
|
|
332
|
+
.run(id, runId, type, JSON.stringify(data), Date.now(), meta.correlationId ?? null);
|
|
333
|
+
return Number(lastInsertRowid);
|
|
334
|
+
}
|
|
335
|
+
runRow(r) {
|
|
336
|
+
return {
|
|
337
|
+
id: String(r.id),
|
|
338
|
+
session: String(r.session),
|
|
339
|
+
status: r.status,
|
|
340
|
+
...(r.request_id != null ? { requestId: String(r.request_id) } : {}),
|
|
341
|
+
...(r.correlation_id != null ? { correlationId: String(r.correlation_id) } : {}),
|
|
342
|
+
...(r.owner_pid != null ? { ownerPid: Number(r.owner_pid) } : {}),
|
|
343
|
+
...(r.model != null ? { model: String(r.model) } : {}),
|
|
344
|
+
createdAt: Number(r.created_at),
|
|
345
|
+
...(r.started_at != null ? { startedAt: Number(r.started_at) } : {}),
|
|
346
|
+
...(r.ended_at != null ? { endedAt: Number(r.ended_at) } : {}),
|
|
347
|
+
...(r.error != null ? { error: String(r.error) } : {}),
|
|
348
|
+
...(r.usage != null ? { usage: JSON.parse(String(r.usage)) } : {}),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
runById(id) {
|
|
352
|
+
const row = this.db.prepare("SELECT * FROM runs WHERE id=?").get(id);
|
|
353
|
+
return row ? this.runRow(row) : undefined;
|
|
354
|
+
}
|
|
355
|
+
beginRun(input) {
|
|
356
|
+
return this.db.transaction(() => {
|
|
357
|
+
const status = input.status ?? "running";
|
|
358
|
+
const now = Date.now();
|
|
359
|
+
const existing = this.runById(input.id);
|
|
360
|
+
if (existing) {
|
|
361
|
+
if (existing.status === "queued" && status === "running")
|
|
362
|
+
this.db
|
|
363
|
+
.prepare("UPDATE runs SET status='running', started_at=?, owner_pid=?, model=COALESCE(?,model) WHERE id=?")
|
|
364
|
+
.run(now, process.pid, input.model ?? null, input.id);
|
|
365
|
+
return { run: this.runById(input.id), created: false };
|
|
366
|
+
}
|
|
367
|
+
if (input.requestId !== undefined) {
|
|
368
|
+
const retried = this.runByRequest(input.session, input.requestId);
|
|
369
|
+
if (retried)
|
|
370
|
+
return { run: retried, created: false };
|
|
371
|
+
}
|
|
372
|
+
this.db
|
|
373
|
+
.prepare(`INSERT INTO runs(id,session,status,request_id,correlation_id,owner_pid,model,created_at,started_at)
|
|
374
|
+
VALUES(?,?,?,?,?,?,?,?,?)`)
|
|
375
|
+
.run(input.id, input.session, status, input.requestId ?? null, input.correlationId ?? null, process.pid, input.model ?? null, now, status === "running" ? now : null);
|
|
376
|
+
return { run: this.runById(input.id), created: true };
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
endRun(id, input) {
|
|
380
|
+
const now = Date.now();
|
|
381
|
+
this.db.transaction(() => {
|
|
382
|
+
this.db
|
|
383
|
+
.prepare(`UPDATE runs SET status=?, ended_at=?, error=?, usage=COALESCE(?,usage)
|
|
384
|
+
WHERE id=? AND status NOT IN ${TERMINAL_RUN}`)
|
|
385
|
+
.run(input.status, now, input.error ?? null, input.usage ? JSON.stringify(input.usage) : null, id);
|
|
386
|
+
const run = this.runById(id);
|
|
387
|
+
if (run)
|
|
388
|
+
this.touch(run.session);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
runByRequest(session, requestId) {
|
|
392
|
+
const row = this.db
|
|
393
|
+
.prepare("SELECT * FROM runs WHERE session=? AND request_id=?")
|
|
394
|
+
.get(session, requestId);
|
|
395
|
+
return row ? this.runRow(row) : undefined;
|
|
396
|
+
}
|
|
397
|
+
runs(session, options = {}) {
|
|
398
|
+
return this.db
|
|
399
|
+
.prepare("SELECT * FROM runs WHERE session=? ORDER BY created_at DESC, rowid DESC LIMIT ?")
|
|
400
|
+
.all(session, options.limit ?? 100)
|
|
401
|
+
.map((r) => this.runRow(r));
|
|
402
|
+
}
|
|
403
|
+
interruptRuns() {
|
|
404
|
+
const rows = this.db
|
|
405
|
+
.prepare("SELECT id, owner_pid FROM runs WHERE status IN ('queued','running')")
|
|
406
|
+
.all();
|
|
407
|
+
let count = 0;
|
|
408
|
+
for (const row of rows) {
|
|
409
|
+
// Runs of this process (another Application in it) or of another live process stay.
|
|
410
|
+
if (row.owner_pid && (row.owner_pid === process.pid || alive(row.owner_pid)))
|
|
411
|
+
continue;
|
|
412
|
+
this.endRun(row.id, { status: "interrupted" });
|
|
413
|
+
count++;
|
|
414
|
+
}
|
|
415
|
+
return count;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* A page of messages by `seq`. With `after`, the first `limit` rows after it; otherwise the
|
|
419
|
+
* newest `limit` rows (before `before` when given). Items are always in ascending order.
|
|
420
|
+
*/
|
|
421
|
+
messagesPage(session, options = {}) {
|
|
422
|
+
const limit = Math.max(1, Math.min(options.limit ?? 100, 1000));
|
|
423
|
+
const active = options.compacted ? "" : " AND compacted=0";
|
|
424
|
+
const rows = (options.after !== undefined
|
|
425
|
+
? this.db
|
|
426
|
+
.prepare(`SELECT seq,body,compacted FROM messages WHERE session=? AND seq>?${active} ORDER BY seq LIMIT ?`)
|
|
427
|
+
.all(session, options.after, limit + 1)
|
|
428
|
+
: this.db
|
|
429
|
+
.prepare(`SELECT seq,body,compacted FROM messages WHERE session=? AND seq<?${active} ORDER BY seq DESC LIMIT ?`)
|
|
430
|
+
.all(session, options.before ?? Number.MAX_SAFE_INTEGER, limit + 1));
|
|
431
|
+
const hasMore = rows.length > limit;
|
|
432
|
+
const page = rows.slice(0, limit);
|
|
433
|
+
if (options.after === undefined)
|
|
434
|
+
page.reverse();
|
|
435
|
+
return {
|
|
436
|
+
items: page.map((r) => ({
|
|
437
|
+
seq: Number(r.seq),
|
|
438
|
+
message: JSON.parse(r.body),
|
|
439
|
+
compacted: !!r.compacted,
|
|
440
|
+
})),
|
|
441
|
+
hasMore,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
/** Durable events of a session after `after` (a previous `eventId`), ascending. */
|
|
445
|
+
eventsPage(session, options = {}) {
|
|
446
|
+
const limit = Math.max(1, Math.min(options.limit ?? 500, 5000));
|
|
447
|
+
const rows = this.db
|
|
448
|
+
.prepare("SELECT seq,run_id,type,body,created_at,correlation_id FROM events WHERE session=? AND seq>? AND seq<? ORDER BY seq LIMIT ?")
|
|
449
|
+
.all(session, options.after ?? 0, options.before ?? Number.MAX_SAFE_INTEGER, limit + 1);
|
|
450
|
+
return {
|
|
451
|
+
items: rows.slice(0, limit).map((r) => ({
|
|
452
|
+
eventId: String(r.seq),
|
|
453
|
+
runId: String(r.run_id),
|
|
454
|
+
type: String(r.type),
|
|
455
|
+
data: r.body == null ? undefined : JSON.parse(String(r.body)),
|
|
456
|
+
...(r.created_at != null ? { createdAt: Number(r.created_at) } : {}),
|
|
457
|
+
...(r.correlation_id != null ? { correlationId: String(r.correlation_id) } : {}),
|
|
458
|
+
})),
|
|
459
|
+
hasMore: rows.length > limit,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Known workspaces for web clients: the workspaces of root sessions plus the ones recorded with
|
|
464
|
+
* `recordWorkspace` (v4 `workspaces` table), with their UI metadata and root-session count.
|
|
465
|
+
*/
|
|
466
|
+
workspaces() {
|
|
467
|
+
const rows = this.db
|
|
468
|
+
.prepare(`SELECT p.path, w.label, COALESCE(w.pinned, 0) AS pinned, w.last_opened_at,
|
|
469
|
+
(SELECT count(*) FROM sessions s WHERE s.workspace=p.path AND s.parent_id IS NULL) AS sessions
|
|
470
|
+
FROM (SELECT workspace AS path FROM sessions WHERE parent_id IS NULL AND workspace IS NOT NULL
|
|
471
|
+
UNION SELECT path FROM workspaces) p
|
|
472
|
+
LEFT JOIN workspaces w ON w.path=p.path
|
|
473
|
+
ORDER BY p.path`)
|
|
474
|
+
.all();
|
|
475
|
+
return rows.map((r) => ({
|
|
476
|
+
path: String(r.path),
|
|
477
|
+
...(r.label != null ? { label: String(r.label) } : {}),
|
|
478
|
+
pinned: !!r.pinned,
|
|
479
|
+
...(r.last_opened_at != null ? { lastOpenedAt: Number(r.last_opened_at) } : {}),
|
|
480
|
+
sessions: Number(r.sessions ?? 0),
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
/** Records a workspace and patches its UI metadata (`label: null` clears the label). */
|
|
484
|
+
recordWorkspace(path, patch = {}) {
|
|
485
|
+
this.db.transaction(() => {
|
|
486
|
+
this.db.prepare("INSERT OR IGNORE INTO workspaces(path) VALUES(?)").run(path);
|
|
487
|
+
if (patch.label !== undefined)
|
|
488
|
+
this.db.prepare("UPDATE workspaces SET label=? WHERE path=?").run(patch.label, path);
|
|
489
|
+
if (patch.pinned !== undefined)
|
|
490
|
+
this.db
|
|
491
|
+
.prepare("UPDATE workspaces SET pinned=? WHERE path=?")
|
|
492
|
+
.run(patch.pinned ? 1 : 0, path);
|
|
493
|
+
if (patch.lastOpenedAt !== undefined)
|
|
494
|
+
this.db
|
|
495
|
+
.prepare("UPDATE workspaces SET last_opened_at=? WHERE path=?")
|
|
496
|
+
.run(patch.lastOpenedAt, path);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Patches web-facing session metadata: title, pin, archive flag and `options` (shallow-merged
|
|
501
|
+
* into the stored options object). Does not touch the run-relevant columns.
|
|
502
|
+
*/
|
|
503
|
+
updateSessionMeta(id, patch) {
|
|
504
|
+
this.db.transaction(() => {
|
|
505
|
+
const current = this.get(id);
|
|
506
|
+
if (patch.title !== undefined)
|
|
507
|
+
this.db.prepare("UPDATE sessions SET title=? WHERE id=?").run(patch.title, id);
|
|
508
|
+
if (patch.pinned !== undefined)
|
|
509
|
+
this.db.prepare("UPDATE sessions SET pinned=? WHERE id=?").run(patch.pinned ? 1 : 0, id);
|
|
510
|
+
if (patch.archived !== undefined)
|
|
511
|
+
this.db
|
|
512
|
+
.prepare("UPDATE sessions SET archived_at=? WHERE id=?")
|
|
513
|
+
.run(patch.archived ? Date.now() : null, id);
|
|
514
|
+
if (patch.options)
|
|
515
|
+
this.db
|
|
516
|
+
.prepare("UPDATE sessions SET options=? WHERE id=?")
|
|
517
|
+
.run(JSON.stringify({ ...(current.options ?? {}), ...patch.options }), id);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/** `MAX(events.seq)` of a session (0 without events): the web snapshot cursor. */
|
|
521
|
+
lastEventId(session) {
|
|
522
|
+
const row = this.db
|
|
523
|
+
.prepare("SELECT MAX(seq) AS seq FROM events WHERE session=?")
|
|
524
|
+
.get(session);
|
|
525
|
+
return Number(row?.seq ?? 0);
|
|
526
|
+
}
|
|
527
|
+
/** The pid of another live process holding the session lock, if any. */
|
|
528
|
+
lockedBy(id) {
|
|
529
|
+
const row = this.db.prepare("SELECT locked_pid FROM sessions WHERE id=?").get(id);
|
|
530
|
+
const pid = row?.locked_pid;
|
|
531
|
+
return pid && pid !== process.pid && alive(pid) ? pid : undefined;
|
|
251
532
|
}
|
|
252
533
|
getState(plugin, key) {
|
|
253
534
|
const row = this.db
|
package/dist/tools/standard.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, relative } from "node:path";
|
|
3
|
+
import { dirname, extname, relative } from "node:path";
|
|
4
4
|
import { textResult, } from "@alisio/sdk";
|
|
5
|
+
import { clipLines, unifiedPatch } from "../runtime/diff.js";
|
|
5
6
|
import { fileSize, readHead, readText } from "../runtime/fs.js";
|
|
6
7
|
import { safePath } from "../runtime/paths.js";
|
|
7
|
-
import { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess } from "../runtime/process.js";
|
|
8
|
+
import { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess, } from "../runtime/process.js";
|
|
8
9
|
import { EXECUTE_TIMEOUT_MS, MAX_NESTED_CALLS, runExecute } from "./execute.js";
|
|
9
10
|
import { searchWithFallback } from "./search.js";
|
|
10
11
|
import { DEFAULT_TIMEOUT_SECONDS, MAX_RESPONSE_BYTES, MAX_TIMEOUT_SECONDS, webfetch, } from "./webfetch.js";
|
|
@@ -49,6 +50,50 @@ async function atomicWrite(resolve, path, content, expected) {
|
|
|
49
50
|
finally {
|
|
50
51
|
await rm(temporary, { force: true });
|
|
51
52
|
}
|
|
53
|
+
return before;
|
|
54
|
+
}
|
|
55
|
+
/** Producer-side bounds of the web-only ui parts (spec §8.6). */
|
|
56
|
+
const DIFF_LIMIT = 200 * 1024;
|
|
57
|
+
const TERMINAL_LIMIT = 256 * 1024;
|
|
58
|
+
/**
|
|
59
|
+
* Appends a display-only ui part after the tool's text (which stays the first part and is all
|
|
60
|
+
* that providers and headless output see, via `textProjection`).
|
|
61
|
+
*/
|
|
62
|
+
const withUi = (result, block) => block ? { ...result, content: [...result.content, { type: "ui", block }] } : result;
|
|
63
|
+
/** A `diff` block of a file write, bounded to 200 KB at a line boundary. */
|
|
64
|
+
function diffBlock(path, before, after) {
|
|
65
|
+
const patch = unifiedPatch(path, before, after);
|
|
66
|
+
if (!patch)
|
|
67
|
+
return undefined;
|
|
68
|
+
const { text, clipped } = clipLines(patch, DIFF_LIMIT);
|
|
69
|
+
const lang = extname(path).slice(1).toLowerCase();
|
|
70
|
+
return {
|
|
71
|
+
kind: "diff",
|
|
72
|
+
path,
|
|
73
|
+
patch: text,
|
|
74
|
+
...(lang ? { lang } : {}),
|
|
75
|
+
...(clipped ? { caption: `${path} (diff truncated to 200 KB)` } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** A `terminal` block of a finished process: stdout then stderr, keeping the last 256 KB. */
|
|
79
|
+
function terminalBlock(command, result, durationMs) {
|
|
80
|
+
const separator = result.stdout && result.stderr && !result.stdout.endsWith("\n") ? "\n" : "";
|
|
81
|
+
let output = `${result.stdout}${separator}${result.stderr}`;
|
|
82
|
+
const bytes = Buffer.byteLength(output);
|
|
83
|
+
const clipped = bytes > TERMINAL_LIMIT;
|
|
84
|
+
if (clipped)
|
|
85
|
+
output = Buffer.from(output)
|
|
86
|
+
.subarray(bytes - TERMINAL_LIMIT)
|
|
87
|
+
.toString("utf8")
|
|
88
|
+
.replace(/^\uFFFD+/, "");
|
|
89
|
+
return {
|
|
90
|
+
kind: "terminal",
|
|
91
|
+
command,
|
|
92
|
+
output,
|
|
93
|
+
exitCode: result.exitCode,
|
|
94
|
+
durationMs,
|
|
95
|
+
...(result.truncated || clipped ? { truncated: true } : {}),
|
|
96
|
+
};
|
|
52
97
|
}
|
|
53
98
|
export function registerStandard(registry, workspace, skills, context, ui,
|
|
54
99
|
/** Live policy accessor for `execute`'s nested tool calls (bound once the runner exists). */
|
|
@@ -113,8 +158,8 @@ pathAccess) {
|
|
|
113
158
|
const content = String(i.content);
|
|
114
159
|
if (Buffer.byteLength(content) > 1_048_576)
|
|
115
160
|
throw new Error("Write exceeds 1 MiB");
|
|
116
|
-
await atomicWrite((path) => resolvePath(c, path), String(i.path), content, i.expectedHash);
|
|
117
|
-
return textResult(JSON.stringify({ path: i.path, sha256: hash(content) }));
|
|
161
|
+
const before = await atomicWrite((path) => resolvePath(c, path), String(i.path), content, i.expectedHash);
|
|
162
|
+
return withUi(textResult(JSON.stringify({ path: i.path, sha256: hash(content) })), diffBlock(String(i.path), before, content));
|
|
118
163
|
},
|
|
119
164
|
});
|
|
120
165
|
register({
|
|
@@ -132,7 +177,7 @@ pathAccess) {
|
|
|
132
177
|
throw new Error("Edit must match exactly one occurrence");
|
|
133
178
|
const after = before.slice(0, index) + String(i.newText) + before.slice(index + needle.length);
|
|
134
179
|
await atomicWrite((p) => resolvePath(c, p), path, after, String(i.expectedHash));
|
|
135
|
-
return textResult(JSON.stringify({ path: i.path, sha256: hash(after) }));
|
|
180
|
+
return withUi(textResult(JSON.stringify({ path: i.path, sha256: hash(after) })), diffBlock(String(i.path), before, after));
|
|
136
181
|
},
|
|
137
182
|
});
|
|
138
183
|
register({
|
|
@@ -201,12 +246,15 @@ pathAccess) {
|
|
|
201
246
|
timeoutMs: { type: "integer", minimum: 1, maximum: 120000 },
|
|
202
247
|
}, ["command", "args"]),
|
|
203
248
|
async execute(i, c) {
|
|
204
|
-
|
|
249
|
+
const started = Date.now();
|
|
250
|
+
const result = await runProcess(String(i.command), i.args, {
|
|
205
251
|
cwd: c.workspace,
|
|
206
252
|
signal: c.signal,
|
|
207
253
|
timeoutMs: Number(i.timeoutMs ?? 30000),
|
|
208
254
|
onData: c.emit,
|
|
209
|
-
})
|
|
255
|
+
});
|
|
256
|
+
const command = [String(i.command), ...i.args].join(" ");
|
|
257
|
+
return withUi(textResult(JSON.stringify(result)), terminalBlock(command, result, Date.now() - started));
|
|
210
258
|
},
|
|
211
259
|
});
|
|
212
260
|
register({
|
|
@@ -215,9 +263,11 @@ pathAccess) {
|
|
|
215
263
|
description: "Run an arbitrary shell command. Explicit process capability required; not sandboxed.",
|
|
216
264
|
inputSchema: objectSchema({ command: str }, ["command"]),
|
|
217
265
|
async execute(i, c) {
|
|
218
|
-
|
|
266
|
+
const started = Date.now();
|
|
267
|
+
const result = await runProcess(process.platform === "win32" ? "cmd.exe" : "/bin/sh", process.platform === "win32"
|
|
219
268
|
? ["/d", "/s", "/c", String(i.command)]
|
|
220
|
-
: ["-c", String(i.command)], { cwd: c.workspace, signal: c.signal, onData: c.emit })
|
|
269
|
+
: ["-c", String(i.command)], { cwd: c.workspace, signal: c.signal, onData: c.emit });
|
|
270
|
+
return withUi(textResult(JSON.stringify(result)), terminalBlock(String(i.command), result, Date.now() - started));
|
|
221
271
|
},
|
|
222
272
|
});
|
|
223
273
|
for (const [name, args] of [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.18",
|
|
4
4
|
"description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=22.16"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@alisio/sdk": "0.1.0-alpha.
|
|
44
|
+
"@alisio/sdk": "0.1.0-alpha.15",
|
|
45
45
|
"@mixmark-io/domino": "2.2.0",
|
|
46
46
|
"@modelcontextprotocol/client": "2.1.0",
|
|
47
47
|
"ajv": "8.20.0",
|