deepdebug-local-agent 1.12.1 → 1.13.1
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/CHANGELOG.md +2 -0
- package/docs/la-chat.md +35 -0
- package/docs/la-jobs-and-llm.md +1 -1
- package/package.json +1 -1
- package/src/dlp/rules.js +2 -2
- package/src/la-protocol-v2/chat/config.js +60 -0
- package/src/la-protocol-v2/chat/cursors.js +57 -0
- package/src/la-protocol-v2/chat/dlp.js +92 -0
- package/src/la-protocol-v2/chat/index.js +28 -0
- package/src/la-protocol-v2/chat/store.js +337 -0
- package/src/la-protocol-v2/chat/stream-source.js +23 -0
- package/src/la-protocol-v2/content-handlers/chat-session.js +41 -0
- package/src/la-protocol-v2/content-handlers/handler.js +10 -2
- package/src/la-protocol-v2/content-handlers/install.js +2 -2
- package/src/la-protocol-v2/content-handlers/job-stream.js +62 -0
- package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +13 -8
- package/src/la-protocol-v2/contract/README.md +42 -0
- package/src/la-protocol-v2/contract/chat.session.body.schema.json +47 -0
- package/src/la-protocol-v2/contract/index.json +1 -1
- package/src/la-protocol-v2/contract/job.assign.schema.json +3 -2
- package/src/la-protocol-v2/contract/job.stream.body.schema.json +49 -0
- package/src/la-protocol-v2/jobs/agent2/api-client.js +78 -51
- package/src/la-protocol-v2/jobs/agent2/config.js +1 -0
- package/src/la-protocol-v2/jobs/agent2/runner.js +92 -11
- package/src/la-protocol-v2/jobs/agent2/supervisor.js +2 -0
- package/src/la-protocol-v2/jobs/manager.js +5 -3
- package/src/la-protocol-v2/llm/client.js +21 -1
- package/src/la-protocol-v2/runtime.js +32 -3
- package/test/contract/la-protocol-v2/vectors/chat.session.body.vectors.json +10379 -0
- package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +245 -0
- package/test/contract/la-protocol-v2/vectors/hello.vectors.json +112 -0
- package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +568 -3
- package/test/contract/la-protocol-v2/vectors/job.stream.body.vectors.json +4572 -0
- package/test/contract/la-protocol-v2/vectors/register.vectors.json +106 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-chat-job-and-stream.json +311 -0
- package/test/la-protocol-v2/agent2-runner.test.js +3 -1
- package/test/la-protocol-v2/chat-dlp-json.test.js +169 -0
- package/test/la-protocol-v2/chat-la.test.js +490 -0
- package/test/la-protocol-v2/chat-real-e2e.test.js +69 -0
- package/test/la-protocol-v2/chat-store.test.js +301 -0
- package/test/la-protocol-v2/contract-v21.test.js +2 -2
- package/test/la-protocol-v2/contract.test.js +6 -6
- package/test/la-protocol-v2/helpers/agent2-rig.js +2 -2
- package/test/la-protocol-v2/helpers/fake-agent2-cli.js +1 -0
- package/test/la-protocol-v2/helpers/fake-agent2.js +80 -5
- package/test/la-protocol-v2/jobs-e2e.test.js +2 -1
- package/test/la-protocol-v2/llm-client.test.js +1 -1
- package/test/la-protocol-v2/llm-messages.test.js +2 -0
- package/test/la-protocol-v2/llm-scope-ids.test.js +129 -0
- package/test/la-protocol-v2/traces.test.js +3 -3
|
@@ -281,12 +281,32 @@ export function createLlmClient({ holder, config, tenantId, agentId, env = proce
|
|
|
281
281
|
}));
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
const SAFE_ID = /^[A-Za-z0-9_-]{1,64}$/; // same rule as the agents' LiteLlmScopeTags: a crafted id must never smuggle a second tag
|
|
285
|
+
const SAFE_TAG_VALUE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The `metadata` of every LiteLLM request (chat/completions AND /v1/messages). vh-litellm's scope-tag guard needs the caller ids IN the body
|
|
289
|
+
* (`vh_tenant_id`, `vh_workspace_id`) and the scope tags to agree with them. The tenant is this agent's own (session); the workspace is the job's
|
|
290
|
+
* (the one this agent serves); nothing here comes from agent2 or a job payload. Tags are unique by value. null = nothing to send.
|
|
291
|
+
*/
|
|
292
|
+
function requestMetadata(job) {
|
|
293
|
+
const md = {};
|
|
294
|
+
const tags = new Set();
|
|
295
|
+
if (SAFE_ID.test(String(tenantId))) { md.vh_tenant_id = String(tenantId); tags.add(`vh-tenant:${tenantId}`); }
|
|
296
|
+
const ws = typeof job?.workspaceId === "string" ? job.workspaceId.trim() : "";
|
|
297
|
+
if (md.vh_tenant_id && SAFE_ID.test(ws)) { md.vh_workspace_id = ws; tags.add(`vh-ws:${ws}`); }
|
|
298
|
+
if (SAFE_TAG_VALUE.test(String(agentId))) tags.add(`vh-la:${agentId}`);
|
|
299
|
+
if (job?.jobId && SAFE_TAG_VALUE.test(String(job.jobId))) tags.add(`vh-job:${job.jobId}`);
|
|
300
|
+
if (job?.agent && /^[a-z0-9][a-z0-9._-]{0,31}$/.test(job.agent)) tags.add(`vh-agent:${job.agent}`);
|
|
301
|
+
return { ...md, tags: [...tags] };
|
|
302
|
+
}
|
|
303
|
+
|
|
284
304
|
/** The slot, the HTTP call with its retries and the bookkeeping, shared by chat() and messages(). `hooks` = { path, parseJson } of the API in use. */
|
|
285
305
|
async function dispatch(o, ctx, hooks, makeBody) {
|
|
286
306
|
if (active >= config.maxConcurrent) return fail("busy", ctx);
|
|
287
307
|
active++;
|
|
288
308
|
try {
|
|
289
|
-
const tags = config.tags ?
|
|
309
|
+
const tags = config.tags ? requestMetadata(o.job) : null;
|
|
290
310
|
const bodyText = makeBody(tags);
|
|
291
311
|
stats.bytesOut += byteLength(bodyText);
|
|
292
312
|
let attempts = 0;
|
|
@@ -13,6 +13,7 @@ import { resolveWebhookConfig } from "./webhooks/config.js";
|
|
|
13
13
|
import { resolveLlmClientConfig } from "./llm/config.js";
|
|
14
14
|
import { resolveJobsConfig } from "./jobs/config.js";
|
|
15
15
|
import { loadContract } from "./contract.js";
|
|
16
|
+
import { resolveChatConfig, chatRequested } from "./chat/config.js";
|
|
16
17
|
|
|
17
18
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
18
19
|
const WORKSPACE_ID = /^[A-Za-z0-9_.-]{0,64}$/;
|
|
@@ -86,6 +87,10 @@ export async function buildV2Link({ tenantId, agentId, signing, wsManager, getWo
|
|
|
86
87
|
// ./jobs/agent2 is loaded. It needs jobs, the LLM client (agent2 reaches the model ONLY through this agent: DLP + key holder) and DLP enforce; when one is
|
|
87
88
|
// missing the job receiver is NOT installed (never silently another runner).
|
|
88
89
|
let agent2Runner = null;
|
|
90
|
+
// LA-1/LA-3 (contract 2.3, LOCAL_AGENT_CHAT, default off): chat on the Local Agent. It rides on the agent2 runner and on the content handlers (job.stream / chat.session are
|
|
91
|
+
// served by them), so it exists only when both do; otherwise it says why and stays off (never a half-working chat: `job.chat` is announced only by a complete one).
|
|
92
|
+
let chat = null;
|
|
93
|
+
let chatReported = false;
|
|
89
94
|
if (String(env.LOCAL_AGENT_RUNNER ?? "").trim().toLowerCase() === "agent2") {
|
|
90
95
|
const a2 = await import("./jobs/agent2/config.js");
|
|
91
96
|
const a2cfg = a2.resolveAgent2RunnerConfig(env, (m) => log.warn(m));
|
|
@@ -97,12 +102,23 @@ export async function buildV2Link({ tenantId, agentId, signing, wsManager, getWo
|
|
|
97
102
|
else {
|
|
98
103
|
const { createAgent2Runner } = await import("./jobs/agent2/runner.js");
|
|
99
104
|
const { makeWorkspaceResolver } = await import("./jobs/install.js");
|
|
105
|
+
const chatCfg = resolveChatConfig(env, (m) => log.warn(m));
|
|
106
|
+
if (chatCfg.enabled) {
|
|
107
|
+
chatReported = true;
|
|
108
|
+
if (!contentCfg.enabled) err("[la-chat] ERROR: LOCAL_AGENT_CHAT=true but the content handlers are not on (LOCAL_AGENT_CONTENT_HANDLERS=true and LOCAL_AGENT_DLP_MODE=enforce): nothing could serve job.stream / chat.session. The chat stays off");
|
|
109
|
+
else {
|
|
110
|
+
const { createChat } = await import("./chat/index.js");
|
|
111
|
+
try { chat = createChat({ config: chatCfg, tenantId: tenant, env, log, isActive: (id) => !!(link.jobs && link.jobs.isActiveChat(id)) }); } catch { err("[la-chat] ERROR: the chat store could not be created (LOCAL_AGENT_CHAT_DIR): the chat stays off"); }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
a2cfg.chat = !!chat;
|
|
100
115
|
const resolveWs = makeWorkspaceResolver({ wsManager, getWorkspaceRoot, tenantId: tenant });
|
|
101
|
-
agent2Runner = createAgent2Runner({ config: a2cfg, tenantId: tenant, resolveWorkspaceRoot: (id) => resolveWs(id)?.root || null, getSession: () => link.session, signing, env, log });
|
|
116
|
+
agent2Runner = createAgent2Runner({ config: a2cfg, tenantId: tenant, resolveWorkspaceRoot: (id) => resolveWs(id)?.root || null, getSession: () => link.session, signing, env, log, chat });
|
|
102
117
|
jobRunner = agent2Runner;
|
|
103
118
|
if (jobsCfg.maxConcurrent !== 1) { log.warn("[la-agent2] agent2 runs one job at a time: LOCAL_AGENT_JOBS_MAX_CONCURRENT is forced to 1"); jobsCfg = { ...jobsCfg, maxConcurrent: 1 }; }
|
|
104
119
|
}
|
|
105
120
|
}
|
|
121
|
+
if (chatRequested(env) && !chat && !chatReported) err("[la-chat] ERROR: LOCAL_AGENT_CHAT=true needs LOCAL_AGENT_JOBS, LOCAL_AGENT_RUNNER=agent2 (with the LLM client and DLP enforce) and the content handlers: the chat stays off");
|
|
106
122
|
if (jobsCfg.enabled) {
|
|
107
123
|
const { validateRunner } = await import("./jobs/manager.js");
|
|
108
124
|
const given = jobRunner && validateRunner(jobRunner) ? jobRunner : null;
|
|
@@ -138,7 +154,10 @@ export async function buildV2Link({ tenantId, agentId, signing, wsManager, getWo
|
|
|
138
154
|
capabilities: ["protocol.v2", "v1.commands", "chunk.base64", ...contentCaps, ...incidentCaps, ...webhookCaps, ...llmCaps, ...jobCaps, ...jobOptionCaps, ...policyCaps, ...v21Caps],
|
|
139
155
|
// contract 2.1 (e): `llm.key.held` is a snapshot taken at every connect (register and hello alike): announced only while the holder has a
|
|
140
156
|
// valid, unexpired key (a revoked or expired one is not announced from the next connection on). `link` is assigned below, before any attach.
|
|
141
|
-
|
|
157
|
+
// contract 2.3: `job.chat` (+ `job.inline.16k`, a prompt of 16 KiB) only from a complete chat: the store, the content handlers, the agent2 runner and an agent2 whose /health
|
|
158
|
+
// listed `chat` (probeChat below). The CP sends kind:"chat" and a long inline input only to a session that announced them.
|
|
159
|
+
dynamicCapabilities: () => [...(llmCfg.enabled && link.llm && link.llm.holder.held ? ["llm.key.held"] : []),
|
|
160
|
+
...(chat && agent2Runner && runner === agent2Runner && agent2Runner.chatReady ? ["job.chat", "job.inline.16k"] : [])],
|
|
142
161
|
getWorkspaces: () => {
|
|
143
162
|
const open = (wsManager?.list?.() || []).map((w) => ({
|
|
144
163
|
workspaceId: String(w.id || ""),
|
|
@@ -158,7 +177,12 @@ export async function buildV2Link({ tenantId, agentId, signing, wsManager, getWo
|
|
|
158
177
|
});
|
|
159
178
|
if (contentCfg.enabled) {
|
|
160
179
|
const { installContentHandlers } = await import("./content-handlers/install.js");
|
|
161
|
-
installContentHandlers({ link, tenantId: tenant, wsManager, getWorkspaceRoot, config: contentCfg, env, log });
|
|
180
|
+
installContentHandlers({ link, tenantId: tenant, wsManager, getWorkspaceRoot, config: contentCfg, env, log, chat: chat ? chat.content : null });
|
|
181
|
+
}
|
|
182
|
+
if (chat) {
|
|
183
|
+
link.chat = chat;
|
|
184
|
+
const closeBase = link.close.bind(link);
|
|
185
|
+
link.close = () => { chat.close(); closeBase(); };
|
|
162
186
|
}
|
|
163
187
|
if (llmCfg.enabled) {
|
|
164
188
|
const { installLlmKey } = await import("./llm-key/install.js");
|
|
@@ -182,6 +206,11 @@ export async function buildV2Link({ tenantId, agentId, signing, wsManager, getWo
|
|
|
182
206
|
link.close = () => { closeJobs(); agent2Runner.close().catch(() => {}); };
|
|
183
207
|
}
|
|
184
208
|
}
|
|
209
|
+
if (chat && agent2Runner && runner === agent2Runner) {
|
|
210
|
+
// which kinds does agent2 run? (/health). Waits at most LOCAL_AGENT_CHAT_PROBE_MS so the first register already says `job.chat`; in spawn mode this starts agent2 now
|
|
211
|
+
const probe = agent2Runner.probeChat();
|
|
212
|
+
if (chat.config.probeWaitMs > 0) await Promise.race([probe, new Promise((r) => { const t = setTimeout(r, chat.config.probeWaitMs); if (t.unref) t.unref(); })]);
|
|
213
|
+
}
|
|
185
214
|
let webhookSource = null;
|
|
186
215
|
if (webhookCfg.enabled) {
|
|
187
216
|
const { createWebhookSource } = await import("../incident-detector/sources/webhook-source.js");
|