@lightcone-ai/daemon 0.15.64 → 0.15.66
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/agent-manager.js +14 -0
- package/src/drivers/codex.js +1 -1
- package/src/mcp-config.js +30 -15
- package/src/tools/compose-video-v2.js +40 -2
- package/src/workspace-file-upload.js +12 -24
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -1629,6 +1629,17 @@ export class AgentManager {
|
|
|
1629
1629
|
}
|
|
1630
1630
|
}
|
|
1631
1631
|
|
|
1632
|
+
_reportTurnUsage(connection, { agentId, workspaceId, agent, usage = null }) {
|
|
1633
|
+
connection.send({
|
|
1634
|
+
type: 'agent:turn_usage',
|
|
1635
|
+
agentId,
|
|
1636
|
+
workspaceId,
|
|
1637
|
+
sessionId: agent?.sessionId ?? null,
|
|
1638
|
+
runtime: agent?.runtime ?? null,
|
|
1639
|
+
usage,
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1632
1643
|
async _postInternalActionComplete({ agentId, actionId, ok, result, error }) {
|
|
1633
1644
|
const url = `${this.serverUrl.replace(/\/$/, '')}/internal/agent/${encodeURIComponent(agentId)}/actions/${encodeURIComponent(actionId)}/complete`;
|
|
1634
1645
|
const res = await fetch(url, {
|
|
@@ -1985,6 +1996,7 @@ export class AgentManager {
|
|
|
1985
1996
|
case 'turn_end':
|
|
1986
1997
|
agent.kimiIdle = true;
|
|
1987
1998
|
this._resetVisibleReplyTracking(agent);
|
|
1999
|
+
this._reportTurnUsage(connection, { agentId, workspaceId, agent });
|
|
1988
2000
|
this._emitLifecycle(connection, {
|
|
1989
2001
|
agentId,
|
|
1990
2002
|
workspaceId,
|
|
@@ -2077,6 +2089,7 @@ export class AgentManager {
|
|
|
2077
2089
|
agent.codexVisibleTextSeen = false;
|
|
2078
2090
|
agent.codexSendMessageUsed = false;
|
|
2079
2091
|
this._resetVisibleReplyTracking(agent);
|
|
2092
|
+
this._reportTurnUsage(connection, { agentId, workspaceId, agent, usage: evt.usage ?? null });
|
|
2080
2093
|
this._emitLifecycle(connection, {
|
|
2081
2094
|
agentId,
|
|
2082
2095
|
workspaceId,
|
|
@@ -2195,6 +2208,7 @@ export class AgentManager {
|
|
|
2195
2208
|
}
|
|
2196
2209
|
this._resetVisibleReplyTracking(agent);
|
|
2197
2210
|
console.log(`[AgentManager][${displayName}] turn done (stop_reason=${event.stop_reason ?? '?'})`);
|
|
2211
|
+
this._reportTurnUsage(connection, { agentId, workspaceId, agent, usage: event.usage ?? null });
|
|
2198
2212
|
this._emitLifecycle(connection, {
|
|
2199
2213
|
agentId,
|
|
2200
2214
|
workspaceId,
|
package/src/drivers/codex.js
CHANGED
|
@@ -306,7 +306,7 @@ export function parseCodexLine(line) {
|
|
|
306
306
|
break;
|
|
307
307
|
}
|
|
308
308
|
case 'turn.completed':
|
|
309
|
-
events.push({ kind: 'turn_end' });
|
|
309
|
+
events.push({ kind: 'turn_end', usage: event.usage && typeof event.usage === 'object' ? event.usage : null });
|
|
310
310
|
break;
|
|
311
311
|
case 'turn.failed':
|
|
312
312
|
if (event.error?.message) events.push({ kind: 'error', message: event.error.message });
|
package/src/mcp-config.js
CHANGED
|
@@ -62,25 +62,40 @@ function resolveSkillArg(arg, config) {
|
|
|
62
62
|
return arg;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// MCP servers that talk to the lightcone server at runtime — either a full
|
|
66
|
+
// server-backed MCP (mysql / publisher / platform / domain-data services) or a
|
|
67
|
+
// thin-proxy (`daemon/mcp-servers/_thin-proxy/forward.js`) whose tools forward
|
|
68
|
+
// to /internal/agent/:agentId/mcp/:serverId/:toolName. All of these need the
|
|
69
|
+
// SERVER_URL / MACHINE_API_KEY / AGENT_ID triple injected, or the proxy throws
|
|
70
|
+
// "thin-proxy missing SERVER_URL / MACHINE_API_KEY / AGENT_ID env".
|
|
71
|
+
const SERVER_BACKED_MCP_SERVERS = new Set([
|
|
72
|
+
'mysql',
|
|
73
|
+
'publisher',
|
|
74
|
+
'platform',
|
|
75
|
+
'research-fetch',
|
|
76
|
+
'wechat-mp-fetch',
|
|
77
|
+
'market-data-query',
|
|
78
|
+
'company-fundamentals',
|
|
79
|
+
'industry-report',
|
|
80
|
+
'risk-metrics',
|
|
81
|
+
'compliance-check',
|
|
82
|
+
'portfolio-read',
|
|
83
|
+
'portfolio-analysis',
|
|
84
|
+
// thin-proxy MCP services migrated to the server (roadmap §4) — every one of
|
|
85
|
+
// these uses startThinProxy() and requires the SERVER_URL/MACHINE_API_KEY/AGENT_ID triple.
|
|
86
|
+
'video-narration-planner',
|
|
87
|
+
'page-understanding',
|
|
88
|
+
'platform-policy-db',
|
|
89
|
+
'keyword-research',
|
|
90
|
+
'audience-research',
|
|
91
|
+
'hook-pattern-library',
|
|
92
|
+
]);
|
|
93
|
+
|
|
65
94
|
function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, workspaceId, workspaceDir }) {
|
|
66
95
|
if (serverKey === 'workspace-migrate') {
|
|
67
96
|
return { SERVER_URL: serverUrl, MACHINE_API_KEY: authToken, AGENT_ID: agentId };
|
|
68
97
|
}
|
|
69
|
-
if (
|
|
70
|
-
serverKey === 'mysql'
|
|
71
|
-
|| serverKey === 'publisher'
|
|
72
|
-
|| serverKey === 'platform'
|
|
73
|
-
|| serverKey === 'research-fetch'
|
|
74
|
-
|| serverKey === 'wechat-mp-fetch'
|
|
75
|
-
|| serverKey === 'market-data-query'
|
|
76
|
-
|| serverKey === 'company-fundamentals'
|
|
77
|
-
|| serverKey === 'industry-report'
|
|
78
|
-
|| serverKey === 'risk-metrics'
|
|
79
|
-
|| serverKey === 'compliance-check'
|
|
80
|
-
|| serverKey === 'portfolio-read'
|
|
81
|
-
|| serverKey === 'portfolio-analysis'
|
|
82
|
-
|| serverKey === 'video-narration-planner'
|
|
83
|
-
) {
|
|
98
|
+
if (SERVER_BACKED_MCP_SERVERS.has(serverKey)) {
|
|
84
99
|
return {
|
|
85
100
|
SERVER_URL: serverUrl,
|
|
86
101
|
MACHINE_API_KEY: authToken,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import os from 'os';
|
|
3
|
+
import fs from 'fs';
|
|
3
4
|
import { randomUUID } from 'crypto';
|
|
4
5
|
import { composeVideoV2 } from '../_vendor/video/composer-v2/index.js';
|
|
5
6
|
|
|
@@ -11,11 +12,22 @@ function toolError(text) {
|
|
|
11
12
|
return { isError: true, content: [{ type: 'text', text }] };
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
// A real screenshot / rendered card / video frame is at least tens of KB. A
|
|
16
|
+
// few-KB image is almost certainly a blank or broken capture (e.g. the page
|
|
17
|
+
// hadn't finished loading when screenshotted). Composing from those produces a
|
|
18
|
+
// near-blank video, so reject up front rather than emit garbage.
|
|
19
|
+
const MIN_IMAGE_BYTES = 4 * 1024;
|
|
20
|
+
|
|
21
|
+
function statSizeOrNull(p) {
|
|
22
|
+
try { return fs.statSync(p).size; } catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
export async function runComposeVideoV2Tool({ segments, outro_paths, format, resolution, output_path, workspaceDir }) {
|
|
15
26
|
if (!Array.isArray(segments) || segments.length === 0) {
|
|
16
27
|
return toolError('segments must be a non-empty array.');
|
|
17
28
|
}
|
|
18
29
|
|
|
30
|
+
const imagePaths = [];
|
|
19
31
|
for (let i = 0; i < segments.length; i++) {
|
|
20
32
|
const seg = segments[i];
|
|
21
33
|
const kind = seg.visual_kind;
|
|
@@ -30,6 +42,30 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
|
|
|
30
42
|
if (kind !== 'carousel' && !seg.visual_path) {
|
|
31
43
|
return toolError(`segments[${i}]: visual_path required for kind=${kind}.`);
|
|
32
44
|
}
|
|
45
|
+
if (kind === 'image' && seg.visual_path) imagePaths.push(seg.visual_path);
|
|
46
|
+
if (kind === 'carousel' && Array.isArray(seg.visual_paths)) imagePaths.push(...seg.visual_paths.filter(Boolean));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Garbage-in guard: refuse to compose from blank/broken source images.
|
|
50
|
+
for (const p of new Set(imagePaths)) {
|
|
51
|
+
const size = statSizeOrNull(p);
|
|
52
|
+
if (size === null) {
|
|
53
|
+
return toolError(`Source image not found: ${p}. Re-capture/render it before composing.`);
|
|
54
|
+
}
|
|
55
|
+
if (size < MIN_IMAGE_BYTES) {
|
|
56
|
+
return toolError(
|
|
57
|
+
`Source image ${p} is only ${size} bytes — almost certainly a blank or broken capture `
|
|
58
|
+
+ `(e.g. the page hadn't finished loading when screenshotted). Re-capture with the page fully loaded, then retry.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Heuristic warning: a multi-segment image video that reuses one single image
|
|
63
|
+
// will look near-static — usually a sign the source page didn't render and the
|
|
64
|
+
// agent fell back to one blank screenshot.
|
|
65
|
+
let warning = null;
|
|
66
|
+
if (imagePaths.length >= 2 && new Set(imagePaths).size === 1) {
|
|
67
|
+
warning = `WARNING: all ${imagePaths.length} image segments reuse the same file (${imagePaths[0]}). `
|
|
68
|
+
+ 'The output will be near-static — verify the source page actually rendered before submitting this video.';
|
|
33
69
|
}
|
|
34
70
|
|
|
35
71
|
const outDir = workspaceDir
|
|
@@ -46,13 +82,15 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
|
|
|
46
82
|
output_path: outPath,
|
|
47
83
|
});
|
|
48
84
|
|
|
49
|
-
|
|
85
|
+
const lines = [
|
|
50
86
|
'compose_video_v2 completed.',
|
|
51
87
|
`path=${result.path}`,
|
|
52
88
|
`duration_ms=${result.duration_ms}`,
|
|
53
89
|
`segments=${segments.length}`,
|
|
54
90
|
`outro_clips=${(outro_paths ?? []).length}`,
|
|
55
|
-
]
|
|
91
|
+
];
|
|
92
|
+
if (warning) lines.push(warning);
|
|
93
|
+
return toolText(lines.join('\n'));
|
|
56
94
|
} catch (error) {
|
|
57
95
|
return toolError(`compose_video_v2 failed: ${error.message}`);
|
|
58
96
|
}
|
|
@@ -20,8 +20,6 @@ const TEXT_EXTS = new Set([
|
|
|
20
20
|
'.csv', '.xml', '.yaml', '.yml', '.sh',
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
-
const LARGE_FILE_THRESHOLD = 5 * 1024 * 1024; // 5MB
|
|
24
|
-
|
|
25
23
|
export function resolveWorkspaceFileUploadPlan({ localPath, workspacePath }) {
|
|
26
24
|
const target = String(workspacePath ?? '').trim() || String(localPath ?? '').trim();
|
|
27
25
|
const ext = extname(target).toLowerCase();
|
|
@@ -36,9 +34,11 @@ function sha256ofBuffer(buf) {
|
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
|
-
* Upload a local file to the workspace via the
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* Upload a local file to the workspace via the presign → PUT → confirm flow.
|
|
38
|
+
* The PUT + confirm are awaited: a fire-and-forget background upload could be
|
|
39
|
+
* abandoned when the agent run / daemon process ends, leaving the DB row stuck
|
|
40
|
+
* at object_status='uploading' forever (the file then 202s "uploading" on every
|
|
41
|
+
* read). Blocking the tool call for a few seconds is cheaper than that.
|
|
42
42
|
*/
|
|
43
43
|
async function uploadBinaryFile({
|
|
44
44
|
localPath,
|
|
@@ -64,25 +64,13 @@ async function uploadBinaryFile({
|
|
|
64
64
|
return { mode: 'object-storage', mime, bytes: size, objectKey, deduped: true };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
if (!resp.ok) throw new Error(`COS PUT failed: ${resp.status} ${await resp.text()}`);
|
|
76
|
-
await confirmUpload({ workspaceId, path: workspacePath, objectKey });
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
if (isLarge) {
|
|
80
|
-
// Fire-and-forget for large files; server already wrote `uploading` status
|
|
81
|
-
doPut().catch(err => console.error('[workspace-upload] large file background upload failed:', err?.message));
|
|
82
|
-
return { mode: 'object-storage', mime, bytes: size, objectKey, async: true };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
await doPut();
|
|
67
|
+
const resp = await fetch(uploadUrl, {
|
|
68
|
+
method: 'PUT',
|
|
69
|
+
body: buf,
|
|
70
|
+
headers: { 'Content-Type': mime, 'Content-Length': String(size) },
|
|
71
|
+
});
|
|
72
|
+
if (!resp.ok) throw new Error(`COS PUT failed: ${resp.status} ${await resp.text()}`);
|
|
73
|
+
await confirmUpload({ workspaceId, path: workspacePath, objectKey });
|
|
86
74
|
return { mode: 'object-storage', mime, bytes: size, objectKey, async: false };
|
|
87
75
|
}
|
|
88
76
|
|