@jangts/agent-core 0.2.0 → 0.2.5
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/CHANGELOG.md +14 -0
- package/LICENSE +55 -0
- package/NOTICE +6 -0
- package/README.md +8 -0
- package/development/docs/README.en.md +70 -0
- package/development/docs/README.md +1 -1
- package/development/docs/acceptance.en.md +34 -0
- package/development/docs/agent-development.en.md +42 -0
- package/development/docs/api/README.en.md +62 -0
- package/development/docs/api/advanced-agents.en.md +135 -0
- package/development/docs/api/context-processing.en.md +110 -0
- package/development/docs/api/core.en.md +274 -0
- package/development/docs/api/examples.en.md +118 -0
- package/development/docs/api/kits.en.md +183 -0
- package/development/docs/api/knowledge-answering.en.md +166 -0
- package/development/docs/api/mock.en.md +186 -0
- package/development/docs/api/observability.en.md +166 -0
- package/development/docs/api/reference.en.md +2235 -0
- package/development/docs/api/reference.md +290 -257
- package/development/docs/api/support.en.md +252 -0
- package/development/docs/api/types.en.md +181 -0
- package/development/docs/api/workflow.en.md +198 -0
- package/development/docs/api/workflow.md +22 -0
- package/development/docs/basic-usage.en.md +78 -0
- package/development/docs/create-app.en.md +76 -0
- package/development/docs/examples/offline-agent.en.md +273 -0
- package/development/docs/examples.en.md +36 -0
- package/development/docs/flows/knowledge-maintenance.en.md +38 -0
- package/development/docs/getting-started.en.md +71 -0
- package/development/docs/knowledge.en.md +89 -0
- package/development/docs/maintenance.en.md +62 -0
- package/development/docs/materials.en.md +38 -0
- package/development/docs/mechanisms.en.md +227 -0
- package/development/docs/memory-and-learning.en.md +42 -0
- package/development/docs/operations.en.md +51 -0
- package/development/docs/records.en.md +63 -0
- package/development/docs/registration.en.md +63 -0
- package/development/skills/agent-core-app/SKILL.md +4 -0
- package/development/skills/agent-core-app/scripts/create-app.mjs +19 -0
- package/development/skills/agent-core-app/scripts/lib/knowledge.mjs +37 -1
- package/development/skills/agent-core-app/scripts/plan.mjs +9 -0
- package/development/skills/agent-core-app/scripts/resources.mjs +17 -0
- package/dist/cjs/diagnostics/catalog.js +6 -0
- package/dist/cjs/execution/cache.js +162 -0
- package/dist/cjs/execution/engine.js +10 -0
- package/dist/cjs/kits/vector-search.js +4 -7
- package/dist/cjs/structs/execution/cache.js +6 -0
- package/dist/cjs/support/model/chat-completions.js +1 -1
- package/dist/cjs/support/model/completion/default.js +1 -0
- package/dist/cjs/support/model/completion/session.js +5 -0
- package/dist/esm/diagnostics/catalog.d.ts +6 -0
- package/dist/esm/diagnostics/catalog.js +6 -0
- package/dist/esm/execution/cache.d.ts +20 -0
- package/dist/esm/execution/cache.js +156 -0
- package/dist/esm/execution/engine.d.ts +5 -0
- package/dist/esm/execution/engine.js +10 -0
- package/dist/esm/kits/vector-search.js +4 -7
- package/dist/esm/structs/execution/cache.d.ts +54 -0
- package/dist/esm/structs/execution/cache.js +5 -0
- package/dist/esm/structs/execution/engine.d.ts +6 -1
- package/dist/esm/structs/execution/workflow.d.ts +2 -1
- package/dist/esm/structs/index.d.ts +1 -0
- package/dist/esm/structs/kits/retrieval.d.ts +1 -0
- package/dist/esm/structs/kits/search.d.ts +2 -0
- package/dist/esm/structs/model/messages.d.ts +4 -0
- package/dist/esm/structs/support/model/messages.d.ts +2 -0
- package/dist/esm/support/context/nodes.d.ts +16 -0
- package/dist/esm/support/context/processing.d.ts +7 -0
- package/dist/esm/support/model/chat-completions.js +1 -1
- package/dist/esm/support/model/children.d.ts +2 -0
- package/dist/esm/support/model/completion/default.d.ts +1 -0
- package/dist/esm/support/model/completion/default.js +1 -0
- package/dist/esm/support/model/completion/session.d.ts +5 -0
- package/dist/esm/support/model/completion/session.js +5 -0
- package/dist/esm/support/model/preview-rules.d.ts +6 -0
- package/dist/esm/support/model/reply-preview.d.ts +7 -0
- package/dist/esm/support/model/strategies.d.ts +4 -0
- package/package.json +7 -4
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EngineCache = void 0;
|
|
4
|
+
exports.cachePort = cachePort;
|
|
5
|
+
exports.cacheContext = cacheContext;
|
|
6
|
+
const cancellation_js_1 = require("../utils/cancellation.js");
|
|
7
|
+
/** Validate the small cache-specific boundary; business permissions remain App-owned. */
|
|
8
|
+
function textKey(value) {
|
|
9
|
+
if (typeof value !== 'string' || !value.length)
|
|
10
|
+
throw new TypeError('Cache keys must be nonempty strings');
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/** Reject accidental cross-partition writes and unknown options instead of ignoring them. */
|
|
14
|
+
function optionsOf(value, allowed) {
|
|
15
|
+
if (value !== undefined &&
|
|
16
|
+
(!value ||
|
|
17
|
+
typeof value !== 'object' ||
|
|
18
|
+
Reflect.ownKeys(value).some((key) => typeof key !== 'string' || !allowed.includes(key))))
|
|
19
|
+
throw new TypeError('Invalid cache options');
|
|
20
|
+
}
|
|
21
|
+
/** Bounded LRU for one Engine; forks get new memory, and values are deliberately not cloned. */
|
|
22
|
+
class EngineCache {
|
|
23
|
+
entries = new Map();
|
|
24
|
+
limit;
|
|
25
|
+
ttl;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.limit = options.maxEntries ?? 1024;
|
|
28
|
+
this.ttl = options.ttlMs ?? Infinity;
|
|
29
|
+
if (!Number.isSafeInteger(this.limit) ||
|
|
30
|
+
this.limit < 1 ||
|
|
31
|
+
(this.ttl !== Infinity && (!Number.isSafeInteger(this.ttl) || this.ttl < 1)))
|
|
32
|
+
throw new TypeError('Invalid Engine cache bounds');
|
|
33
|
+
}
|
|
34
|
+
/** Remove expired entries lazily, without timers or background tasks. */
|
|
35
|
+
prune() {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
for (const [key, item] of this.entries)
|
|
38
|
+
if (item.expires <= now)
|
|
39
|
+
this.entries.delete(key);
|
|
40
|
+
}
|
|
41
|
+
/** Capture an immutable partition and visit signal; no per-run cache is introduced. */
|
|
42
|
+
view(nodeId, signal) {
|
|
43
|
+
textKey(nodeId);
|
|
44
|
+
const prefix = JSON.stringify([nodeId]).slice(0, -1) + ',';
|
|
45
|
+
const address = (key, node = nodeId) => JSON.stringify([textKey(node), textKey(key)]);
|
|
46
|
+
const read = (key, node = nodeId) => {
|
|
47
|
+
signal.throwIfAborted();
|
|
48
|
+
const id = address(key, node);
|
|
49
|
+
const item = this.entries.get(id);
|
|
50
|
+
if (item && item.expires <= Date.now()) {
|
|
51
|
+
this.entries.delete(id);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (item) {
|
|
55
|
+
this.entries.delete(id);
|
|
56
|
+
this.entries.set(id, item);
|
|
57
|
+
}
|
|
58
|
+
return item;
|
|
59
|
+
};
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
get: (key, options) => {
|
|
62
|
+
optionsOf(options, ['nodeId']);
|
|
63
|
+
return read(key, options?.nodeId)?.value;
|
|
64
|
+
},
|
|
65
|
+
has: (key) => read(key) !== undefined,
|
|
66
|
+
set: (key, value) => {
|
|
67
|
+
signal.throwIfAborted();
|
|
68
|
+
const id = address(key);
|
|
69
|
+
this.prune();
|
|
70
|
+
this.entries.delete(id);
|
|
71
|
+
this.entries.set(id, { value, expires: Date.now() + this.ttl });
|
|
72
|
+
if (this.entries.size > this.limit)
|
|
73
|
+
this.entries.delete(this.entries.keys().next().value);
|
|
74
|
+
},
|
|
75
|
+
delete: (key) => {
|
|
76
|
+
signal.throwIfAborted();
|
|
77
|
+
return this.entries.delete(address(key));
|
|
78
|
+
},
|
|
79
|
+
clear: () => {
|
|
80
|
+
signal.throwIfAborted();
|
|
81
|
+
for (const key of this.entries.keys())
|
|
82
|
+
if (key.startsWith(prefix))
|
|
83
|
+
this.entries.delete(key);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.EngineCache = EngineCache;
|
|
89
|
+
/** Snapshot port callbacks with their receiver, rejecting incomplete App storage early. */
|
|
90
|
+
function cachePort(port) {
|
|
91
|
+
if (port === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
const handlers = ['get', 'has', 'set', 'delete', 'clear'];
|
|
94
|
+
if (!port ||
|
|
95
|
+
handlers.some((key) => typeof port[key] !== 'function') ||
|
|
96
|
+
(port.supportsPersistence !== undefined && typeof port.supportsPersistence !== 'boolean'))
|
|
97
|
+
throw new TypeError('Invalid Agent cache port');
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
supportsPersistence: port.supportsPersistence === true,
|
|
100
|
+
get: port.get.bind(port),
|
|
101
|
+
has: port.has.bind(port),
|
|
102
|
+
set: port.set.bind(port),
|
|
103
|
+
delete: port.delete.bind(port),
|
|
104
|
+
clear: port.clear.bind(port),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Build visit-local views. Persistent writes remain external side effects owned by the App. */
|
|
108
|
+
function cacheContext(memory, port, nodeId, signal) {
|
|
109
|
+
const partition = (options, crossRead = false) => {
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
optionsOf(options, crossRead ? ['nodeId', 'persistence'] : ['persistence']);
|
|
112
|
+
const persistence = options?.persistence ?? false;
|
|
113
|
+
if (typeof persistence !== 'boolean')
|
|
114
|
+
throw new TypeError('Invalid cache persistence');
|
|
115
|
+
if (persistence && !port?.supportsPersistence)
|
|
116
|
+
throw new TypeError('Agent cache persistence is unsupported');
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
nodeId: textKey(crossRead ? (options?.nodeId ?? nodeId) : nodeId),
|
|
119
|
+
persistence,
|
|
120
|
+
signal,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const address = (key, options, crossRead = false) => Object.freeze({ ...partition(options, crossRead), key: textKey(key) });
|
|
124
|
+
const call = async (invoke) => {
|
|
125
|
+
signal.throwIfAborted();
|
|
126
|
+
const value = await (0, cancellation_js_1.awaitWithSignal)(invoke(), signal);
|
|
127
|
+
signal.throwIfAborted();
|
|
128
|
+
return value;
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
cache: memory.view(nodeId, signal),
|
|
132
|
+
...(port
|
|
133
|
+
? {
|
|
134
|
+
agentCache: Object.freeze({
|
|
135
|
+
get: (key, options) => call(() => port.get(address(key, options, true))),
|
|
136
|
+
has: (key, options) => call(async () => {
|
|
137
|
+
const result = await port.has(address(key, options));
|
|
138
|
+
if (typeof result !== 'boolean')
|
|
139
|
+
throw new TypeError('Agent cache has must return boolean');
|
|
140
|
+
return result;
|
|
141
|
+
}),
|
|
142
|
+
set: (key, value, options) => call(async () => {
|
|
143
|
+
const result = await port.set(address(key, options), value);
|
|
144
|
+
if (result !== undefined)
|
|
145
|
+
throw new TypeError('Agent cache set must return void');
|
|
146
|
+
}),
|
|
147
|
+
delete: (key, options) => call(async () => {
|
|
148
|
+
const result = await port.delete(address(key, options));
|
|
149
|
+
if (typeof result !== 'boolean')
|
|
150
|
+
throw new TypeError('Agent cache delete must return boolean');
|
|
151
|
+
return result;
|
|
152
|
+
}),
|
|
153
|
+
clear: (options) => call(async () => {
|
|
154
|
+
const result = await port.clear(partition(options));
|
|
155
|
+
if (result !== undefined)
|
|
156
|
+
throw new TypeError('Agent cache clear must return void');
|
|
157
|
+
}),
|
|
158
|
+
}),
|
|
159
|
+
}
|
|
160
|
+
: {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -26,6 +26,7 @@ exports.AgentCoreEngine = void 0;
|
|
|
26
26
|
*
|
|
27
27
|
* Exports: `AgentCoreEngine` and the engine option/identity types.
|
|
28
28
|
*/
|
|
29
|
+
const cache_js_1 = require("./cache.js");
|
|
29
30
|
const call_budget_js_1 = require("../model/call-budget.js");
|
|
30
31
|
const runner_js_1 = require("../model/runner.js");
|
|
31
32
|
const binding_js_1 = require("../model/binding.js");
|
|
@@ -56,6 +57,8 @@ class AgentCoreEngine {
|
|
|
56
57
|
static AttemptBudget = workflow_js_1.AttemptBudget;
|
|
57
58
|
/** Factory for a validated workflow definition. */
|
|
58
59
|
static createWorkflow = workflow_js_1.defineWorkflow;
|
|
60
|
+
cacheMemory;
|
|
61
|
+
serviceCache;
|
|
59
62
|
maxRounds;
|
|
60
63
|
exhausted;
|
|
61
64
|
options;
|
|
@@ -306,6 +309,8 @@ class AgentCoreEngine {
|
|
|
306
309
|
(0, rules_js_1.breakWhile)(!!this.invokeTool && !dependencies?.tools, 'Fork requires a restored tool runtime');
|
|
307
310
|
const engine = new AgentCoreEngine({
|
|
308
311
|
maxRounds: this.maxRounds,
|
|
312
|
+
cache: this.options.cache,
|
|
313
|
+
agentCache: dependencies?.agentCache,
|
|
309
314
|
errors: { exhausted: this.exhausted },
|
|
310
315
|
execution: {
|
|
311
316
|
diagnostics: this.options.execution?.diagnostics,
|
|
@@ -350,6 +355,8 @@ class AgentCoreEngine {
|
|
|
350
355
|
['maxSteps', 'exhausted', 'signal', 'diagnostics', 'onWorkflowEvent', 'observe'].some((key) => key in settings)) {
|
|
351
356
|
throw new TypeError('Use maxRounds, execution (including observe) and errors');
|
|
352
357
|
}
|
|
358
|
+
this.cacheMemory = new cache_js_1.EngineCache(settings.cache);
|
|
359
|
+
this.serviceCache = (0, cache_js_1.cachePort)(settings.agentCache);
|
|
353
360
|
const execution = settings.execution;
|
|
354
361
|
if (settings.branch !== undefined)
|
|
355
362
|
this.branchIdentity = (0, branch_js_1.snapshotBranch)(settings.branch);
|
|
@@ -359,6 +366,7 @@ class AgentCoreEngine {
|
|
|
359
366
|
settings.errors?.exhausted?.bind(settings.errors) ??
|
|
360
367
|
(() => new errors_js_2.BudgetError('Agent round budget exhausted'));
|
|
361
368
|
this.options = {
|
|
369
|
+
cache: { ...settings.cache },
|
|
362
370
|
execution: {
|
|
363
371
|
signal: execution?.signal,
|
|
364
372
|
diagnostics: execution?.diagnostics,
|
|
@@ -478,6 +486,7 @@ class AgentCoreEngine {
|
|
|
478
486
|
phase = 'run';
|
|
479
487
|
signal.throwIfAborted();
|
|
480
488
|
const result = await (0, cancellation_js_1.awaitWithSignal)(step(index, {
|
|
489
|
+
...(0, cache_js_1.cacheContext)(this.cacheMemory, this.serviceCache, '$step', signal),
|
|
481
490
|
signal,
|
|
482
491
|
diagnostics,
|
|
483
492
|
report: reports.report.bind(reports),
|
|
@@ -673,6 +682,7 @@ class AgentCoreEngine {
|
|
|
673
682
|
});
|
|
674
683
|
const reports = new reporting_js_1.NodeReports(nodeDiagnostics, signal);
|
|
675
684
|
const context = {
|
|
685
|
+
...(0, cache_js_1.cacheContext)(this.cacheMemory, this.serviceCache, node.id, signal),
|
|
676
686
|
report: reports.report.bind(reports),
|
|
677
687
|
createSubFlowLogger: reports.createSubFlowLogger.bind(reports),
|
|
678
688
|
roundIndex: round,
|
|
@@ -80,11 +80,8 @@ class VectorSearch {
|
|
|
80
80
|
(!Number.isFinite(options.minScore) || options.minScore < -1 || options.minScore > 1)), 'Invalid vector query');
|
|
81
81
|
const query = normalize(vector, this.space.dimensions), groups = new Map();
|
|
82
82
|
for (const [id, { document, vector: value }] of this.entries) {
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
// removes the redundant clones without exposing the index.
|
|
86
|
-
const candidate = structuredClone(document);
|
|
87
|
-
const allowed = (0, synchronous_js_1.booleanResult)(options.filter(candidate), 'Vector filters must be synchronous', 'Vector filters must return boolean');
|
|
83
|
+
// Each callback receives its own snapshot; readonly types cannot prevent runtime mutation.
|
|
84
|
+
const allowed = (0, synchronous_js_1.booleanResult)(options.filter(structuredClone(document)), 'Vector filters must be synchronous', 'Vector filters must return boolean');
|
|
88
85
|
if (!allowed)
|
|
89
86
|
continue;
|
|
90
87
|
let score = 0;
|
|
@@ -93,12 +90,12 @@ class VectorSearch {
|
|
|
93
90
|
score = Math.max(-1, Math.min(1, score));
|
|
94
91
|
if (score < (options.minScore ?? -1))
|
|
95
92
|
continue;
|
|
96
|
-
const group = (0, synchronous_js_1.synchronous)(options.groupBy?.(
|
|
93
|
+
const group = (0, synchronous_js_1.synchronous)(options.groupBy?.(structuredClone(document)), 'Vector grouping must be synchronous') ?? id;
|
|
97
94
|
if (typeof group !== 'string')
|
|
98
95
|
throw new TypeError('Vector groups must be strings');
|
|
99
96
|
const prior = groups.get(group);
|
|
100
97
|
if (!prior || score > prior.score || (score === prior.score && id < prior.id))
|
|
101
|
-
groups.set(group, { id, score, document:
|
|
98
|
+
groups.set(group, { id, score, document: structuredClone(document) });
|
|
102
99
|
}
|
|
103
100
|
return [...groups.values()]
|
|
104
101
|
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Node-partitioned cache contracts. Engine memory is synchronous; service-owned storage is
|
|
4
|
+
* asynchronous and explicitly selects volatile or persistent space. Neither is workflow state.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -92,7 +92,7 @@ function chatCompletionParts(delta) {
|
|
|
92
92
|
*/
|
|
93
93
|
function chatCompletionMessages(messages) {
|
|
94
94
|
return messages.map((message) => {
|
|
95
|
-
const { role, content, images, toolCalls, toolCallId } = message;
|
|
95
|
+
const { role, content, images, toolCalls, toolCallId, } = message;
|
|
96
96
|
if (!['system', 'user', 'assistant', 'tool'].includes(role) || typeof content !== 'string')
|
|
97
97
|
throw new TypeError('Invalid provider message role or content');
|
|
98
98
|
if (toolCallId !== undefined && role !== 'tool')
|
|
@@ -99,4 +99,9 @@ const policy = {
|
|
|
99
99
|
};
|
|
100
100
|
},
|
|
101
101
|
};
|
|
102
|
+
/**
|
|
103
|
+
* Frozen opt-in policy that judges rules by the request's effective `tool_choice`.
|
|
104
|
+
*
|
|
105
|
+
* Consumers that do not select it keep `defaultCompletionPolicy`.
|
|
106
|
+
*/
|
|
102
107
|
exports.sessionCompletionPolicy = Object.freeze(policy);
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default visibility tier for every event Core can emit.
|
|
3
|
+
*
|
|
4
|
+
* The same object is the allow-list used when normalizing persisted records; adding an event to
|
|
5
|
+
* `CoreEventName` without a tier here fails type checking.
|
|
6
|
+
*/
|
|
1
7
|
export declare const coreEventCatalog: {
|
|
2
8
|
readonly 'node.progress': "metadata";
|
|
3
9
|
readonly 'model.start': "metadata";
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default visibility tier for every event Core can emit.
|
|
3
|
+
*
|
|
4
|
+
* The same object is the allow-list used when normalizing persisted records; adding an event to
|
|
5
|
+
* `CoreEventName` without a tier here fails type checking.
|
|
6
|
+
*/
|
|
1
7
|
export const coreEventCatalog = {
|
|
2
8
|
'node.progress': 'metadata',
|
|
3
9
|
'model.start': 'metadata',
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine memory and borrowed service storage views. Partition ownership is captured per visit,
|
|
3
|
+
* never in a mutable service field. Cache calls do not create model budgets or execute tools.
|
|
4
|
+
*/
|
|
5
|
+
import type { AgentCachePort, CacheContext, EngineCacheOptions, NodeCache } from '../structs/execution/cache.js';
|
|
6
|
+
/** Bounded LRU for one Engine; forks get new memory, and values are deliberately not cloned. */
|
|
7
|
+
export declare class EngineCache {
|
|
8
|
+
private readonly entries;
|
|
9
|
+
private readonly limit;
|
|
10
|
+
private readonly ttl;
|
|
11
|
+
constructor(options?: EngineCacheOptions);
|
|
12
|
+
/** Remove expired entries lazily, without timers or background tasks. */
|
|
13
|
+
private prune;
|
|
14
|
+
/** Capture an immutable partition and visit signal; no per-run cache is introduced. */
|
|
15
|
+
view(nodeId: string, signal: AbortSignal): NodeCache;
|
|
16
|
+
}
|
|
17
|
+
/** Snapshot port callbacks with their receiver, rejecting incomplete App storage early. */
|
|
18
|
+
export declare function cachePort(port?: AgentCachePort): AgentCachePort | undefined;
|
|
19
|
+
/** Build visit-local views. Persistent writes remain external side effects owned by the App. */
|
|
20
|
+
export declare function cacheContext(memory: EngineCache, port: AgentCachePort | undefined, nodeId: string, signal: AbortSignal): CacheContext;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { awaitWithSignal } from '../utils/cancellation.js';
|
|
2
|
+
/** Validate the small cache-specific boundary; business permissions remain App-owned. */
|
|
3
|
+
function textKey(value) {
|
|
4
|
+
if (typeof value !== 'string' || !value.length)
|
|
5
|
+
throw new TypeError('Cache keys must be nonempty strings');
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
/** Reject accidental cross-partition writes and unknown options instead of ignoring them. */
|
|
9
|
+
function optionsOf(value, allowed) {
|
|
10
|
+
if (value !== undefined &&
|
|
11
|
+
(!value ||
|
|
12
|
+
typeof value !== 'object' ||
|
|
13
|
+
Reflect.ownKeys(value).some((key) => typeof key !== 'string' || !allowed.includes(key))))
|
|
14
|
+
throw new TypeError('Invalid cache options');
|
|
15
|
+
}
|
|
16
|
+
/** Bounded LRU for one Engine; forks get new memory, and values are deliberately not cloned. */
|
|
17
|
+
export class EngineCache {
|
|
18
|
+
entries = new Map();
|
|
19
|
+
limit;
|
|
20
|
+
ttl;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.limit = options.maxEntries ?? 1024;
|
|
23
|
+
this.ttl = options.ttlMs ?? Infinity;
|
|
24
|
+
if (!Number.isSafeInteger(this.limit) ||
|
|
25
|
+
this.limit < 1 ||
|
|
26
|
+
(this.ttl !== Infinity && (!Number.isSafeInteger(this.ttl) || this.ttl < 1)))
|
|
27
|
+
throw new TypeError('Invalid Engine cache bounds');
|
|
28
|
+
}
|
|
29
|
+
/** Remove expired entries lazily, without timers or background tasks. */
|
|
30
|
+
prune() {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
for (const [key, item] of this.entries)
|
|
33
|
+
if (item.expires <= now)
|
|
34
|
+
this.entries.delete(key);
|
|
35
|
+
}
|
|
36
|
+
/** Capture an immutable partition and visit signal; no per-run cache is introduced. */
|
|
37
|
+
view(nodeId, signal) {
|
|
38
|
+
textKey(nodeId);
|
|
39
|
+
const prefix = JSON.stringify([nodeId]).slice(0, -1) + ',';
|
|
40
|
+
const address = (key, node = nodeId) => JSON.stringify([textKey(node), textKey(key)]);
|
|
41
|
+
const read = (key, node = nodeId) => {
|
|
42
|
+
signal.throwIfAborted();
|
|
43
|
+
const id = address(key, node);
|
|
44
|
+
const item = this.entries.get(id);
|
|
45
|
+
if (item && item.expires <= Date.now()) {
|
|
46
|
+
this.entries.delete(id);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (item) {
|
|
50
|
+
this.entries.delete(id);
|
|
51
|
+
this.entries.set(id, item);
|
|
52
|
+
}
|
|
53
|
+
return item;
|
|
54
|
+
};
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
get: (key, options) => {
|
|
57
|
+
optionsOf(options, ['nodeId']);
|
|
58
|
+
return read(key, options?.nodeId)?.value;
|
|
59
|
+
},
|
|
60
|
+
has: (key) => read(key) !== undefined,
|
|
61
|
+
set: (key, value) => {
|
|
62
|
+
signal.throwIfAborted();
|
|
63
|
+
const id = address(key);
|
|
64
|
+
this.prune();
|
|
65
|
+
this.entries.delete(id);
|
|
66
|
+
this.entries.set(id, { value, expires: Date.now() + this.ttl });
|
|
67
|
+
if (this.entries.size > this.limit)
|
|
68
|
+
this.entries.delete(this.entries.keys().next().value);
|
|
69
|
+
},
|
|
70
|
+
delete: (key) => {
|
|
71
|
+
signal.throwIfAborted();
|
|
72
|
+
return this.entries.delete(address(key));
|
|
73
|
+
},
|
|
74
|
+
clear: () => {
|
|
75
|
+
signal.throwIfAborted();
|
|
76
|
+
for (const key of this.entries.keys())
|
|
77
|
+
if (key.startsWith(prefix))
|
|
78
|
+
this.entries.delete(key);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Snapshot port callbacks with their receiver, rejecting incomplete App storage early. */
|
|
84
|
+
export function cachePort(port) {
|
|
85
|
+
if (port === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
const handlers = ['get', 'has', 'set', 'delete', 'clear'];
|
|
88
|
+
if (!port ||
|
|
89
|
+
handlers.some((key) => typeof port[key] !== 'function') ||
|
|
90
|
+
(port.supportsPersistence !== undefined && typeof port.supportsPersistence !== 'boolean'))
|
|
91
|
+
throw new TypeError('Invalid Agent cache port');
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
supportsPersistence: port.supportsPersistence === true,
|
|
94
|
+
get: port.get.bind(port),
|
|
95
|
+
has: port.has.bind(port),
|
|
96
|
+
set: port.set.bind(port),
|
|
97
|
+
delete: port.delete.bind(port),
|
|
98
|
+
clear: port.clear.bind(port),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Build visit-local views. Persistent writes remain external side effects owned by the App. */
|
|
102
|
+
export function cacheContext(memory, port, nodeId, signal) {
|
|
103
|
+
const partition = (options, crossRead = false) => {
|
|
104
|
+
signal.throwIfAborted();
|
|
105
|
+
optionsOf(options, crossRead ? ['nodeId', 'persistence'] : ['persistence']);
|
|
106
|
+
const persistence = options?.persistence ?? false;
|
|
107
|
+
if (typeof persistence !== 'boolean')
|
|
108
|
+
throw new TypeError('Invalid cache persistence');
|
|
109
|
+
if (persistence && !port?.supportsPersistence)
|
|
110
|
+
throw new TypeError('Agent cache persistence is unsupported');
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
nodeId: textKey(crossRead ? (options?.nodeId ?? nodeId) : nodeId),
|
|
113
|
+
persistence,
|
|
114
|
+
signal,
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
const address = (key, options, crossRead = false) => Object.freeze({ ...partition(options, crossRead), key: textKey(key) });
|
|
118
|
+
const call = async (invoke) => {
|
|
119
|
+
signal.throwIfAborted();
|
|
120
|
+
const value = await awaitWithSignal(invoke(), signal);
|
|
121
|
+
signal.throwIfAborted();
|
|
122
|
+
return value;
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
cache: memory.view(nodeId, signal),
|
|
126
|
+
...(port
|
|
127
|
+
? {
|
|
128
|
+
agentCache: Object.freeze({
|
|
129
|
+
get: (key, options) => call(() => port.get(address(key, options, true))),
|
|
130
|
+
has: (key, options) => call(async () => {
|
|
131
|
+
const result = await port.has(address(key, options));
|
|
132
|
+
if (typeof result !== 'boolean')
|
|
133
|
+
throw new TypeError('Agent cache has must return boolean');
|
|
134
|
+
return result;
|
|
135
|
+
}),
|
|
136
|
+
set: (key, value, options) => call(async () => {
|
|
137
|
+
const result = await port.set(address(key, options), value);
|
|
138
|
+
if (result !== undefined)
|
|
139
|
+
throw new TypeError('Agent cache set must return void');
|
|
140
|
+
}),
|
|
141
|
+
delete: (key, options) => call(async () => {
|
|
142
|
+
const result = await port.delete(address(key, options));
|
|
143
|
+
if (typeof result !== 'boolean')
|
|
144
|
+
throw new TypeError('Agent cache delete must return boolean');
|
|
145
|
+
return result;
|
|
146
|
+
}),
|
|
147
|
+
clear: (options) => call(async () => {
|
|
148
|
+
const result = await port.clear(partition(options));
|
|
149
|
+
if (result !== undefined)
|
|
150
|
+
throw new TypeError('Agent cache clear must return void');
|
|
151
|
+
}),
|
|
152
|
+
}),
|
|
153
|
+
}
|
|
154
|
+
: {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentCachePort } from '../structs/execution/cache.js';
|
|
1
2
|
import { ModelRunner } from '../model/runner.js';
|
|
2
3
|
import type { ModelRunnerOptions } from '../structs/model/runtime.js';
|
|
3
4
|
import type { ToolRegistry } from '../capabilities/tools.js';
|
|
@@ -5,6 +6,7 @@ import type { AgentCoreEngineOptions, CoreStep } from '../structs/execution/engi
|
|
|
5
6
|
import { AgentWorkflow, AttemptBudget, defineWorkflow } from './workflow.js';
|
|
6
7
|
import type { WorkflowExtension } from '../structs/execution/workflow.js';
|
|
7
8
|
export * from './workflow.js';
|
|
9
|
+
export type * from '../structs/execution/cache.js';
|
|
8
10
|
type InvokeTool = (request: import('../structs/capabilities/tools.js').ToolInvocationRequest) => Promise<import('../structs/invocation.js').ToolResult<unknown>>;
|
|
9
11
|
type ToolBinding = {
|
|
10
12
|
invoke: InvokeTool;
|
|
@@ -109,6 +111,8 @@ export declare class AgentCoreEngine<Result, State = unknown, App = undefined> {
|
|
|
109
111
|
static readonly AttemptBudget: typeof AttemptBudget;
|
|
110
112
|
/** Factory for a validated workflow definition. */
|
|
111
113
|
static readonly createWorkflow: typeof defineWorkflow;
|
|
114
|
+
private readonly cacheMemory;
|
|
115
|
+
private readonly serviceCache?;
|
|
112
116
|
private readonly maxRounds;
|
|
113
117
|
private readonly exhausted;
|
|
114
118
|
private readonly options;
|
|
@@ -264,6 +268,7 @@ export declare class AgentCoreEngine<Result, State = unknown, App = undefined> {
|
|
|
264
268
|
app?: App;
|
|
265
269
|
tools?: ToolBinding;
|
|
266
270
|
model?: AgentModelRuntime;
|
|
271
|
+
agentCache?: AgentCachePort;
|
|
267
272
|
};
|
|
268
273
|
}): AgentCoreEngine<Result, State, App>;
|
|
269
274
|
/**
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Exports: `AgentCoreEngine` and the engine option/identity types.
|
|
11
11
|
*/
|
|
12
|
+
import { EngineCache, cacheContext, cachePort } from './cache.js';
|
|
12
13
|
import { ModelCallBudget, parentModelBudget } from '../model/call-budget.js';
|
|
13
14
|
import { ModelRunner } from '../model/runner.js';
|
|
14
15
|
import { compiledModelBinding, modelBindingId, snapshotModelBinding, } from '../model/binding.js';
|
|
@@ -39,6 +40,8 @@ export class AgentCoreEngine {
|
|
|
39
40
|
static AttemptBudget = AttemptBudget;
|
|
40
41
|
/** Factory for a validated workflow definition. */
|
|
41
42
|
static createWorkflow = defineWorkflow;
|
|
43
|
+
cacheMemory;
|
|
44
|
+
serviceCache;
|
|
42
45
|
maxRounds;
|
|
43
46
|
exhausted;
|
|
44
47
|
options;
|
|
@@ -289,6 +292,8 @@ export class AgentCoreEngine {
|
|
|
289
292
|
breakWhile(!!this.invokeTool && !dependencies?.tools, 'Fork requires a restored tool runtime');
|
|
290
293
|
const engine = new AgentCoreEngine({
|
|
291
294
|
maxRounds: this.maxRounds,
|
|
295
|
+
cache: this.options.cache,
|
|
296
|
+
agentCache: dependencies?.agentCache,
|
|
292
297
|
errors: { exhausted: this.exhausted },
|
|
293
298
|
execution: {
|
|
294
299
|
diagnostics: this.options.execution?.diagnostics,
|
|
@@ -333,6 +338,8 @@ export class AgentCoreEngine {
|
|
|
333
338
|
['maxSteps', 'exhausted', 'signal', 'diagnostics', 'onWorkflowEvent', 'observe'].some((key) => key in settings)) {
|
|
334
339
|
throw new TypeError('Use maxRounds, execution (including observe) and errors');
|
|
335
340
|
}
|
|
341
|
+
this.cacheMemory = new EngineCache(settings.cache);
|
|
342
|
+
this.serviceCache = cachePort(settings.agentCache);
|
|
336
343
|
const execution = settings.execution;
|
|
337
344
|
if (settings.branch !== undefined)
|
|
338
345
|
this.branchIdentity = snapshotBranch(settings.branch);
|
|
@@ -342,6 +349,7 @@ export class AgentCoreEngine {
|
|
|
342
349
|
settings.errors?.exhausted?.bind(settings.errors) ??
|
|
343
350
|
(() => new BudgetError('Agent round budget exhausted'));
|
|
344
351
|
this.options = {
|
|
352
|
+
cache: { ...settings.cache },
|
|
345
353
|
execution: {
|
|
346
354
|
signal: execution?.signal,
|
|
347
355
|
diagnostics: execution?.diagnostics,
|
|
@@ -461,6 +469,7 @@ export class AgentCoreEngine {
|
|
|
461
469
|
phase = 'run';
|
|
462
470
|
signal.throwIfAborted();
|
|
463
471
|
const result = await awaitWithSignal(step(index, {
|
|
472
|
+
...cacheContext(this.cacheMemory, this.serviceCache, '$step', signal),
|
|
464
473
|
signal,
|
|
465
474
|
diagnostics,
|
|
466
475
|
report: reports.report.bind(reports),
|
|
@@ -656,6 +665,7 @@ export class AgentCoreEngine {
|
|
|
656
665
|
});
|
|
657
666
|
const reports = new NodeReports(nodeDiagnostics, signal);
|
|
658
667
|
const context = {
|
|
668
|
+
...cacheContext(this.cacheMemory, this.serviceCache, node.id, signal),
|
|
659
669
|
report: reports.report.bind(reports),
|
|
660
670
|
createSubFlowLogger: reports.createSubFlowLogger.bind(reports),
|
|
661
671
|
roundIndex: round,
|
|
@@ -77,11 +77,8 @@ export class VectorSearch {
|
|
|
77
77
|
(!Number.isFinite(options.minScore) || options.minScore < -1 || options.minScore > 1)), 'Invalid vector query');
|
|
78
78
|
const query = normalize(vector, this.space.dimensions), groups = new Map();
|
|
79
79
|
for (const [id, { document, vector: value }] of this.entries) {
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
// removes the redundant clones without exposing the index.
|
|
83
|
-
const candidate = structuredClone(document);
|
|
84
|
-
const allowed = booleanResult(options.filter(candidate), 'Vector filters must be synchronous', 'Vector filters must return boolean');
|
|
80
|
+
// Each callback receives its own snapshot; readonly types cannot prevent runtime mutation.
|
|
81
|
+
const allowed = booleanResult(options.filter(structuredClone(document)), 'Vector filters must be synchronous', 'Vector filters must return boolean');
|
|
85
82
|
if (!allowed)
|
|
86
83
|
continue;
|
|
87
84
|
let score = 0;
|
|
@@ -90,12 +87,12 @@ export class VectorSearch {
|
|
|
90
87
|
score = Math.max(-1, Math.min(1, score));
|
|
91
88
|
if (score < (options.minScore ?? -1))
|
|
92
89
|
continue;
|
|
93
|
-
const group = synchronous(options.groupBy?.(
|
|
90
|
+
const group = synchronous(options.groupBy?.(structuredClone(document)), 'Vector grouping must be synchronous') ?? id;
|
|
94
91
|
if (typeof group !== 'string')
|
|
95
92
|
throw new TypeError('Vector groups must be strings');
|
|
96
93
|
const prior = groups.get(group);
|
|
97
94
|
if (!prior || score > prior.score || (score === prior.score && id < prior.id))
|
|
98
|
-
groups.set(group, { id, score, document:
|
|
95
|
+
groups.set(group, { id, score, document: structuredClone(document) });
|
|
99
96
|
}
|
|
100
97
|
return [...groups.values()]
|
|
101
98
|
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-partitioned cache contracts. Engine memory is synchronous; service-owned storage is
|
|
3
|
+
* asynchronous and explicitly selects volatile or persistent space. Neither is workflow state.
|
|
4
|
+
*/
|
|
5
|
+
/** Optional Engine memory bounds; TTL is measured from the latest write. */
|
|
6
|
+
export interface EngineCacheOptions {
|
|
7
|
+
readonly maxEntries?: number;
|
|
8
|
+
readonly ttlMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Only reads may name another node. Partition names are not authorization boundaries. */
|
|
11
|
+
export interface CacheReadOptions {
|
|
12
|
+
readonly nodeId?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Synchronous view of the current node's Engine-owned memory. Values retain identity. */
|
|
15
|
+
export interface NodeCache {
|
|
16
|
+
get(key: string, options?: CacheReadOptions): unknown;
|
|
17
|
+
has(key: string): boolean;
|
|
18
|
+
set(key: string, value: unknown): void;
|
|
19
|
+
delete(key: string): boolean;
|
|
20
|
+
clear(): void;
|
|
21
|
+
}
|
|
22
|
+
/** Persistent and volatile storage are independent; there is no implicit fallback or mirroring. */
|
|
23
|
+
export interface AgentCacheOptions {
|
|
24
|
+
readonly persistence?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Exact immutable address passed to the App port, including cancellation for this visit. */
|
|
27
|
+
export interface AgentCacheAddress {
|
|
28
|
+
readonly nodeId: string;
|
|
29
|
+
readonly key: string;
|
|
30
|
+
readonly persistence: boolean;
|
|
31
|
+
readonly signal: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
/** App-owned storage. Capacity, expiry, serialization and user isolation belong to the App. */
|
|
34
|
+
export interface AgentCachePort {
|
|
35
|
+
readonly supportsPersistence?: boolean;
|
|
36
|
+
get(address: AgentCacheAddress): Promise<unknown>;
|
|
37
|
+
has(address: AgentCacheAddress): Promise<boolean>;
|
|
38
|
+
set(address: AgentCacheAddress, value: unknown): Promise<void>;
|
|
39
|
+
delete(address: AgentCacheAddress): Promise<boolean>;
|
|
40
|
+
clear(partition: Omit<AgentCacheAddress, 'key'>): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/** Async node view. Writes and clear can only address the current partition. */
|
|
43
|
+
export interface NodeAgentCache {
|
|
44
|
+
get(key: string, options?: AgentCacheOptions & CacheReadOptions): Promise<unknown>;
|
|
45
|
+
has(key: string, options?: AgentCacheOptions): Promise<boolean>;
|
|
46
|
+
set(key: string, value: unknown, options?: AgentCacheOptions): Promise<void>;
|
|
47
|
+
delete(key: string, options?: AgentCacheOptions): Promise<boolean>;
|
|
48
|
+
clear(options?: AgentCacheOptions): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/** Cache access shared by workflow visits and the low-level $step loop. */
|
|
51
|
+
export interface CacheContext {
|
|
52
|
+
readonly cache: NodeCache;
|
|
53
|
+
readonly agentCache?: NodeAgentCache;
|
|
54
|
+
}
|