@gethmy/mcp 2.4.6 → 2.5.0
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/README.md +34 -1
- package/dist/cli.js +20867 -18386
- package/dist/index.js +20999 -18518
- package/dist/lib/api-client.js +130 -926
- package/dist/lib/config.js +5 -1
- package/package.json +2 -2
- package/src/__tests__/mcp-integration.test.ts +141 -0
- package/src/__tests__/memory-floor.test.ts +126 -0
- package/src/__tests__/memory-park.test.ts +213 -0
- package/src/__tests__/memory-session.test.ts +77 -0
- package/src/__tests__/prompt-builder.test.ts +234 -0
- package/src/__tests__/remote-routing.test.ts +285 -0
- package/src/__tests__/skills.test.ts +111 -0
- package/src/__tests__/tool-dispatch.test.ts +260 -0
- package/src/api-client.ts +133 -96
- package/src/memory-floor.ts +264 -0
- package/src/memory-park.ts +252 -0
- package/src/memory-session.ts +61 -0
- package/src/prompt-builder.ts +93 -0
- package/src/remote.ts +270 -77
- package/src/server.ts +351 -1467
- package/src/__tests__/active-learning.test.ts +0 -483
- package/src/__tests__/agent-performance-profiles.test.ts +0 -468
- package/src/__tests__/context-assembly.test.ts +0 -506
- package/src/__tests__/lifecycle-maintenance.test.ts +0 -238
- package/src/__tests__/memory-audit.test.ts +0 -528
- package/src/__tests__/pattern-detection.test.ts +0 -438
- package/src/active-learning.ts +0 -1165
- package/src/consolidation.ts +0 -383
- package/src/context-assembly.ts +0 -1175
- package/src/lifecycle-maintenance.ts +0 -120
- package/src/memory-audit.ts +0 -578
- package/src/memory-cleanup.ts +0 -902
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification harness for MCP tool dispatch. Card #182.
|
|
3
|
+
*
|
|
4
|
+
* Pins the contract between Harmony's MCP server and any MCP client (Claude
|
|
5
|
+
* Code, Codex, Cursor, etc.). If the dispatch shape drifts, agents lose tools
|
|
6
|
+
* silently, so this is the floor:
|
|
7
|
+
*
|
|
8
|
+
* 1. TOOLS registry is well-formed (shape, names, schemas).
|
|
9
|
+
* 2. ListTools handler emits the same set TOOLS declares.
|
|
10
|
+
* 3. CallTool handler routes valid names and isErrors unknown ones.
|
|
11
|
+
* 4. ListResources / ReadResource cover the published URIs.
|
|
12
|
+
* 5. Tool ↔ skill name spaces are disjoint.
|
|
13
|
+
*
|
|
14
|
+
* Run with: bun test packages/mcp-server/src/__tests__/tool-dispatch.test.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
18
|
+
import {
|
|
19
|
+
CallToolRequestSchema,
|
|
20
|
+
ListResourcesRequestSchema,
|
|
21
|
+
ListToolsRequestSchema,
|
|
22
|
+
ReadResourceRequestSchema,
|
|
23
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
24
|
+
import {
|
|
25
|
+
RESOURCES,
|
|
26
|
+
registerHandlers,
|
|
27
|
+
TOOLS,
|
|
28
|
+
type ToolDeps,
|
|
29
|
+
} from "../server.js";
|
|
30
|
+
import { SKILL_DEFINITIONS } from "../skills.js";
|
|
31
|
+
|
|
32
|
+
const TOOL_NAMES = Object.keys(TOOLS);
|
|
33
|
+
|
|
34
|
+
// ── Fake transport ────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
type Schema = unknown;
|
|
37
|
+
type Handler = (req: unknown) => Promise<unknown>;
|
|
38
|
+
|
|
39
|
+
class FakeServer {
|
|
40
|
+
handlers = new Map<Schema, Handler>();
|
|
41
|
+
setRequestHandler(schema: Schema, handler: Handler): void {
|
|
42
|
+
this.handlers.set(schema, handler);
|
|
43
|
+
}
|
|
44
|
+
call<T = unknown>(schema: Schema, request: unknown): Promise<T> {
|
|
45
|
+
const handler = this.handlers.get(schema);
|
|
46
|
+
if (!handler) throw new Error("Handler not registered");
|
|
47
|
+
return handler(request) as Promise<T>;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeDeps(overrides: Partial<ToolDeps> = {}): ToolDeps {
|
|
52
|
+
return {
|
|
53
|
+
getClient: () => ({}) as never,
|
|
54
|
+
isConfigured: () => true,
|
|
55
|
+
getActiveProjectId: () => "11111111-1111-1111-1111-111111111111",
|
|
56
|
+
getActiveWorkspaceId: () => "22222222-2222-2222-2222-222222222222",
|
|
57
|
+
setActiveProject: () => {},
|
|
58
|
+
setActiveWorkspace: () => {},
|
|
59
|
+
getApiUrl: () => "http://localhost",
|
|
60
|
+
getMemoryDir: () => null,
|
|
61
|
+
getUserEmail: () => null,
|
|
62
|
+
saveConfig: () => {},
|
|
63
|
+
resetClient: () => {},
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Static registry shape ────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
describe("TOOLS registry", () => {
|
|
71
|
+
test("is non-empty", () => {
|
|
72
|
+
expect(TOOL_NAMES.length).toBeGreaterThan(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("every name is unique", () => {
|
|
76
|
+
expect(new Set(TOOL_NAMES).size).toBe(TOOL_NAMES.length);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test.each(TOOL_NAMES)("%s is namespaced under harmony_", (name) => {
|
|
80
|
+
expect(name).toMatch(/^harmony_[a-z0-9_]+$/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test.each(TOOL_NAMES)("%s has description and inputSchema", (name) => {
|
|
84
|
+
const tool = (
|
|
85
|
+
TOOLS as Record<string, { description?: unknown; inputSchema?: unknown }>
|
|
86
|
+
)[name];
|
|
87
|
+
expect(typeof tool.description).toBe("string");
|
|
88
|
+
expect((tool.description as string).length).toBeGreaterThan(0);
|
|
89
|
+
expect(typeof tool.inputSchema).toBe("object");
|
|
90
|
+
expect(tool.inputSchema).not.toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test.each(TOOL_NAMES)("%s inputSchema is a JSON Schema object", (name) => {
|
|
94
|
+
const schema = (
|
|
95
|
+
TOOLS as Record<
|
|
96
|
+
string,
|
|
97
|
+
{ inputSchema: { type?: string; properties?: unknown } }
|
|
98
|
+
>
|
|
99
|
+
)[name].inputSchema;
|
|
100
|
+
expect(schema.type).toBe("object");
|
|
101
|
+
if ("properties" in schema && schema.properties !== undefined) {
|
|
102
|
+
expect(typeof schema.properties).toBe("object");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── Skill ↔ tool boundary ────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
describe("skill ↔ tool namespace boundary", () => {
|
|
110
|
+
test("skill names and tool names are disjoint", () => {
|
|
111
|
+
const skillNames = new Set(Object.keys(SKILL_DEFINITIONS));
|
|
112
|
+
const collisions = TOOL_NAMES.filter((name) => skillNames.has(name));
|
|
113
|
+
expect(collisions).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── Dispatch round-trip ──────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
describe("registerHandlers — ListTools", () => {
|
|
120
|
+
let server: FakeServer;
|
|
121
|
+
|
|
122
|
+
beforeAll(() => {
|
|
123
|
+
server = new FakeServer();
|
|
124
|
+
registerHandlers(server as never, makeDeps());
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("registers all four request handlers", () => {
|
|
128
|
+
expect(server.handlers.has(ListToolsRequestSchema)).toBe(true);
|
|
129
|
+
expect(server.handlers.has(CallToolRequestSchema)).toBe(true);
|
|
130
|
+
expect(server.handlers.has(ListResourcesRequestSchema)).toBe(true);
|
|
131
|
+
expect(server.handlers.has(ReadResourceRequestSchema)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("emits one entry per TOOLS row, with name+description+inputSchema", async () => {
|
|
135
|
+
const result = await server.call<{
|
|
136
|
+
tools: Array<{ name: string; description: string; inputSchema: unknown }>;
|
|
137
|
+
}>(ListToolsRequestSchema, { method: "tools/list" });
|
|
138
|
+
|
|
139
|
+
expect(result.tools.length).toBe(TOOL_NAMES.length);
|
|
140
|
+
|
|
141
|
+
const emittedNames = result.tools.map((t) => t.name).sort();
|
|
142
|
+
expect(emittedNames).toEqual([...TOOL_NAMES].sort());
|
|
143
|
+
|
|
144
|
+
for (const tool of result.tools) {
|
|
145
|
+
expect(typeof tool.name).toBe("string");
|
|
146
|
+
expect(typeof tool.description).toBe("string");
|
|
147
|
+
expect(typeof tool.inputSchema).toBe("object");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("registerHandlers — CallTool", () => {
|
|
153
|
+
let server: FakeServer;
|
|
154
|
+
let workspaceId: string | null;
|
|
155
|
+
let projectId: string | null;
|
|
156
|
+
|
|
157
|
+
beforeAll(() => {
|
|
158
|
+
server = new FakeServer();
|
|
159
|
+
workspaceId = "22222222-2222-2222-2222-222222222222";
|
|
160
|
+
projectId = "11111111-1111-1111-1111-111111111111";
|
|
161
|
+
const deps = makeDeps({
|
|
162
|
+
getActiveWorkspaceId: () => workspaceId,
|
|
163
|
+
getActiveProjectId: () => projectId,
|
|
164
|
+
});
|
|
165
|
+
registerHandlers(server as never, deps);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("routes harmony_get_context and returns active ids", async () => {
|
|
169
|
+
const result = await server.call<{
|
|
170
|
+
content: Array<{ type: string; text: string }>;
|
|
171
|
+
isError?: boolean;
|
|
172
|
+
}>(CallToolRequestSchema, {
|
|
173
|
+
method: "tools/call",
|
|
174
|
+
params: { name: "harmony_get_context", arguments: {} },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(result.isError).toBeUndefined();
|
|
178
|
+
expect(result.content[0].type).toBe("text");
|
|
179
|
+
const payload = JSON.parse(result.content[0].text);
|
|
180
|
+
expect(payload.success).toBe(true);
|
|
181
|
+
expect(payload.context.activeWorkspaceId).toBe(workspaceId);
|
|
182
|
+
expect(payload.context.activeProjectId).toBe(projectId);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("unknown tool name returns isError without throwing", async () => {
|
|
186
|
+
const result = await server.call<{
|
|
187
|
+
content: Array<{ type: string; text: string }>;
|
|
188
|
+
isError?: boolean;
|
|
189
|
+
}>(CallToolRequestSchema, {
|
|
190
|
+
method: "tools/call",
|
|
191
|
+
params: { name: "harmony_does_not_exist", arguments: {} },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(result.isError).toBe(true);
|
|
195
|
+
expect(result.content[0].text.toLowerCase()).toContain("error");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("not-configured rejects authenticated tools", async () => {
|
|
199
|
+
const unconfigured = new FakeServer();
|
|
200
|
+
registerHandlers(
|
|
201
|
+
unconfigured as never,
|
|
202
|
+
makeDeps({ isConfigured: () => false }),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const result = await unconfigured.call<{
|
|
206
|
+
content: Array<{ type: string; text: string }>;
|
|
207
|
+
isError?: boolean;
|
|
208
|
+
}>(CallToolRequestSchema, {
|
|
209
|
+
method: "tools/call",
|
|
210
|
+
params: { name: "harmony_get_context", arguments: {} },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(result.isError).toBe(true);
|
|
214
|
+
expect(result.content[0].text).toContain("Not configured");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── Resources ────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
describe("registerHandlers — Resources", () => {
|
|
221
|
+
let server: FakeServer;
|
|
222
|
+
|
|
223
|
+
beforeAll(() => {
|
|
224
|
+
server = new FakeServer();
|
|
225
|
+
registerHandlers(server as never, makeDeps());
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("ListResources mirrors the static RESOURCES array", async () => {
|
|
229
|
+
const result = await server.call<{ resources: typeof RESOURCES }>(
|
|
230
|
+
ListResourcesRequestSchema,
|
|
231
|
+
{ method: "resources/list" },
|
|
232
|
+
);
|
|
233
|
+
expect(result.resources).toEqual(RESOURCES);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("ReadResource serves harmony://context as JSON", async () => {
|
|
237
|
+
const result = await server.call<{
|
|
238
|
+
contents: Array<{ uri: string; mimeType: string; text: string }>;
|
|
239
|
+
}>(ReadResourceRequestSchema, {
|
|
240
|
+
method: "resources/read",
|
|
241
|
+
params: { uri: "harmony://context" },
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(result.contents[0].uri).toBe("harmony://context");
|
|
245
|
+
expect(result.contents[0].mimeType).toBe("application/json");
|
|
246
|
+
const parsed = JSON.parse(result.contents[0].text);
|
|
247
|
+
expect(parsed).toHaveProperty("configured");
|
|
248
|
+
expect(parsed).toHaveProperty("activeWorkspaceId");
|
|
249
|
+
expect(parsed).toHaveProperty("activeProjectId");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("ReadResource on unknown URI throws", async () => {
|
|
253
|
+
await expect(
|
|
254
|
+
server.call(ReadResourceRequestSchema, {
|
|
255
|
+
method: "resources/read",
|
|
256
|
+
params: { uri: "harmony://nope" },
|
|
257
|
+
}),
|
|
258
|
+
).rejects.toThrow(/Unknown resource/);
|
|
259
|
+
});
|
|
260
|
+
});
|
package/src/api-client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getDisplayLinkType } from "@harmony/shared";
|
|
1
2
|
import { getApiKey, getApiUrl } from "./config.js";
|
|
2
3
|
|
|
3
4
|
export interface ApiResponse<T = unknown> {
|
|
@@ -149,6 +150,10 @@ export class HarmonyApiClient {
|
|
|
149
150
|
this.apiKey = apiKey;
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
getApiKey(): string {
|
|
154
|
+
return this.apiKey;
|
|
155
|
+
}
|
|
156
|
+
|
|
152
157
|
private async request<T>(
|
|
153
158
|
method: string,
|
|
154
159
|
path: string,
|
|
@@ -638,34 +643,6 @@ export class HarmonyApiClient {
|
|
|
638
643
|
return this.request("GET", `/cards/${cardId}/agent-context${query}`);
|
|
639
644
|
}
|
|
640
645
|
|
|
641
|
-
// ============ AGENT PERFORMANCE PROFILES ============
|
|
642
|
-
|
|
643
|
-
async getAgentProfile(
|
|
644
|
-
workspaceId: string,
|
|
645
|
-
agentIdentifier: string,
|
|
646
|
-
): Promise<{ profile: unknown }> {
|
|
647
|
-
const params = new URLSearchParams({
|
|
648
|
-
workspace_id: workspaceId,
|
|
649
|
-
agent_identifier: agentIdentifier,
|
|
650
|
-
});
|
|
651
|
-
return this.request("GET", `/agent-profiles?${params.toString()}`);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
async listAgentProfiles(
|
|
655
|
-
workspaceId: string,
|
|
656
|
-
): Promise<{ profiles: unknown[] }> {
|
|
657
|
-
const params = new URLSearchParams({ workspace_id: workspaceId });
|
|
658
|
-
return this.request("GET", `/agent-profiles?${params.toString()}`);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
async refreshAgentProfiles(
|
|
662
|
-
workspaceId: string,
|
|
663
|
-
): Promise<{ refreshed: boolean }> {
|
|
664
|
-
return this.request("POST", "/agent-profiles/refresh", {
|
|
665
|
-
workspace_id: workspaceId,
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
|
|
669
646
|
// ============ MEMORY OPERATIONS ============
|
|
670
647
|
|
|
671
648
|
async createMemoryEntity(data: {
|
|
@@ -678,6 +655,7 @@ export class HarmonyApiClient {
|
|
|
678
655
|
content: string;
|
|
679
656
|
metadata?: Record<string, unknown>;
|
|
680
657
|
confidence?: number;
|
|
658
|
+
importance?: number;
|
|
681
659
|
tags?: string[];
|
|
682
660
|
agent_identifier?: string;
|
|
683
661
|
}): Promise<{ entity: unknown; warnings?: string[] }> {
|
|
@@ -695,6 +673,7 @@ export class HarmonyApiClient {
|
|
|
695
673
|
q?: string;
|
|
696
674
|
limit?: number;
|
|
697
675
|
offset?: number;
|
|
676
|
+
include_superseded?: boolean;
|
|
698
677
|
}): Promise<{ entities: unknown[]; count: number }> {
|
|
699
678
|
const params = new URLSearchParams();
|
|
700
679
|
params.set("workspace_id", options.workspace_id);
|
|
@@ -710,6 +689,7 @@ export class HarmonyApiClient {
|
|
|
710
689
|
if (options.limit !== undefined) params.set("limit", String(options.limit));
|
|
711
690
|
if (options.offset !== undefined)
|
|
712
691
|
params.set("offset", String(options.offset));
|
|
692
|
+
if (options.include_superseded) params.set("include_superseded", "true");
|
|
713
693
|
return this.request("GET", `/memory/entities?${params.toString()}`);
|
|
714
694
|
}
|
|
715
695
|
|
|
@@ -728,6 +708,10 @@ export class HarmonyApiClient {
|
|
|
728
708
|
scope?: string;
|
|
729
709
|
type?: string;
|
|
730
710
|
memory_tier?: string;
|
|
711
|
+
// AGP lifecycle fields. Backend may not yet whitelist these — extra keys
|
|
712
|
+
// are dropped server-side, leaving the call as a no-op for those fields.
|
|
713
|
+
superseded_by?: string | null;
|
|
714
|
+
version?: number;
|
|
731
715
|
},
|
|
732
716
|
): Promise<{ entity: unknown; warnings?: string[] }> {
|
|
733
717
|
return this.request("PUT", `/memory/entities/${entityId}`, updates);
|
|
@@ -1063,6 +1047,44 @@ export class HarmonyApiClient {
|
|
|
1063
1047
|
return this.request("POST", "/api-keys", { name });
|
|
1064
1048
|
}
|
|
1065
1049
|
|
|
1050
|
+
// ============ PROMPT HISTORY (AGP P2) ============
|
|
1051
|
+
|
|
1052
|
+
async recordPromptHistory(data: {
|
|
1053
|
+
cardId: string;
|
|
1054
|
+
generatedPrompt: string;
|
|
1055
|
+
variant: "analysis" | "draft" | "execute";
|
|
1056
|
+
contextIncluded?: Record<string, unknown>;
|
|
1057
|
+
sessionId?: string | null;
|
|
1058
|
+
contentHash?: string;
|
|
1059
|
+
templateVersion?: number;
|
|
1060
|
+
confidence?: number;
|
|
1061
|
+
templateId?: string | null;
|
|
1062
|
+
isPinned?: boolean;
|
|
1063
|
+
}): Promise<{ entry: unknown }> {
|
|
1064
|
+
return this.request("POST", "/prompt-history", data);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async recordPromptHistoryFeedback(
|
|
1068
|
+
sessionId: string,
|
|
1069
|
+
outcome: "success" | "blocker" | "neutral",
|
|
1070
|
+
): Promise<{ adjusted: number }> {
|
|
1071
|
+
return this.request("POST", "/prompt-history/feedback", {
|
|
1072
|
+
sessionId,
|
|
1073
|
+
outcome,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async getPromptHistoryCohort(contentHash: string): Promise<{
|
|
1078
|
+
cohort: Array<{
|
|
1079
|
+
status: string | null;
|
|
1080
|
+
progressPercent: number | null;
|
|
1081
|
+
hadBlockers: boolean;
|
|
1082
|
+
}>;
|
|
1083
|
+
}> {
|
|
1084
|
+
const params = new URLSearchParams({ content_hash: contentHash });
|
|
1085
|
+
return this.request("GET", `/prompt-history/cohort?${params.toString()}`);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1066
1088
|
// ============ PROMPT GENERATION ============
|
|
1067
1089
|
|
|
1068
1090
|
/**
|
|
@@ -1083,6 +1105,8 @@ export class HarmonyApiClient {
|
|
|
1083
1105
|
includeLinks: boolean;
|
|
1084
1106
|
includeDescription: boolean;
|
|
1085
1107
|
}>;
|
|
1108
|
+
/** Optional active session ID to associate with the prompt snapshot. */
|
|
1109
|
+
sessionId?: string | null;
|
|
1086
1110
|
}): Promise<{
|
|
1087
1111
|
prompt: string;
|
|
1088
1112
|
variant: string;
|
|
@@ -1101,14 +1125,42 @@ export class HarmonyApiClient {
|
|
|
1101
1125
|
cardId: string;
|
|
1102
1126
|
shortId: number;
|
|
1103
1127
|
title: string;
|
|
1128
|
+
/** Local UUID identifying the persisted snapshot (AGP P2). */
|
|
1129
|
+
promptId: string;
|
|
1130
|
+
/** SHA-256 of the generated prompt body (AGP P2 cohort key). */
|
|
1131
|
+
contentHash: string;
|
|
1132
|
+
/** Template version that produced this prompt. */
|
|
1133
|
+
version: number;
|
|
1104
1134
|
}> {
|
|
1105
|
-
const {
|
|
1106
|
-
await loadPromptModules();
|
|
1135
|
+
const { generatePrompt } = await loadPromptModules();
|
|
1107
1136
|
|
|
1108
1137
|
// Fetch card data
|
|
1109
1138
|
const cardResult = await this.getCard(options.cardId);
|
|
1110
1139
|
const cardData = cardResult.card as CardPromptData;
|
|
1111
1140
|
|
|
1141
|
+
// Fetch card reference links so the prompt can render the "Related Cards"
|
|
1142
|
+
// section and the blocker-aware "Recommended Next Step" synthesis.
|
|
1143
|
+
// Best-effort — link fetch failures must not break prompt generation.
|
|
1144
|
+
try {
|
|
1145
|
+
const linksResult = await this.getCardLinks(options.cardId);
|
|
1146
|
+
const rawLinks =
|
|
1147
|
+
(linksResult.links as Array<{
|
|
1148
|
+
link_type: "relates_to" | "blocks" | "duplicates" | "is_part_of";
|
|
1149
|
+
direction: "outgoing" | "incoming";
|
|
1150
|
+
target_card: { short_id: number; title: string } | null;
|
|
1151
|
+
}>) || [];
|
|
1152
|
+
cardData.links = rawLinks
|
|
1153
|
+
.filter((l) => l.target_card)
|
|
1154
|
+
.map((l) => ({
|
|
1155
|
+
target_card: l.target_card as { short_id: number; title: string },
|
|
1156
|
+
direction: l.direction,
|
|
1157
|
+
display_type: getDisplayLinkType(l.link_type, l.direction),
|
|
1158
|
+
}));
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1161
|
+
console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1112
1164
|
// Try to get column info
|
|
1113
1165
|
let columnData: { name: string } | null = null;
|
|
1114
1166
|
const projectIdForBoard = options.projectId || cardData.project_id;
|
|
@@ -1128,67 +1180,37 @@ export class HarmonyApiClient {
|
|
|
1128
1180
|
|
|
1129
1181
|
const variant = options.variant || "execute";
|
|
1130
1182
|
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1183
|
+
// Phase 0 (memory architecture v2): full context assembly removed.
|
|
1184
|
+
// Use the basic memory search path so callers still get _some_ memory
|
|
1185
|
+
// hints. Phase 1 will reintroduce a session-scoped working memory layer.
|
|
1186
|
+
const assembledContextStr: string | undefined = undefined;
|
|
1187
|
+
const assemblyId: string | undefined = undefined;
|
|
1134
1188
|
let memories: MemoryItem[] | undefined;
|
|
1135
1189
|
|
|
1136
1190
|
try {
|
|
1137
1191
|
if (options.workspaceId && cardData.title) {
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
.
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
cacheManifest(assembled.manifest);
|
|
1192
|
+
const memoryResult = await this.searchMemoryEntities(
|
|
1193
|
+
options.workspaceId,
|
|
1194
|
+
cardData.title,
|
|
1195
|
+
{
|
|
1196
|
+
project_id: options.projectId,
|
|
1197
|
+
limit: 5,
|
|
1198
|
+
},
|
|
1199
|
+
);
|
|
1200
|
+
if (memoryResult.entities?.length > 0) {
|
|
1201
|
+
memories = (memoryResult.entities as MemoryItem[]).map((e) => ({
|
|
1202
|
+
id: e.id,
|
|
1203
|
+
type: e.type,
|
|
1204
|
+
title: e.title,
|
|
1205
|
+
content: e.content,
|
|
1206
|
+
confidence: e.confidence,
|
|
1207
|
+
tags: e.tags || [],
|
|
1208
|
+
}));
|
|
1156
1209
|
}
|
|
1157
1210
|
}
|
|
1158
1211
|
} catch (err) {
|
|
1159
|
-
// Context assembly failed, try legacy fallback
|
|
1160
1212
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1161
|
-
console.debug(`[generateCardPrompt]
|
|
1162
|
-
try {
|
|
1163
|
-
if (options.workspaceId && cardData.title) {
|
|
1164
|
-
const memoryResult = await this.searchMemoryEntities(
|
|
1165
|
-
options.workspaceId,
|
|
1166
|
-
cardData.title,
|
|
1167
|
-
{
|
|
1168
|
-
project_id: options.projectId,
|
|
1169
|
-
limit: 5,
|
|
1170
|
-
},
|
|
1171
|
-
);
|
|
1172
|
-
if (memoryResult.entities?.length > 0) {
|
|
1173
|
-
memories = (memoryResult.entities as MemoryItem[]).map((e) => ({
|
|
1174
|
-
id: e.id,
|
|
1175
|
-
type: e.type,
|
|
1176
|
-
title: e.title,
|
|
1177
|
-
content: e.content,
|
|
1178
|
-
confidence: e.confidence,
|
|
1179
|
-
tags: e.tags || [],
|
|
1180
|
-
}));
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
} catch (fallbackErr) {
|
|
1184
|
-
const fallbackMsg =
|
|
1185
|
-
fallbackErr instanceof Error
|
|
1186
|
-
? fallbackErr.message
|
|
1187
|
-
: String(fallbackErr);
|
|
1188
|
-
console.debug(
|
|
1189
|
-
`[generateCardPrompt] Memory fallback also failed: ${fallbackMsg}`,
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1213
|
+
console.debug(`[generateCardPrompt] Memory search failed: ${msg}`);
|
|
1192
1214
|
}
|
|
1193
1215
|
|
|
1194
1216
|
const result = generatePrompt({
|
|
@@ -1202,6 +1224,30 @@ export class HarmonyApiClient {
|
|
|
1202
1224
|
assemblyId,
|
|
1203
1225
|
});
|
|
1204
1226
|
|
|
1227
|
+
// AGP P2: persist a session-linked snapshot. Best-effort — never fail
|
|
1228
|
+
// prompt generation just because logging didn't land.
|
|
1229
|
+
try {
|
|
1230
|
+
await this.recordPromptHistory({
|
|
1231
|
+
cardId: cardData.id,
|
|
1232
|
+
generatedPrompt: result.prompt,
|
|
1233
|
+
variant: variant as "analysis" | "draft" | "execute",
|
|
1234
|
+
contextIncluded: {
|
|
1235
|
+
assemblyId: result.assemblyId ?? null,
|
|
1236
|
+
tokenEstimate: result.tokenEstimate,
|
|
1237
|
+
contextSummary: result.contextSummary,
|
|
1238
|
+
},
|
|
1239
|
+
sessionId: options.sessionId ?? null,
|
|
1240
|
+
contentHash: result.contentHash,
|
|
1241
|
+
templateVersion: result.version,
|
|
1242
|
+
confidence: 0.5,
|
|
1243
|
+
});
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1246
|
+
console.debug(
|
|
1247
|
+
`[generateCardPrompt] prompt_history persistence failed: ${msg}`,
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1205
1251
|
return {
|
|
1206
1252
|
...result,
|
|
1207
1253
|
cardId: cardData.id,
|
|
@@ -1241,14 +1287,10 @@ interface MemoryItem {
|
|
|
1241
1287
|
tags: string[];
|
|
1242
1288
|
}
|
|
1243
1289
|
|
|
1244
|
-
// Cached dynamic
|
|
1290
|
+
// Cached dynamic import for prompt-builder.
|
|
1291
|
+
// Phase 0 (memory architecture v2): context-assembly module deleted; prompt
|
|
1292
|
+
// generation falls back to a basic memory search path until Phase 1.
|
|
1245
1293
|
let _promptModules: {
|
|
1246
|
-
assembleContext: Awaited<
|
|
1247
|
-
typeof import("./context-assembly.js")
|
|
1248
|
-
>["assembleContext"];
|
|
1249
|
-
cacheManifest: Awaited<
|
|
1250
|
-
typeof import("./context-assembly.js")
|
|
1251
|
-
>["cacheManifest"];
|
|
1252
1294
|
generatePrompt: Awaited<
|
|
1253
1295
|
typeof import("./prompt-builder.js")
|
|
1254
1296
|
>["generatePrompt"];
|
|
@@ -1256,13 +1298,8 @@ let _promptModules: {
|
|
|
1256
1298
|
|
|
1257
1299
|
async function loadPromptModules() {
|
|
1258
1300
|
if (!_promptModules) {
|
|
1259
|
-
const
|
|
1260
|
-
import("./context-assembly.js"),
|
|
1261
|
-
import("./prompt-builder.js"),
|
|
1262
|
-
]);
|
|
1301
|
+
const pb = await import("./prompt-builder.js");
|
|
1263
1302
|
_promptModules = {
|
|
1264
|
-
assembleContext: ca.assembleContext,
|
|
1265
|
-
cacheManifest: ca.cacheManifest,
|
|
1266
1303
|
generatePrompt: pb.generatePrompt,
|
|
1267
1304
|
};
|
|
1268
1305
|
}
|