@hilbras/omninode 2.0.0-alpha.6 → 2.0.0-alpha.8
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 +15 -3
- package/dist/cli/index.js +261 -41
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +179 -31
- package/dist/index.js +264 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@ existing intelligence; it does not try to become another model.
|
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
|
21
|
-
**v2.0.0-alpha.
|
|
21
|
+
**v2.0.0-alpha.8 — v2 Phase 8: Remembera Integration v2**
|
|
22
22
|
(the reliability & interoperability line: architecture → execution →
|
|
23
|
-
protocol → adapters → pipelines → providers) (the v2
|
|
23
|
+
protocol → adapters → pipelines → providers → OmniHilbras → memory) (the v2
|
|
24
24
|
roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
|
|
25
25
|
the
|
|
26
26
|
full loop from §28 runs in one command —
|
|
@@ -31,6 +31,18 @@ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
|
|
|
31
31
|
|
|
32
32
|
### What's new in v2 (so far)
|
|
33
33
|
|
|
34
|
+
- **Phase 8 — Remembera / Memory v2**: the memory contract is formalized
|
|
35
|
+
(`retrieve` / `store` / `search` / `metadata` with v1 `query` / `write`
|
|
36
|
+
aliases kept working), memory gains structured **categories** (decisions,
|
|
37
|
+
problems, conventions, tasks…), retrieval is **task-oriented and budgeted**
|
|
38
|
+
(never "load all memory"), and `memory.required: true` makes backend
|
|
39
|
+
failures fatal instead of tolerated. Details: [MEMORY.md](docs/MEMORY.md).
|
|
40
|
+
- **Phase 7 — OmniHilbras Integration v2**: the dedicated adapter now
|
|
41
|
+
captures gateway-level metadata (version, tier, region) and attaches it
|
|
42
|
+
to discovered models, honors provider-level `timeout_ms`, and supports
|
|
43
|
+
streaming completions (`stream()` over SSE) — while a guard test proves
|
|
44
|
+
the core references OmniHilbras *only* through the provider layer, and a
|
|
45
|
+
full pipeline runs with no OmniHilbras configured at all.
|
|
34
46
|
- **Phase 6 — Provider Infrastructure v2**: standardized provider contract
|
|
35
47
|
(`providerId`, secret-free `authentication`, declared `capabilities`,
|
|
36
48
|
`connect()` / `getModel()` discovery), richer model metadata (context
|
|
@@ -327,7 +339,7 @@ required for the core engine.
|
|
|
327
339
|
- [Architecture](docs/ARCHITECTURE.md) — module map, workflow, local state
|
|
328
340
|
- [Security model](docs/SECURITY.md) — secrets, process execution, env isolation, audit log
|
|
329
341
|
- [API stability policy](docs/API_STABILITY.md) and [public API inventory](docs/API.md)
|
|
330
|
-
- [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
|
|
342
|
+
- [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Memory](docs/MEMORY.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
|
|
331
343
|
- [Example project](examples/demo/) — a runnable end-to-end demo (`./demo.sh`)
|
|
332
344
|
|
|
333
345
|
Prefer a container? `docker build -t omninode .` then
|
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
9
|
+
var OMNINODE_VERSION = "2.0.0-alpha.8";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -68,6 +68,7 @@ var providerConfigSchema = z.object({
|
|
|
68
68
|
type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
|
|
69
69
|
base_url: z.string().url(),
|
|
70
70
|
api_key_env_var: z.string().min(1).optional(),
|
|
71
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
71
72
|
headers: z.record(z.string()).optional(),
|
|
72
73
|
enabled: z.boolean().optional(),
|
|
73
74
|
metadata: z.record(z.unknown()).optional()
|
|
@@ -132,7 +133,9 @@ var projectConfigSchema = z.object({
|
|
|
132
133
|
memory: z.object({
|
|
133
134
|
provider: z.string().min(1).default("local"),
|
|
134
135
|
base_url: z.string().url().optional(),
|
|
135
|
-
api_key_env_var: z.string().min(1).optional()
|
|
136
|
+
api_key_env_var: z.string().min(1).optional(),
|
|
137
|
+
/** When true, memory failures fail tasks instead of being tolerated (§12). */
|
|
138
|
+
required: z.boolean().optional()
|
|
136
139
|
}).strict().optional()
|
|
137
140
|
}).strict();
|
|
138
141
|
var appConfigSchema = z.object({
|
|
@@ -214,7 +217,8 @@ function toAppConfig(yaml) {
|
|
|
214
217
|
memory: {
|
|
215
218
|
provider: yaml.project.memory.provider,
|
|
216
219
|
baseUrl: yaml.project.memory.base_url,
|
|
217
|
-
apiKeyEnvVar: yaml.project.memory.api_key_env_var
|
|
220
|
+
apiKeyEnvVar: yaml.project.memory.api_key_env_var,
|
|
221
|
+
required: yaml.project.memory.required
|
|
218
222
|
}
|
|
219
223
|
} : {}
|
|
220
224
|
},
|
|
@@ -245,6 +249,7 @@ function toProviderConfig(raw) {
|
|
|
245
249
|
type: raw.type,
|
|
246
250
|
baseUrl: raw.base_url,
|
|
247
251
|
apiKeyEnvVar: raw.api_key_env_var,
|
|
252
|
+
timeoutMs: raw.timeout_ms,
|
|
248
253
|
headers: raw.headers,
|
|
249
254
|
enabled: raw.enabled,
|
|
250
255
|
metadata: raw.metadata
|
|
@@ -546,6 +551,7 @@ function providerTransportError(context) {
|
|
|
546
551
|
}
|
|
547
552
|
|
|
548
553
|
// src/providers/openai-compatible/index.ts
|
|
554
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
549
555
|
var OpenAICompatibleProvider = class {
|
|
550
556
|
config;
|
|
551
557
|
providerId;
|
|
@@ -565,6 +571,10 @@ var OpenAICompatibleProvider = class {
|
|
|
565
571
|
};
|
|
566
572
|
this.log = log.child({ provider: config.name });
|
|
567
573
|
}
|
|
574
|
+
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
575
|
+
get requestTimeoutMs() {
|
|
576
|
+
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
577
|
+
}
|
|
568
578
|
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
569
579
|
async connect() {
|
|
570
580
|
authHeaders(this.config, this.env);
|
|
@@ -576,11 +586,17 @@ var OpenAICompatibleProvider = class {
|
|
|
576
586
|
const models = this.discovered ?? await this.connect();
|
|
577
587
|
return models.find((m) => m.id === modelId);
|
|
578
588
|
}
|
|
579
|
-
|
|
589
|
+
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
590
|
+
async rawModelsResponse() {
|
|
591
|
+
const response = await this.modelsRequest();
|
|
592
|
+
return response.body;
|
|
593
|
+
}
|
|
594
|
+
async modelsRequest() {
|
|
580
595
|
let response;
|
|
581
596
|
try {
|
|
582
597
|
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
583
|
-
headers: authHeaders(this.config, this.env)
|
|
598
|
+
headers: authHeaders(this.config, this.env),
|
|
599
|
+
timeoutMs: this.requestTimeoutMs
|
|
584
600
|
});
|
|
585
601
|
} catch (error) {
|
|
586
602
|
if (error instanceof ProviderError) throw error;
|
|
@@ -595,6 +611,10 @@ var OpenAICompatibleProvider = class {
|
|
|
595
611
|
body
|
|
596
612
|
});
|
|
597
613
|
}
|
|
614
|
+
return response;
|
|
615
|
+
}
|
|
616
|
+
async listModels() {
|
|
617
|
+
const body = await this.rawModelsResponse();
|
|
598
618
|
const entries = extractModelEntries(body, this.config);
|
|
599
619
|
const models = [];
|
|
600
620
|
let skipped = 0;
|
|
@@ -637,6 +657,10 @@ var OpenAICompatibleProvider = class {
|
|
|
637
657
|
}
|
|
638
658
|
};
|
|
639
659
|
}
|
|
660
|
+
/** Streaming where the gateway supports it (§11). */
|
|
661
|
+
stream(request, onChunk) {
|
|
662
|
+
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
663
|
+
}
|
|
640
664
|
async healthCheck() {
|
|
641
665
|
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
642
666
|
try {
|
|
@@ -671,6 +695,7 @@ var OpenAICompatibleProvider = class {
|
|
|
671
695
|
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
672
696
|
method: "POST",
|
|
673
697
|
headers: authHeaders(this.config, this.env),
|
|
698
|
+
timeoutMs: this.requestTimeoutMs,
|
|
674
699
|
body: {
|
|
675
700
|
model: request.model,
|
|
676
701
|
messages: request.messages,
|
|
@@ -715,6 +740,74 @@ var OpenAICompatibleProvider = class {
|
|
|
715
740
|
};
|
|
716
741
|
}
|
|
717
742
|
};
|
|
743
|
+
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
744
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
745
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
746
|
+
let response;
|
|
747
|
+
try {
|
|
748
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
749
|
+
method: "POST",
|
|
750
|
+
headers: {
|
|
751
|
+
accept: "text/event-stream",
|
|
752
|
+
"content-type": "application/json",
|
|
753
|
+
...authHeaders(config, env)
|
|
754
|
+
},
|
|
755
|
+
body: JSON.stringify({
|
|
756
|
+
model: request.model,
|
|
757
|
+
messages: request.messages,
|
|
758
|
+
stream: true,
|
|
759
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
760
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
761
|
+
}),
|
|
762
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
763
|
+
});
|
|
764
|
+
} catch (error) {
|
|
765
|
+
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
766
|
+
}
|
|
767
|
+
if (!response.ok) {
|
|
768
|
+
const body = await response.json().catch(() => void 0);
|
|
769
|
+
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
770
|
+
}
|
|
771
|
+
if (!response.body) {
|
|
772
|
+
throw providerHttpError({
|
|
773
|
+
provider: config.name,
|
|
774
|
+
operation: "stream",
|
|
775
|
+
status: 500,
|
|
776
|
+
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
let content = "";
|
|
780
|
+
let model = request.model;
|
|
781
|
+
let finishReason;
|
|
782
|
+
const decoder = new TextDecoder();
|
|
783
|
+
let buffer = "";
|
|
784
|
+
for await (const piece of response.body) {
|
|
785
|
+
buffer += decoder.decode(piece, { stream: true });
|
|
786
|
+
let newline = buffer.indexOf("\n");
|
|
787
|
+
while (newline >= 0) {
|
|
788
|
+
const line = buffer.slice(0, newline).trim();
|
|
789
|
+
buffer = buffer.slice(newline + 1);
|
|
790
|
+
newline = buffer.indexOf("\n");
|
|
791
|
+
if (line.length === 0 || line.startsWith(":")) continue;
|
|
792
|
+
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
793
|
+
if (data === "[DONE]") continue;
|
|
794
|
+
let event;
|
|
795
|
+
try {
|
|
796
|
+
event = JSON.parse(data);
|
|
797
|
+
} catch {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (typeof event.model === "string") model = event.model;
|
|
801
|
+
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
802
|
+
const first = choices[0];
|
|
803
|
+
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
804
|
+
if (delta.length > 0) content += delta;
|
|
805
|
+
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
806
|
+
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
810
|
+
}
|
|
718
811
|
function normalizeModalities(value) {
|
|
719
812
|
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
720
813
|
if (!list) return void 0;
|
|
@@ -748,10 +841,32 @@ function inferHealth(error) {
|
|
|
748
841
|
|
|
749
842
|
// src/providers/omnihilbras/index.ts
|
|
750
843
|
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
844
|
+
gateway;
|
|
845
|
+
async listModels() {
|
|
846
|
+
const raw = await this.rawModelsResponse();
|
|
847
|
+
await this.extractGatewayMetadata(raw);
|
|
848
|
+
const models = await super.listModels();
|
|
849
|
+
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
850
|
+
}
|
|
751
851
|
/**
|
|
752
852
|
* Run the full connect flow. Throws on authentication or transport failure
|
|
753
853
|
* (use healthCheck() for a non-throwing probe).
|
|
754
854
|
*/
|
|
855
|
+
/**
|
|
856
|
+
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
857
|
+
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
858
|
+
*/
|
|
859
|
+
gatewayMetadata() {
|
|
860
|
+
return this.gateway;
|
|
861
|
+
}
|
|
862
|
+
async extractGatewayMetadata(body) {
|
|
863
|
+
if (body !== null && typeof body === "object") {
|
|
864
|
+
const gateway = body.gateway;
|
|
865
|
+
if (gateway !== null && typeof gateway === "object") {
|
|
866
|
+
this.gateway = gateway;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
755
870
|
async connectAndRegister(registry) {
|
|
756
871
|
const apiKey = resolveApiKey(this.config);
|
|
757
872
|
const discovered = await this.listModels();
|
|
@@ -2377,6 +2492,23 @@ var FileAuditLog = class {
|
|
|
2377
2492
|
}
|
|
2378
2493
|
};
|
|
2379
2494
|
|
|
2495
|
+
// src/memory/base.ts
|
|
2496
|
+
function defaultSearch(provider, text, options = {}) {
|
|
2497
|
+
return provider.retrieve({
|
|
2498
|
+
text,
|
|
2499
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2500
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
function defaultMetadata(name, options = {}) {
|
|
2504
|
+
return {
|
|
2505
|
+
name,
|
|
2506
|
+
capabilities: options.capabilities ?? ["retrieve", "store", "search"],
|
|
2507
|
+
relevanceRanking: options.relevanceRanking ?? false,
|
|
2508
|
+
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2380
2512
|
// src/memory/local.ts
|
|
2381
2513
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2382
2514
|
"the",
|
|
@@ -2439,9 +2571,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
2439
2571
|
constructor(directory) {
|
|
2440
2572
|
super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
|
|
2441
2573
|
}
|
|
2442
|
-
|
|
2574
|
+
metadata() {
|
|
2575
|
+
return defaultMetadata(this.name, {
|
|
2576
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2577
|
+
relevanceRanking: true,
|
|
2578
|
+
backend: { kind: "json-file", deterministicScoring: true }
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
search(text, options = {}) {
|
|
2582
|
+
return defaultSearch(this, text, options);
|
|
2583
|
+
}
|
|
2584
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2585
|
+
query(query) {
|
|
2586
|
+
return this.retrieve(query);
|
|
2587
|
+
}
|
|
2588
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2589
|
+
write(entry2) {
|
|
2590
|
+
return this.store(entry2);
|
|
2591
|
+
}
|
|
2592
|
+
async retrieve(query) {
|
|
2443
2593
|
const entries = await this.readAll();
|
|
2444
|
-
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
|
|
2594
|
+
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
|
|
2445
2595
|
(entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
|
|
2446
2596
|
);
|
|
2447
2597
|
if (!query.text || query.text.trim().length === 0) {
|
|
@@ -2450,7 +2600,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
2450
2600
|
const queryKeywords = keywords(query.text);
|
|
2451
2601
|
return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
|
|
2452
2602
|
}
|
|
2453
|
-
|
|
2603
|
+
store(entry2) {
|
|
2454
2604
|
return this.mutate((entries) => {
|
|
2455
2605
|
const index = entries.findIndex((e) => e.key === entry2.key);
|
|
2456
2606
|
const stamped = {
|
|
@@ -2481,11 +2631,40 @@ var RememberaMemoryProvider = class {
|
|
|
2481
2631
|
this.env = config.env ?? process.env;
|
|
2482
2632
|
this.log = log.child({ component: "remembera" });
|
|
2483
2633
|
}
|
|
2634
|
+
get authenticationConfigured() {
|
|
2635
|
+
return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
|
|
2636
|
+
}
|
|
2484
2637
|
headers() {
|
|
2485
2638
|
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
2486
2639
|
return key ? { authorization: `Bearer ${key}` } : {};
|
|
2487
2640
|
}
|
|
2488
|
-
|
|
2641
|
+
/** Search delegates to the gateway's relevance query. */
|
|
2642
|
+
search(text, options = {}) {
|
|
2643
|
+
return defaultSearch(
|
|
2644
|
+
this,
|
|
2645
|
+
text,
|
|
2646
|
+
{
|
|
2647
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2648
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2649
|
+
}
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
metadata() {
|
|
2653
|
+
return defaultMetadata(this.name, {
|
|
2654
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2655
|
+
relevanceRanking: true,
|
|
2656
|
+
backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2660
|
+
query(query) {
|
|
2661
|
+
return this.retrieve(query);
|
|
2662
|
+
}
|
|
2663
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2664
|
+
write(entry2) {
|
|
2665
|
+
return this.store(entry2);
|
|
2666
|
+
}
|
|
2667
|
+
async retrieve(query) {
|
|
2489
2668
|
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
2490
2669
|
if (status !== 200) {
|
|
2491
2670
|
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
@@ -2496,7 +2675,7 @@ var RememberaMemoryProvider = class {
|
|
|
2496
2675
|
}
|
|
2497
2676
|
return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
|
|
2498
2677
|
}
|
|
2499
|
-
async
|
|
2678
|
+
async store(entry2) {
|
|
2500
2679
|
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
|
|
2501
2680
|
if (status !== 200 && status !== 201) {
|
|
2502
2681
|
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
@@ -2531,6 +2710,7 @@ var RememberaMemoryProvider = class {
|
|
|
2531
2710
|
content: entry2.content,
|
|
2532
2711
|
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
2533
2712
|
...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
|
|
2713
|
+
...typeof entry2.category === "string" ? { category: entry2.category } : {},
|
|
2534
2714
|
...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
|
|
2535
2715
|
...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
|
|
2536
2716
|
};
|
|
@@ -2561,57 +2741,54 @@ function createMemoryProvider(config) {
|
|
|
2561
2741
|
}
|
|
2562
2742
|
|
|
2563
2743
|
// src/memory/service.ts
|
|
2744
|
+
var DEFAULT_ENTRY_LIMIT = 8;
|
|
2564
2745
|
var MAX_CONTEXT_CHARS = 4e3;
|
|
2565
2746
|
var MemoryService = class {
|
|
2566
|
-
constructor(provider, log = logger) {
|
|
2747
|
+
constructor(provider, log = logger, options = {}) {
|
|
2567
2748
|
this.provider = provider;
|
|
2568
2749
|
this.log = log.child({ component: "memory", provider: provider.name });
|
|
2750
|
+
this.required = options.required ?? false;
|
|
2751
|
+
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
2569
2752
|
}
|
|
2570
2753
|
provider;
|
|
2571
2754
|
log;
|
|
2755
|
+
required;
|
|
2756
|
+
maxContextChars;
|
|
2572
2757
|
get providerRef() {
|
|
2573
2758
|
return this.provider;
|
|
2574
2759
|
}
|
|
2575
2760
|
get providerName() {
|
|
2576
2761
|
return this.provider.name;
|
|
2577
2762
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2763
|
+
get isRequired() {
|
|
2764
|
+
return this.required;
|
|
2765
|
+
}
|
|
2766
|
+
/** Structured, budgeted context for a task (§12 — avoid loading all memory). */
|
|
2767
|
+
async contextFor(query) {
|
|
2768
|
+
let entries;
|
|
2580
2769
|
try {
|
|
2581
|
-
|
|
2770
|
+
entries = await this.provider.retrieve({
|
|
2582
2771
|
text: query.objective,
|
|
2583
2772
|
...query.role ? { tags: [query.role] } : {},
|
|
2584
|
-
limit:
|
|
2773
|
+
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
2585
2774
|
});
|
|
2586
|
-
if (entries.length === 0) return "";
|
|
2587
|
-
const byScope = /* @__PURE__ */ new Map();
|
|
2588
|
-
for (const entry2 of entries) {
|
|
2589
|
-
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
2590
|
-
}
|
|
2591
|
-
const parts = [];
|
|
2592
|
-
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
2593
|
-
const scopeEntries = byScope.get(scope) ?? [];
|
|
2594
|
-
if (scopeEntries.length === 0) continue;
|
|
2595
|
-
const lines = scopeEntries.map((entry2) => `- ${entry2.content}`).join("\n");
|
|
2596
|
-
parts.push(`### Relevant memory (${scope})
|
|
2597
|
-
${lines}`);
|
|
2598
|
-
}
|
|
2599
|
-
const context = parts.join("\n\n");
|
|
2600
|
-
return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
|
|
2601
2775
|
} catch (error) {
|
|
2602
|
-
this.
|
|
2603
|
-
`Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
|
|
2604
|
-
);
|
|
2605
|
-
return "";
|
|
2776
|
+
return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
|
|
2606
2777
|
}
|
|
2778
|
+
return this.format(entries);
|
|
2607
2779
|
}
|
|
2608
|
-
/**
|
|
2780
|
+
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
2781
|
+
async gatherContext(query) {
|
|
2782
|
+
return (await this.contextFor(query)).text;
|
|
2783
|
+
}
|
|
2784
|
+
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
2609
2785
|
async recordTaskOutcome(task) {
|
|
2610
2786
|
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
2611
2787
|
try {
|
|
2612
|
-
await this.provider.
|
|
2788
|
+
await this.provider.store({
|
|
2613
2789
|
key: `task:${task.id}`,
|
|
2614
2790
|
scope: "task",
|
|
2791
|
+
category: "tasks",
|
|
2615
2792
|
content: `Objective: ${task.objective}
|
|
2616
2793
|
Status: ${task.status}
|
|
2617
2794
|
Outcome: ${outcome}`,
|
|
@@ -2622,18 +2799,53 @@ Outcome: ${outcome}`,
|
|
|
2622
2799
|
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
2623
2800
|
);
|
|
2624
2801
|
for (const { report, finding } of significant) {
|
|
2625
|
-
await this.provider.
|
|
2802
|
+
await this.provider.store({
|
|
2626
2803
|
key: `finding:${report.id}:${finding.id}`,
|
|
2627
2804
|
scope: "project",
|
|
2805
|
+
category: "problems",
|
|
2628
2806
|
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
2629
2807
|
tags: ["known-problem", report.agent]
|
|
2630
2808
|
});
|
|
2631
2809
|
}
|
|
2632
2810
|
} catch (error) {
|
|
2633
|
-
this.
|
|
2634
|
-
|
|
2811
|
+
this.handleFailure("store", error, void 0);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
format(entries) {
|
|
2815
|
+
if (entries.length === 0) return { text: "", entries, truncated: false };
|
|
2816
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
2817
|
+
for (const entry2 of entries) {
|
|
2818
|
+
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
2819
|
+
}
|
|
2820
|
+
const parts = [];
|
|
2821
|
+
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
2822
|
+
const scopeEntries = byScope.get(scope) ?? [];
|
|
2823
|
+
if (scopeEntries.length === 0) continue;
|
|
2824
|
+
const lines = scopeEntries.map((entry2) => {
|
|
2825
|
+
const category = entry2.category ? `[${entry2.category}] ` : "";
|
|
2826
|
+
return `- ${category}${entry2.content}`;
|
|
2827
|
+
}).join("\n");
|
|
2828
|
+
parts.push(`### Relevant memory (${scope})
|
|
2829
|
+
${lines}`);
|
|
2830
|
+
}
|
|
2831
|
+
let text = parts.join("\n\n");
|
|
2832
|
+
const truncated = text.length > this.maxContextChars;
|
|
2833
|
+
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
2834
|
+
return { text, entries, truncated };
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Failure policy (§12): continue unless memory is explicitly required.
|
|
2838
|
+
*/
|
|
2839
|
+
handleFailure(operation, error, fallback) {
|
|
2840
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2841
|
+
if (this.required) {
|
|
2842
|
+
throw new MemoryError(
|
|
2843
|
+
`Memory is marked required, but ${operation} failed: ${message}`,
|
|
2844
|
+
{ cause: error }
|
|
2635
2845
|
);
|
|
2636
2846
|
}
|
|
2847
|
+
this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
|
|
2848
|
+
return fallback ?? { text: "", entries: [], truncated: false };
|
|
2637
2849
|
}
|
|
2638
2850
|
};
|
|
2639
2851
|
|
|
@@ -2667,7 +2879,11 @@ function createTaskEngine(config, log = logger, directory) {
|
|
|
2667
2879
|
agents,
|
|
2668
2880
|
roles,
|
|
2669
2881
|
store: new FileTaskStore(directory),
|
|
2670
|
-
...config.project.memory !== void 0 ? {
|
|
2882
|
+
...config.project.memory !== void 0 ? {
|
|
2883
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
2884
|
+
required: config.project.memory.required
|
|
2885
|
+
})
|
|
2886
|
+
} : {},
|
|
2671
2887
|
audit: new FileAuditLog(),
|
|
2672
2888
|
log
|
|
2673
2889
|
});
|
|
@@ -3943,7 +4159,11 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
3943
4159
|
agents,
|
|
3944
4160
|
roles,
|
|
3945
4161
|
store: new FileTaskStore(),
|
|
3946
|
-
...config.project.memory !== void 0 ? {
|
|
4162
|
+
...config.project.memory !== void 0 ? {
|
|
4163
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
4164
|
+
required: config.project.memory.required
|
|
4165
|
+
})
|
|
4166
|
+
} : {},
|
|
3947
4167
|
audit: new FileAuditLog(),
|
|
3948
4168
|
log
|
|
3949
4169
|
});
|