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.
Files changed (50) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/docs/la-chat.md +35 -0
  3. package/docs/la-jobs-and-llm.md +1 -1
  4. package/package.json +1 -1
  5. package/src/dlp/rules.js +2 -2
  6. package/src/la-protocol-v2/chat/config.js +60 -0
  7. package/src/la-protocol-v2/chat/cursors.js +57 -0
  8. package/src/la-protocol-v2/chat/dlp.js +92 -0
  9. package/src/la-protocol-v2/chat/index.js +28 -0
  10. package/src/la-protocol-v2/chat/store.js +337 -0
  11. package/src/la-protocol-v2/chat/stream-source.js +23 -0
  12. package/src/la-protocol-v2/content-handlers/chat-session.js +41 -0
  13. package/src/la-protocol-v2/content-handlers/handler.js +10 -2
  14. package/src/la-protocol-v2/content-handlers/install.js +2 -2
  15. package/src/la-protocol-v2/content-handlers/job-stream.js +62 -0
  16. package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +13 -8
  17. package/src/la-protocol-v2/contract/README.md +42 -0
  18. package/src/la-protocol-v2/contract/chat.session.body.schema.json +47 -0
  19. package/src/la-protocol-v2/contract/index.json +1 -1
  20. package/src/la-protocol-v2/contract/job.assign.schema.json +3 -2
  21. package/src/la-protocol-v2/contract/job.stream.body.schema.json +49 -0
  22. package/src/la-protocol-v2/jobs/agent2/api-client.js +78 -51
  23. package/src/la-protocol-v2/jobs/agent2/config.js +1 -0
  24. package/src/la-protocol-v2/jobs/agent2/runner.js +92 -11
  25. package/src/la-protocol-v2/jobs/agent2/supervisor.js +2 -0
  26. package/src/la-protocol-v2/jobs/manager.js +5 -3
  27. package/src/la-protocol-v2/llm/client.js +21 -1
  28. package/src/la-protocol-v2/runtime.js +32 -3
  29. package/test/contract/la-protocol-v2/vectors/chat.session.body.vectors.json +10379 -0
  30. package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +245 -0
  31. package/test/contract/la-protocol-v2/vectors/hello.vectors.json +112 -0
  32. package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +568 -3
  33. package/test/contract/la-protocol-v2/vectors/job.stream.body.vectors.json +4572 -0
  34. package/test/contract/la-protocol-v2/vectors/register.vectors.json +106 -0
  35. package/test/contract/la-protocol-v2/vectors/traces/valid-chat-job-and-stream.json +311 -0
  36. package/test/la-protocol-v2/agent2-runner.test.js +3 -1
  37. package/test/la-protocol-v2/chat-dlp-json.test.js +169 -0
  38. package/test/la-protocol-v2/chat-la.test.js +490 -0
  39. package/test/la-protocol-v2/chat-real-e2e.test.js +69 -0
  40. package/test/la-protocol-v2/chat-store.test.js +301 -0
  41. package/test/la-protocol-v2/contract-v21.test.js +2 -2
  42. package/test/la-protocol-v2/contract.test.js +6 -6
  43. package/test/la-protocol-v2/helpers/agent2-rig.js +2 -2
  44. package/test/la-protocol-v2/helpers/fake-agent2-cli.js +1 -0
  45. package/test/la-protocol-v2/helpers/fake-agent2.js +80 -5
  46. package/test/la-protocol-v2/jobs-e2e.test.js +2 -1
  47. package/test/la-protocol-v2/llm-client.test.js +1 -1
  48. package/test/la-protocol-v2/llm-messages.test.js +2 -0
  49. package/test/la-protocol-v2/llm-scope-ids.test.js +129 -0
  50. 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 ? { tags: [`vh-tenant:${tenantId}`, `vh-la:${agentId}`, ...(o.job?.jobId ? [`vh-job:${o.job.jobId}`] : []), ...(o.job?.agent && /^[a-z0-9][a-z0-9._-]{0,31}$/.test(o.job.agent) ? [`vh-agent:${o.job.agent}`] : [])] } : null;
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
- dynamicCapabilities: () => (llmCfg.enabled && link.llm && link.llm.holder.held ? ["llm.key.held"] : []),
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");