chainlesschain 0.47.6 → 0.47.7
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/package.json +2 -2
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/Analytics-BFI7jbwM.css +1 -0
- package/src/assets/web-panel/assets/Analytics-DQ135mAd.js +3 -0
- package/src/assets/web-panel/assets/AppLayout-6SPt_8Y_.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-BFJ-Fofn.css +1 -0
- package/src/assets/web-panel/assets/{Backup-Ba9UybpT.js → Backup-DbVRG5vE.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BwXskT21.js → Chat-wVhrFK9C.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-UmOe7qvE.js → Cowork-lOC25IW2.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-JHS-rc-4.js → Cron-3P0eVLTV.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-B95cMCO7.js → Dashboard-Br7kCwKJ.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CSYO0_zk.js → Git-CrDCcBig.js} +2 -2
- package/src/assets/web-panel/assets/{Logs-Hxw_K0km.js → Logs-BfTE8urP.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DIE75TrB.js → McpTools-CsGIijNe.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-C4KVnLlp.js → Memory-BXX_yMKJ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DuzrHMAk.js → Notes-DU6Vf2cL.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DTq6uF82.js → Organization-Bny6yOPV.js} +4 -4
- package/src/assets/web-panel/assets/{P2P-C0hjlhsR.js → P2P-BxFZ1Bit.js} +2 -2
- package/src/assets/web-panel/assets/{Permissions-Ec0NH-xC.js → Permissions-B1j3Mtms.js} +3 -3
- package/src/assets/web-panel/assets/{Projects-U8D0asCS.js → Projects-D-CGscDu.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BngtTLvJ.js → Providers-r6NaBYMf.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B9NbwCKM.js → RssFeed-D7b68C5q.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BL5Rkr1T.js → Security-MJfKv0EJ.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D4MJzLld.js → Services-Yb_Q1V3d.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CQTOMDwF.js → Skills-DLTHcH5T.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-DepbJMnL.js → Tasks-CqycpPjS.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-C24PVZPu.js → Templates-y01u2Zis.js} +1 -1
- package/src/assets/web-panel/assets/VideoEditing-BA1N-5kq.css +1 -0
- package/src/assets/web-panel/assets/VideoEditing-B_nPKw6B.js +1 -0
- package/src/assets/web-panel/assets/{Wallet-PQoSpN_P.js → Wallet-CsRgnjJY.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-BcuyQ4Lr.js → WebAuthn-DWoR5ADp.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-C-SvXbHW.js → WorkflowEditor-DBJhFPMN.js} +1 -1
- package/src/assets/web-panel/assets/{antd-DEjZPGMj.js → antd-Dh2t0vGq.js} +84 -84
- package/src/assets/web-panel/assets/index-tN-8TosE.js +2 -0
- package/src/assets/web-panel/assets/{markdown-CusdXFxb.js → markdown-CBnGGMzE.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent.js +20 -0
- package/src/commands/mcp.js +86 -4
- package/src/commands/memory.js +85 -4
- package/src/commands/sandbox.js +80 -6
- package/src/commands/serve.js +10 -0
- package/src/commands/session.js +250 -0
- package/src/commands/stream.js +75 -0
- package/src/commands/video.js +363 -0
- package/src/gateways/http/envelope-http-server.js +194 -0
- package/src/gateways/ws/message-dispatcher.js +123 -0
- package/src/gateways/ws/session-core-protocol.js +427 -0
- package/src/gateways/ws/session-protocol.js +42 -1
- package/src/gateways/ws/video-protocol.js +230 -0
- package/src/gateways/ws/ws-server.js +72 -0
- package/src/gateways/ws/ws-session-gateway.js +7 -3
- package/src/harness/jsonl-session-store.js +17 -9
- package/src/index.js +8 -0
- package/src/lib/agent-stream.js +63 -0
- package/src/lib/chat-core.js +183 -6
- package/src/lib/cowork/ab-comparator-cli.js +44 -23
- package/src/lib/cowork/agent-group-runner.js +145 -0
- package/src/lib/cowork/debate-review-cli.js +47 -25
- package/src/lib/cowork/project-style-analyzer-cli.js +34 -7
- package/src/lib/interaction-adapter.js +59 -1
- package/src/lib/jsonl-session-store.js +2 -0
- package/src/lib/memory-injection.js +90 -0
- package/src/lib/provider-stream.js +120 -0
- package/src/lib/sandbox-v2.js +198 -3
- package/src/lib/session-consolidator.js +125 -0
- package/src/lib/session-core-singletons.js +56 -0
- package/src/lib/session-tail.js +128 -0
- package/src/lib/session-usage.js +166 -0
- package/src/lib/shell-approval.js +96 -0
- package/src/lib/ws-chat-handler.js +3 -0
- package/src/repl/agent-repl.js +271 -6
- package/src/repl/chat-repl.js +87 -100
- package/src/runtime/agent-core.js +98 -15
- package/src/runtime/agent-runtime.js +105 -3
- package/src/runtime/policies/agent-policy.js +10 -0
- package/src/skills/video-editing/SKILL.md +46 -0
- package/src/skills/video-editing/beat-snap.js +127 -0
- package/src/skills/video-editing/extractors/audio-extractor.js +212 -0
- package/src/skills/video-editing/extractors/subtitle-extractor.js +90 -0
- package/src/skills/video-editing/extractors/video-extractor.js +137 -0
- package/src/skills/video-editing/parallel-orchestrator.js +212 -0
- package/src/skills/video-editing/pipeline.js +480 -0
- package/src/skills/video-editing/prompts/aesthetic-analysis.md +21 -0
- package/src/skills/video-editing/prompts/audio-segment.md +15 -0
- package/src/skills/video-editing/prompts/character-identify.md +19 -0
- package/src/skills/video-editing/prompts/dense-caption.md +20 -0
- package/src/skills/video-editing/prompts/editor-system.md +29 -0
- package/src/skills/video-editing/prompts/hook-dialogue.md +17 -0
- package/src/skills/video-editing/prompts/protagonist-detect.md +20 -0
- package/src/skills/video-editing/prompts/scene-caption.md +16 -0
- package/src/skills/video-editing/prompts/shot-caption.md +25 -0
- package/src/skills/video-editing/prompts/shot-plan.md +28 -0
- package/src/skills/video-editing/prompts/structure-proposal.md +16 -0
- package/src/skills/video-editing/prompts/vlog-scene-caption.md +18 -0
- package/src/skills/video-editing/render/audio-mix.js +128 -0
- package/src/skills/video-editing/render/ffmpeg-concat.js +45 -0
- package/src/skills/video-editing/render/ffmpeg-extract.js +67 -0
- package/src/skills/video-editing/reviewer.js +161 -0
- package/src/skills/video-editing/tools/commit.js +108 -0
- package/src/skills/video-editing/tools/review-clip.js +46 -0
- package/src/skills/video-editing/tools/semantic-retrieval.js +56 -0
- package/src/skills/video-editing/tools/shot-trimming.js +73 -0
- package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +0 -1
- package/src/assets/web-panel/assets/Analytics-DgypYeUB.js +0 -3
- package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +0 -1
- package/src/assets/web-panel/assets/index-CwvzTTw_.js +0 -2
|
@@ -14,6 +14,21 @@ import {
|
|
|
14
14
|
LEGACY_TO_UNIFIED_TYPE,
|
|
15
15
|
} from "../runtime/runtime-events.js";
|
|
16
16
|
import { createAbortError } from "./abort-utils.js";
|
|
17
|
+
import { createEnvelope } from "@chainlesschain/session-core";
|
|
18
|
+
|
|
19
|
+
// Phase 5: parallel service-envelope emission. Map WS agent-handler event
|
|
20
|
+
// types onto the canonical `run.*` run-loop envelope types. Events not in
|
|
21
|
+
// this map are skipped (legacy coding-agent envelope still flows).
|
|
22
|
+
const AGENT_EVENT_TO_ENVELOPE_TYPE = Object.freeze({
|
|
23
|
+
"tool-executing": "run.tool_call",
|
|
24
|
+
"tool-result": "run.tool_result",
|
|
25
|
+
"response-complete": "run.message",
|
|
26
|
+
error: "run.error",
|
|
27
|
+
// Phase 5 bookends — emitted by agentLoop at entry / termination so
|
|
28
|
+
// envelope subscribers can correlate a full run by runId.
|
|
29
|
+
"run-started": "run.started",
|
|
30
|
+
"run-ended": "run.ended",
|
|
31
|
+
});
|
|
17
32
|
|
|
18
33
|
// Whitelist of event types the CLI runtime should emit as unified envelopes
|
|
19
34
|
// (with source: "cli-runtime"). Anything not in this set keeps the legacy
|
|
@@ -101,7 +116,7 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
101
116
|
* @param {import("ws").WebSocket} ws
|
|
102
117
|
* @param {string} sessionId
|
|
103
118
|
*/
|
|
104
|
-
constructor(ws, sessionId) {
|
|
119
|
+
constructor(ws, sessionId, options = {}) {
|
|
105
120
|
super();
|
|
106
121
|
this.ws = ws;
|
|
107
122
|
this.sessionId = sessionId;
|
|
@@ -111,6 +126,11 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
111
126
|
// this WS session instead of leaking across sessions via the process-
|
|
112
127
|
// global default tracker.
|
|
113
128
|
this._sequenceTracker = new CodingAgentSequenceTracker();
|
|
129
|
+
// Phase 5: parallel service-envelope emission. Opt-in (default off) so
|
|
130
|
+
// legacy callers that count ws.send invocations stay green.
|
|
131
|
+
this.enablePhase5Envelopes = options.enablePhase5Envelopes === true;
|
|
132
|
+
// Optional fan-out bus for hosted HTTP SSE subscribers.
|
|
133
|
+
this.envelopeBus = options.envelopeBus || null;
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
/** Generate a unique request id */
|
|
@@ -230,6 +250,8 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
230
250
|
tracker: this._sequenceTracker,
|
|
231
251
|
});
|
|
232
252
|
this._sendWs(envelope);
|
|
253
|
+
// Phase 5: parallel service-envelope emission for unified subscribers.
|
|
254
|
+
this._sendPhase5Envelope(eventType, payload, { sessionId, requestId });
|
|
233
255
|
return;
|
|
234
256
|
}
|
|
235
257
|
|
|
@@ -240,6 +262,42 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
240
262
|
sessionId: this.sessionId,
|
|
241
263
|
...data,
|
|
242
264
|
});
|
|
265
|
+
|
|
266
|
+
// Phase 5: even for non-coding-agent events, forward if the type maps to
|
|
267
|
+
// a canonical envelope (run-started / run-ended bookends flow here).
|
|
268
|
+
if (AGENT_EVENT_TO_ENVELOPE_TYPE[eventType]) {
|
|
269
|
+
const payload = data && typeof data === "object" ? { ...data } : {};
|
|
270
|
+
const requestId = payload.requestId || null;
|
|
271
|
+
const sessionId = payload.sessionId || this.sessionId || null;
|
|
272
|
+
delete payload.requestId;
|
|
273
|
+
delete payload.sessionId;
|
|
274
|
+
this._sendPhase5Envelope(eventType, payload, { sessionId, requestId });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_sendPhase5Envelope(eventType, payload, { sessionId, requestId }) {
|
|
279
|
+
if (!this.enablePhase5Envelopes) return;
|
|
280
|
+
const envType = AGENT_EVENT_TO_ENVELOPE_TYPE[eventType];
|
|
281
|
+
if (!envType) return;
|
|
282
|
+
try {
|
|
283
|
+
const env = createEnvelope({
|
|
284
|
+
type: envType,
|
|
285
|
+
sessionId: sessionId || null,
|
|
286
|
+
runId: payload?.runId || null,
|
|
287
|
+
requestId: requestId || null,
|
|
288
|
+
payload: { ...(payload || {}) },
|
|
289
|
+
});
|
|
290
|
+
this._sendWs(env);
|
|
291
|
+
if (this.envelopeBus && env.sessionId) {
|
|
292
|
+
try {
|
|
293
|
+
this.envelopeBus.publish(env.sessionId, env);
|
|
294
|
+
} catch (_e) {
|
|
295
|
+
// Bus fan-out must never break the WS path.
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (_e) {
|
|
299
|
+
// Phase 5 is additive — never break the legacy path on envelope failure.
|
|
300
|
+
}
|
|
243
301
|
}
|
|
244
302
|
|
|
245
303
|
_sendWs(data) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-injection — recall top-K session-core memories and format them as a
|
|
3
|
+
* system-prompt block for agent-repl / chat-repl startup.
|
|
4
|
+
*
|
|
5
|
+
* Managed Agents parity Phase G item #5: new sessions automatically surface
|
|
6
|
+
* relevant global/agent-scoped memory so the assistant has continuity across
|
|
7
|
+
* runs.
|
|
8
|
+
*
|
|
9
|
+
* Design:
|
|
10
|
+
* - Pull from global scope + (optionally) the session's agent scope.
|
|
11
|
+
* - Deduplicate by memory id, sort by relevance/score, cap at `limit`.
|
|
12
|
+
* - Return `null` when nothing useful was found so callers can skip the
|
|
13
|
+
* extra system message entirely.
|
|
14
|
+
* - Pure formatting — caller decides where to splice it into `messages`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { getMemoryStore } from "./session-core-singletons.js";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_LIMIT = 8;
|
|
20
|
+
const DEFAULT_CONTENT_CHARS = 280;
|
|
21
|
+
|
|
22
|
+
export function recallStartupMemories({
|
|
23
|
+
agentId = null,
|
|
24
|
+
query = "",
|
|
25
|
+
limit = DEFAULT_LIMIT,
|
|
26
|
+
memoryStore = null,
|
|
27
|
+
} = {}) {
|
|
28
|
+
const store = memoryStore || getMemoryStore();
|
|
29
|
+
if (!store || typeof store.recall !== "function") return [];
|
|
30
|
+
|
|
31
|
+
const pools = [];
|
|
32
|
+
try {
|
|
33
|
+
pools.push(store.recall({ scope: "global", query, limit }) || []);
|
|
34
|
+
} catch (_e) {
|
|
35
|
+
/* ignore — missing scope is not fatal */
|
|
36
|
+
}
|
|
37
|
+
if (agentId) {
|
|
38
|
+
try {
|
|
39
|
+
pools.push(
|
|
40
|
+
store.recall({ scope: "agent", scopeId: agentId, query, limit }) || [],
|
|
41
|
+
);
|
|
42
|
+
} catch (_e) {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const merged = [];
|
|
49
|
+
for (const pool of pools) {
|
|
50
|
+
for (const m of pool) {
|
|
51
|
+
if (!m || !m.id || seen.has(m.id)) continue;
|
|
52
|
+
seen.add(m.id);
|
|
53
|
+
merged.push(m);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
merged.sort((a, b) => {
|
|
58
|
+
const ra = Number(a.relevance ?? a.score ?? 0);
|
|
59
|
+
const rb = Number(b.relevance ?? b.score ?? 0);
|
|
60
|
+
return rb - ra;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return merged.slice(0, limit);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatMemoriesAsSystemPrompt(memories, { headline } = {}) {
|
|
67
|
+
if (!Array.isArray(memories) || memories.length === 0) return null;
|
|
68
|
+
|
|
69
|
+
const title =
|
|
70
|
+
headline || "Relevant memory from prior sessions (recall — do not echo):";
|
|
71
|
+
const lines = [title];
|
|
72
|
+
for (const m of memories) {
|
|
73
|
+
const scope = m.scope || "global";
|
|
74
|
+
const scopeTag = m.scopeId
|
|
75
|
+
? `${scope}:${String(m.scopeId).slice(0, 12)}`
|
|
76
|
+
: scope;
|
|
77
|
+
const cat = m.category ? `[${m.category}] ` : "";
|
|
78
|
+
const body = String(m.content || "").slice(0, DEFAULT_CONTENT_CHARS);
|
|
79
|
+
lines.push(`- (${scopeTag}) ${cat}${body}`);
|
|
80
|
+
}
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildMemoryInjection(options = {}) {
|
|
85
|
+
const memories = recallStartupMemories(options);
|
|
86
|
+
const content = formatMemoriesAsSystemPrompt(memories, {
|
|
87
|
+
headline: options.headline,
|
|
88
|
+
});
|
|
89
|
+
return content ? { role: "system", content, count: memories.length } : null;
|
|
90
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider streaming adapters — shared by `cc stream` and the Hosted Session
|
|
3
|
+
* API `stream.run` WS route. Each builder returns an AsyncIterable<string>
|
|
4
|
+
* of token deltas suitable for piping through session-core StreamRouter.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
|
|
8
|
+
|
|
9
|
+
export async function* ollamaTokenStream({ baseUrl, model, prompt, signal }) {
|
|
10
|
+
const res = await fetch(`${baseUrl}/api/generate`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json" },
|
|
13
|
+
body: JSON.stringify({ model, prompt, stream: true }),
|
|
14
|
+
signal,
|
|
15
|
+
});
|
|
16
|
+
if (!res.ok || !res.body) {
|
|
17
|
+
throw new Error(`Ollama ${res.status} ${res.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
const reader = res.body.getReader();
|
|
20
|
+
const decoder = new TextDecoder();
|
|
21
|
+
let buf = "";
|
|
22
|
+
while (true) {
|
|
23
|
+
const { value, done } = await reader.read();
|
|
24
|
+
if (done) break;
|
|
25
|
+
buf += decoder.decode(value, { stream: true });
|
|
26
|
+
let nl;
|
|
27
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
28
|
+
const line = buf.slice(0, nl).trim();
|
|
29
|
+
buf = buf.slice(nl + 1);
|
|
30
|
+
if (!line) continue;
|
|
31
|
+
try {
|
|
32
|
+
const obj = JSON.parse(line);
|
|
33
|
+
if (obj.response) yield obj.response;
|
|
34
|
+
if (obj.done) return;
|
|
35
|
+
} catch {
|
|
36
|
+
/* skip malformed */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function* openAIStream({
|
|
43
|
+
baseUrl,
|
|
44
|
+
apiKey,
|
|
45
|
+
model,
|
|
46
|
+
prompt,
|
|
47
|
+
signal,
|
|
48
|
+
}) {
|
|
49
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
Authorization: `Bearer ${apiKey}`,
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
model,
|
|
57
|
+
messages: [{ role: "user", content: prompt }],
|
|
58
|
+
stream: true,
|
|
59
|
+
}),
|
|
60
|
+
signal,
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok || !res.body) {
|
|
63
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
const reader = res.body.getReader();
|
|
66
|
+
const decoder = new TextDecoder();
|
|
67
|
+
let buf = "";
|
|
68
|
+
while (true) {
|
|
69
|
+
const { value, done } = await reader.read();
|
|
70
|
+
if (done) break;
|
|
71
|
+
buf += decoder.decode(value, { stream: true });
|
|
72
|
+
const lines = buf.split("\n");
|
|
73
|
+
buf = lines.pop() || "";
|
|
74
|
+
for (const raw of lines) {
|
|
75
|
+
const line = raw.trim();
|
|
76
|
+
if (!line || !line.startsWith("data:")) continue;
|
|
77
|
+
const payload = line.slice(5).trim();
|
|
78
|
+
if (payload === "[DONE]") return;
|
|
79
|
+
try {
|
|
80
|
+
const obj = JSON.parse(payload);
|
|
81
|
+
const delta = obj?.choices?.[0]?.delta?.content;
|
|
82
|
+
if (delta) yield delta;
|
|
83
|
+
} catch {
|
|
84
|
+
/* skip */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build an AsyncIterable<string> token stream for the given provider.
|
|
92
|
+
* Throws on unsupported provider / missing API key.
|
|
93
|
+
*/
|
|
94
|
+
export function buildProviderSource(provider, opts = {}) {
|
|
95
|
+
const { model, baseUrl, apiKey, prompt, signal } = opts;
|
|
96
|
+
if (provider === "ollama") {
|
|
97
|
+
return ollamaTokenStream({
|
|
98
|
+
baseUrl: baseUrl || "http://localhost:11434",
|
|
99
|
+
model: model || "qwen2:7b",
|
|
100
|
+
prompt,
|
|
101
|
+
signal,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const def = BUILT_IN_PROVIDERS[provider];
|
|
105
|
+
if (!def) throw new Error(`Unsupported provider: ${provider}`);
|
|
106
|
+
const finalKey =
|
|
107
|
+
apiKey || (def.apiKeyEnv ? process.env[def.apiKeyEnv] : null);
|
|
108
|
+
if (!finalKey) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`API key required for ${provider} (--api-key or ${def.apiKeyEnv})`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return openAIStream({
|
|
114
|
+
baseUrl: baseUrl || def.baseUrl,
|
|
115
|
+
apiKey: finalKey,
|
|
116
|
+
model: model || def.models[0],
|
|
117
|
+
prompt,
|
|
118
|
+
signal,
|
|
119
|
+
});
|
|
120
|
+
}
|
package/src/lib/sandbox-v2.js
CHANGED
|
@@ -4,6 +4,36 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import crypto from "crypto";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
|
|
9
|
+
const _require = createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
// Phase 4: shared sandbox-policy from session-core (lazy, fallback stubs).
|
|
12
|
+
let _sandboxPolicy = null;
|
|
13
|
+
function getSandboxPolicy() {
|
|
14
|
+
if (_sandboxPolicy) return _sandboxPolicy;
|
|
15
|
+
try {
|
|
16
|
+
_sandboxPolicy = _require("@chainlesschain/session-core/sandbox-policy");
|
|
17
|
+
} catch (_e) {
|
|
18
|
+
_sandboxPolicy = {
|
|
19
|
+
mergeSandboxPolicy: (_b, o) => ({ scope: "thread", ...(o || {}) }),
|
|
20
|
+
resolveBundleSandboxPolicy: () => ({ scope: "thread" }),
|
|
21
|
+
shouldReuseSandbox: () => false,
|
|
22
|
+
isSandboxExpired: () => false,
|
|
23
|
+
isSandboxIdleExpired: () => false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return _sandboxPolicy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolvePolicy(options = {}) {
|
|
30
|
+
const { mergeSandboxPolicy, resolveBundleSandboxPolicy } = getSandboxPolicy();
|
|
31
|
+
if (options.bundle) {
|
|
32
|
+
const base = resolveBundleSandboxPolicy(options.bundle);
|
|
33
|
+
return mergeSandboxPolicy(base, options.policy || {});
|
|
34
|
+
}
|
|
35
|
+
return mergeSandboxPolicy({}, options.policy || {});
|
|
36
|
+
}
|
|
7
37
|
|
|
8
38
|
// ─── Constants ────────────────────────────────────────────────
|
|
9
39
|
|
|
@@ -36,6 +66,22 @@ export const DEFAULT_PERMISSIONS = {
|
|
|
36
66
|
const activeSandboxes = new Map();
|
|
37
67
|
const auditLog = [];
|
|
38
68
|
|
|
69
|
+
// Phase 4: sync persisted rows for sandboxes this process hasn't seen yet so
|
|
70
|
+
// reuse/prune decisions honor createdAt/lastUsedAt across CLI restarts.
|
|
71
|
+
// Idempotent and cheap — a single indexed SELECT per call.
|
|
72
|
+
function _syncFromDb(db) {
|
|
73
|
+
if (!db) return;
|
|
74
|
+
try {
|
|
75
|
+
const rows = db
|
|
76
|
+
.prepare(`SELECT id FROM sandbox_instances WHERE status = 'active'`)
|
|
77
|
+
.all();
|
|
78
|
+
const missing = rows.some((r) => !activeSandboxes.has(r.id));
|
|
79
|
+
if (missing) restoreFromDb(db);
|
|
80
|
+
} catch (_e) {
|
|
81
|
+
// best-effort — callers continue with fresh in-memory state
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
39
85
|
// ─── Database helpers ─────────────────────────────────────────
|
|
40
86
|
|
|
41
87
|
/**
|
|
@@ -50,10 +96,26 @@ export function ensureSandboxTables(db) {
|
|
|
50
96
|
permissions TEXT,
|
|
51
97
|
quota TEXT,
|
|
52
98
|
resource_usage TEXT,
|
|
99
|
+
policy TEXT,
|
|
100
|
+
created_at_ms INTEGER,
|
|
101
|
+
last_used_at_ms INTEGER,
|
|
53
102
|
created_at TEXT DEFAULT (datetime('now')),
|
|
54
103
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
55
104
|
)
|
|
56
105
|
`);
|
|
106
|
+
// Phase 4: in-place migration for legacy CLI DBs. ALTER is a no-op if the
|
|
107
|
+
// column already exists, so swallow "duplicate column" errors.
|
|
108
|
+
for (const ddl of [
|
|
109
|
+
"ALTER TABLE sandbox_instances ADD COLUMN policy TEXT",
|
|
110
|
+
"ALTER TABLE sandbox_instances ADD COLUMN created_at_ms INTEGER",
|
|
111
|
+
"ALTER TABLE sandbox_instances ADD COLUMN last_used_at_ms INTEGER",
|
|
112
|
+
]) {
|
|
113
|
+
try {
|
|
114
|
+
db.exec(ddl);
|
|
115
|
+
} catch (_e) {
|
|
116
|
+
// column already present
|
|
117
|
+
}
|
|
118
|
+
}
|
|
57
119
|
db.exec(`
|
|
58
120
|
CREATE TABLE IF NOT EXISTS sandbox_audit (
|
|
59
121
|
id TEXT PRIMARY KEY,
|
|
@@ -123,6 +185,7 @@ export function createSandbox(db, agentId, options = {}) {
|
|
|
123
185
|
const quota = options.quota || { ...DEFAULT_QUOTA };
|
|
124
186
|
const resourceUsage = { cpu: 0, memory: 0, storage: 0, network: 0 };
|
|
125
187
|
|
|
188
|
+
const now = Date.now();
|
|
126
189
|
const sandbox = {
|
|
127
190
|
id,
|
|
128
191
|
agentId,
|
|
@@ -130,12 +193,16 @@ export function createSandbox(db, agentId, options = {}) {
|
|
|
130
193
|
permissions,
|
|
131
194
|
quota,
|
|
132
195
|
resourceUsage,
|
|
133
|
-
createdAt: new Date().toISOString(),
|
|
196
|
+
createdAt: new Date(now).toISOString(),
|
|
197
|
+
// Phase 4: lifecycle policy + timestamps (ms) for ttl / idleTtl math.
|
|
198
|
+
policy: resolvePolicy(options),
|
|
199
|
+
createdAtMs: now,
|
|
200
|
+
lastUsedAtMs: now,
|
|
134
201
|
};
|
|
135
202
|
|
|
136
203
|
db.prepare(
|
|
137
|
-
`INSERT INTO sandbox_instances (id, agent_id, status, permissions, quota, resource_usage)
|
|
138
|
-
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
204
|
+
`INSERT INTO sandbox_instances (id, agent_id, status, permissions, quota, resource_usage, policy, created_at_ms, last_used_at_ms)
|
|
205
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
139
206
|
).run(
|
|
140
207
|
id,
|
|
141
208
|
agentId,
|
|
@@ -143,6 +210,9 @@ export function createSandbox(db, agentId, options = {}) {
|
|
|
143
210
|
JSON.stringify(permissions),
|
|
144
211
|
JSON.stringify(quota),
|
|
145
212
|
JSON.stringify(resourceUsage),
|
|
213
|
+
JSON.stringify(sandbox.policy || null),
|
|
214
|
+
sandbox.createdAtMs,
|
|
215
|
+
sandbox.lastUsedAtMs,
|
|
146
216
|
);
|
|
147
217
|
|
|
148
218
|
activeSandboxes.set(id, sandbox);
|
|
@@ -494,6 +564,131 @@ function logBehavior(db, sandboxId, eventType, eventData) {
|
|
|
494
564
|
).run(id, sandboxId, eventType, JSON.stringify(eventData));
|
|
495
565
|
}
|
|
496
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Phase 4: refresh lastUsedAt so idle TTL doesn't expire the sandbox.
|
|
569
|
+
*/
|
|
570
|
+
export function touchSandbox(sandboxId, db = null) {
|
|
571
|
+
const sandbox = activeSandboxes.get(sandboxId);
|
|
572
|
+
if (!sandbox) return false;
|
|
573
|
+
sandbox.lastUsedAtMs = Date.now();
|
|
574
|
+
if (db) {
|
|
575
|
+
try {
|
|
576
|
+
db.prepare(
|
|
577
|
+
`UPDATE sandbox_instances SET last_used_at_ms = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
578
|
+
).run(sandbox.lastUsedAtMs, sandboxId);
|
|
579
|
+
} catch (_e) {
|
|
580
|
+
// persistence best-effort — in-memory touch already applied
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Phase 4: rehydrate live sandbox rows from DB into the in-memory map so
|
|
588
|
+
* idle/ttl math and reuse decisions survive a CLI process restart.
|
|
589
|
+
* Returns the number of restored sandboxes.
|
|
590
|
+
*/
|
|
591
|
+
export function restoreFromDb(db) {
|
|
592
|
+
ensureSandboxTables(db);
|
|
593
|
+
let restored = 0;
|
|
594
|
+
const rows = db
|
|
595
|
+
.prepare(
|
|
596
|
+
`SELECT id, agent_id, status, permissions, quota, resource_usage, policy, created_at_ms, last_used_at_ms, created_at
|
|
597
|
+
FROM sandbox_instances WHERE status = 'active'`,
|
|
598
|
+
)
|
|
599
|
+
.all();
|
|
600
|
+
const now = Date.now();
|
|
601
|
+
for (const row of rows) {
|
|
602
|
+
let policy = null;
|
|
603
|
+
try {
|
|
604
|
+
policy = row.policy ? JSON.parse(row.policy) : null;
|
|
605
|
+
} catch (_e) {
|
|
606
|
+
policy = null;
|
|
607
|
+
}
|
|
608
|
+
activeSandboxes.set(row.id, {
|
|
609
|
+
id: row.id,
|
|
610
|
+
agentId: row.agent_id,
|
|
611
|
+
status: row.status || "active",
|
|
612
|
+
permissions: JSON.parse(row.permissions || "{}"),
|
|
613
|
+
quota: JSON.parse(row.quota || "{}"),
|
|
614
|
+
resourceUsage: JSON.parse(row.resource_usage || "{}"),
|
|
615
|
+
createdAt: row.created_at || new Date(now).toISOString(),
|
|
616
|
+
policy,
|
|
617
|
+
createdAtMs: row.created_at_ms || now,
|
|
618
|
+
lastUsedAtMs: row.last_used_at_ms || now,
|
|
619
|
+
});
|
|
620
|
+
restored += 1;
|
|
621
|
+
}
|
|
622
|
+
return restored;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Phase 4: acquire a sandbox for an agent — reuse an existing one if the
|
|
627
|
+
* policy permits, else create fresh. Returns `{ id, reused, scope, ... }`.
|
|
628
|
+
*/
|
|
629
|
+
export function acquireSandbox(db, agentId, options = {}) {
|
|
630
|
+
const { shouldReuseSandbox } = getSandboxPolicy();
|
|
631
|
+
_syncFromDb(db);
|
|
632
|
+
const policy = resolvePolicy(options);
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
|
|
635
|
+
if (policy.reuseAcrossRuns) {
|
|
636
|
+
for (const sandbox of activeSandboxes.values()) {
|
|
637
|
+
if (sandbox.agentId !== agentId) continue;
|
|
638
|
+
if (sandbox.status !== "active") continue;
|
|
639
|
+
if (
|
|
640
|
+
shouldReuseSandbox(
|
|
641
|
+
sandbox.policy || policy,
|
|
642
|
+
{ createdAt: sandbox.createdAtMs, lastUsedAt: sandbox.lastUsedAtMs },
|
|
643
|
+
now,
|
|
644
|
+
)
|
|
645
|
+
) {
|
|
646
|
+
sandbox.lastUsedAtMs = now;
|
|
647
|
+
return {
|
|
648
|
+
id: sandbox.id,
|
|
649
|
+
reused: true,
|
|
650
|
+
scope: sandbox.policy?.scope || policy.scope,
|
|
651
|
+
status: sandbox.status,
|
|
652
|
+
permissions: sandbox.permissions,
|
|
653
|
+
quota: sandbox.quota,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const created = createSandbox(db, agentId, { ...options, policy });
|
|
660
|
+
return { ...created, reused: false, scope: policy.scope };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Phase 4: sweep sandboxes whose ttl / idle ttl expired, mark them destroyed,
|
|
665
|
+
* and return `[{ id, reason }]`. Safe to call from a timer.
|
|
666
|
+
*/
|
|
667
|
+
export function pruneExpired(db, now = Date.now()) {
|
|
668
|
+
const { isSandboxExpired, isSandboxIdleExpired } = getSandboxPolicy();
|
|
669
|
+
_syncFromDb(db);
|
|
670
|
+
const destroyed = [];
|
|
671
|
+
for (const sandbox of Array.from(activeSandboxes.values())) {
|
|
672
|
+
const policy = sandbox.policy;
|
|
673
|
+
if (!policy) continue;
|
|
674
|
+
if (sandbox.status !== "active") continue;
|
|
675
|
+
const ttlExpired = isSandboxExpired(policy, sandbox.createdAtMs, now);
|
|
676
|
+
const idleExpired = isSandboxIdleExpired(policy, sandbox.lastUsedAtMs, now);
|
|
677
|
+
if (ttlExpired || idleExpired) {
|
|
678
|
+
try {
|
|
679
|
+
destroySandbox(db, sandbox.id);
|
|
680
|
+
destroyed.push({
|
|
681
|
+
id: sandbox.id,
|
|
682
|
+
reason: ttlExpired ? "ttl" : "idle",
|
|
683
|
+
});
|
|
684
|
+
} catch (_e) {
|
|
685
|
+
// skip — destroySandbox may throw if already gone
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return destroyed;
|
|
690
|
+
}
|
|
691
|
+
|
|
497
692
|
/**
|
|
498
693
|
* Clear in-memory state (for testing).
|
|
499
694
|
*/
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-consolidator — bridges CLI JSONL sessions into session-core
|
|
3
|
+
* MemoryConsolidator.
|
|
4
|
+
*
|
|
5
|
+
* Managed Agents parity Phase G: `chainlesschain memory consolidate --session <id>`
|
|
6
|
+
* reads the session's append-only JSONL log, projects the events into
|
|
7
|
+
* `TRACE_TYPES.{MESSAGE,TOOL_CALL,TOOL_RESULT}` payloads, and runs the shared
|
|
8
|
+
* MemoryConsolidator against the CLI MemoryStore singleton.
|
|
9
|
+
*
|
|
10
|
+
* The JSONL store holds `session_start / user_message / assistant_message /
|
|
11
|
+
* tool_call / tool_result / compact` events. Only the user/assistant messages
|
|
12
|
+
* and tool call/result events are carried through — `session_start` and
|
|
13
|
+
* `compact` don't contribute to memory extraction.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
TraceStore,
|
|
18
|
+
TRACE_TYPES,
|
|
19
|
+
MemoryConsolidator,
|
|
20
|
+
} from "@chainlesschain/session-core";
|
|
21
|
+
import { readEvents, sessionExists } from "../harness/jsonl-session-store.js";
|
|
22
|
+
import { getMemoryStore } from "./session-core-singletons.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build an in-memory TraceStore populated with events from a JSONL session.
|
|
26
|
+
* Used so MemoryConsolidator can run against sessions that were never live
|
|
27
|
+
* in this CLI process.
|
|
28
|
+
*/
|
|
29
|
+
export function buildTraceStoreFromJsonl(sessionId, events) {
|
|
30
|
+
const trace = new TraceStore();
|
|
31
|
+
const source = events || readEvents(sessionId);
|
|
32
|
+
|
|
33
|
+
for (const ev of source) {
|
|
34
|
+
if (!ev || !ev.type) continue;
|
|
35
|
+
const ts = ev.timestamp || Date.now();
|
|
36
|
+
const d = ev.data || {};
|
|
37
|
+
|
|
38
|
+
switch (ev.type) {
|
|
39
|
+
case "user_message":
|
|
40
|
+
trace.record({
|
|
41
|
+
sessionId,
|
|
42
|
+
type: TRACE_TYPES.MESSAGE,
|
|
43
|
+
ts,
|
|
44
|
+
payload: { role: "user", content: d.content || "" },
|
|
45
|
+
});
|
|
46
|
+
break;
|
|
47
|
+
case "assistant_message":
|
|
48
|
+
trace.record({
|
|
49
|
+
sessionId,
|
|
50
|
+
type: TRACE_TYPES.MESSAGE,
|
|
51
|
+
ts,
|
|
52
|
+
payload: { role: "assistant", content: d.content || "" },
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
case "tool_call":
|
|
56
|
+
trace.record({
|
|
57
|
+
sessionId,
|
|
58
|
+
type: TRACE_TYPES.TOOL_CALL,
|
|
59
|
+
ts,
|
|
60
|
+
payload: { tool: d.tool, args: d.args },
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
case "tool_result": {
|
|
64
|
+
const raw = d.result;
|
|
65
|
+
const ok = !(raw && typeof raw === "object" && raw.error);
|
|
66
|
+
const summary =
|
|
67
|
+
typeof raw === "string"
|
|
68
|
+
? raw
|
|
69
|
+
: raw && typeof raw === "object"
|
|
70
|
+
? raw.summary || raw.message || null
|
|
71
|
+
: null;
|
|
72
|
+
trace.record({
|
|
73
|
+
sessionId,
|
|
74
|
+
type: TRACE_TYPES.TOOL_RESULT,
|
|
75
|
+
ts,
|
|
76
|
+
payload: { tool: d.tool, ok, summary, result: raw },
|
|
77
|
+
});
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
// session_start / compact / other — not memory-relevant
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return trace;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Consolidate a JSONL session into the CLI MemoryStore.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} sessionId
|
|
93
|
+
* @param {object} options
|
|
94
|
+
* @param {"session"|"agent"|"global"} [options.scope="agent"]
|
|
95
|
+
* @param {string|null} [options.scopeId]
|
|
96
|
+
* @param {string|null} [options.agentId] — used as SessionHandle.agentId when
|
|
97
|
+
* scope=agent and no scopeId given
|
|
98
|
+
* @param {object} [options.memoryStore] — override (tests)
|
|
99
|
+
* @param {Array} [options.events] — override (tests)
|
|
100
|
+
* @returns {Promise<object>} MemoryConsolidator result
|
|
101
|
+
*/
|
|
102
|
+
export async function consolidateJsonlSession(sessionId, options = {}) {
|
|
103
|
+
if (!sessionId) throw new Error("sessionId required");
|
|
104
|
+
if (!options.events && !sessionExists(sessionId)) {
|
|
105
|
+
const err = new Error(`Session not found: ${sessionId}`);
|
|
106
|
+
err.code = "SESSION_NOT_FOUND";
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const trace = buildTraceStoreFromJsonl(sessionId, options.events);
|
|
111
|
+
const memoryStore = options.memoryStore || getMemoryStore();
|
|
112
|
+
const consolidator = new MemoryConsolidator({
|
|
113
|
+
memoryStore,
|
|
114
|
+
traceStore: trace,
|
|
115
|
+
scope: options.scope || "agent",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return consolidator.consolidate(
|
|
119
|
+
{ sessionId, agentId: options.agentId || sessionId },
|
|
120
|
+
{
|
|
121
|
+
scope: options.scope,
|
|
122
|
+
scopeId: options.scopeId,
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
}
|