@aexol/spectral 0.9.15 → 0.9.16
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/dist/memory/config.d.ts +12 -0
- package/dist/memory/config.d.ts.map +1 -1
- package/dist/memory/config.js +27 -0
- package/dist/memory/hooks/compaction-hook.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-hook.js +2 -0
- package/dist/memory/hooks/inter-agent-receiver.d.ts +3 -0
- package/dist/memory/hooks/inter-agent-receiver.d.ts.map +1 -0
- package/dist/memory/hooks/inter-agent-receiver.js +183 -0
- package/dist/memory/hooks/observer-trigger.d.ts.map +1 -1
- package/dist/memory/hooks/observer-trigger.js +14 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +6 -0
- package/dist/memory/inter-agent-broker-singleton.d.ts +5 -0
- package/dist/memory/inter-agent-broker-singleton.d.ts.map +1 -0
- package/dist/memory/inter-agent-broker-singleton.js +7 -0
- package/dist/memory/inter-agent-relay.d.ts +7 -0
- package/dist/memory/inter-agent-relay.d.ts.map +1 -0
- package/dist/memory/inter-agent-relay.js +41 -0
- package/dist/memory/project-observations-store.d.ts +7 -0
- package/dist/memory/project-observations-store.d.ts.map +1 -1
- package/dist/memory/tools/receive-agent-observations.d.ts +17 -0
- package/dist/memory/tools/receive-agent-observations.d.ts.map +1 -0
- package/dist/memory/tools/receive-agent-observations.js +68 -0
- package/dist/memory/tools/share-project-observation.d.ts +8 -0
- package/dist/memory/tools/share-project-observation.d.ts.map +1 -0
- package/dist/memory/tools/share-project-observation.js +106 -0
- package/dist/relay/dispatcher.d.ts +1 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +13 -0
- package/dist/server/handlers/files-search.d.ts +26 -0
- package/dist/server/handlers/files-search.d.ts.map +1 -0
- package/dist/server/handlers/files-search.js +89 -0
- package/dist/server/inter-agent-broker.d.ts +111 -0
- package/dist/server/inter-agent-broker.d.ts.map +1 -0
- package/dist/server/inter-agent-broker.js +136 -0
- package/dist/server/session-stream.d.ts +1 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +43 -0
- package/dist/server/storage.d.ts +28 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +101 -0
- package/dist/server/wire.d.ts +15 -0
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Type } from "../../sdk/ai/index.js";
|
|
2
|
+
import { defineTool } from "../../sdk/coding-agent/index.js";
|
|
3
|
+
import { getProjectObsStore } from "../project-observations-store.js";
|
|
4
|
+
import { getInterAgentBroker } from "../inter-agent-broker-singleton.js";
|
|
5
|
+
import { getMemoryState } from "../branch.js";
|
|
6
|
+
import { RELEVANCE_VALUES } from "../types.js";
|
|
7
|
+
export const SHARE_PROJECT_OBSERVATION_TOOL_NAME = "share_project_observation";
|
|
8
|
+
function findObservationInBranch(branch, memoryId) {
|
|
9
|
+
if (!Array.isArray(branch))
|
|
10
|
+
return null;
|
|
11
|
+
const state = getMemoryState(branch);
|
|
12
|
+
for (const obs of state.committedObs) {
|
|
13
|
+
if (obs.id === memoryId) {
|
|
14
|
+
return { id: obs.id, content: obs.content, relevance: obs.relevance, originType: "observation" };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
for (const obs of state.pendingObs) {
|
|
18
|
+
if (obs.id === memoryId) {
|
|
19
|
+
return { id: obs.id, content: obs.content, relevance: obs.relevance, originType: "observation" };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const reflection of state.reflections) {
|
|
23
|
+
const id = typeof reflection === "string" ? undefined : reflection.id;
|
|
24
|
+
const content = typeof reflection === "string" ? reflection : reflection.content;
|
|
25
|
+
if (id === memoryId) {
|
|
26
|
+
return { id, content, relevance: "high", originType: "reflection" };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function findObservationInStore(store, projectId, memoryId) {
|
|
32
|
+
const obs = store.getProjectObservationById(projectId, memoryId);
|
|
33
|
+
if (!obs)
|
|
34
|
+
return null;
|
|
35
|
+
const relevance = RELEVANCE_VALUES.includes(obs.relevance)
|
|
36
|
+
? obs.relevance
|
|
37
|
+
: "medium";
|
|
38
|
+
return { id: obs.id, content: obs.content, relevance, originType: "observation" };
|
|
39
|
+
}
|
|
40
|
+
export const shareProjectObservationTool = defineTool({
|
|
41
|
+
name: SHARE_PROJECT_OBSERVATION_TOOL_NAME,
|
|
42
|
+
label: "Share observation with other agents",
|
|
43
|
+
description: "Broadcast a project observation or reflection to other active sessions working on this project. " +
|
|
44
|
+
"Use this when you discover a fact another agent should know immediately, " +
|
|
45
|
+
"rather than waiting for them to search project memory.",
|
|
46
|
+
promptSnippet: "Use share_project_observation(memory_id) to push a known observation to other active sessions.",
|
|
47
|
+
promptGuidelines: [
|
|
48
|
+
"Only share observations that are relevant to collaborators on the same project.",
|
|
49
|
+
"Prefer sharing high/critical relevance facts — avoid noise.",
|
|
50
|
+
"The memory_id must be a 12-character lowercase hex id from the current branch or project memory.",
|
|
51
|
+
],
|
|
52
|
+
parameters: Type.Object({
|
|
53
|
+
memory_id: Type.String({
|
|
54
|
+
pattern: "^[a-f0-9]{12}$",
|
|
55
|
+
description: "12-character observation/reflection id.",
|
|
56
|
+
}),
|
|
57
|
+
}),
|
|
58
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
59
|
+
const store = getProjectObsStore();
|
|
60
|
+
const broker = getInterAgentBroker();
|
|
61
|
+
if (!store || !broker) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: "Sharing is unavailable outside spectral serve." }],
|
|
64
|
+
details: { status: "unavailable" },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const projectId = store.getProjectByCwd(ctx.cwd);
|
|
68
|
+
const sessionId = ctx.sessionManager.getSessionId?.();
|
|
69
|
+
if (!projectId || !sessionId) {
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: "No project/session context." }],
|
|
72
|
+
details: { status: "no_context" },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const branch = ctx.sessionManager.getBranch?.();
|
|
76
|
+
const obs = findObservationInBranch(branch, params.memory_id)
|
|
77
|
+
?? findObservationInStore(store, projectId, params.memory_id);
|
|
78
|
+
if (!obs) {
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text", text: `Observation \`${params.memory_id}\` not found.` }],
|
|
81
|
+
details: { status: "not_found" },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
broker.send({
|
|
85
|
+
projectId,
|
|
86
|
+
channel: "memory",
|
|
87
|
+
senderSessionId: sessionId,
|
|
88
|
+
kind: "observation",
|
|
89
|
+
payload: JSON.stringify({
|
|
90
|
+
memoryId: obs.id,
|
|
91
|
+
content: obs.content,
|
|
92
|
+
relevance: obs.relevance,
|
|
93
|
+
sourceSessionId: sessionId,
|
|
94
|
+
originType: obs.originType,
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text", text: `Shared observation \`${obs.id}\` with other project sessions.` }],
|
|
100
|
+
details: { status: "ok", memoryId: obs.id },
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
export function registerShareProjectObservationTool(ext) {
|
|
105
|
+
ext.registerTool(shareProjectObservationTool);
|
|
106
|
+
}
|
|
@@ -48,7 +48,7 @@ import type { RelayClient } from "./client.js";
|
|
|
48
48
|
* `id` is populated when the route had an `:id` placeholder.
|
|
49
49
|
*/
|
|
50
50
|
interface RouteMatch {
|
|
51
|
-
route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "list_project_sessions" | "create_session" | "get_session" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "pick_directory" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "get_settings" | "put_settings";
|
|
51
|
+
route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "list_project_sessions" | "create_session" | "get_session" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "pick_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "get_settings" | "put_settings";
|
|
52
52
|
id?: string;
|
|
53
53
|
/** Parsed query params, if the path carried a `?...` suffix. */
|
|
54
54
|
query?: URLSearchParams;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAI/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
|
package/dist/relay/dispatcher.js
CHANGED
|
@@ -43,6 +43,7 @@ import { BadRequestError, NotFoundError } from "../server/handlers/errors.js";
|
|
|
43
43
|
import { handleListMcpStatus } from "../server/handlers/mcp-status.js";
|
|
44
44
|
import { handlePathAutocomplete } from "../server/handlers/paths-autocomplete.js";
|
|
45
45
|
import { handlePickDirectory } from "../server/handlers/paths-pick-directory.js";
|
|
46
|
+
import { handleSearchProjectFiles } from "../server/handlers/files-search.js";
|
|
46
47
|
import { handleBindStudioProject, handleCreateProject, handleDeleteProject, handleListProjects, handleListSessionsByProject, handleUpdateProject, } from "../server/handlers/projects.js";
|
|
47
48
|
import { handleCompactSession, handleCreateSession, handleDeleteSession, handleForkSession, handleRememberAndDeleteSession, handleGetSessionDetail, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, } from "../server/handlers/sessions.js";
|
|
48
49
|
import { handleClearPromptQueue, handleEnqueuePrompt, handleGetPromptQueue, handleRemovePrompt, } from "../server/handlers/queue.js";
|
|
@@ -101,6 +102,14 @@ export function matchRoute(method, path) {
|
|
|
101
102
|
return { route: "create_session" };
|
|
102
103
|
return null;
|
|
103
104
|
}
|
|
105
|
+
// /api/projects/:id/files/search
|
|
106
|
+
const projectFilesMatch = /^\/api\/projects\/([^/]+)\/files\/search$/.exec(cleanPath);
|
|
107
|
+
if (projectFilesMatch) {
|
|
108
|
+
const id = decodeURIComponent(projectFilesMatch[1]);
|
|
109
|
+
if (method === "GET")
|
|
110
|
+
return { route: "search_project_files", id, query };
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
104
113
|
// /api/projects/:id/bind-studio
|
|
105
114
|
const bindStudioMatch = /^\/api\/projects\/([^/]+)\/bind-studio$/.exec(cleanPath);
|
|
106
115
|
if (bindStudioMatch) {
|
|
@@ -406,6 +415,10 @@ async function dispatchRoute(match, body, deps) {
|
|
|
406
415
|
}
|
|
407
416
|
case "pick_directory":
|
|
408
417
|
return handlePickDirectory();
|
|
418
|
+
case "search_project_files": {
|
|
419
|
+
const searchQuery = match.query?.get("query") ?? "";
|
|
420
|
+
return handleSearchProjectFiles(store, id, searchQuery);
|
|
421
|
+
}
|
|
409
422
|
case "enqueue_prompt":
|
|
410
423
|
return handleEnqueuePrompt(store, manager, id, asObject(body));
|
|
411
424
|
case "get_prompt_queue":
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project file search for the agent composer `@` mention dropdown.
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/projects/:id/files/search?query=<query>` returns files whose
|
|
5
|
+
* relative path contains the query (case-insensitive). Results are limited
|
|
6
|
+
* to `MAX_RESULTS` and common generated directories are always excluded.
|
|
7
|
+
*
|
|
8
|
+
* The handler is synchronous-style but returns a Promise because `glob`
|
|
9
|
+
* exposes an async API.
|
|
10
|
+
*/
|
|
11
|
+
import type { SessionStore } from "../storage.js";
|
|
12
|
+
export interface ProjectFileSearchResult {
|
|
13
|
+
/** Absolute filesystem path. */
|
|
14
|
+
path: string;
|
|
15
|
+
/** Path relative to the project root (POSIX separators). */
|
|
16
|
+
relativePath: string;
|
|
17
|
+
/** Filename (basename). */
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Search files in a project whose relative path contains `query`
|
|
22
|
+
* (case-insensitive). Returns an empty array when the project has no
|
|
23
|
+
* files, when the query is empty, or when the project path is unreadable.
|
|
24
|
+
*/
|
|
25
|
+
export declare function handleSearchProjectFiles(store: SessionStore, projectId: string, query: string): Promise<ProjectFileSearchResult[]>;
|
|
26
|
+
//# sourceMappingURL=files-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files-search.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/files-search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,WAAW,uBAAuB;IACtC,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd;AAyBD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAqDpC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project file search for the agent composer `@` mention dropdown.
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/projects/:id/files/search?query=<query>` returns files whose
|
|
5
|
+
* relative path contains the query (case-insensitive). Results are limited
|
|
6
|
+
* to `MAX_RESULTS` and common generated directories are always excluded.
|
|
7
|
+
*
|
|
8
|
+
* The handler is synchronous-style but returns a Promise because `glob`
|
|
9
|
+
* exposes an async API.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { glob } from "glob";
|
|
14
|
+
import { NotFoundError } from "./errors.js";
|
|
15
|
+
/** Maximum number of suggestions shown in the composer dropdown. */
|
|
16
|
+
const MAX_RESULTS = 20;
|
|
17
|
+
/**
|
|
18
|
+
* Directories/patterns that are never useful as `@` mention targets.
|
|
19
|
+
* These are applied in addition to the project's own `.gitignore`.
|
|
20
|
+
*/
|
|
21
|
+
const ALWAYS_IGNORE = [
|
|
22
|
+
"**/node_modules/**",
|
|
23
|
+
"**/.git/**",
|
|
24
|
+
"**/.next/**",
|
|
25
|
+
"**/.nuxt/**",
|
|
26
|
+
"**/.svelte-kit/**",
|
|
27
|
+
"**/.astro/**",
|
|
28
|
+
"**/dist/**",
|
|
29
|
+
"**/build/**",
|
|
30
|
+
"**/.spectral/**",
|
|
31
|
+
"**/.vscode/**",
|
|
32
|
+
"**/.idea/**",
|
|
33
|
+
"**/coverage/**",
|
|
34
|
+
"**/*.log",
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Search files in a project whose relative path contains `query`
|
|
38
|
+
* (case-insensitive). Returns an empty array when the project has no
|
|
39
|
+
* files, when the query is empty, or when the project path is unreadable.
|
|
40
|
+
*/
|
|
41
|
+
export async function handleSearchProjectFiles(store, projectId, query) {
|
|
42
|
+
const project = store.getProject(projectId);
|
|
43
|
+
if (!project) {
|
|
44
|
+
throw new NotFoundError("Project not found");
|
|
45
|
+
}
|
|
46
|
+
const projectPath = project.path;
|
|
47
|
+
const trimmedQuery = query.trim();
|
|
48
|
+
if (!trimmedQuery) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const ignorePatterns = [...ALWAYS_IGNORE];
|
|
52
|
+
// Read root .gitignore when present. Nested .gitignores are ignored for
|
|
53
|
+
// now — this keeps the search fast and the implementation small; the
|
|
54
|
+
// hard-coded ALWAYS_IGNORE already catches the largest generated dirs.
|
|
55
|
+
try {
|
|
56
|
+
const gitignore = readFileSync(join(projectPath, ".gitignore"), "utf8");
|
|
57
|
+
const patterns = gitignore
|
|
58
|
+
.split("\n")
|
|
59
|
+
.map((line) => line.trim())
|
|
60
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
61
|
+
ignorePatterns.push(...patterns);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// .gitignore missing or unreadable — proceed without it.
|
|
65
|
+
}
|
|
66
|
+
const lowerQuery = trimmedQuery.toLowerCase();
|
|
67
|
+
let matches;
|
|
68
|
+
try {
|
|
69
|
+
matches = await glob("**/*", {
|
|
70
|
+
cwd: projectPath,
|
|
71
|
+
nodir: true,
|
|
72
|
+
dot: false,
|
|
73
|
+
ignore: ignorePatterns,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Project path unreadable or not a directory — return empty rather
|
|
78
|
+
// than crashing the composer dropdown.
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const filtered = matches
|
|
82
|
+
.filter((relativePath) => relativePath.toLowerCase().includes(lowerQuery))
|
|
83
|
+
.slice(0, MAX_RESULTS);
|
|
84
|
+
return filtered.map((relativePath) => ({
|
|
85
|
+
path: join(projectPath, relativePath),
|
|
86
|
+
relativePath,
|
|
87
|
+
name: relativePath.split("/").pop() ?? relativePath,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inter-agent communication broker for `spectral serve`.
|
|
3
|
+
*
|
|
4
|
+
* Provides project-scoped, channel-based messaging between active agent
|
|
5
|
+
* sessions running in the same spectral process on the same machine. Messages
|
|
6
|
+
* are durably backed by SQLite so they survive reconnects and can be polled by
|
|
7
|
+
* sessions that were not attached when the message was sent.
|
|
8
|
+
*
|
|
9
|
+
* The broker is intentionally process-local: subscriptions are in-memory
|
|
10
|
+
* inside the `spectral serve` daemon. Cross-process delivery on the same
|
|
11
|
+
* machine happens through the shared SQLite backing when each process creates
|
|
12
|
+
* its own broker instance pointing at the same `sessions.db`.
|
|
13
|
+
*/
|
|
14
|
+
import type { SessionStore } from "./storage.js";
|
|
15
|
+
export interface InterAgentMessage {
|
|
16
|
+
/** Unique message id (UUID). */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Owning project. */
|
|
19
|
+
projectId: string;
|
|
20
|
+
/** Logical channel within the project (e.g. "memory", "task-handoff"). */
|
|
21
|
+
channel: string;
|
|
22
|
+
/** Session that sent the message. */
|
|
23
|
+
senderSessionId: string;
|
|
24
|
+
/** Optional human-readable sender name. */
|
|
25
|
+
senderName?: string;
|
|
26
|
+
/** When set, the message is a private direct message; otherwise broadcast. */
|
|
27
|
+
recipientSessionId?: string;
|
|
28
|
+
/** Message kind used to route interpretation (e.g. "observation"). */
|
|
29
|
+
kind: string;
|
|
30
|
+
/** Opaque JSON payload. */
|
|
31
|
+
payload: string;
|
|
32
|
+
/** Timestamp (ms since epoch) when the message was created. */
|
|
33
|
+
createdAt: number;
|
|
34
|
+
/** Timestamp (ms) when the recipient first polled it, if ever. */
|
|
35
|
+
deliveredAt?: number;
|
|
36
|
+
/** Timestamp (ms) after which the message may be garbage collected. */
|
|
37
|
+
expiresAt?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface InterAgentSendInput {
|
|
40
|
+
projectId: string;
|
|
41
|
+
channel: string;
|
|
42
|
+
senderSessionId: string;
|
|
43
|
+
senderName?: string;
|
|
44
|
+
/** Omit for broadcast. */
|
|
45
|
+
recipientSessionId?: string;
|
|
46
|
+
kind: string;
|
|
47
|
+
payload: string;
|
|
48
|
+
/** Default: 24 hours. Set to 0 to disable expiry. */
|
|
49
|
+
ttlSeconds?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface InterAgentPollInput {
|
|
52
|
+
projectId: string;
|
|
53
|
+
/** Filter by channel. Omit to poll all channels in the project. */
|
|
54
|
+
channel?: string;
|
|
55
|
+
/** Poll only messages addressed to this session (or broadcast). Omit to poll all. */
|
|
56
|
+
recipientSessionId?: string;
|
|
57
|
+
/** Only return messages created after this timestamp (exclusive). */
|
|
58
|
+
since?: number;
|
|
59
|
+
/** Maximum messages to return. Default 100. */
|
|
60
|
+
limit?: number;
|
|
61
|
+
/** Mark returned messages as delivered. Default true. */
|
|
62
|
+
markDelivered?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface InterAgentSubscribeInput {
|
|
65
|
+
sessionId: string;
|
|
66
|
+
projectId: string;
|
|
67
|
+
channel?: string;
|
|
68
|
+
handler: (msg: InterAgentMessage) => void;
|
|
69
|
+
}
|
|
70
|
+
export interface InterAgentBrokerOptions {
|
|
71
|
+
store: SessionStore;
|
|
72
|
+
/** Default TTL in seconds for messages that don't specify one. Default 86400. */
|
|
73
|
+
defaultTtlSeconds?: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* In-process broker for inter-session messages.
|
|
77
|
+
*
|
|
78
|
+
* All public methods are synchronous because the underlying `SessionStore`
|
|
79
|
+
* uses better-sqlite3. Live subscribers are called synchronously from `send`.
|
|
80
|
+
*/
|
|
81
|
+
export declare class InterAgentBroker {
|
|
82
|
+
private readonly store;
|
|
83
|
+
private readonly defaultTtlSeconds;
|
|
84
|
+
/**
|
|
85
|
+
* In-process subscriptions keyed by `projectId:channel`. Each active session
|
|
86
|
+
* in the current process registers a handler here to receive live pushes.
|
|
87
|
+
*/
|
|
88
|
+
private readonly subscriptions;
|
|
89
|
+
constructor(opts: InterAgentBrokerOptions);
|
|
90
|
+
/**
|
|
91
|
+
* Send a message. Persists it to SQLite and synchronously pushes it to any
|
|
92
|
+
* in-process subscribers whose session/channel filters match.
|
|
93
|
+
*/
|
|
94
|
+
send(input: InterAgentSendInput): InterAgentMessage;
|
|
95
|
+
/**
|
|
96
|
+
* Poll for messages matching the filter. By default marks returned messages
|
|
97
|
+
* as delivered so they won't be returned again on subsequent polls.
|
|
98
|
+
*/
|
|
99
|
+
poll(input: InterAgentPollInput): InterAgentMessage[];
|
|
100
|
+
/**
|
|
101
|
+
* Subscribe a session to live pushes for a project/channel. Returns an
|
|
102
|
+
* unsubscribe function. Subscriptions are in-process only.
|
|
103
|
+
*/
|
|
104
|
+
subscribe(input: InterAgentSubscribeInput): () => void;
|
|
105
|
+
/**
|
|
106
|
+
* Delete expired messages from the backing store. Call periodically.
|
|
107
|
+
*/
|
|
108
|
+
cleanup(maxAgeSeconds?: number): void;
|
|
109
|
+
private pushToSubscribers;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=inter-agent-broker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inter-agent-broker.d.ts","sourceRoot":"","sources":["../../src/server/inter-agent-broker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGjD,MAAM,WAAW,iBAAiB;IAChC,gCAAgC;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,sBAAsB;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,eAAe,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,YAAY,CAAC;IACpB,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE3C;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4D;gBAE9E,IAAI,EAAE,uBAAuB;IAKzC;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,mBAAmB,GAAG,iBAAiB;IAyBnD;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,mBAAmB,GAAG,iBAAiB,EAAE;IAWrD;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,IAAI;IA4BtD;;OAEG;IACH,OAAO,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI;IAQrC,OAAO,CAAC,iBAAiB;CAqB1B"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inter-agent communication broker for `spectral serve`.
|
|
3
|
+
*
|
|
4
|
+
* Provides project-scoped, channel-based messaging between active agent
|
|
5
|
+
* sessions running in the same spectral process on the same machine. Messages
|
|
6
|
+
* are durably backed by SQLite so they survive reconnects and can be polled by
|
|
7
|
+
* sessions that were not attached when the message was sent.
|
|
8
|
+
*
|
|
9
|
+
* The broker is intentionally process-local: subscriptions are in-memory
|
|
10
|
+
* inside the `spectral serve` daemon. Cross-process delivery on the same
|
|
11
|
+
* machine happens through the shared SQLite backing when each process creates
|
|
12
|
+
* its own broker instance pointing at the same `sessions.db`.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
/**
|
|
16
|
+
* In-process broker for inter-session messages.
|
|
17
|
+
*
|
|
18
|
+
* All public methods are synchronous because the underlying `SessionStore`
|
|
19
|
+
* uses better-sqlite3. Live subscribers are called synchronously from `send`.
|
|
20
|
+
*/
|
|
21
|
+
export class InterAgentBroker {
|
|
22
|
+
store;
|
|
23
|
+
defaultTtlSeconds;
|
|
24
|
+
/**
|
|
25
|
+
* In-process subscriptions keyed by `projectId:channel`. Each active session
|
|
26
|
+
* in the current process registers a handler here to receive live pushes.
|
|
27
|
+
*/
|
|
28
|
+
subscriptions = new Map();
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
this.store = opts.store;
|
|
31
|
+
this.defaultTtlSeconds = opts.defaultTtlSeconds ?? 24 * 60 * 60;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Send a message. Persists it to SQLite and synchronously pushes it to any
|
|
35
|
+
* in-process subscribers whose session/channel filters match.
|
|
36
|
+
*/
|
|
37
|
+
send(input) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const ttl = input.ttlSeconds === undefined
|
|
40
|
+
? this.defaultTtlSeconds
|
|
41
|
+
: input.ttlSeconds;
|
|
42
|
+
const msg = {
|
|
43
|
+
id: randomUUID(),
|
|
44
|
+
projectId: input.projectId,
|
|
45
|
+
channel: input.channel,
|
|
46
|
+
senderSessionId: input.senderSessionId,
|
|
47
|
+
senderName: input.senderName,
|
|
48
|
+
recipientSessionId: input.recipientSessionId,
|
|
49
|
+
kind: input.kind,
|
|
50
|
+
payload: input.payload,
|
|
51
|
+
createdAt: now,
|
|
52
|
+
expiresAt: ttl > 0 ? now + ttl * 1000 : undefined,
|
|
53
|
+
};
|
|
54
|
+
this.store.insertInterAgentMessage(msg);
|
|
55
|
+
this.pushToSubscribers(msg);
|
|
56
|
+
return msg;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Poll for messages matching the filter. By default marks returned messages
|
|
60
|
+
* as delivered so they won't be returned again on subsequent polls.
|
|
61
|
+
*/
|
|
62
|
+
poll(input) {
|
|
63
|
+
return this.store.pollInterAgentMessages({
|
|
64
|
+
projectId: input.projectId,
|
|
65
|
+
channel: input.channel,
|
|
66
|
+
recipientSessionId: input.recipientSessionId,
|
|
67
|
+
since: input.since,
|
|
68
|
+
limit: input.limit ?? 100,
|
|
69
|
+
markDelivered: input.markDelivered ?? true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Subscribe a session to live pushes for a project/channel. Returns an
|
|
74
|
+
* unsubscribe function. Subscriptions are in-process only.
|
|
75
|
+
*/
|
|
76
|
+
subscribe(input) {
|
|
77
|
+
const key = subscriptionKey(input.projectId, input.channel);
|
|
78
|
+
let set = this.subscriptions.get(key);
|
|
79
|
+
if (!set) {
|
|
80
|
+
set = new Set();
|
|
81
|
+
this.subscriptions.set(key, set);
|
|
82
|
+
}
|
|
83
|
+
const wrapped = (msg) => {
|
|
84
|
+
// Don't deliver the sender's own messages back to itself.
|
|
85
|
+
if (msg.senderSessionId === input.sessionId)
|
|
86
|
+
return;
|
|
87
|
+
// Respect direct-message targeting.
|
|
88
|
+
if (msg.recipientSessionId && msg.recipientSessionId !== input.sessionId)
|
|
89
|
+
return;
|
|
90
|
+
input.handler(msg);
|
|
91
|
+
};
|
|
92
|
+
set.add(wrapped);
|
|
93
|
+
return () => {
|
|
94
|
+
const current = this.subscriptions.get(key);
|
|
95
|
+
if (!current)
|
|
96
|
+
return;
|
|
97
|
+
current.delete(wrapped);
|
|
98
|
+
if (current.size === 0) {
|
|
99
|
+
this.subscriptions.delete(key);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Delete expired messages from the backing store. Call periodically.
|
|
105
|
+
*/
|
|
106
|
+
cleanup(maxAgeSeconds) {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (maxAgeSeconds !== undefined) {
|
|
109
|
+
this.store.deleteInterAgentMessagesOlderThan(now - maxAgeSeconds * 1000);
|
|
110
|
+
}
|
|
111
|
+
this.store.deleteExpiredInterAgentMessages(now);
|
|
112
|
+
}
|
|
113
|
+
pushToSubscribers(msg) {
|
|
114
|
+
// Try both channel-specific and project-wide subscriptions.
|
|
115
|
+
const keys = [
|
|
116
|
+
subscriptionKey(msg.projectId, msg.channel),
|
|
117
|
+
subscriptionKey(msg.projectId, undefined),
|
|
118
|
+
];
|
|
119
|
+
for (const key of keys) {
|
|
120
|
+
const set = this.subscriptions.get(key);
|
|
121
|
+
if (!set)
|
|
122
|
+
continue;
|
|
123
|
+
for (const handler of set) {
|
|
124
|
+
try {
|
|
125
|
+
handler(msg);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.error(`[inter-agent] subscriber error: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function subscriptionKey(projectId, channel) {
|
|
135
|
+
return channel ? `${projectId}:${channel}` : projectId;
|
|
136
|
+
}
|
|
@@ -196,6 +196,7 @@ export declare class SessionStreamManager {
|
|
|
196
196
|
private readonly machineJwt;
|
|
197
197
|
private readonly bridgeFactory;
|
|
198
198
|
private readonly agentDir;
|
|
199
|
+
private readonly broker;
|
|
199
200
|
private readonly streams;
|
|
200
201
|
private disposed;
|
|
201
202
|
constructor(opts: SessionStreamManagerOptions);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAMH,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { randomUUID } from "node:crypto";
|
|
40
40
|
import { AgentBridge } from "./agent-bridge.js";
|
|
41
|
+
import { InterAgentBroker } from "./inter-agent-broker.js";
|
|
42
|
+
import { setInterAgentBroker } from "../memory/inter-agent-broker-singleton.js";
|
|
41
43
|
import { getMemoryState, isSourceEntry, rawTokensSinceLastBound, rawTokensSinceLastCompaction, } from "../memory/branch.js";
|
|
42
44
|
import { observationPoolTokens, renderSummary } from "../memory/compaction.js";
|
|
43
45
|
import { loadConfig } from "../memory/config.js";
|
|
@@ -233,6 +235,7 @@ export class SessionStreamManager {
|
|
|
233
235
|
machineJwt;
|
|
234
236
|
bridgeFactory;
|
|
235
237
|
agentDir;
|
|
238
|
+
broker;
|
|
236
239
|
streams = new Map();
|
|
237
240
|
disposed = false;
|
|
238
241
|
constructor(opts) {
|
|
@@ -242,13 +245,19 @@ export class SessionStreamManager {
|
|
|
242
245
|
this.machineJwt = opts.machineJwt;
|
|
243
246
|
this.bridgeFactory = opts.bridgeFactory ?? DEFAULT_BRIDGE_FACTORY;
|
|
244
247
|
this.agentDir = opts.agentDir;
|
|
248
|
+
// Broker for project-scoped inter-session communication.
|
|
249
|
+
this.broker = new InterAgentBroker({ store: this.store });
|
|
245
250
|
// Wire the project observations store singleton so the memory extension's
|
|
246
251
|
// read_project_observations tool can query cross-session observations.
|
|
247
252
|
setProjectObsStore({
|
|
248
253
|
insertProjectObservations: (pid, sid, obs, ts) => this.store.insertProjectObservations(pid, sid, obs, ts),
|
|
249
254
|
searchProjectObservations: (pid, q, limit) => this.store.searchProjectObservations(pid, q, limit),
|
|
255
|
+
getProjectObservationById: (pid, oid) => this.store.getProjectObservationById(pid, oid),
|
|
250
256
|
getProjectByCwd: (cwd) => this.store.getProjectByCwd(cwd),
|
|
251
257
|
});
|
|
258
|
+
// Wire the inter-agent broker singleton so the memory extension can send
|
|
259
|
+
// and receive live observation messages between active sessions.
|
|
260
|
+
setInterAgentBroker(this.broker);
|
|
252
261
|
}
|
|
253
262
|
/**
|
|
254
263
|
* Attach a subscriber to a session. Lazily creates the underlying spectral
|
|
@@ -1048,6 +1057,40 @@ export class SessionStreamManager {
|
|
|
1048
1057
|
},
|
|
1049
1058
|
};
|
|
1050
1059
|
stream.bridge = this.bridgeFactory(bridgeOpts);
|
|
1060
|
+
// Subscribe this session to live inter-agent observation messages so the
|
|
1061
|
+
// browser UI can surface them even before the memory extension injects
|
|
1062
|
+
// them into the agent context.
|
|
1063
|
+
if (projectId) {
|
|
1064
|
+
stream.interAgentUnsubscribe = this.broker.subscribe({
|
|
1065
|
+
sessionId,
|
|
1066
|
+
projectId,
|
|
1067
|
+
channel: "memory",
|
|
1068
|
+
handler: (msg) => {
|
|
1069
|
+
if (msg.senderSessionId === sessionId)
|
|
1070
|
+
return;
|
|
1071
|
+
if (msg.kind !== "observation")
|
|
1072
|
+
return;
|
|
1073
|
+
try {
|
|
1074
|
+
const payload = JSON.parse(msg.payload);
|
|
1075
|
+
const relevance = payload.relevance;
|
|
1076
|
+
this.broadcast(stream, {
|
|
1077
|
+
type: "inter_agent_observation",
|
|
1078
|
+
projectId: msg.projectId,
|
|
1079
|
+
sourceSessionId: msg.senderSessionId,
|
|
1080
|
+
sourceSessionName: msg.senderName,
|
|
1081
|
+
memoryId: String(payload.memoryId ?? ""),
|
|
1082
|
+
content: String(payload.content ?? ""),
|
|
1083
|
+
relevance,
|
|
1084
|
+
originType: payload.originType === "reflection" ? "reflection" : "observation",
|
|
1085
|
+
timestamp: String(payload.timestamp ?? new Date().toISOString()),
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
catch (err) {
|
|
1089
|
+
console.error(`[spectral] error: failed to broadcast inter-agent observation: ${err instanceof Error ? err.message : String(err)}`);
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1051
1094
|
stream.ready = stream.bridge
|
|
1052
1095
|
.start()
|
|
1053
1096
|
.then(() => {
|