@noetic-tools/context 1.0.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/LICENSE +201 -0
- package/README.md +28 -0
- package/dist/context/budget.d.ts +39 -0
- package/dist/context/budget.js +183 -0
- package/dist/context/exec-context-factory.d.ts +24 -0
- package/dist/context/exec-context-factory.js +30 -0
- package/dist/context/function-call-utils.d.ts +6 -0
- package/dist/context/function-call-utils.js +27 -0
- package/dist/context/layer-api.d.ts +11 -0
- package/dist/context/layer-api.js +91 -0
- package/dist/context/layer-lifecycle.d.ts +239 -0
- package/dist/context/layer-lifecycle.js +800 -0
- package/dist/context/layer-provides.d.ts +32 -0
- package/dist/context/layer-provides.js +33 -0
- package/dist/context/layer-usage.d.ts +13 -0
- package/dist/context/layer-usage.js +54 -0
- package/dist/context/layers/durable-task-state.d.ts +107 -0
- package/dist/context/layers/durable-task-state.js +250 -0
- package/dist/context/layers/file-reference.d.ts +55 -0
- package/dist/context/layers/file-reference.js +729 -0
- package/dist/context/layers/history-window.d.ts +31 -0
- package/dist/context/layers/history-window.js +150 -0
- package/dist/context/layers/observational-context.d.ts +56 -0
- package/dist/context/layers/observational-context.js +152 -0
- package/dist/context/layers/plan-context.d.ts +73 -0
- package/dist/context/layers/plan-context.js +749 -0
- package/dist/context/layers/static-content.d.ts +27 -0
- package/dist/context/layers/static-content.js +46 -0
- package/dist/context/layers/steering.d.ts +50 -0
- package/dist/context/layers/steering.js +291 -0
- package/dist/context/layers/temporal-context.d.ts +91 -0
- package/dist/context/layers/temporal-context.js +327 -0
- package/dist/context/layers/tool-context-layer.d.ts +15 -0
- package/dist/context/layers/tool-context-layer.js +43 -0
- package/dist/context/layers/working-memory-context.d.ts +46 -0
- package/dist/context/layers/working-memory-context.js +155 -0
- package/dist/context/projector.d.ts +19 -0
- package/dist/context/projector.js +89 -0
- package/dist/context/scope.d.ts +3 -0
- package/dist/context/scope.js +42 -0
- package/dist/context/storage-batch.d.ts +18 -0
- package/dist/context/storage-batch.js +35 -0
- package/dist/context/strip-unresolved.d.ts +13 -0
- package/dist/context/strip-unresolved.js +35 -0
- package/dist/deprecated.d.ts +43 -0
- package/dist/deprecated.js +28 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +31 -0
- package/package.json +63 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ContextScope } from '@noetic-tools/types';
|
|
2
|
+
interface StaticContentOpts {
|
|
3
|
+
id?: string;
|
|
4
|
+
slot?: number;
|
|
5
|
+
scope?: ContextScope;
|
|
6
|
+
load: () => Promise<string>;
|
|
7
|
+
tag?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates a read-only context layer that loads static content once at init and injects it into recall.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
* @param opts - Configuration with a `load` function and optional id, slot, scope, and XML tag name.
|
|
14
|
+
* @returns A `ContextLayer` that provides static instructional content to the model.
|
|
15
|
+
*/
|
|
16
|
+
export declare function staticContent(opts: StaticContentOpts): {
|
|
17
|
+
id: string;
|
|
18
|
+
slot: number;
|
|
19
|
+
scope: ContextScope;
|
|
20
|
+
hooks: {
|
|
21
|
+
init(): Promise<{
|
|
22
|
+
state: string;
|
|
23
|
+
}>;
|
|
24
|
+
recall({ state, budget }: import("@noetic-tools/types").RecallParams<string>): Promise<string | null>;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { estimateTokens, Slot } from '@noetic-tools/types';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a read-only context layer that loads static content once at init and injects it into recall.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
* @param opts - Configuration with a `load` function and optional id, slot, scope, and XML tag name.
|
|
7
|
+
* @returns A `ContextLayer` that provides static instructional content to the model.
|
|
8
|
+
*/
|
|
9
|
+
export function staticContent(opts) {
|
|
10
|
+
const tag = opts.tag ?? 'instructions';
|
|
11
|
+
return {
|
|
12
|
+
id: opts.id ?? 'static-content',
|
|
13
|
+
slot: opts.slot ?? Slot.WORKING_MEMORY + 5,
|
|
14
|
+
scope: opts.scope ?? 'resource',
|
|
15
|
+
hooks: {
|
|
16
|
+
async init() {
|
|
17
|
+
const raw = await opts.load();
|
|
18
|
+
if (!raw) {
|
|
19
|
+
return {
|
|
20
|
+
state: '',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
state: `<${tag}>\n${raw}\n</${tag}>`,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
async recall({ state, budget }) {
|
|
28
|
+
if (!state) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
// `budget > 0` is a deliberate fail-open guard: a zero allocation must
|
|
32
|
+
// not delete the instructions (slice(0, 0) would silently drop the
|
|
33
|
+
// whole layer from the view).
|
|
34
|
+
if (budget > 0 && estimateTokens(state) > budget) {
|
|
35
|
+
// Trim so the recalled text fits the token budget (~4 chars/token),
|
|
36
|
+
// reserving room for the closing tag so the block stays well-formed
|
|
37
|
+
// XML instead of being cut mid-tag/mid-sentence.
|
|
38
|
+
const closing = `\n</${tag}>`;
|
|
39
|
+
const maxChars = Math.max(0, budget * 4 - closing.length);
|
|
40
|
+
return `${state.slice(0, maxChars)}${closing}`;
|
|
41
|
+
}
|
|
42
|
+
return state;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AfterModelCallParams, BeforeToolCallParams, ContextScope, LedgerEntry, SteeringConfig, SteeringDecision, SteeringState } from '@noetic-tools/types';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a steering context layer that evaluates rules before tool calls and after model calls.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
* @param config - Steering configuration including rules, scope, and max ledger size.
|
|
7
|
+
* @returns A `ContextLayer` that enforces steering rules via allow/deny/guide decisions.
|
|
8
|
+
*/
|
|
9
|
+
export declare function steering(config: SteeringConfig): {
|
|
10
|
+
id: 'steering';
|
|
11
|
+
name: string;
|
|
12
|
+
slot: 90;
|
|
13
|
+
scope: ContextScope;
|
|
14
|
+
budget: {
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
};
|
|
18
|
+
timeouts: {
|
|
19
|
+
beforeToolCall: number;
|
|
20
|
+
afterModelCall: number;
|
|
21
|
+
};
|
|
22
|
+
hooks: {
|
|
23
|
+
init({ storage }: import("@noetic-tools/types").InitParams): Promise<{
|
|
24
|
+
state: SteeringState;
|
|
25
|
+
}>;
|
|
26
|
+
recall({ state }: import("@noetic-tools/types").RecallParams<SteeringState>): Promise<{
|
|
27
|
+
items: import("@noetic-tools/types").InputMessageItem[];
|
|
28
|
+
tokenCount: number;
|
|
29
|
+
state: SteeringState;
|
|
30
|
+
} | null>;
|
|
31
|
+
store(): Promise<undefined>;
|
|
32
|
+
beforeToolCall({ toolName, toolArgs, ctx, state }: BeforeToolCallParams<SteeringState>): Promise<{
|
|
33
|
+
decision: SteeringDecision;
|
|
34
|
+
state: SteeringState;
|
|
35
|
+
}>;
|
|
36
|
+
afterModelCall({ response, ctx, state }: AfterModelCallParams<SteeringState>): Promise<{
|
|
37
|
+
decision: SteeringDecision;
|
|
38
|
+
state: SteeringState;
|
|
39
|
+
}>;
|
|
40
|
+
onSpawn({ parentState }: import("@noetic-tools/types").SpawnParams<SteeringState>): Promise<{
|
|
41
|
+
childState: {
|
|
42
|
+
ledger: LedgerEntry[];
|
|
43
|
+
pendingAsync: never[];
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
onComplete({ state, outcome }: import("@noetic-tools/types").CompleteParams<SteeringState>): Promise<{
|
|
47
|
+
state: SteeringState;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { createMessage, estimateTokens, extractAssistantText, LedgerEntryKind, NoeticConfigError, Slot, SteeringAction, } from '@noetic-tools/types';
|
|
2
|
+
import { mostRestrictive } from '../layer-lifecycle.js';
|
|
3
|
+
//#region Constants
|
|
4
|
+
const DEFAULT_MAX_LEDGER_ENTRIES = 100;
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region Rule Evaluation Helpers
|
|
7
|
+
function requireCallModel(params, ruleId) {
|
|
8
|
+
if (!params.ctx.callModel) {
|
|
9
|
+
throw new NoeticConfigError({
|
|
10
|
+
code: 'MISSING_CALL_MODEL',
|
|
11
|
+
message: `LLM-evaluated steering rule "${ruleId}" requires a callModel but none is available.`,
|
|
12
|
+
hint: 'Pass `llm: { provider: "openrouter", apiKey: "..." }` to AgentHarness or set OPENROUTER_API_KEY.',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function evaluateProgrammaticRule(rule, params) {
|
|
17
|
+
if (!rule.predicate) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return rule.predicate(params);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse an LLM steering verdict. Returns `null` when the output is unparseable
|
|
24
|
+
* (neither ALLOW/DENY/GUIDE) so the caller can retry. The verdict keyword is
|
|
25
|
+
* matched case-insensitively, but the guidance text is preserved verbatim.
|
|
26
|
+
*/
|
|
27
|
+
function parseLlmVerdict(raw) {
|
|
28
|
+
const text = raw.trim();
|
|
29
|
+
// Match the verdict keyword on a word boundary (so "DENYALL" is NOT a DENY),
|
|
30
|
+
// case-insensitively, but keep the guidance text's original casing.
|
|
31
|
+
if (/^ALLOW\b/i.test(text)) {
|
|
32
|
+
return {
|
|
33
|
+
action: SteeringAction.Allow,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (/^DENY\b/i.test(text)) {
|
|
37
|
+
return {
|
|
38
|
+
action: SteeringAction.Deny,
|
|
39
|
+
guidance: text
|
|
40
|
+
.slice(4)
|
|
41
|
+
.replace(/^[:\s]+/, '')
|
|
42
|
+
.trim() || undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (/^GUIDE\b/i.test(text)) {
|
|
46
|
+
return {
|
|
47
|
+
action: SteeringAction.Guide,
|
|
48
|
+
guidance: text
|
|
49
|
+
.slice(5)
|
|
50
|
+
.replace(/^[:\s]+/, '')
|
|
51
|
+
.trim() || undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
async function evaluateLlmRuleSync(rule, params, maxRetries) {
|
|
57
|
+
if (!rule.llmEval) {
|
|
58
|
+
return {
|
|
59
|
+
action: SteeringAction.Allow,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const { callModel } = params.ctx;
|
|
63
|
+
if (!callModel) {
|
|
64
|
+
return {
|
|
65
|
+
action: SteeringAction.Allow,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const contextSummary = 'toolName' in params
|
|
69
|
+
? `Tool: ${params.toolName}, Args: ${JSON.stringify(params.toolArgs)}`
|
|
70
|
+
: `Response items: ${params.response.items.length}`;
|
|
71
|
+
const prompt = `${rule.llmEval.prompt}\n\nContext: ${contextSummary}\n\nRespond with exactly "ALLOW", "DENY", or "GUIDE: <guidance text>".`;
|
|
72
|
+
const userMessage = createMessage(prompt, 'user');
|
|
73
|
+
const model = rule.llmEval.model ?? 'openai/gpt-4o-mini';
|
|
74
|
+
// Retry on unparseable output up to maxRetries; treat as pass on exhaustion.
|
|
75
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
76
|
+
const response = await callModel({
|
|
77
|
+
model,
|
|
78
|
+
items: [
|
|
79
|
+
userMessage,
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
const verdict = parseLlmVerdict(extractAssistantText(response.items));
|
|
83
|
+
if (verdict) {
|
|
84
|
+
return verdict;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
action: SteeringAction.Allow,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function fireLlmRuleAsync({ rule, params, pendingAsync, maxRetries, }) {
|
|
92
|
+
evaluateLlmRuleSync(rule, params, maxRetries)
|
|
93
|
+
.then((decision) => {
|
|
94
|
+
if (decision.action !== SteeringAction.Allow) {
|
|
95
|
+
pendingAsync.push({
|
|
96
|
+
ruleId: rule.id,
|
|
97
|
+
guidance: decision.guidance ?? `Rule ${rule.id}: ${decision.action}`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.catch(() => {
|
|
102
|
+
// Async LLM eval failures are silently swallowed — they must not disrupt the agent
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function trimLedger(ledger, max) {
|
|
106
|
+
if (ledger.length <= max) {
|
|
107
|
+
return ledger;
|
|
108
|
+
}
|
|
109
|
+
return ledger.slice(ledger.length - max);
|
|
110
|
+
}
|
|
111
|
+
async function evaluateRules({ rules, hookName, params, state, maxRetries, }) {
|
|
112
|
+
const applicable = rules.filter((r) => r.appliesTo.includes(hookName));
|
|
113
|
+
const decisions = [];
|
|
114
|
+
for (const rule of applicable) {
|
|
115
|
+
// Programmatic predicate (fast)
|
|
116
|
+
const programmatic = evaluateProgrammaticRule(rule, params);
|
|
117
|
+
if (programmatic) {
|
|
118
|
+
decisions.push(programmatic);
|
|
119
|
+
if (programmatic.action === SteeringAction.Deny) {
|
|
120
|
+
return mostRestrictive(decisions);
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// LLM eval — throws NoeticConfigError if callModel is missing
|
|
125
|
+
if (!rule.llmEval) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
requireCallModel(params, rule.id);
|
|
129
|
+
if (rule.llmEval.mode === 'sync') {
|
|
130
|
+
const decision = await evaluateLlmRuleSync(rule, params, maxRetries);
|
|
131
|
+
decisions.push(decision);
|
|
132
|
+
if (decision.action === SteeringAction.Deny) {
|
|
133
|
+
return mostRestrictive(decisions);
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Async — fire and forget
|
|
138
|
+
fireLlmRuleAsync({
|
|
139
|
+
rule,
|
|
140
|
+
params,
|
|
141
|
+
pendingAsync: state.pendingAsync,
|
|
142
|
+
maxRetries,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return mostRestrictive(decisions);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region Public API
|
|
149
|
+
/**
|
|
150
|
+
* Creates a steering context layer that evaluates rules before tool calls and after model calls.
|
|
151
|
+
*
|
|
152
|
+
* @public
|
|
153
|
+
* @param config - Steering configuration including rules, scope, and max ledger size.
|
|
154
|
+
* @returns A `ContextLayer` that enforces steering rules via allow/deny/guide decisions.
|
|
155
|
+
*/
|
|
156
|
+
export function steering(config) {
|
|
157
|
+
const maxLedger = config.maxLedgerEntries ?? DEFAULT_MAX_LEDGER_ENTRIES;
|
|
158
|
+
const maxRetries = config.maxRetries ?? 1;
|
|
159
|
+
const scope = config.scope ?? 'execution';
|
|
160
|
+
return {
|
|
161
|
+
id: 'steering',
|
|
162
|
+
name: 'Steering',
|
|
163
|
+
slot: Slot.STEERING,
|
|
164
|
+
scope,
|
|
165
|
+
budget: {
|
|
166
|
+
min: 0,
|
|
167
|
+
max: 500,
|
|
168
|
+
},
|
|
169
|
+
timeouts: {
|
|
170
|
+
beforeToolCall: 5e3,
|
|
171
|
+
afterModelCall: 1e4,
|
|
172
|
+
},
|
|
173
|
+
hooks: {
|
|
174
|
+
async init({ storage }) {
|
|
175
|
+
const saved = await storage.get('state');
|
|
176
|
+
const state = saved ?? {
|
|
177
|
+
ledger: [],
|
|
178
|
+
pendingAsync: [],
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
state,
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
async recall({ state }) {
|
|
185
|
+
if (state.pendingAsync.length === 0) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
// Drain IN PLACE (splice, not reassignment): in-flight async LLM rules
|
|
189
|
+
// hold this array by reference (fireLlmRuleAsync) — replacing it would
|
|
190
|
+
// orphan their late verdicts. Late pushes land in the live array and
|
|
191
|
+
// surface on the next recall.
|
|
192
|
+
const drained = state.pendingAsync.splice(0, state.pendingAsync.length);
|
|
193
|
+
const feedback = drained.map((p) => `[${p.ruleId}] ${p.guidance}`).join('\n');
|
|
194
|
+
const content = `<steering_feedback>\n${feedback}\n</steering_feedback>`;
|
|
195
|
+
return {
|
|
196
|
+
items: [
|
|
197
|
+
createMessage(content, 'developer'),
|
|
198
|
+
],
|
|
199
|
+
tokenCount: estimateTokens(content),
|
|
200
|
+
state,
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
async store() {
|
|
204
|
+
// Model turn ledger entries are recorded in afterModelCall to avoid duplicates
|
|
205
|
+
return undefined;
|
|
206
|
+
},
|
|
207
|
+
async beforeToolCall({ toolName, toolArgs, ctx, state }) {
|
|
208
|
+
const decision = await evaluateRules({
|
|
209
|
+
rules: config.rules,
|
|
210
|
+
hookName: 'beforeToolCall',
|
|
211
|
+
params: {
|
|
212
|
+
toolName,
|
|
213
|
+
toolArgs,
|
|
214
|
+
ctx,
|
|
215
|
+
state,
|
|
216
|
+
},
|
|
217
|
+
state,
|
|
218
|
+
maxRetries,
|
|
219
|
+
});
|
|
220
|
+
const entry = {
|
|
221
|
+
kind: LedgerEntryKind.ToolCall,
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
toolName,
|
|
224
|
+
toolArgs,
|
|
225
|
+
ruleId: decision.action !== SteeringAction.Allow ? 'steering' : undefined,
|
|
226
|
+
action: decision.action,
|
|
227
|
+
guidance: decision.guidance,
|
|
228
|
+
};
|
|
229
|
+
state.ledger.push(entry);
|
|
230
|
+
state.ledger = trimLedger(state.ledger, maxLedger);
|
|
231
|
+
return {
|
|
232
|
+
decision,
|
|
233
|
+
state,
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
async afterModelCall({ response, ctx, state }) {
|
|
237
|
+
const decision = await evaluateRules({
|
|
238
|
+
rules: config.rules,
|
|
239
|
+
hookName: 'afterModelCall',
|
|
240
|
+
params: {
|
|
241
|
+
response,
|
|
242
|
+
ctx,
|
|
243
|
+
state,
|
|
244
|
+
},
|
|
245
|
+
state,
|
|
246
|
+
maxRetries,
|
|
247
|
+
});
|
|
248
|
+
const entry = {
|
|
249
|
+
kind: LedgerEntryKind.ModelTurn,
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
tokenUsage: {
|
|
252
|
+
input: response.usage.inputTokens,
|
|
253
|
+
output: response.usage.outputTokens,
|
|
254
|
+
},
|
|
255
|
+
ruleId: decision.action !== SteeringAction.Allow ? 'steering' : undefined,
|
|
256
|
+
action: decision.action,
|
|
257
|
+
guidance: decision.guidance,
|
|
258
|
+
};
|
|
259
|
+
state.ledger.push(entry);
|
|
260
|
+
state.ledger = trimLedger(state.ledger, maxLedger);
|
|
261
|
+
return {
|
|
262
|
+
decision,
|
|
263
|
+
state,
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
async onSpawn({ parentState }) {
|
|
267
|
+
return {
|
|
268
|
+
childState: {
|
|
269
|
+
ledger: structuredClone(parentState.ledger),
|
|
270
|
+
pendingAsync: [],
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
async onComplete({ state, outcome }) {
|
|
275
|
+
const entry = {
|
|
276
|
+
kind: LedgerEntryKind.Custom,
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
custom: {
|
|
279
|
+
outcome,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
state.ledger.push(entry);
|
|
283
|
+
state.ledger = trimLedger(state.ledger, maxLedger);
|
|
284
|
+
return {
|
|
285
|
+
state,
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ContextLayer, ContextScope } from '@noetic-tools/types';
|
|
2
|
+
/**
|
|
3
|
+
* A single remembered fact tied to an instant.
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export interface TemporalFact {
|
|
7
|
+
/** ISO-8601 timestamp the fact is associated with (when it happened / was stated). */
|
|
8
|
+
ts: string;
|
|
9
|
+
/** The fact, stated tersely. */
|
|
10
|
+
fact: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Result of a temporal fact search.
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export interface TemporalSearchResult {
|
|
17
|
+
/** Matching facts, most relevant first. */
|
|
18
|
+
facts: string[];
|
|
19
|
+
/** A resolved date for the query, when one applies (ISO-8601 or a natural phrase). */
|
|
20
|
+
date?: string;
|
|
21
|
+
/** True when `date` is approximate ("around March", "a few weeks ago"). */
|
|
22
|
+
fuzzy?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extracts timestamped facts from a batch of conversation text. Host-injected so
|
|
26
|
+
* the layer stays LLM-agnostic and tree-shakable (mirrors `observationalContext`'s
|
|
27
|
+
* `observer`). When omitted, the layer only buffers — it never fabricates facts.
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export type FactExtractor = (input: {
|
|
31
|
+
transcript: string;
|
|
32
|
+
now: string;
|
|
33
|
+
}) => Promise<TemporalFact[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Searches the stored fact ledger for a query, optionally resolving a date.
|
|
36
|
+
* Host-injected. When omitted, search falls back to returning the raw ledger.
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export type FactSearcher = (input: {
|
|
40
|
+
query: string;
|
|
41
|
+
facts: ReadonlyArray<TemporalFact>;
|
|
42
|
+
now: string;
|
|
43
|
+
}) => Promise<TemporalSearchResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Configuration for {@link temporalContext}.
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export interface TemporalContextConfig {
|
|
49
|
+
/** Returns "now" for date grounding/extraction. Defaults to the system clock. */
|
|
50
|
+
now?: () => Date;
|
|
51
|
+
/** Context scope. Defaults to `'resource'` (long-term, cross-session). */
|
|
52
|
+
scope?: ContextScope;
|
|
53
|
+
/** LLM-backed fact extractor. Without it, `store` only buffers (no extraction). */
|
|
54
|
+
extract?: FactExtractor;
|
|
55
|
+
/** LLM-backed fact searcher. Without it, `searchMemory` returns the raw ledger. */
|
|
56
|
+
search?: FactSearcher;
|
|
57
|
+
/** Buffered output tokens that trigger an extraction pass. Default 2000. */
|
|
58
|
+
bufferThreshold?: number;
|
|
59
|
+
/** Maximum facts retained (oldest dropped beyond this). Default 200. */
|
|
60
|
+
maxFacts?: number;
|
|
61
|
+
/** Inject a `<current_datetime>` grounding block on recall. Default true. */
|
|
62
|
+
groundDateTime?: boolean;
|
|
63
|
+
/** Inject the fact ledger on recall (vs. on-demand via the search tool). Default false. */
|
|
64
|
+
injectLedger?: boolean;
|
|
65
|
+
}
|
|
66
|
+
interface TemporalState {
|
|
67
|
+
/** ISO timestamp → facts recorded at that instant. */
|
|
68
|
+
facts: Record<string, string[]>;
|
|
69
|
+
/** Accumulated output text awaiting extraction. */
|
|
70
|
+
buffer: string[];
|
|
71
|
+
bufferTokens: number;
|
|
72
|
+
version: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A non-atomic, LLM-backed context layer that extracts timestamped facts from the
|
|
76
|
+
* conversation into a key-value ledger (key = ISO timestamp) and exposes a
|
|
77
|
+
* `temporal/searchMemory` tool for relative-time recall — given a fact/query it
|
|
78
|
+
* returns matching facts and, when applicable, a resolved (possibly fuzzy) date.
|
|
79
|
+
*
|
|
80
|
+
* The layer is LLM-agnostic: the host injects `extract` and `search` callbacks
|
|
81
|
+
* (mirrors `observationalContext`'s `observer`), keeping `context/` tree-shakable.
|
|
82
|
+
* Without them, the layer still buffers text and the search tool returns the raw
|
|
83
|
+
* ledger — it never fabricates facts. A `<current_datetime>` grounding block is
|
|
84
|
+
* injected on recall by default to anchor relative-time reasoning.
|
|
85
|
+
*
|
|
86
|
+
* @public
|
|
87
|
+
* @param config - Clock, scope, injected LLM callbacks, and tuning knobs.
|
|
88
|
+
* @returns A `ContextLayer` with a fact-ledger store + `searchMemory` tool.
|
|
89
|
+
*/
|
|
90
|
+
export declare function temporalContext(config?: TemporalContextConfig): ContextLayer<TemporalState>;
|
|
91
|
+
export {};
|