chainlesschain 0.45.12 → 0.45.19
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 +1 -1
- package/src/assets/web-panel/assets/{AppLayout-BfLjLMsm.js → AppLayout-B00RARl2.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DP7PO9Li.js → Chat-DXtvKoM0.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-DyQF-7R1.js → Cron-BJ4ODHOy.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Logs-BOii-AoO.js → Logs-CSeKZEG_.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DmiJtJYr.js → McpTools-BYQAK11r.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-CDRMMobU.js → Memory-gkUAPyuZ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CVhqqoS1.js → Notes-bjNrQgAo.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Dkt7021l.js → Providers-Dbf57Tbv.js} +1 -1
- package/src/assets/web-panel/assets/{Services-DUDL_UGb.js → Services-CS0oMdxh.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DXXELJc3.js → Skills-B2fgruv8.js} +1 -1
- package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
- package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
- package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
- package/src/assets/web-panel/assets/{index-vW799KpE.js → index-CF2CqPYX.js} +2 -2
- package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +7 -8
- package/src/commands/chat.js +9 -11
- package/src/commands/serve.js +11 -106
- package/src/commands/session.js +101 -0
- package/src/commands/ui.js +10 -151
- package/src/gateways/repl/agent-repl.js +1 -0
- package/src/gateways/repl/chat-repl.js +1 -0
- package/src/gateways/ui/web-ui-server.js +1 -0
- package/src/gateways/ws/action-protocol.js +83 -0
- package/src/gateways/ws/message-dispatcher.js +73 -0
- package/src/gateways/ws/session-protocol.js +396 -0
- package/src/gateways/ws/task-protocol.js +55 -0
- package/src/gateways/ws/worktree-protocol.js +315 -0
- package/src/gateways/ws/ws-server.js +4 -0
- package/src/gateways/ws/ws-session-gateway.js +1 -0
- package/src/harness/background-task-manager.js +506 -0
- package/src/harness/background-task-worker.js +48 -0
- package/src/harness/compression-telemetry.js +214 -0
- package/src/harness/feature-flags.js +157 -0
- package/src/harness/jsonl-session-store.js +452 -0
- package/src/harness/prompt-compressor.js +416 -0
- package/src/harness/worktree-isolator.js +845 -0
- package/src/lib/agent-core.js +246 -45
- package/src/lib/background-task-manager.js +1 -305
- package/src/lib/background-task-worker.js +1 -50
- package/src/lib/compression-telemetry.js +5 -0
- package/src/lib/feature-flags.js +7 -182
- package/src/lib/interaction-adapter.js +32 -6
- package/src/lib/jsonl-session-store.js +21 -237
- package/src/lib/prompt-compressor.js +10 -481
- package/src/lib/sub-agent-context.js +21 -1
- package/src/lib/worktree-isolator.js +13 -231
- package/src/lib/ws-agent-handler.js +1 -0
- package/src/lib/ws-server.js +138 -387
- package/src/lib/ws-session-manager.js +82 -1
- package/src/repl/agent-repl.js +11 -0
- package/src/runtime/agent-runtime.js +417 -0
- package/src/runtime/contracts/agent-turn.js +11 -0
- package/src/runtime/contracts/session-record.js +31 -0
- package/src/runtime/contracts/task-record.js +18 -0
- package/src/runtime/contracts/telemetry-record.js +23 -0
- package/src/runtime/contracts/worktree-record.js +14 -0
- package/src/runtime/index.js +13 -0
- package/src/runtime/policies/agent-policy.js +45 -0
- package/src/runtime/runtime-context.js +14 -0
- package/src/runtime/runtime-events.js +37 -0
- package/src/runtime/runtime-factory.js +50 -0
- package/src/tools/index.js +22 -0
- package/src/tools/legacy-agent-tools.js +171 -0
- package/src/tools/registry.js +141 -0
- package/src/tools/tool-context.js +28 -0
- package/src/tools/tool-permissions.js +28 -0
- package/src/tools/tool-telemetry.js +39 -0
- package/src/assets/web-panel/assets/Dashboard-BGGdnr6t.js +0 -3
- package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
- package/src/assets/web-panel/assets/Tasks-BwZ63-mq.js +0 -1
- package/src/assets/web-panel/assets/Tasks-Cr_XXNyQ.css +0 -1
- package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
- package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
} from "./session-manager.js";
|
|
21
21
|
import { buildSystemPrompt } from "./agent-core.js";
|
|
22
22
|
import { SubAgentRegistry } from "./sub-agent-registry.js";
|
|
23
|
+
import { createWorktree, removeWorktree } from "./worktree-isolator.js";
|
|
24
|
+
import { isGitRepo } from "./git-integration.js";
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* @typedef {object} Session
|
|
@@ -32,7 +34,11 @@ import { SubAgentRegistry } from "./sub-agent-registry.js";
|
|
|
32
34
|
* @property {string|null} apiKey
|
|
33
35
|
* @property {string|null} baseUrl
|
|
34
36
|
* @property {string} projectRoot
|
|
37
|
+
* @property {string} baseProjectRoot
|
|
35
38
|
* @property {string|null} rulesContent
|
|
39
|
+
* @property {object|null} hostManagedToolPolicy
|
|
40
|
+
* @property {boolean} worktreeIsolation
|
|
41
|
+
* @property {object|null} worktree
|
|
36
42
|
* @property {PlanModeManager} planManager
|
|
37
43
|
* @property {CLIContextEngineering|null} contextEngine
|
|
38
44
|
* @property {CLIPermanentMemory|null} permanentMemory
|
|
@@ -78,12 +84,13 @@ export class WSSessionManager {
|
|
|
78
84
|
* @param {string} [options.model]
|
|
79
85
|
* @param {string} [options.apiKey]
|
|
80
86
|
* @param {string} [options.baseUrl]
|
|
87
|
+
* @param {object} [options.hostManagedToolPolicy]
|
|
81
88
|
* @returns {{ sessionId: string }}
|
|
82
89
|
*/
|
|
83
90
|
createSession(options = {}) {
|
|
84
91
|
const sessionId = this._generateId();
|
|
85
92
|
const type = options.type || "agent";
|
|
86
|
-
const
|
|
93
|
+
const baseProjectRoot = options.projectRoot || this.defaultProjectRoot;
|
|
87
94
|
const cfgLlm = this.config?.llm || {};
|
|
88
95
|
const provider = options.provider || cfgLlm.provider || "ollama";
|
|
89
96
|
const model =
|
|
@@ -93,6 +100,16 @@ export class WSSessionManager {
|
|
|
93
100
|
const baseUrl =
|
|
94
101
|
options.baseUrl || cfgLlm.baseUrl || "http://localhost:11434";
|
|
95
102
|
const apiKey = options.apiKey || cfgLlm.apiKey || null;
|
|
103
|
+
const worktreeIsolationRequested = options.worktreeIsolation === true;
|
|
104
|
+
const isolatedWorkspace = this._prepareSessionWorkspace(
|
|
105
|
+
baseProjectRoot,
|
|
106
|
+
sessionId,
|
|
107
|
+
{
|
|
108
|
+
worktreeIsolation: worktreeIsolationRequested,
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
const projectRoot = isolatedWorkspace.projectRoot;
|
|
112
|
+
const worktree = isolatedWorkspace.worktree;
|
|
96
113
|
|
|
97
114
|
// Project context (rules.md, persona) is now loaded by buildSystemPrompt()
|
|
98
115
|
|
|
@@ -151,8 +168,12 @@ export class WSSessionManager {
|
|
|
151
168
|
model,
|
|
152
169
|
apiKey,
|
|
153
170
|
baseUrl,
|
|
171
|
+
hostManagedToolPolicy: options.hostManagedToolPolicy || null,
|
|
154
172
|
projectRoot,
|
|
173
|
+
baseProjectRoot,
|
|
155
174
|
rulesContent: null,
|
|
175
|
+
worktreeIsolation: worktreeIsolationRequested,
|
|
176
|
+
worktree,
|
|
156
177
|
planManager,
|
|
157
178
|
contextEngine,
|
|
158
179
|
permanentMemory,
|
|
@@ -226,8 +247,12 @@ export class WSSessionManager {
|
|
|
226
247
|
model: dbSession.model || null,
|
|
227
248
|
apiKey: null,
|
|
228
249
|
baseUrl: "http://localhost:11434",
|
|
250
|
+
hostManagedToolPolicy: null,
|
|
229
251
|
projectRoot: this.defaultProjectRoot,
|
|
252
|
+
baseProjectRoot: this.defaultProjectRoot,
|
|
230
253
|
rulesContent: null,
|
|
254
|
+
worktreeIsolation: false,
|
|
255
|
+
worktree: null,
|
|
231
256
|
planManager,
|
|
232
257
|
contextEngine,
|
|
233
258
|
permanentMemory,
|
|
@@ -284,6 +309,16 @@ export class WSSessionManager {
|
|
|
284
309
|
session.planManager.removeAllListeners();
|
|
285
310
|
}
|
|
286
311
|
|
|
312
|
+
if (session.worktree?.path && session.baseProjectRoot) {
|
|
313
|
+
try {
|
|
314
|
+
removeWorktree(session.baseProjectRoot, session.worktree.path, {
|
|
315
|
+
deleteBranch: true,
|
|
316
|
+
});
|
|
317
|
+
} catch (_err) {
|
|
318
|
+
// Best-effort cleanup.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
287
322
|
this.sessions.delete(sessionId);
|
|
288
323
|
}
|
|
289
324
|
|
|
@@ -304,6 +339,9 @@ export class WSSessionManager {
|
|
|
304
339
|
provider: session.provider,
|
|
305
340
|
model: session.model,
|
|
306
341
|
messageCount: session.messages.length,
|
|
342
|
+
baseProjectRoot: session.baseProjectRoot,
|
|
343
|
+
worktreeIsolation: session.worktreeIsolation === true,
|
|
344
|
+
worktree: session.worktree || null,
|
|
307
345
|
createdAt: session.createdAt,
|
|
308
346
|
lastActivity: session.lastActivity,
|
|
309
347
|
});
|
|
@@ -346,6 +384,22 @@ export class WSSessionManager {
|
|
|
346
384
|
return this.sessions.get(sessionId) || null;
|
|
347
385
|
}
|
|
348
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Update host-managed tool policy for an active session.
|
|
389
|
+
*
|
|
390
|
+
* @param {string} sessionId
|
|
391
|
+
* @param {object|null} hostManagedToolPolicy
|
|
392
|
+
* @returns {Session|null}
|
|
393
|
+
*/
|
|
394
|
+
updateSessionPolicy(sessionId, hostManagedToolPolicy) {
|
|
395
|
+
const session = this.sessions.get(sessionId);
|
|
396
|
+
if (!session) return null;
|
|
397
|
+
|
|
398
|
+
session.hostManagedToolPolicy = hostManagedToolPolicy || null;
|
|
399
|
+
session.lastActivity = new Date().toISOString();
|
|
400
|
+
return session;
|
|
401
|
+
}
|
|
402
|
+
|
|
349
403
|
/**
|
|
350
404
|
* Persist current messages for a session.
|
|
351
405
|
*/
|
|
@@ -361,4 +415,31 @@ export class WSSessionManager {
|
|
|
361
415
|
|
|
362
416
|
session.lastActivity = new Date().toISOString();
|
|
363
417
|
}
|
|
418
|
+
|
|
419
|
+
_prepareSessionWorkspace(projectRoot, sessionId, options = {}) {
|
|
420
|
+
if (options.worktreeIsolation !== true) {
|
|
421
|
+
return {
|
|
422
|
+
projectRoot,
|
|
423
|
+
worktree: null,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!isGitRepo(projectRoot)) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`Worktree isolation requires a git repository: ${projectRoot}`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const branchName = `coding-agent/${sessionId}`;
|
|
434
|
+
const worktree = createWorktree(projectRoot, branchName);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
projectRoot: worktree.path,
|
|
438
|
+
worktree: {
|
|
439
|
+
branch: worktree.branch,
|
|
440
|
+
path: worktree.path,
|
|
441
|
+
baseProjectRoot: projectRoot,
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
364
445
|
}
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -48,6 +48,7 @@ import { CLIPermanentMemory } from "../lib/permanent-memory.js";
|
|
|
48
48
|
import { CLIAutonomousAgent, GoalStatus } from "../lib/autonomous-agent.js";
|
|
49
49
|
import { PromptCompressor } from "../lib/prompt-compressor.js";
|
|
50
50
|
import { feature } from "../lib/feature-flags.js";
|
|
51
|
+
import { recordCompressionMetric } from "../lib/compression-telemetry.js";
|
|
51
52
|
import {
|
|
52
53
|
AGENT_TOOLS,
|
|
53
54
|
buildSystemPrompt,
|
|
@@ -426,6 +427,11 @@ export async function startAgentRepl(options = {}) {
|
|
|
426
427
|
await _compressor.compress(messages);
|
|
427
428
|
messages.length = 0;
|
|
428
429
|
messages.push(...compacted);
|
|
430
|
+
recordCompressionMetric(stats, {
|
|
431
|
+
source: "manual-compact",
|
|
432
|
+
provider,
|
|
433
|
+
model,
|
|
434
|
+
});
|
|
429
435
|
logger.info(
|
|
430
436
|
`Compacted: ${stats.originalMessages} → ${stats.compressedMessages} messages, saved ${stats.saved} tokens (${stats.strategy})`,
|
|
431
437
|
);
|
|
@@ -1126,6 +1132,11 @@ export async function startAgentRepl(options = {}) {
|
|
|
1126
1132
|
await _compressor.compress(messages);
|
|
1127
1133
|
messages.length = 0;
|
|
1128
1134
|
messages.push(...compacted);
|
|
1135
|
+
recordCompressionMetric(stats, {
|
|
1136
|
+
source: "auto-compact",
|
|
1137
|
+
provider,
|
|
1138
|
+
model: activeModel,
|
|
1139
|
+
});
|
|
1129
1140
|
if (stats.saved > 0) {
|
|
1130
1141
|
logger.verbose(
|
|
1131
1142
|
`Auto-compacted: ${stats.strategy} (saved ${stats.saved} tokens)`,
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {
|
|
5
|
+
RuntimeEventEmitter,
|
|
6
|
+
RUNTIME_EVENTS,
|
|
7
|
+
createRuntimeEvent,
|
|
8
|
+
} from "./runtime-events.js";
|
|
9
|
+
import { createRuntimeContext } from "./runtime-context.js";
|
|
10
|
+
import { createAgentTurnRecord } from "./contracts/agent-turn.js";
|
|
11
|
+
import { logger } from "../lib/logger.js";
|
|
12
|
+
import { bootstrap } from "./bootstrap.js";
|
|
13
|
+
import { startAgentRepl } from "../gateways/repl/agent-repl.js";
|
|
14
|
+
import { startChatRepl } from "../gateways/repl/chat-repl.js";
|
|
15
|
+
import { ChainlessChainWSServer } from "../gateways/ws/ws-server.js";
|
|
16
|
+
import { WSSessionManager } from "../gateways/ws/ws-session-gateway.js";
|
|
17
|
+
import { createWebUIServer } from "../gateways/ui/web-ui-server.js";
|
|
18
|
+
import {
|
|
19
|
+
findProjectRoot,
|
|
20
|
+
loadProjectConfig,
|
|
21
|
+
} from "../lib/project-detector.js";
|
|
22
|
+
import { loadConfig } from "../lib/config-manager.js";
|
|
23
|
+
|
|
24
|
+
function openBrowser(url) {
|
|
25
|
+
try {
|
|
26
|
+
const platform = process.platform;
|
|
27
|
+
if (platform === "win32") {
|
|
28
|
+
execSync(`start "" "${url}"`, { stdio: "ignore" });
|
|
29
|
+
} else if (platform === "darwin") {
|
|
30
|
+
execSync(`open "${url}"`, { stdio: "ignore" });
|
|
31
|
+
} else {
|
|
32
|
+
execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
33
|
+
}
|
|
34
|
+
} catch (_err) {
|
|
35
|
+
// Non-critical.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class AgentRuntime {
|
|
40
|
+
constructor({ kind, policy, config = null, deps = {} } = {}) {
|
|
41
|
+
this.kind = kind;
|
|
42
|
+
this.policy = policy;
|
|
43
|
+
this.config = config;
|
|
44
|
+
this.context = createRuntimeContext({ kind, policy, config });
|
|
45
|
+
this.events = deps.events || new RuntimeEventEmitter();
|
|
46
|
+
this.deps = {
|
|
47
|
+
startAgentRepl: deps.startAgentRepl || startAgentRepl,
|
|
48
|
+
startChatRepl: deps.startChatRepl || startChatRepl,
|
|
49
|
+
bootstrap: deps.bootstrap || bootstrap,
|
|
50
|
+
createServer:
|
|
51
|
+
deps.createServer ||
|
|
52
|
+
((options) => new ChainlessChainWSServer(options)),
|
|
53
|
+
createSessionManager:
|
|
54
|
+
deps.createSessionManager ||
|
|
55
|
+
((options) => new WSSessionManager(options)),
|
|
56
|
+
createWebServer:
|
|
57
|
+
deps.createWebServer || ((options) => createWebUIServer(options)),
|
|
58
|
+
findProjectRoot: deps.findProjectRoot || findProjectRoot,
|
|
59
|
+
loadProjectConfig: deps.loadProjectConfig || loadProjectConfig,
|
|
60
|
+
loadConfig: deps.loadConfig || loadConfig,
|
|
61
|
+
openBrowser: deps.openBrowser || openBrowser,
|
|
62
|
+
runTurn: deps.runTurn || null,
|
|
63
|
+
logger: deps.logger || logger,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
on(eventName, listener) {
|
|
68
|
+
this.events.on(eventName, listener);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
emit(eventName, payload) {
|
|
73
|
+
const event = createRuntimeEvent(eventName, payload, {
|
|
74
|
+
kind: this.kind,
|
|
75
|
+
sessionId: this.policy?.sessionId || null,
|
|
76
|
+
});
|
|
77
|
+
this.events.emit(eventName, event);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async resumeSession(sessionId) {
|
|
81
|
+
const nextSessionId = sessionId || this.policy.sessionId;
|
|
82
|
+
if (!nextSessionId) {
|
|
83
|
+
throw new Error("resumeSession requires a sessionId.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.policy = {
|
|
87
|
+
...this.policy,
|
|
88
|
+
sessionId: nextSessionId,
|
|
89
|
+
};
|
|
90
|
+
this.context = createRuntimeContext({
|
|
91
|
+
kind: this.kind,
|
|
92
|
+
policy: this.policy,
|
|
93
|
+
config: this.config,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.emit(RUNTIME_EVENTS.SESSION_RESUME, {
|
|
97
|
+
kind: this.kind,
|
|
98
|
+
sessionId: nextSessionId,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (this.kind === "chat") {
|
|
102
|
+
return this.startChatSession();
|
|
103
|
+
}
|
|
104
|
+
if (this.kind === "agent") {
|
|
105
|
+
return this.startAgentSession();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error(`resumeSession is not supported for runtime kind "${this.kind}".`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async runTurn(input, meta = {}) {
|
|
112
|
+
if (typeof this.deps.runTurn !== "function") {
|
|
113
|
+
throw new Error(`runTurn is not configured for runtime kind "${this.kind}".`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
this.emit(RUNTIME_EVENTS.TURN_START, createAgentTurnRecord({
|
|
118
|
+
kind: this.kind,
|
|
119
|
+
input,
|
|
120
|
+
meta,
|
|
121
|
+
sessionId: this.policy.sessionId || null,
|
|
122
|
+
startedAt,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
const result = await this.deps.runTurn({
|
|
126
|
+
input,
|
|
127
|
+
meta,
|
|
128
|
+
kind: this.kind,
|
|
129
|
+
policy: this.policy,
|
|
130
|
+
context: this.context,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.emit(RUNTIME_EVENTS.TURN_END, createAgentTurnRecord({
|
|
134
|
+
kind: this.kind,
|
|
135
|
+
input,
|
|
136
|
+
meta,
|
|
137
|
+
result,
|
|
138
|
+
sessionId: this.policy.sessionId || null,
|
|
139
|
+
startedAt,
|
|
140
|
+
endedAt: Date.now(),
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async startAgentSession() {
|
|
147
|
+
this.emit(RUNTIME_EVENTS.RUNTIME_START, {
|
|
148
|
+
kind: this.kind,
|
|
149
|
+
policy: this.policy,
|
|
150
|
+
});
|
|
151
|
+
this.emit(RUNTIME_EVENTS.SESSION_START, {
|
|
152
|
+
kind: this.kind,
|
|
153
|
+
sessionId: this.policy.sessionId || null,
|
|
154
|
+
});
|
|
155
|
+
return this.deps.startAgentRepl(this.policy);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async startChatSession() {
|
|
159
|
+
this.emit(RUNTIME_EVENTS.RUNTIME_START, {
|
|
160
|
+
kind: this.kind,
|
|
161
|
+
policy: this.policy,
|
|
162
|
+
});
|
|
163
|
+
this.emit(RUNTIME_EVENTS.SESSION_START, {
|
|
164
|
+
kind: this.kind,
|
|
165
|
+
sessionId: this.policy.sessionId || null,
|
|
166
|
+
});
|
|
167
|
+
return this.deps.startChatRepl(this.policy);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async startServer() {
|
|
171
|
+
const { logger: runtimeLogger } = this.deps;
|
|
172
|
+
const {
|
|
173
|
+
port,
|
|
174
|
+
maxConnections,
|
|
175
|
+
timeout,
|
|
176
|
+
token,
|
|
177
|
+
allowRemote,
|
|
178
|
+
project,
|
|
179
|
+
} = this.policy;
|
|
180
|
+
let { host } = this.policy;
|
|
181
|
+
|
|
182
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
183
|
+
throw new Error("Invalid port number. Must be between 1 and 65535.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (allowRemote) {
|
|
187
|
+
if (!token) {
|
|
188
|
+
throw new Error("--allow-remote requires --token for security.");
|
|
189
|
+
}
|
|
190
|
+
host = "0.0.0.0";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let db = null;
|
|
194
|
+
try {
|
|
195
|
+
const ctx = await this.deps.bootstrap({ skipDb: false });
|
|
196
|
+
db = ctx.db?.getDb?.() || null;
|
|
197
|
+
} catch (_err) {
|
|
198
|
+
runtimeLogger.log(
|
|
199
|
+
chalk.yellow(
|
|
200
|
+
" Warning: Database not available, sessions will be in-memory only",
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const sessionManager = this.deps.createSessionManager({
|
|
206
|
+
db,
|
|
207
|
+
defaultProjectRoot: project,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const server = this.deps.createServer({
|
|
211
|
+
port,
|
|
212
|
+
host,
|
|
213
|
+
token,
|
|
214
|
+
maxConnections,
|
|
215
|
+
timeout,
|
|
216
|
+
sessionManager,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
server.on("connection", ({ clientId, ip }) => {
|
|
220
|
+
runtimeLogger.log(chalk.green(` + Client connected: ${clientId} (${ip})`));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
server.on("disconnection", ({ clientId, reason }) => {
|
|
224
|
+
const extra = reason ? ` (${reason})` : "";
|
|
225
|
+
runtimeLogger.log(
|
|
226
|
+
chalk.yellow(` - Client disconnected: ${clientId}${extra}`),
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
server.on("command:start", ({ id, command }) => {
|
|
231
|
+
runtimeLogger.log(chalk.cyan(` > [${id}] ${command}`));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
server.on("command:end", ({ id, exitCode }) => {
|
|
235
|
+
const color = exitCode === 0 ? chalk.green : chalk.red;
|
|
236
|
+
runtimeLogger.log(color(` < [${id}] exit ${exitCode}`));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
server.on("session:create", ({ sessionId, type }) => {
|
|
240
|
+
runtimeLogger.log(
|
|
241
|
+
chalk.green(` + Session created: ${sessionId} (${type})`),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
server.on("session:close", ({ sessionId }) => {
|
|
246
|
+
runtimeLogger.log(chalk.yellow(` - Session closed: ${sessionId}`));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const shutdownHandler = async () => {
|
|
250
|
+
runtimeLogger.log(
|
|
251
|
+
"\n" + chalk.yellow("Shutting down WebSocket server..."),
|
|
252
|
+
);
|
|
253
|
+
await server.stop();
|
|
254
|
+
process.exit(0);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
process.on("SIGINT", shutdownHandler);
|
|
258
|
+
process.on("SIGTERM", shutdownHandler);
|
|
259
|
+
|
|
260
|
+
await server.start();
|
|
261
|
+
|
|
262
|
+
this.emit(RUNTIME_EVENTS.RUNTIME_START, {
|
|
263
|
+
kind: this.kind,
|
|
264
|
+
policy: { ...this.policy, host },
|
|
265
|
+
});
|
|
266
|
+
this.emit(RUNTIME_EVENTS.SERVER_START, {
|
|
267
|
+
host,
|
|
268
|
+
port,
|
|
269
|
+
project,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
runtimeLogger.log("");
|
|
273
|
+
runtimeLogger.log(chalk.bold(" ChainlessChain WebSocket Server"));
|
|
274
|
+
runtimeLogger.log("");
|
|
275
|
+
runtimeLogger.log(` Address: ${chalk.cyan(`ws://${host}:${port}`)}`);
|
|
276
|
+
runtimeLogger.log(
|
|
277
|
+
` Auth: ${token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
|
|
278
|
+
);
|
|
279
|
+
runtimeLogger.log(` Sessions: ${chalk.green("enabled")}`);
|
|
280
|
+
runtimeLogger.log(` Project: ${project}`);
|
|
281
|
+
runtimeLogger.log(` Max conn: ${maxConnections}`);
|
|
282
|
+
runtimeLogger.log(` Timeout: ${timeout}ms`);
|
|
283
|
+
runtimeLogger.log("");
|
|
284
|
+
runtimeLogger.log(chalk.dim(" Press Ctrl+C to stop"));
|
|
285
|
+
runtimeLogger.log("");
|
|
286
|
+
|
|
287
|
+
return server;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async startUiServer() {
|
|
291
|
+
const { logger: runtimeLogger } = this.deps;
|
|
292
|
+
const httpPort = this.policy.port;
|
|
293
|
+
const wsPort = this.policy.wsPort;
|
|
294
|
+
const host = this.policy.host;
|
|
295
|
+
|
|
296
|
+
if (Number.isNaN(httpPort) || httpPort < 1 || httpPort > 65535) {
|
|
297
|
+
throw new Error("Invalid --port. Must be between 1 and 65535.");
|
|
298
|
+
}
|
|
299
|
+
if (Number.isNaN(wsPort) || wsPort < 1 || wsPort > 65535) {
|
|
300
|
+
throw new Error("Invalid --ws-port. Must be between 1 and 65535.");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const projectRoot = this.deps.findProjectRoot(process.cwd());
|
|
304
|
+
const projectConfig = projectRoot
|
|
305
|
+
? this.deps.loadProjectConfig(projectRoot)
|
|
306
|
+
: null;
|
|
307
|
+
const projectName =
|
|
308
|
+
projectConfig?.name ||
|
|
309
|
+
(projectRoot ? path.basename(projectRoot) : null);
|
|
310
|
+
const mode = projectRoot ? "project" : "global";
|
|
311
|
+
|
|
312
|
+
let db = null;
|
|
313
|
+
try {
|
|
314
|
+
const ctx = await this.deps.bootstrap({ skipDb: false });
|
|
315
|
+
db = ctx.db?.getDb?.() || null;
|
|
316
|
+
} catch (_err) {
|
|
317
|
+
runtimeLogger.log(
|
|
318
|
+
chalk.yellow(
|
|
319
|
+
" Warning: Database not available, sessions will be in-memory only",
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const appConfig = this.deps.loadConfig();
|
|
325
|
+
const sessionManager = this.deps.createSessionManager({
|
|
326
|
+
db,
|
|
327
|
+
defaultProjectRoot: projectRoot || process.cwd(),
|
|
328
|
+
config: appConfig,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const wsServer = this.deps.createServer({
|
|
332
|
+
port: wsPort,
|
|
333
|
+
host,
|
|
334
|
+
token: this.policy.token,
|
|
335
|
+
maxConnections: 20,
|
|
336
|
+
timeout: 60000,
|
|
337
|
+
sessionManager,
|
|
338
|
+
});
|
|
339
|
+
await wsServer.start();
|
|
340
|
+
|
|
341
|
+
const httpServer = this.deps.createWebServer({
|
|
342
|
+
wsPort,
|
|
343
|
+
wsToken: this.policy.token,
|
|
344
|
+
wsHost: host === "0.0.0.0" ? "127.0.0.1" : host,
|
|
345
|
+
projectRoot,
|
|
346
|
+
projectName,
|
|
347
|
+
mode,
|
|
348
|
+
staticDir: this.policy.webPanelDir,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await new Promise((resolve, reject) => {
|
|
352
|
+
httpServer.listen(httpPort, host, () => resolve());
|
|
353
|
+
httpServer.on("error", reject);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const uiUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${httpPort}`;
|
|
357
|
+
|
|
358
|
+
this.emit(RUNTIME_EVENTS.RUNTIME_START, {
|
|
359
|
+
kind: this.kind,
|
|
360
|
+
policy: this.policy,
|
|
361
|
+
});
|
|
362
|
+
this.emit(RUNTIME_EVENTS.SERVER_START, {
|
|
363
|
+
host,
|
|
364
|
+
port: httpPort,
|
|
365
|
+
wsPort,
|
|
366
|
+
mode,
|
|
367
|
+
projectRoot,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
runtimeLogger.log("");
|
|
371
|
+
runtimeLogger.log(chalk.bold(" ChainlessChain 管理面板"));
|
|
372
|
+
runtimeLogger.log("");
|
|
373
|
+
if (mode === "project") {
|
|
374
|
+
runtimeLogger.log(
|
|
375
|
+
` Mode: ${chalk.cyan("project")} ${chalk.dim(projectRoot)}`,
|
|
376
|
+
);
|
|
377
|
+
if (projectName) {
|
|
378
|
+
runtimeLogger.log(` Project: ${chalk.green(projectName)}`);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
runtimeLogger.log(` Mode: ${chalk.cyan("global")}`);
|
|
382
|
+
}
|
|
383
|
+
runtimeLogger.log(` UI: ${chalk.cyan(uiUrl)}`);
|
|
384
|
+
runtimeLogger.log(` WS: ${chalk.dim(`ws://${host}:${wsPort}`)}`);
|
|
385
|
+
runtimeLogger.log(
|
|
386
|
+
` Auth: ${this.policy.token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
|
|
387
|
+
);
|
|
388
|
+
runtimeLogger.log("");
|
|
389
|
+
runtimeLogger.log(chalk.dim(" Press Ctrl+C to stop"));
|
|
390
|
+
runtimeLogger.log("");
|
|
391
|
+
|
|
392
|
+
if (this.policy.open) {
|
|
393
|
+
this.deps.openBrowser(uiUrl);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const shutdown = async () => {
|
|
397
|
+
runtimeLogger.log("\n" + chalk.yellow("Shutting down UI server..."));
|
|
398
|
+
await Promise.all([
|
|
399
|
+
new Promise((resolve) => httpServer.close(resolve)),
|
|
400
|
+
wsServer.stop(),
|
|
401
|
+
]);
|
|
402
|
+
process.exit(0);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
process.on("SIGINT", shutdown);
|
|
406
|
+
process.on("SIGTERM", shutdown);
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
wsServer,
|
|
410
|
+
httpServer,
|
|
411
|
+
uiUrl,
|
|
412
|
+
mode,
|
|
413
|
+
projectRoot,
|
|
414
|
+
projectName,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function createAgentTurnRecord(turn = {}) {
|
|
2
|
+
return {
|
|
3
|
+
kind: turn.kind || null,
|
|
4
|
+
sessionId: turn.sessionId || null,
|
|
5
|
+
input: turn.input,
|
|
6
|
+
meta: turn.meta || {},
|
|
7
|
+
result: turn.result,
|
|
8
|
+
startedAt: turn.startedAt || null,
|
|
9
|
+
endedAt: turn.endedAt || null,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createWorktreeRecord } from "./worktree-record.js";
|
|
2
|
+
|
|
3
|
+
export function createSessionRecord(session = {}, extras = {}) {
|
|
4
|
+
const history = Array.isArray(extras.history)
|
|
5
|
+
? extras.history
|
|
6
|
+
: Array.isArray(session.messages)
|
|
7
|
+
? session.messages.filter((item) => item.role !== "system")
|
|
8
|
+
: [];
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
id: session.id || extras.sessionId || null,
|
|
12
|
+
type: session.type || extras.sessionType || null,
|
|
13
|
+
provider: session.provider || extras.provider || null,
|
|
14
|
+
model: session.model || extras.model || null,
|
|
15
|
+
projectRoot: session.projectRoot || extras.projectRoot || null,
|
|
16
|
+
baseProjectRoot: session.baseProjectRoot || extras.baseProjectRoot || null,
|
|
17
|
+
worktreeIsolation:
|
|
18
|
+
session.worktreeIsolation === true || extras.worktreeIsolation === true,
|
|
19
|
+
worktree:
|
|
20
|
+
session.worktree || extras.worktree
|
|
21
|
+
? createWorktreeRecord(session.worktree || extras.worktree, {
|
|
22
|
+
requested:
|
|
23
|
+
session.worktreeIsolation === true ||
|
|
24
|
+
extras.worktreeIsolation === true,
|
|
25
|
+
})
|
|
26
|
+
: null,
|
|
27
|
+
messageCount: extras.messageCount ?? history.length,
|
|
28
|
+
history,
|
|
29
|
+
status: extras.status || null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function createTaskRecord(task = {}, meta = {}) {
|
|
2
|
+
return {
|
|
3
|
+
id: task.id || null,
|
|
4
|
+
status: task.status || null,
|
|
5
|
+
type: task.type || null,
|
|
6
|
+
description: task.description || null,
|
|
7
|
+
ownerNodeId: task.ownerNodeId || null,
|
|
8
|
+
createdAt: task.createdAt || null,
|
|
9
|
+
startedAt: task.startedAt || null,
|
|
10
|
+
completedAt: task.completedAt || null,
|
|
11
|
+
result: task.result ?? null,
|
|
12
|
+
error: task.error ?? null,
|
|
13
|
+
recoveredFromRestart: Boolean(task.recoveredFromRestart),
|
|
14
|
+
recoverySourceStatus: task.recoverySourceStatus || null,
|
|
15
|
+
outputSummary: task.outputSummary || null,
|
|
16
|
+
meta,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createTelemetryRecord(metric = {}, meta = {}) {
|
|
2
|
+
return {
|
|
3
|
+
kind: metric.kind || "telemetry",
|
|
4
|
+
provider: metric.provider || null,
|
|
5
|
+
model: metric.model || null,
|
|
6
|
+
source: metric.source || null,
|
|
7
|
+
strategy: metric.strategy || null,
|
|
8
|
+
variant: metric.variant || metric.abVariant || null,
|
|
9
|
+
savedTokens:
|
|
10
|
+
typeof metric.savedTokens === "number"
|
|
11
|
+
? metric.savedTokens
|
|
12
|
+
: typeof metric.saved === "number"
|
|
13
|
+
? metric.saved
|
|
14
|
+
: null,
|
|
15
|
+
originalTokens:
|
|
16
|
+
typeof metric.originalTokens === "number" ? metric.originalTokens : null,
|
|
17
|
+
compressedTokens:
|
|
18
|
+
typeof metric.compressedTokens === "number" ? metric.compressedTokens : null,
|
|
19
|
+
ratio: typeof metric.ratio === "number" ? metric.ratio : null,
|
|
20
|
+
timestamp: metric.timestamp || Date.now(),
|
|
21
|
+
meta,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createWorktreeRecord(worktree = {}, meta = {}) {
|
|
2
|
+
return {
|
|
3
|
+
branch: worktree.branch || null,
|
|
4
|
+
path: worktree.path || worktree.worktreePath || null,
|
|
5
|
+
baseBranch: worktree.baseBranch || null,
|
|
6
|
+
hasChanges: typeof worktree.hasChanges === "boolean" ? worktree.hasChanges : null,
|
|
7
|
+
summary: worktree.summary || null,
|
|
8
|
+
conflicts: Array.isArray(worktree.conflicts) ? worktree.conflicts : [],
|
|
9
|
+
previewEntrypoints: Array.isArray(worktree.previewEntrypoints)
|
|
10
|
+
? worktree.previewEntrypoints
|
|
11
|
+
: [],
|
|
12
|
+
meta,
|
|
13
|
+
};
|
|
14
|
+
}
|