@circuitwall/jarela 0.7.2 → 0.7.3

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 (91) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +2 -2
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  15. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  16. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js +51 -35
  23. package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js.map +1 -1
  24. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +2 -2
  25. package/.next/standalone/.next/server/app/index.html +2 -2
  26. package/.next/standalone/.next/server/app/index.rsc +3 -3
  27. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  30. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  31. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  32. package/.next/standalone/.next/server/app/page.js +515 -104
  33. package/.next/standalone/.next/server/app/page.js.map +1 -1
  34. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/setup.html +1 -1
  37. package/.next/standalone/.next/server/app/setup.rsc +2 -2
  38. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/setup.segments/_head.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/setup.segments/_index.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  42. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +1 -1
  43. package/.next/standalone/.next/server/app/setup.segments/setup.segment.rsc +1 -1
  44. package/.next/standalone/.next/server/chunks/1683.js +26 -16
  45. package/.next/standalone/.next/server/chunks/1683.js.map +1 -1
  46. package/.next/standalone/.next/server/chunks/{317.js → 5432.js} +11100 -10858
  47. package/.next/standalone/.next/server/chunks/5432.js.map +1 -0
  48. package/.next/standalone/.next/server/chunks/7885.js +606 -353
  49. package/.next/standalone/.next/server/chunks/7885.js.map +1 -1
  50. package/.next/standalone/.next/server/chunks/8135.js +59 -16
  51. package/.next/standalone/.next/server/chunks/8135.js.map +1 -1
  52. package/.next/standalone/.next/server/chunks/9032.js +3 -3
  53. package/.next/standalone/.next/server/chunks/9032.js.map +1 -1
  54. package/.next/standalone/.next/server/instrumentation.js +3 -3
  55. package/.next/standalone/.next/server/instrumentation.js.map +1 -1
  56. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  57. package/.next/standalone/.next/server/pages/404.html +2 -2
  58. package/.next/standalone/.next/server/pages/500.html +1 -1
  59. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/static/chunks/app/{page-a20902703c0a4f10.js → page-9fb006074fb13526.js} +582 -171
  61. package/.next/standalone/.next/static/chunks/app/page-9fb006074fb13526.js.map +1 -0
  62. package/.next/standalone/.next/static/css/5507dbe1cdc6c599.css +5 -0
  63. package/.next/standalone/.next/static/css/5507dbe1cdc6c599.css.map +1 -0
  64. package/.next/standalone/package.json +1 -1
  65. package/CHANGELOG.md +11 -0
  66. package/README.md +83 -1
  67. package/api/types.ts +7 -0
  68. package/app/api/v1/agents/[id]/compact/route.ts +2 -40
  69. package/components/bridges/BridgeEditor.tsx +8 -0
  70. package/components/chat/MessageBubble.tsx +3 -36
  71. package/components/models/ModelEditor.tsx +141 -0
  72. package/components/scheduled-tasks/ScheduledTasksPanel.tsx +5 -0
  73. package/components/scheduled-tasks/WatchersSection.tsx +5 -0
  74. package/lib/agents/context-budget.test.ts +128 -0
  75. package/lib/agents/context-budget.ts +128 -0
  76. package/lib/agents/conversation-summary.test.ts +68 -0
  77. package/lib/agents/conversation-summary.ts +51 -0
  78. package/lib/agents/run-thread.ts +112 -2
  79. package/lib/bridges/dispatcher.test.ts +134 -0
  80. package/lib/bridges/dispatcher.ts +34 -16
  81. package/lib/bridges/message-role.test.ts +83 -0
  82. package/lib/bridges/message-role.ts +46 -0
  83. package/lib/triggers/handlers/watcher.test.ts +23 -4
  84. package/lib/triggers/handlers/watcher.ts +56 -8
  85. package/package.json +1 -1
  86. package/.next/standalone/.next/server/chunks/317.js.map +0 -1
  87. package/.next/standalone/.next/static/chunks/app/page-a20902703c0a4f10.js.map +0 -1
  88. package/.next/standalone/.next/static/css/cc66c456aba91258.css +0 -5
  89. package/.next/standalone/.next/static/css/cc66c456aba91258.css.map +0 -1
  90. /package/.next/standalone/.next/static/{IauO0rNZkUVPX834k-SBa → AbCOWpaxP4v4lUSeFWWYz}/_buildManifest.js +0 -0
  91. /package/.next/standalone/.next/static/{IauO0rNZkUVPX834k-SBa → AbCOWpaxP4v4lUSeFWWYz}/_ssgManifest.js +0 -0
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatBridgePrompt, parseBridgePrompt } from "./message-role";
3
+
4
+ describe("bridge prompt envelope", () => {
5
+ it("round-trips DM prompt metadata and body", () => {
6
+ const raw = formatBridgePrompt({
7
+ bridge_id: "b1",
8
+ chat_id: "dm@jid",
9
+ chat_name: "Alice",
10
+ is_group: false,
11
+ role: "counterpart",
12
+ sender_id: "alice@jid",
13
+ sender_name: "Alice",
14
+ text: "hello from dm",
15
+ });
16
+ const parsed = parseBridgePrompt(raw);
17
+ expect(parsed).not.toBeNull();
18
+ expect(parsed?.bridgeId).toBe("b1");
19
+ expect(parsed?.chatJid).toBe("dm@jid");
20
+ expect(parsed?.isGroup).toBe(false);
21
+ expect(parsed?.senderJid).toBe("alice@jid");
22
+ expect(parsed?.body).toBe("hello from dm");
23
+ });
24
+
25
+ it("round-trips group prompt metadata and body", () => {
26
+ const raw = formatBridgePrompt({
27
+ bridge_id: "b1",
28
+ chat_id: "group@jid",
29
+ chat_name: "Family Group",
30
+ is_group: true,
31
+ role: "counterpart",
32
+ sender_id: "bob@jid",
33
+ sender_name: "Bob",
34
+ text: "group message",
35
+ });
36
+ const parsed = parseBridgePrompt(raw);
37
+ expect(parsed).not.toBeNull();
38
+ expect(parsed?.chatJid).toBe("group@jid");
39
+ expect(parsed?.chatName).toBe("Family Group");
40
+ expect(parsed?.isGroup).toBe(true);
41
+ expect(parsed?.senderName).toBe("Bob");
42
+ expect(parsed?.body).toBe("group message");
43
+ });
44
+
45
+ it("parses envelopes with prose preface before bracket headers", () => {
46
+ const raw = [
47
+ "The paired user themselves sent the message below.",
48
+ "",
49
+ "[bridge:b1]",
50
+ "[chat_id:dm@jid]",
51
+ "[chat_name:Alice]",
52
+ "[chat_type:dm]",
53
+ "[sender_id:alice@jid]",
54
+ "[sender_name:Alice]",
55
+ "",
56
+ "body",
57
+ ].join("\n");
58
+ const parsed = parseBridgePrompt(raw);
59
+ expect(parsed?.bridgeId).toBe("b1");
60
+ expect(parsed?.body).toBe("body");
61
+ });
62
+
63
+ it("accepts legacy key names for compatibility", () => {
64
+ const raw = [
65
+ "[bridge:b1]",
66
+ "[chat_jid:legacy@jid]",
67
+ "[chat_name:Legacy]",
68
+ "[chat_type:dm]",
69
+ "[sender_jid:sender@jid]",
70
+ "[sender_name:Sender]",
71
+ "",
72
+ "legacy body",
73
+ ].join("\n");
74
+ const parsed = parseBridgePrompt(raw);
75
+ expect(parsed?.chatJid).toBe("legacy@jid");
76
+ expect(parsed?.senderJid).toBe("sender@jid");
77
+ expect(parsed?.body).toBe("legacy body");
78
+ });
79
+
80
+ it("returns null when not a bridge envelope", () => {
81
+ expect(parseBridgePrompt("plain text")).toBeNull();
82
+ });
83
+ });
@@ -56,6 +56,18 @@ export interface BridgePromptInput {
56
56
  text: string;
57
57
  }
58
58
 
59
+ // Parsed chat-friendly envelope extracted from a bridge prompt body.
60
+ // Used by the chat UI so rendering stays in lockstep with formatter changes.
61
+ export interface BridgePromptContext {
62
+ bridgeId: string;
63
+ chatJid: string;
64
+ chatName: string;
65
+ isGroup: boolean;
66
+ senderJid: string;
67
+ senderName: string;
68
+ body: string;
69
+ }
70
+
59
71
  /**
60
72
  * Build the prompt prefix the agent receives for one bridge-inbound message.
61
73
  *
@@ -97,6 +109,40 @@ export function formatBridgePrompt(input: BridgePromptInput): string {
97
109
  return `${note}\n\n${lines.join("\n")}\n\n${input.text}`;
98
110
  }
99
111
 
112
+ // Parses bridge prompt envelopes rendered by formatBridgePrompt().
113
+ // Back-compat: also accepts legacy keys (chat_jid/sender_jid) and optional
114
+ // prose preface before the [bridge:...] metadata block.
115
+ export function parseBridgePrompt(raw: string): BridgePromptContext | null {
116
+ const start = raw.indexOf("[bridge:");
117
+ if (start < 0) return null;
118
+ const src = raw.slice(start);
119
+
120
+ const headers: Record<string, string> = {};
121
+ const lines = src.split("\n");
122
+ let i = 0;
123
+ for (; i < lines.length; i++) {
124
+ const line = lines[i];
125
+ if (line === "") { i++; break; }
126
+ const m = /^\[([a-z_]+):([\s\S]*)\]$/.exec(line);
127
+ if (!m) return null;
128
+ headers[m[1]] = m[2];
129
+ }
130
+
131
+ const chatId = headers.chat_id || headers.chat_jid;
132
+ const senderId = headers.sender_id || headers.sender_jid || chatId;
133
+ if (!headers.bridge || !chatId || !headers.chat_type) return null;
134
+
135
+ return {
136
+ bridgeId: headers.bridge,
137
+ chatJid: chatId,
138
+ chatName: headers.chat_name || chatId,
139
+ isGroup: headers.chat_type === "group",
140
+ senderJid: senderId,
141
+ senderName: headers.sender_name || senderId || "Unknown",
142
+ body: lines.slice(i).join("\n").trimEnd(),
143
+ };
144
+ }
145
+
100
146
  function roleNote(role: MessageRole, isGroup: boolean): string {
101
147
  switch (role) {
102
148
  case "user":
@@ -75,12 +75,31 @@ describe("watcherHandler (ADR-0027)", () => {
75
75
  expect(fired.agentId).toBe("a");
76
76
  expect(fired.kind).toBe("watcher");
77
77
  expect(fired.prompt).toContain('Watcher "change" detected a change');
78
- expect(fired.prompt).toContain("v1");
79
- expect(fired.prompt).toContain("v2");
78
+ expect(fired.prompt).toContain("--- Diff (previous -> current) ---");
79
+ expect(fired.prompt).toContain("- v1");
80
+ expect(fired.prompt).toContain("+ v2");
81
+ expect(fired.prompt).not.toContain("--- Previous result ---");
82
+ expect(fired.prompt).not.toContain("--- Current result ---");
80
83
  const after = getWatcher(w.id)!;
81
84
  expect(after.last_fired_at).not.toBeNull();
82
85
  });
83
86
 
87
+ it("truncates oversized result payloads in the firing prompt", async () => {
88
+ const w = createWatcher({
89
+ agent_id: "a", label: "big", tool_name: "watcher_test_tool", interval_seconds: 60,
90
+ });
91
+ fakeResult = "A".repeat(5000);
92
+ await watcherHandler.getDueFirings(new Date(Date.parse(w.next_run_at) + 1));
93
+ fakeResult = "B".repeat(5000);
94
+ const w2 = getWatcher(w.id)!;
95
+ const firings = await watcherHandler.getDueFirings(new Date(Date.parse(w2.next_run_at) + 1));
96
+ expect(firings).toHaveLength(1);
97
+ const fired = firings[0];
98
+ if (fired.mode !== "prompt") throw new Error("expected prompt firing");
99
+ expect(fired.prompt).toContain("[diff truncated: showing");
100
+ expect(fired.prompt.length).toBeLessThan(8000);
101
+ });
102
+
84
103
  it("does NOT fire when result is unchanged across polls", async () => {
85
104
  const w = createWatcher({
86
105
  agent_id: "a", label: "stable", tool_name: "watcher_test_tool", interval_seconds: 60,
@@ -123,8 +142,8 @@ describe("watcherHandler (ADR-0027)", () => {
123
142
  expect(fired.prompt).toContain("Open a Jira ticket against the broken dashboard.");
124
143
  expect(fired.prompt).not.toContain("Summarise what changed");
125
144
  // Diff envelope is preserved.
126
- expect(fired.prompt).toContain("v1");
127
- expect(fired.prompt).toContain("v2");
145
+ expect(fired.prompt).toContain("- v1");
146
+ expect(fired.prompt).toContain("+ v2");
128
147
  });
129
148
 
130
149
  it("uses the default directive when reaction_prompt is null", async () => {
@@ -6,7 +6,7 @@
6
6
  // tools must be context-free).
7
7
  // 3. Hash the stringified result and compare to last_fingerprint.
8
8
  // 4. If the hash differs from the previous run, return a TriggerFiring
9
- // whose prompt embeds {previous, current} as context for the agent.
9
+ // whose prompt embeds a compact previous->current diff for the agent.
10
10
  // If it matches (or this is the first run with no previous), record
11
11
  // the fingerprint and skip — no LLM call, no firing.
12
12
  // 5. Either way the watcher's next_run_at is advanced by
@@ -25,6 +25,7 @@ import {
25
25
  } from "@/lib/stores/watchers";
26
26
  import { registeredTools } from "@/lib/tools/registry";
27
27
  import { publish as publishNotification } from "@/lib/notifications/bus";
28
+ import { truncateBytes } from "@/lib/utils/text";
28
29
  import type { TriggerFiring, TriggerHandler, TriggerOutcome } from "../types";
29
30
 
30
31
  export const WATCHER_KIND = "watcher";
@@ -46,14 +47,64 @@ const DEFAULT_REACTION_DIRECTIVE =
46
47
  `Summarise what changed and decide whether the user needs to know. ` +
47
48
  `If nothing material changed, you may stay silent.`;
48
49
 
50
+ // Watcher tool outputs can be very large (full JSON payloads, long lists).
51
+ // Keep the diff context bounded so one firing cannot consume most of an
52
+ // agent's prompt budget.
53
+ const MAX_DIFF_CONTEXT_BYTES = 3500;
54
+
55
+ function normalizeForDiff(raw: string): string {
56
+ try {
57
+ return JSON.stringify(JSON.parse(raw), null, 2);
58
+ } catch {
59
+ return raw;
60
+ }
61
+ }
62
+
63
+ function buildDiffForPrompt(previous: string | null, current: string): string {
64
+ if (previous === null) return "+ (first observation baseline established; no diff available)";
65
+
66
+ const prev = normalizeForDiff(previous).split(/\r?\n/);
67
+ const curr = normalizeForDiff(current).split(/\r?\n/);
68
+
69
+ let start = 0;
70
+ while (start < prev.length && start < curr.length && prev[start] === curr[start]) {
71
+ start += 1;
72
+ }
73
+
74
+ let prevEnd = prev.length - 1;
75
+ let currEnd = curr.length - 1;
76
+ while (prevEnd >= start && currEnd >= start && prev[prevEnd] === curr[currEnd]) {
77
+ prevEnd -= 1;
78
+ currEnd -= 1;
79
+ }
80
+
81
+ const removed = prev.slice(start, prevEnd + 1);
82
+ const added = curr.slice(start, currEnd + 1);
83
+ if (removed.length === 0 && added.length === 0) {
84
+ return "(no textual diff after normalization)";
85
+ }
86
+
87
+ const hunkHeader = `@@ old:${start + 1}-${Math.max(start, prevEnd + 1)} new:${start + 1}-${Math.max(start, currEnd + 1)} @@`;
88
+ const raw = [
89
+ hunkHeader,
90
+ ...removed.map((l) => `- ${l}`),
91
+ ...added.map((l) => `+ ${l}`),
92
+ ].join("\n");
93
+
94
+ const bytes = Buffer.byteLength(raw, "utf8");
95
+ const clipped = truncateBytes(raw, MAX_DIFF_CONTEXT_BYTES);
96
+ if (!clipped.truncated) return raw;
97
+ return `${clipped.text}\n… [diff truncated: showing ${MAX_DIFF_CONTEXT_BYTES} of ${bytes} bytes; full values retained in watcher state]`;
98
+ }
99
+
49
100
  function buildFiringPrompt(watcher: WatcherRow, previous: string | null, current: string): string {
50
101
  const argsPretty = (() => {
51
102
  try { return JSON.stringify(JSON.parse(watcher.tool_args), null, 2); }
52
103
  catch { return watcher.tool_args; }
53
104
  })();
54
105
  // ADR-0030: a non-null reaction_prompt swaps in for the default directive.
55
- // The diff envelope (label/tool/args/previous/current) is unchanged so the
56
- // agent always has the change context regardless of the user's instruction.
106
+ // The diff envelope (label/tool/args/diff) is unchanged so the agent
107
+ // always has the change context regardless of the user's instruction.
57
108
  const directive = watcher.reaction_prompt?.trim() || DEFAULT_REACTION_DIRECTIVE;
58
109
  return [
59
110
  `Watcher "${watcher.label}" detected a change.`,
@@ -61,11 +112,8 @@ function buildFiringPrompt(watcher: WatcherRow, previous: string | null, current
61
112
  `Tool: ${watcher.tool_name}`,
62
113
  `Args: ${argsPretty}`,
63
114
  ``,
64
- `--- Previous result ---`,
65
- previous ?? "(none — first observation)",
66
- ``,
67
- `--- Current result ---`,
68
- current,
115
+ `--- Diff (previous -> current) ---`,
116
+ buildDiffForPrompt(previous, current),
69
117
  ``,
70
118
  directive,
71
119
  ].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circuitwall/jarela",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Jarela — local chat interface for LangGraph agents (multi-provider, single-process, SQLite-backed).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Andrew Ge Wu",