@lightcone-ai/daemon 0.15.58 → 0.15.60
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.
|
@@ -138,13 +138,14 @@ export async function callOfficialTool({
|
|
|
138
138
|
toolName,
|
|
139
139
|
argumentsPayload = {},
|
|
140
140
|
timeoutMs = 10000,
|
|
141
|
+
env = {},
|
|
141
142
|
}) {
|
|
142
|
-
const entrypointPath = resolveMcpServerEntrypoint(serverId
|
|
143
|
+
const entrypointPath = resolveMcpServerEntrypoint(serverId);
|
|
143
144
|
if (!entrypointPath) {
|
|
144
145
|
throw new Error(`official_server_not_found:${serverId}`);
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
const client = new StdioJsonRpcClient(process.execPath, [entrypointPath]);
|
|
148
|
+
const client = new StdioJsonRpcClient(process.execPath, [entrypointPath], env);
|
|
148
149
|
try {
|
|
149
150
|
await client.start();
|
|
150
151
|
await client.request('initialize', {
|
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -179,10 +179,53 @@ export class AgentManager {
|
|
|
179
179
|
return `${workspaceId ?? ''}:${agentId}`;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Snapshot of agents this daemon process is currently managing. Sent to the
|
|
184
|
+
* server alongside 'ready' so the server can reconcile lifecycle state
|
|
185
|
+
* after a reconnect — agents previously on this machine but absent from
|
|
186
|
+
* the inventory get reset from stale `crashed unreachable` back to a clean
|
|
187
|
+
* `standby reachable available` baseline (machine is healthy, agent will
|
|
188
|
+
* be spawned on demand).
|
|
189
|
+
*
|
|
190
|
+
* Each entry mirrors the daemon-internal agent state machine:
|
|
191
|
+
* - 'starting': spawn in progress (entry exists in this.starting)
|
|
192
|
+
* - 'running': child process is alive (this.agents has it)
|
|
193
|
+
* The server treats whatever is NOT in this list as 'standby' (spawnable
|
|
194
|
+
* but not currently running on this machine).
|
|
195
|
+
*/
|
|
196
|
+
getAgentInventory() {
|
|
197
|
+
const inventory = [];
|
|
198
|
+
for (const key of this.starting) {
|
|
199
|
+
const colonIdx = key.indexOf(':');
|
|
200
|
+
const workspaceId = colonIdx === -1 ? null : (key.slice(0, colonIdx) || null);
|
|
201
|
+
const agentId = colonIdx === -1 ? key : key.slice(colonIdx + 1);
|
|
202
|
+
inventory.push({
|
|
203
|
+
agentId,
|
|
204
|
+
workspaceId,
|
|
205
|
+
runtimeState: 'starting',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
for (const [key, agent] of this.agents.entries()) {
|
|
209
|
+
const colonIdx = key.indexOf(':');
|
|
210
|
+
const workspaceId = agent.workspaceId
|
|
211
|
+
?? (colonIdx === -1 ? null : (key.slice(0, colonIdx) || null));
|
|
212
|
+
const agentId = agent.agentId
|
|
213
|
+
?? (colonIdx === -1 ? key : key.slice(colonIdx + 1));
|
|
214
|
+
inventory.push({
|
|
215
|
+
agentId,
|
|
216
|
+
workspaceId,
|
|
217
|
+
runtimeState: 'running',
|
|
218
|
+
sessionId: agent.sessionId ?? null,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return inventory;
|
|
222
|
+
}
|
|
223
|
+
|
|
182
224
|
handle(msg, connection) {
|
|
183
225
|
switch (msg.type) {
|
|
184
226
|
case 'agent:start': return this._startAgent(msg, connection);
|
|
185
227
|
case 'agent:stop': return this._stopAgent(msg.agentId, msg.workspaceId, connection);
|
|
228
|
+
case 'agent:cancel_turn': return this._cancelAgentTurn(msg.agentId, msg.workspaceId, connection);
|
|
186
229
|
case 'agent:deliver': return this._deliverMessage(msg, connection);
|
|
187
230
|
case 'agent:reprobe': return this._reprobeCapability(msg, connection);
|
|
188
231
|
case 'publish:job': return this._handlePublishJob(msg, connection);
|
|
@@ -1714,6 +1757,61 @@ export class AgentManager {
|
|
|
1714
1757
|
// exit handler will report status
|
|
1715
1758
|
}
|
|
1716
1759
|
|
|
1760
|
+
_cancelAgentTurn(agentId, workspaceId, connection) {
|
|
1761
|
+
const key = this._key(agentId, workspaceId);
|
|
1762
|
+
const queued = this._pendingMessages?.get(key)?.length ?? 0;
|
|
1763
|
+
if (queued > 0) {
|
|
1764
|
+
this._pendingMessages.delete(key);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
const agent = this.agents.get(key);
|
|
1768
|
+
if (!agent?.proc) {
|
|
1769
|
+
console.log(`[AgentManager] Cancel turn requested for inactive agent ${agentId} workspace=${workspaceId ?? 'none'} queued_cleared=${queued}`);
|
|
1770
|
+
connection.send({
|
|
1771
|
+
type: 'agent:cancel_turn:ack',
|
|
1772
|
+
agentId,
|
|
1773
|
+
workspaceId,
|
|
1774
|
+
ok: queued > 0,
|
|
1775
|
+
queuedCleared: queued,
|
|
1776
|
+
running: false,
|
|
1777
|
+
});
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
console.log(`[AgentManager] Cancelling current turn for agent ${agentId} workspace=${workspaceId ?? 'none'} queued_cleared=${queued}`);
|
|
1782
|
+
agent.stopCause = 'user_cancelled';
|
|
1783
|
+
connection.send({
|
|
1784
|
+
type: 'agent:cancel_turn:ack',
|
|
1785
|
+
agentId,
|
|
1786
|
+
workspaceId,
|
|
1787
|
+
ok: true,
|
|
1788
|
+
queuedCleared: queued,
|
|
1789
|
+
running: true,
|
|
1790
|
+
});
|
|
1791
|
+
this._emitLifecycle(connection, {
|
|
1792
|
+
agentId,
|
|
1793
|
+
workspaceId,
|
|
1794
|
+
runtime: agent.runtime ?? 'claude',
|
|
1795
|
+
reachability: 'reachable',
|
|
1796
|
+
availability: 'available',
|
|
1797
|
+
runtimeState: 'running',
|
|
1798
|
+
reason: 'user_cancelled',
|
|
1799
|
+
sessionId: agent.sessionId ?? null,
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
try {
|
|
1803
|
+
agent.proc.kill('SIGINT');
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
console.error(`[AgentManager] cancel SIGINT failed for ${key}:`, err?.message ?? err);
|
|
1806
|
+
}
|
|
1807
|
+
setTimeout(() => {
|
|
1808
|
+
if (this.agents.get(key) !== agent) return;
|
|
1809
|
+
try {
|
|
1810
|
+
agent.proc.kill('SIGTERM');
|
|
1811
|
+
} catch {}
|
|
1812
|
+
}, 1500);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1717
1815
|
_deliverMessage(msg, connection) {
|
|
1718
1816
|
const { agentId, workspaceId, seq, message } = msg;
|
|
1719
1817
|
const key = this._key(agentId, workspaceId);
|
package/src/chat-bridge.js
CHANGED
|
@@ -141,6 +141,7 @@ const DEFAULT_TOOL_CLASSIFICATION = {
|
|
|
141
141
|
write_governance_correction: 'local',
|
|
142
142
|
get_orchestrate_context: 'local',
|
|
143
143
|
complete_orchestrate_trigger: 'local',
|
|
144
|
+
propose_goal_candidate: 'local',
|
|
144
145
|
|
|
145
146
|
send_message: 'mandatory',
|
|
146
147
|
create_tasks: 'mandatory',
|
|
@@ -302,6 +303,7 @@ function inferToolForApi(method, apiPath, body) {
|
|
|
302
303
|
if (method === 'POST' && cleanPath === '/orchestrate/correction') return 'write_governance_correction';
|
|
303
304
|
if (method === 'GET' && cleanPath === '/orchestrate/context') return 'get_orchestrate_context';
|
|
304
305
|
if (method === 'POST' && cleanPath === '/orchestrate/complete') return 'complete_orchestrate_trigger';
|
|
306
|
+
if (method === 'POST' && cleanPath === '/orchestrate/propose-goal-candidate') return 'propose_goal_candidate';
|
|
305
307
|
if (method === 'POST' && cleanPath === '/context-proposals') return 'promote_context';
|
|
306
308
|
|
|
307
309
|
if (method === 'GET' && cleanPath === '/host/scenarios/search') return 'search_scenarios';
|
|
@@ -1747,6 +1749,49 @@ server.tool('get_orchestrate_context',
|
|
|
1747
1749
|
}
|
|
1748
1750
|
);
|
|
1749
1751
|
|
|
1752
|
+
// ── propose_goal_candidate ────────────────────────────────────────────────────
|
|
1753
|
+
server.tool('propose_goal_candidate',
|
|
1754
|
+
'Orchestrator-only: propose a goal_state_candidate for human review. Use when D1 (goal_extraction_opportunity) fires and you have extracted a goal/constraint shape from the user\'s recent messages. The candidate enters the existing candidate→confirm→promote flow; ONLY Human can promote it to active goal_states. Always include `source_excerpts` (verbatim user quotes the proposal is derived from) so the confirm card can show provenance.',
|
|
1755
|
+
{
|
|
1756
|
+
workspace_id: z.string().describe('Target workspace id; you must be the registered orchestrator for it.'),
|
|
1757
|
+
goal: z.record(z.any()).describe('Proposed goal object. Required: a non-empty object describing the workspace goal, e.g. { title, description, kpis, ... }. Be concrete — vague platitudes like "comply with brand" are useless.'),
|
|
1758
|
+
constraints: z.array(z.any()).optional().describe('Proposed constraints array (optional). Each item should be enforceable by detector A4 / worker review, not a slogan.'),
|
|
1759
|
+
current_phase: z.string().optional().describe('Current phase label (optional, e.g. "exploration" / "stable_publish").'),
|
|
1760
|
+
source_excerpts: z.array(z.string()).describe('Verbatim user quotes the proposal is derived from. REQUIRED for non-trivial proposals — the confirm card surfaces these to the user as provenance.'),
|
|
1761
|
+
source_risk: z.enum(['trusted', 'normal', 'untrusted', 'hostile']).optional().describe('Source risk classification: "trusted" if quoting user directly, "normal" if interpreting user intent, "untrusted" if drawing from worker output / external content. Default normal.'),
|
|
1762
|
+
trigger_id: z.string().optional().describe('Originating trigger id, e.g. "D1". Default "D1".'),
|
|
1763
|
+
detector_version: z.string().optional().describe('Detector version that fired, e.g. "rule-v1".'),
|
|
1764
|
+
reason: z.string().optional().describe('Human-readable rationale for the proposal.'),
|
|
1765
|
+
expires_in_minutes: z.number().int().optional().describe('Candidate TTL in minutes (default 30, max 7 days).'),
|
|
1766
|
+
},
|
|
1767
|
+
async ({ workspace_id, goal, constraints, current_phase, source_excerpts, source_risk, trigger_id, detector_version, reason, expires_in_minutes }) => {
|
|
1768
|
+
try {
|
|
1769
|
+
const data = await api('POST', '/orchestrate/propose-goal-candidate', {
|
|
1770
|
+
workspaceId: workspace_id,
|
|
1771
|
+
goal,
|
|
1772
|
+
constraints,
|
|
1773
|
+
currentPhase: current_phase,
|
|
1774
|
+
sourceExcerpts: source_excerpts,
|
|
1775
|
+
sourceRisk: source_risk,
|
|
1776
|
+
triggerId: trigger_id,
|
|
1777
|
+
detectorVersion: detector_version,
|
|
1778
|
+
reason,
|
|
1779
|
+
expiresInMinutes: expires_in_minutes,
|
|
1780
|
+
});
|
|
1781
|
+
const c = data?.candidate;
|
|
1782
|
+
return {
|
|
1783
|
+
content: [{
|
|
1784
|
+
type: 'text',
|
|
1785
|
+
text: `Goal candidate proposed. id=${c?.id} status=${c?.status} expires_at=${c?.expiresAt}\n`
|
|
1786
|
+
+ `Awaiting human confirm via UI; no active goal_states change yet.`,
|
|
1787
|
+
}],
|
|
1788
|
+
};
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
);
|
|
1794
|
+
|
|
1750
1795
|
// ── complete_orchestrate_trigger ──────────────────────────────────────────────
|
|
1751
1796
|
server.tool('complete_orchestrate_trigger',
|
|
1752
1797
|
'Orchestrator-only: mark a processing orchestrator trigger as completed (status processing→completed) and reset its circuit breaker. Call this once you have written all governance actions for the current trigger.',
|
package/src/connection.js
CHANGED
|
@@ -45,10 +45,16 @@ function parseOptionalDeviceHints() {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export class DaemonConnection {
|
|
48
|
-
constructor({ serverUrl, machineApiKey, onMessage }) {
|
|
48
|
+
constructor({ serverUrl, machineApiKey, onMessage, getAgentInventory = null }) {
|
|
49
49
|
this.serverUrl = serverUrl.replace(/^http/, 'ws');
|
|
50
50
|
this.machineApiKey = machineApiKey;
|
|
51
51
|
this.onMessage = onMessage;
|
|
52
|
+
// Optional callback invoked at 'ready' time to snapshot which agents
|
|
53
|
+
// this daemon is currently managing. Server uses this to reconcile
|
|
54
|
+
// stale lifecycle state after reconnects (see src/daemon/index.js
|
|
55
|
+
// 'ready' handler — agents previously on this machine but missing
|
|
56
|
+
// from inventory get reset from stale crashed→standby).
|
|
57
|
+
this.getAgentInventory = typeof getAgentInventory === 'function' ? getAgentInventory : null;
|
|
52
58
|
this.ws = null;
|
|
53
59
|
this.reconnectDelay = RECONNECT_INITIAL;
|
|
54
60
|
this.stopped = false;
|
|
@@ -111,7 +117,20 @@ export class DaemonConnection {
|
|
|
111
117
|
...parseOptionalDeviceHints(),
|
|
112
118
|
};
|
|
113
119
|
|
|
114
|
-
|
|
120
|
+
let agentInventory = [];
|
|
121
|
+
if (this.getAgentInventory) {
|
|
122
|
+
try {
|
|
123
|
+
const snapshot = this.getAgentInventory();
|
|
124
|
+
if (Array.isArray(snapshot)) agentInventory = snapshot;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.warn(`[Connection] getAgentInventory failed: ${err?.message ?? err}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(
|
|
131
|
+
`[Connection] Ready — host=${hostname} runtimes=[${runtimes.join(',')}] v${DAEMON_VERSION}`
|
|
132
|
+
+ ` inventory=${agentInventory.length}`
|
|
133
|
+
);
|
|
115
134
|
this.send({
|
|
116
135
|
type: 'ready',
|
|
117
136
|
hostname,
|
|
@@ -119,6 +138,7 @@ export class DaemonConnection {
|
|
|
119
138
|
runtimes,
|
|
120
139
|
daemonVersion: DAEMON_VERSION,
|
|
121
140
|
deviceHints,
|
|
141
|
+
agentInventory,
|
|
122
142
|
});
|
|
123
143
|
}
|
|
124
144
|
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export function resolveExitOfflineDetail({ code, signal, stopCause }) {
|
|
2
2
|
if (stopCause === 'manual_stop') return '';
|
|
3
|
+
if (stopCause === 'user_cancelled') return '';
|
|
3
4
|
if (stopCause === 'credential_revoked') return 'credential_revoked';
|
|
4
5
|
if (stopCause === 'contract_violation') return 'contract_violation:visible_text_without_send_message';
|
|
5
6
|
if (stopCause === 'outbound_rate_limited') return 'outbound_rate_limited';
|
|
@@ -27,6 +28,14 @@ export function resolveLifecycleExitState({ runtime, code, signal, stopCause })
|
|
|
27
28
|
reason: 'manual_stop',
|
|
28
29
|
};
|
|
29
30
|
}
|
|
31
|
+
if (stopCause === 'user_cancelled') {
|
|
32
|
+
return {
|
|
33
|
+
reachability: 'reachable',
|
|
34
|
+
availability: 'available',
|
|
35
|
+
runtimeState: 'stopped',
|
|
36
|
+
reason: 'user_cancelled',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
30
39
|
if (stopCause === 'contract_violation') {
|
|
31
40
|
return {
|
|
32
41
|
reachability: 'reachable',
|
|
@@ -608,7 +608,14 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
|
|
|
608
608
|
text,
|
|
609
609
|
tags,
|
|
610
610
|
payload,
|
|
611
|
-
callTool: callOfficialTool
|
|
611
|
+
callTool: (toolArgs) => callOfficialTool({
|
|
612
|
+
...toolArgs,
|
|
613
|
+
env: {
|
|
614
|
+
SERVER_URL: serverUrl,
|
|
615
|
+
MACHINE_API_KEY: machineApiKey,
|
|
616
|
+
AGENT_ID: agentId,
|
|
617
|
+
},
|
|
618
|
+
}),
|
|
612
619
|
});
|
|
613
620
|
} catch (error) {
|
|
614
621
|
emitPublishJobProgress(onProgress, 'precheck_error', {
|