agent-working-memory 0.15.9 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +36 -0
- package/README.md +24 -12
- package/dist/adapters/common.d.ts +1 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +33 -20
- package/dist/adapters/common.js.map +1 -1
- package/dist/core/embeddings.d.ts +1 -1
- package/dist/core/embeddings.js +1 -1
- package/dist/core/entity-extract.js +1 -1
- package/dist/core/query-expander.d.ts +4 -2
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +4 -2
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/recall-config.d.ts +1 -1
- package/dist/core/recall-config.d.ts.map +1 -1
- package/dist/core/recall-config.js +6 -1
- package/dist/core/recall-config.js.map +1 -1
- package/dist/core/recall-telemetry.d.ts +15 -0
- package/dist/core/recall-telemetry.d.ts.map +1 -0
- package/dist/core/recall-telemetry.js +108 -0
- package/dist/core/recall-telemetry.js.map +1 -0
- package/dist/core/rerank-window.d.ts +5 -4
- package/dist/core/rerank-window.d.ts.map +1 -1
- package/dist/core/rerank-window.js +5 -4
- package/dist/core/rerank-window.js.map +1 -1
- package/dist/core/reranker.d.ts +2 -1
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +33 -5
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/salience.js +1 -1
- package/dist/core/salience.js.map +1 -1
- package/dist/core/token-budget.d.ts +2 -2
- package/dist/core/token-budget.js +2 -2
- package/dist/core/whoami.d.ts +1 -1
- package/dist/engine/activation.d.ts +30 -13
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +159 -50
- package/dist/engine/activation.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +98 -98
- package/dist/mcp.js.map +1 -1
- package/dist/storage/pglite.d.ts +28 -0
- package/dist/storage/pglite.d.ts.map +1 -1
- package/dist/storage/pglite.js +55 -0
- package/dist/storage/pglite.js.map +1 -1
- package/dist/storage/postgres.d.ts +28 -0
- package/dist/storage/postgres.d.ts.map +1 -1
- package/dist/storage/postgres.js +55 -0
- package/dist/storage/postgres.js.map +1 -1
- package/dist/storage/sqlite.d.ts +28 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +56 -0
- package/dist/storage/sqlite.js.map +1 -1
- package/llms.txt +437 -0
- package/package.json +106 -100
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +1 -1
- package/plugin/bin/awm-mcp-launcher.cjs +2 -2
- package/plugin/skills/awm-memory/SKILL.md +33 -20
- package/src/adapters/common.ts +33 -20
- package/src/core/embeddings.ts +1 -1
- package/src/core/entity-extract.ts +1 -1
- package/src/core/query-expander.ts +4 -2
- package/src/core/recall-config.ts +6 -1
- package/src/core/recall-telemetry.ts +131 -0
- package/src/core/rerank-window.ts +159 -158
- package/src/core/reranker.ts +150 -121
- package/src/core/salience.ts +1 -1
- package/src/core/token-budget.ts +160 -160
- package/src/core/whoami.ts +120 -120
- package/src/engine/activation.ts +159 -51
- package/src/index.ts +1 -1
- package/src/mcp.ts +1475 -1475
- package/src/storage/pglite.ts +49 -0
- package/src/storage/postgres.ts +49 -0
- package/src/storage/sqlite.ts +51 -0
package/src/core/token-budget.ts
CHANGED
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Token budgeting for memory_recall (0.13.3).
|
|
5
|
-
*
|
|
6
|
-
* WHY THIS EXISTS
|
|
7
|
-
* ---------------
|
|
8
|
-
* `limit: N` is a COUNT, which is token-blind: a 5-result recall can cost 400
|
|
9
|
-
* tokens or 4,000 depending on how long the memories happen to be. AWM's own
|
|
10
|
-
* benchmark shows the problem — in real sessions AWM retrieval averaged 4,514
|
|
11
|
-
* tokens per call against 2,106 for plain file retrieval. AWM wins 9.8:1 in
|
|
12
|
-
* AGGREGATE (it needs far fewer calls) but loses 2.1:1 PER CALL. Per-call cost
|
|
13
|
-
* is the exposed flank, and a caller had no way to bound it.
|
|
14
|
-
*
|
|
15
|
-
* `max_tokens` lets the caller say "I have 800 tokens of context to spare"
|
|
16
|
-
* instead of guessing at a result count.
|
|
17
|
-
*
|
|
18
|
-
* PACKING STRATEGY
|
|
19
|
-
* ----------------
|
|
20
|
-
* This is a 0/1 knapsack (maximise score within a token budget), so we use the
|
|
21
|
-
* standard greedy density heuristic — order by score-per-token — with one
|
|
22
|
-
* deliberate exception: the top-scored result is always admitted first if it
|
|
23
|
-
* fits at all. Density packing alone can drop the single most relevant memory
|
|
24
|
-
* purely for being long, which is exactly the answer the caller wanted.
|
|
25
|
-
*
|
|
26
|
-
* Selection is by density; OUTPUT is restored to score order, because a reader
|
|
27
|
-
* scanning results expects the best one first.
|
|
28
|
-
*
|
|
29
|
-
* ESTIMATOR
|
|
30
|
-
* ---------
|
|
31
|
-
* Deliberately dependency-free: no tokenizer, no model load, no async. It
|
|
32
|
-
* takes max(words x 1.3, chars / 4) — the two standard English/code
|
|
33
|
-
* approximations — and takes the larger so the estimate errs toward
|
|
34
|
-
* OVER-counting. Over-counting keeps us inside the budget; under-counting
|
|
35
|
-
* would silently blow it, which is the failure that matters.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import type { ActivationResult } from '../types/engram.js';
|
|
39
|
-
|
|
40
|
-
/** Rough token count. Over-estimates by design — see the note above. */
|
|
41
|
-
export function estimateTokens(text: string): number {
|
|
42
|
-
if (!text) return 0;
|
|
43
|
-
const chars = text.length;
|
|
44
|
-
const words = text.split(/\s+/).filter(Boolean).length;
|
|
45
|
-
return Math.max(Math.ceil(words * 1.3), Math.ceil(chars / 4));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface PackedRecall {
|
|
49
|
-
/** Formatted lines that fit the budget, in score order. */
|
|
50
|
-
lines: string[];
|
|
51
|
-
/** How many results were kept. */
|
|
52
|
-
kept: number;
|
|
53
|
-
/** How many results were available before budgeting. */
|
|
54
|
-
total: number;
|
|
55
|
-
/** Estimated tokens of the kept lines. */
|
|
56
|
-
tokens: number;
|
|
57
|
-
/** Estimated tokens of everything dropped (0 when nothing was dropped). */
|
|
58
|
-
withheldTokens: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Select and format results to fit `maxTokens`.
|
|
63
|
-
*
|
|
64
|
-
* When `maxTokens` is undefined the
|
|
65
|
-
* everything is returned — but the accounting fields are still populated, so
|
|
66
|
-
* callers get per-call token visibility without opting into budgeting.
|
|
67
|
-
*/
|
|
68
|
-
export function packRecallByBudget(
|
|
69
|
-
results: ActivationResult[],
|
|
70
|
-
format: (r: ActivationResult, index: number) => string,
|
|
71
|
-
maxTokens?: number,
|
|
72
|
-
/**
|
|
73
|
-
* Tokens the caller will spend on the SAME reply outside the result lines —
|
|
74
|
-
* the accounting footer, and any peer-decisions suffix. Reserved up front so
|
|
75
|
-
* the budget bounds the whole reply rather than just the part this function
|
|
76
|
-
* happens to build.
|
|
77
|
-
*
|
|
78
|
-
* Found by the end-to-end eval, not by the unit tests: budgets of 600/250/80
|
|
79
|
-
* came back as 601/256/95 because the footer spent tokens it never counted.
|
|
80
|
-
*/
|
|
81
|
-
reservedTokens = 0,
|
|
82
|
-
): PackedRecall {
|
|
83
|
-
const total = results.length;
|
|
84
|
-
if (total === 0) return { lines: [], kept: 0, total: 0, tokens: 0, withheldTokens: 0 };
|
|
85
|
-
const effectiveBudget = maxTokens === undefined ? undefined
|
|
86
|
-
: Math.max(0, maxTokens - reservedTokens);
|
|
87
|
-
|
|
88
|
-
// Format once against the ORIGINAL index so displayed numbering matches the
|
|
89
|
-
// caller's mental model of "result 1 is the best match".
|
|
90
|
-
const measured = results.map((r, i) => {
|
|
91
|
-
const line = format(r, i);
|
|
92
|
-
return { line, tokens: estimateTokens(line), score: r.score, order: i };
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const allTokens = measured.reduce((n, m) => n + m.tokens, 0);
|
|
96
|
-
|
|
97
|
-
// NOTE the asymmetry, which a unit test caught: `maxTokens` being absent or
|
|
98
|
-
// nonsense (<= 0) means "no budget — return everything", but an
|
|
99
|
-
// *effectiveBudget* of 0 means the reserve ate the entire budget, which must
|
|
100
|
-
// admit NOTHING. Collapsing those two cases returned the full result set on
|
|
101
|
-
// the tightest budgets — the exact opposite of what was asked for.
|
|
102
|
-
const unbudgeted = maxTokens === undefined || maxTokens <= 0;
|
|
103
|
-
if (unbudgeted || allTokens <= effectiveBudget!) {
|
|
104
|
-
return {
|
|
105
|
-
lines: measured.map(m => m.line),
|
|
106
|
-
kept: total,
|
|
107
|
-
total,
|
|
108
|
-
tokens: allTokens,
|
|
109
|
-
withheldTokens: 0,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const chosen: typeof measured = [];
|
|
114
|
-
let spent = 0;
|
|
115
|
-
|
|
116
|
-
// 1. Top-scored result gets first refusal, so a long best-match is never
|
|
117
|
-
// dropped in favour of several short weak ones.
|
|
118
|
-
const top = measured[0];
|
|
119
|
-
if (top.tokens <= effectiveBudget!) {
|
|
120
|
-
chosen.push(top);
|
|
121
|
-
spent += top.tokens;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 2. Everything else by value density (score per token), greedily.
|
|
125
|
-
const rest = measured
|
|
126
|
-
.filter(m => m !== top)
|
|
127
|
-
.sort((a, b) => (b.score / Math.max(b.tokens, 1)) - (a.score / Math.max(a.tokens, 1)));
|
|
128
|
-
|
|
129
|
-
for (const m of rest) {
|
|
130
|
-
if (spent + m.tokens > effectiveBudget!) continue; // skip, don't stop: a later
|
|
131
|
-
chosen.push(m); // shorter result may still fit
|
|
132
|
-
spent += m.tokens;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// 3. Restore score order for display.
|
|
136
|
-
chosen.sort((a, b) => a.order - b.order);
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
lines: chosen.map(m => m.line),
|
|
140
|
-
kept: chosen.length,
|
|
141
|
-
total,
|
|
142
|
-
tokens: spent,
|
|
143
|
-
withheldTokens: allTokens - spent,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* One-line accounting footer. Costs ~15-25 tokens to report, which is a good
|
|
149
|
-
* trade against a recall that can run to thousands — and it is what makes the
|
|
150
|
-
* per-call cost visible to both the human and the harness. Without this,
|
|
151
|
-
* AWM's token
|
|
152
|
-
*/
|
|
153
|
-
export function formatTokenFooter(p: PackedRecall, maxTokens?: number): string {
|
|
154
|
-
if (p.total === 0) return '';
|
|
155
|
-
const parts = [`~${p.tokens} tok`];
|
|
156
|
-
parts.push(p.kept === p.total ? `${p.total} results` : `${p.kept}/${p.total} results`);
|
|
157
|
-
if (maxTokens !== undefined && maxTokens > 0) parts.push(`budget ${maxTokens}`);
|
|
158
|
-
if (p.withheldTokens > 0) parts.push(`~${p.withheldTokens} tok withheld`);
|
|
159
|
-
return `\n\n[awm: ${parts.join(' · ')}]`;
|
|
160
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Token budgeting for memory_recall (0.13.3).
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS
|
|
7
|
+
* ---------------
|
|
8
|
+
* `limit: N` is a COUNT, which is token-blind: a 5-result recall can cost 400
|
|
9
|
+
* tokens or 4,000 depending on how long the memories happen to be. AWM's own
|
|
10
|
+
* benchmark shows the problem — in real sessions AWM retrieval averaged 4,514
|
|
11
|
+
* tokens per call against 2,106 for plain file retrieval. AWM wins 9.8:1 in
|
|
12
|
+
* AGGREGATE (it needs far fewer calls) but loses 2.1:1 PER CALL. Per-call cost
|
|
13
|
+
* is the exposed flank, and a caller had no way to bound it.
|
|
14
|
+
*
|
|
15
|
+
* `max_tokens` lets the caller say "I have 800 tokens of context to spare"
|
|
16
|
+
* instead of guessing at a result count.
|
|
17
|
+
*
|
|
18
|
+
* PACKING STRATEGY
|
|
19
|
+
* ----------------
|
|
20
|
+
* This is a 0/1 knapsack (maximise score within a token budget), so we use the
|
|
21
|
+
* standard greedy density heuristic — order by score-per-token — with one
|
|
22
|
+
* deliberate exception: the top-scored result is always admitted first if it
|
|
23
|
+
* fits at all. Density packing alone can drop the single most relevant memory
|
|
24
|
+
* purely for being long, which is exactly the answer the caller wanted.
|
|
25
|
+
*
|
|
26
|
+
* Selection is by density; OUTPUT is restored to score order, because a reader
|
|
27
|
+
* scanning results expects the best one first.
|
|
28
|
+
*
|
|
29
|
+
* ESTIMATOR
|
|
30
|
+
* ---------
|
|
31
|
+
* Deliberately dependency-free: no tokenizer, no model load, no async. It
|
|
32
|
+
* takes max(words x 1.3, chars / 4) — the two standard English/code
|
|
33
|
+
* approximations — and takes the larger so the estimate errs toward
|
|
34
|
+
* OVER-counting. Over-counting keeps us inside the budget; under-counting
|
|
35
|
+
* would silently blow it, which is the failure that matters.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import type { ActivationResult } from '../types/engram.js';
|
|
39
|
+
|
|
40
|
+
/** Rough token count. Over-estimates by design — see the note above. */
|
|
41
|
+
export function estimateTokens(text: string): number {
|
|
42
|
+
if (!text) return 0;
|
|
43
|
+
const chars = text.length;
|
|
44
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
45
|
+
return Math.max(Math.ceil(words * 1.3), Math.ceil(chars / 4));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PackedRecall {
|
|
49
|
+
/** Formatted lines that fit the budget, in score order. */
|
|
50
|
+
lines: string[];
|
|
51
|
+
/** How many results were kept. */
|
|
52
|
+
kept: number;
|
|
53
|
+
/** How many results were available before budgeting. */
|
|
54
|
+
total: number;
|
|
55
|
+
/** Estimated tokens of the kept lines. */
|
|
56
|
+
tokens: number;
|
|
57
|
+
/** Estimated tokens of everything dropped (0 when nothing was dropped). */
|
|
58
|
+
withheldTokens: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Select and format results to fit `maxTokens`.
|
|
63
|
+
*
|
|
64
|
+
* When `maxTokens` is undefined the behavior is unchanged from pre-0.13.3 —
|
|
65
|
+
* everything is returned — but the accounting fields are still populated, so
|
|
66
|
+
* callers get per-call token visibility without opting into budgeting.
|
|
67
|
+
*/
|
|
68
|
+
export function packRecallByBudget(
|
|
69
|
+
results: ActivationResult[],
|
|
70
|
+
format: (r: ActivationResult, index: number) => string,
|
|
71
|
+
maxTokens?: number,
|
|
72
|
+
/**
|
|
73
|
+
* Tokens the caller will spend on the SAME reply outside the result lines —
|
|
74
|
+
* the accounting footer, and any peer-decisions suffix. Reserved up front so
|
|
75
|
+
* the budget bounds the whole reply rather than just the part this function
|
|
76
|
+
* happens to build.
|
|
77
|
+
*
|
|
78
|
+
* Found by the end-to-end eval, not by the unit tests: budgets of 600/250/80
|
|
79
|
+
* came back as 601/256/95 because the footer spent tokens it never counted.
|
|
80
|
+
*/
|
|
81
|
+
reservedTokens = 0,
|
|
82
|
+
): PackedRecall {
|
|
83
|
+
const total = results.length;
|
|
84
|
+
if (total === 0) return { lines: [], kept: 0, total: 0, tokens: 0, withheldTokens: 0 };
|
|
85
|
+
const effectiveBudget = maxTokens === undefined ? undefined
|
|
86
|
+
: Math.max(0, maxTokens - reservedTokens);
|
|
87
|
+
|
|
88
|
+
// Format once against the ORIGINAL index so displayed numbering matches the
|
|
89
|
+
// caller's mental model of "result 1 is the best match".
|
|
90
|
+
const measured = results.map((r, i) => {
|
|
91
|
+
const line = format(r, i);
|
|
92
|
+
return { line, tokens: estimateTokens(line), score: r.score, order: i };
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const allTokens = measured.reduce((n, m) => n + m.tokens, 0);
|
|
96
|
+
|
|
97
|
+
// NOTE the asymmetry, which a unit test caught: `maxTokens` being absent or
|
|
98
|
+
// nonsense (<= 0) means "no budget — return everything", but an
|
|
99
|
+
// *effectiveBudget* of 0 means the reserve ate the entire budget, which must
|
|
100
|
+
// admit NOTHING. Collapsing those two cases returned the full result set on
|
|
101
|
+
// the tightest budgets — the exact opposite of what was asked for.
|
|
102
|
+
const unbudgeted = maxTokens === undefined || maxTokens <= 0;
|
|
103
|
+
if (unbudgeted || allTokens <= effectiveBudget!) {
|
|
104
|
+
return {
|
|
105
|
+
lines: measured.map(m => m.line),
|
|
106
|
+
kept: total,
|
|
107
|
+
total,
|
|
108
|
+
tokens: allTokens,
|
|
109
|
+
withheldTokens: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const chosen: typeof measured = [];
|
|
114
|
+
let spent = 0;
|
|
115
|
+
|
|
116
|
+
// 1. Top-scored result gets first refusal, so a long best-match is never
|
|
117
|
+
// dropped in favour of several short weak ones.
|
|
118
|
+
const top = measured[0];
|
|
119
|
+
if (top.tokens <= effectiveBudget!) {
|
|
120
|
+
chosen.push(top);
|
|
121
|
+
spent += top.tokens;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. Everything else by value density (score per token), greedily.
|
|
125
|
+
const rest = measured
|
|
126
|
+
.filter(m => m !== top)
|
|
127
|
+
.sort((a, b) => (b.score / Math.max(b.tokens, 1)) - (a.score / Math.max(a.tokens, 1)));
|
|
128
|
+
|
|
129
|
+
for (const m of rest) {
|
|
130
|
+
if (spent + m.tokens > effectiveBudget!) continue; // skip, don't stop: a later
|
|
131
|
+
chosen.push(m); // shorter result may still fit
|
|
132
|
+
spent += m.tokens;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 3. Restore score order for display.
|
|
136
|
+
chosen.sort((a, b) => a.order - b.order);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
lines: chosen.map(m => m.line),
|
|
140
|
+
kept: chosen.length,
|
|
141
|
+
total,
|
|
142
|
+
tokens: spent,
|
|
143
|
+
withheldTokens: allTokens - spent,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* One-line accounting footer. Costs ~15-25 tokens to report, which is a good
|
|
149
|
+
* trade against a recall that can run to thousands — and it is what makes the
|
|
150
|
+
* per-call cost visible to both the human and the harness. Without this,
|
|
151
|
+
* AWM's token behavior is only observable in an offline benchmark.
|
|
152
|
+
*/
|
|
153
|
+
export function formatTokenFooter(p: PackedRecall, maxTokens?: number): string {
|
|
154
|
+
if (p.total === 0) return '';
|
|
155
|
+
const parts = [`~${p.tokens} tok`];
|
|
156
|
+
parts.push(p.kept === p.total ? `${p.total} results` : `${p.kept}/${p.total} results`);
|
|
157
|
+
if (maxTokens !== undefined && maxTokens > 0) parts.push(`budget ${maxTokens}`);
|
|
158
|
+
if (p.withheldTokens > 0) parts.push(`~${p.withheldTokens} tok withheld`);
|
|
159
|
+
return `\n\n[awm: ${parts.join(' · ')}]`;
|
|
160
|
+
}
|
package/src/core/whoami.ts
CHANGED
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Instance identity (D3, 2026-07-30).
|
|
6
|
-
*
|
|
7
|
-
* Answers "which AWM am I talking to?" — the recurring confusion class where
|
|
8
|
-
* sessions mix up the hosted multi-agent connector with the local instance,
|
|
9
|
-
* or run blind to sibling agent spaces in the same store (an evaluation once
|
|
10
|
-
* analyzed this project without seeing the `personal` space that held its
|
|
11
|
-
* design decisions).
|
|
12
|
-
*
|
|
13
|
-
* Exposed as the `memory_whoami` MCP tool and the `GET /whoami` HTTP route.
|
|
14
|
-
* Read-only; additive module.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { dirname, join, resolve } from 'node:path';
|
|
18
|
-
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import { VERSION } from '../version.js';
|
|
20
|
-
import { getConfiguredBackend, getConfiguredPath } from '../storage/factory.js';
|
|
21
|
-
import { activeRecallConfig, recallConfigFingerprint } from './recall-config.js';
|
|
22
|
-
import { embeddingHealth } from './embeddings.js';
|
|
23
|
-
|
|
24
|
-
export interface WhoamiInfo {
|
|
25
|
-
agentId: string;
|
|
26
|
-
workspace: string | null;
|
|
27
|
-
surface: 'mcp' | 'http';
|
|
28
|
-
mode: 'standalone' | 'hive';
|
|
29
|
-
backend: string;
|
|
30
|
-
storePath: string;
|
|
31
|
-
version: string;
|
|
32
|
-
codePath: string;
|
|
33
|
-
pid: number;
|
|
34
|
-
ports: { http: number | null; hookSidecar: number | null };
|
|
35
|
-
/**
|
|
36
|
-
* Effective recall configuration. `whoami` is the "what am I actually
|
|
37
|
-
* running" tool, and the version alone does not answer that: a build can be
|
|
38
|
-
* current while the recall flags that change its
|
|
39
|
-
* to something unintended. GET /health already reports this, but HTTP is off
|
|
40
|
-
* by default, so an MCP-only session had no way to see it.
|
|
41
|
-
*/
|
|
42
|
-
recall: { fingerprint: string; flags: Record<string, string> };
|
|
43
|
-
siblingAgents: string[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Store subset whoami needs — every backend implements listAgentIds. */
|
|
47
|
-
export interface WhoamiStore {
|
|
48
|
-
listAgentIds(): string[] | Promise<string[]>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function packageRoot(): string {
|
|
52
|
-
// src/core/whoami.ts (dev) and dist/core/whoami.js (build) both sit two
|
|
53
|
-
// levels below the package root.
|
|
54
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
55
|
-
return resolve(join(here, '..', '..'));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function buildWhoami(
|
|
59
|
-
store: WhoamiStore,
|
|
60
|
-
agentId: string,
|
|
61
|
-
surface: 'mcp' | 'http',
|
|
62
|
-
/**
|
|
63
|
-
* 0.14.2: the port the hook sidecar actually BOUND, from the running handle.
|
|
64
|
-
* Before this, whoami echoed AWM_HOOK_PORT from the environment — so a session
|
|
65
|
-
* whose sidecar had lost the port fight and disabled hooks still reported
|
|
66
|
-
* "hookSidecar=8401". Pass `null` for "tried and failed"; omit (undefined)
|
|
67
|
-
* for surfaces that have no sidecar, which falls back to the env value.
|
|
68
|
-
*/
|
|
69
|
-
boundHookPort?: number | null,
|
|
70
|
-
): Promise<WhoamiInfo> {
|
|
71
|
-
const coordination = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
|
|
72
|
-
let siblings: string[] = [];
|
|
73
|
-
try {
|
|
74
|
-
const all = await store.listAgentIds();
|
|
75
|
-
siblings = all.filter(a => a !== agentId).sort();
|
|
76
|
-
} catch { /* identity must never fail on a listing error */ }
|
|
77
|
-
const httpPort = Number(process.env.AWM_PORT ?? '');
|
|
78
|
-
const hookPort = Number(process.env.AWM_HOOK_PORT ?? '');
|
|
79
|
-
return {
|
|
80
|
-
agentId,
|
|
81
|
-
workspace: process.env.AWM_WORKSPACE ?? null,
|
|
82
|
-
surface,
|
|
83
|
-
mode: coordination ? 'hive' : 'standalone',
|
|
84
|
-
backend: getConfiguredBackend(),
|
|
85
|
-
storePath: getConfiguredPath(),
|
|
86
|
-
version: VERSION,
|
|
87
|
-
codePath: packageRoot(),
|
|
88
|
-
pid: process.pid,
|
|
89
|
-
ports: {
|
|
90
|
-
http: Number.isFinite(httpPort) && httpPort > 0 ? httpPort : null,
|
|
91
|
-
hookSidecar: boundHookPort !== undefined
|
|
92
|
-
? boundHookPort
|
|
93
|
-
: (Number.isFinite(hookPort) && hookPort > 0 ? hookPort : null),
|
|
94
|
-
},
|
|
95
|
-
recall: { fingerprint: recallConfigFingerprint(), flags: activeRecallConfig() },
|
|
96
|
-
siblingAgents: siblings,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Human-readable rendering for the MCP tool response. */
|
|
101
|
-
export function formatWhoami(w: WhoamiInfo): string {
|
|
102
|
-
return [
|
|
103
|
-
`Agent: ${w.agentId}${w.workspace ? ` (workspace: ${w.workspace})` : ''}`,
|
|
104
|
-
`Mode: ${w.mode} · Surface: ${w.surface} · Version: ${w.version}`,
|
|
105
|
-
`Backend: ${w.backend}`,
|
|
106
|
-
`Store: ${w.storePath}`,
|
|
107
|
-
`Code: ${w.codePath} (pid ${w.pid})`,
|
|
108
|
-
`Ports: http=${w.ports.http ?? 'off'} hookSidecar=${w.ports.hookSidecar ?? 'off'}`,
|
|
109
|
-
`Recall config: ${w.recall.fingerprint}`,
|
|
110
|
-
...(embeddingHealth().dimensionMismatches > 0
|
|
111
|
-
? [`\u26a0 EMBEDDING INTEGRITY: ${embeddingHealth().dimensionMismatches} dimension mismatches ` +
|
|
112
|
-
`(expected ${embeddingHealth().expectedDimensions}d). Affected memories score 0 on the ` +
|
|
113
|
-
`vector channel — the corpus is probably half-migrated. Re-embed the whole store or ` +
|
|
114
|
-
`revert AWM_EMBED_MODEL/AWM_EMBED_DIMS.`]
|
|
115
|
-
: []),
|
|
116
|
-
w.siblingAgents.length
|
|
117
|
-
? `Sibling agent spaces in this store: ${w.siblingAgents.join(', ')} — recall is scoped to '${w.agentId}'; other spaces need workspace recall, \`awm export --agent <id>\`, or a session configured for that agent.`
|
|
118
|
-
: 'Sibling agent spaces in this store: none',
|
|
119
|
-
].join('\n');
|
|
120
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Instance identity (D3, 2026-07-30).
|
|
6
|
+
*
|
|
7
|
+
* Answers "which AWM am I talking to?" — the recurring confusion class where
|
|
8
|
+
* sessions mix up the hosted multi-agent connector with the local instance,
|
|
9
|
+
* or run blind to sibling agent spaces in the same store (an evaluation once
|
|
10
|
+
* analyzed this project without seeing the `personal` space that held its
|
|
11
|
+
* design decisions).
|
|
12
|
+
*
|
|
13
|
+
* Exposed as the `memory_whoami` MCP tool and the `GET /whoami` HTTP route.
|
|
14
|
+
* Read-only; additive module.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { dirname, join, resolve } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { VERSION } from '../version.js';
|
|
20
|
+
import { getConfiguredBackend, getConfiguredPath } from '../storage/factory.js';
|
|
21
|
+
import { activeRecallConfig, recallConfigFingerprint } from './recall-config.js';
|
|
22
|
+
import { embeddingHealth } from './embeddings.js';
|
|
23
|
+
|
|
24
|
+
export interface WhoamiInfo {
|
|
25
|
+
agentId: string;
|
|
26
|
+
workspace: string | null;
|
|
27
|
+
surface: 'mcp' | 'http';
|
|
28
|
+
mode: 'standalone' | 'hive';
|
|
29
|
+
backend: string;
|
|
30
|
+
storePath: string;
|
|
31
|
+
version: string;
|
|
32
|
+
codePath: string;
|
|
33
|
+
pid: number;
|
|
34
|
+
ports: { http: number | null; hookSidecar: number | null };
|
|
35
|
+
/**
|
|
36
|
+
* Effective recall configuration. `whoami` is the "what am I actually
|
|
37
|
+
* running" tool, and the version alone does not answer that: a build can be
|
|
38
|
+
* current while the recall flags that change its behavior are unset, or set
|
|
39
|
+
* to something unintended. GET /health already reports this, but HTTP is off
|
|
40
|
+
* by default, so an MCP-only session had no way to see it.
|
|
41
|
+
*/
|
|
42
|
+
recall: { fingerprint: string; flags: Record<string, string> };
|
|
43
|
+
siblingAgents: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Store subset whoami needs — every backend implements listAgentIds. */
|
|
47
|
+
export interface WhoamiStore {
|
|
48
|
+
listAgentIds(): string[] | Promise<string[]>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function packageRoot(): string {
|
|
52
|
+
// src/core/whoami.ts (dev) and dist/core/whoami.js (build) both sit two
|
|
53
|
+
// levels below the package root.
|
|
54
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
return resolve(join(here, '..', '..'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function buildWhoami(
|
|
59
|
+
store: WhoamiStore,
|
|
60
|
+
agentId: string,
|
|
61
|
+
surface: 'mcp' | 'http',
|
|
62
|
+
/**
|
|
63
|
+
* 0.14.2: the port the hook sidecar actually BOUND, from the running handle.
|
|
64
|
+
* Before this, whoami echoed AWM_HOOK_PORT from the environment — so a session
|
|
65
|
+
* whose sidecar had lost the port fight and disabled hooks still reported
|
|
66
|
+
* "hookSidecar=8401". Pass `null` for "tried and failed"; omit (undefined)
|
|
67
|
+
* for surfaces that have no sidecar, which falls back to the env value.
|
|
68
|
+
*/
|
|
69
|
+
boundHookPort?: number | null,
|
|
70
|
+
): Promise<WhoamiInfo> {
|
|
71
|
+
const coordination = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
|
|
72
|
+
let siblings: string[] = [];
|
|
73
|
+
try {
|
|
74
|
+
const all = await store.listAgentIds();
|
|
75
|
+
siblings = all.filter(a => a !== agentId).sort();
|
|
76
|
+
} catch { /* identity must never fail on a listing error */ }
|
|
77
|
+
const httpPort = Number(process.env.AWM_PORT ?? '');
|
|
78
|
+
const hookPort = Number(process.env.AWM_HOOK_PORT ?? '');
|
|
79
|
+
return {
|
|
80
|
+
agentId,
|
|
81
|
+
workspace: process.env.AWM_WORKSPACE ?? null,
|
|
82
|
+
surface,
|
|
83
|
+
mode: coordination ? 'hive' : 'standalone',
|
|
84
|
+
backend: getConfiguredBackend(),
|
|
85
|
+
storePath: getConfiguredPath(),
|
|
86
|
+
version: VERSION,
|
|
87
|
+
codePath: packageRoot(),
|
|
88
|
+
pid: process.pid,
|
|
89
|
+
ports: {
|
|
90
|
+
http: Number.isFinite(httpPort) && httpPort > 0 ? httpPort : null,
|
|
91
|
+
hookSidecar: boundHookPort !== undefined
|
|
92
|
+
? boundHookPort
|
|
93
|
+
: (Number.isFinite(hookPort) && hookPort > 0 ? hookPort : null),
|
|
94
|
+
},
|
|
95
|
+
recall: { fingerprint: recallConfigFingerprint(), flags: activeRecallConfig() },
|
|
96
|
+
siblingAgents: siblings,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Human-readable rendering for the MCP tool response. */
|
|
101
|
+
export function formatWhoami(w: WhoamiInfo): string {
|
|
102
|
+
return [
|
|
103
|
+
`Agent: ${w.agentId}${w.workspace ? ` (workspace: ${w.workspace})` : ''}`,
|
|
104
|
+
`Mode: ${w.mode} · Surface: ${w.surface} · Version: ${w.version}`,
|
|
105
|
+
`Backend: ${w.backend}`,
|
|
106
|
+
`Store: ${w.storePath}`,
|
|
107
|
+
`Code: ${w.codePath} (pid ${w.pid})`,
|
|
108
|
+
`Ports: http=${w.ports.http ?? 'off'} hookSidecar=${w.ports.hookSidecar ?? 'off'}`,
|
|
109
|
+
`Recall config: ${w.recall.fingerprint}`,
|
|
110
|
+
...(embeddingHealth().dimensionMismatches > 0
|
|
111
|
+
? [`\u26a0 EMBEDDING INTEGRITY: ${embeddingHealth().dimensionMismatches} dimension mismatches ` +
|
|
112
|
+
`(expected ${embeddingHealth().expectedDimensions}d). Affected memories score 0 on the ` +
|
|
113
|
+
`vector channel — the corpus is probably half-migrated. Re-embed the whole store or ` +
|
|
114
|
+
`revert AWM_EMBED_MODEL/AWM_EMBED_DIMS.`]
|
|
115
|
+
: []),
|
|
116
|
+
w.siblingAgents.length
|
|
117
|
+
? `Sibling agent spaces in this store: ${w.siblingAgents.join(', ')} — recall is scoped to '${w.agentId}'; other spaces need workspace recall, \`awm export --agent <id>\`, or a session configured for that agent.`
|
|
118
|
+
: 'Sibling agent spaces in this store: none',
|
|
119
|
+
].join('\n');
|
|
120
|
+
}
|