@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +55 -0
  3. package/NOTICE +6 -0
  4. package/README.md +8 -0
  5. package/development/docs/README.en.md +70 -0
  6. package/development/docs/README.md +1 -1
  7. package/development/docs/acceptance.en.md +34 -0
  8. package/development/docs/agent-development.en.md +42 -0
  9. package/development/docs/api/README.en.md +62 -0
  10. package/development/docs/api/advanced-agents.en.md +135 -0
  11. package/development/docs/api/context-processing.en.md +110 -0
  12. package/development/docs/api/core.en.md +274 -0
  13. package/development/docs/api/examples.en.md +118 -0
  14. package/development/docs/api/kits.en.md +183 -0
  15. package/development/docs/api/knowledge-answering.en.md +166 -0
  16. package/development/docs/api/mock.en.md +186 -0
  17. package/development/docs/api/observability.en.md +166 -0
  18. package/development/docs/api/reference.en.md +2235 -0
  19. package/development/docs/api/reference.md +290 -257
  20. package/development/docs/api/support.en.md +252 -0
  21. package/development/docs/api/types.en.md +181 -0
  22. package/development/docs/api/workflow.en.md +198 -0
  23. package/development/docs/api/workflow.md +22 -0
  24. package/development/docs/basic-usage.en.md +78 -0
  25. package/development/docs/create-app.en.md +76 -0
  26. package/development/docs/examples/offline-agent.en.md +273 -0
  27. package/development/docs/examples.en.md +36 -0
  28. package/development/docs/flows/knowledge-maintenance.en.md +38 -0
  29. package/development/docs/getting-started.en.md +71 -0
  30. package/development/docs/knowledge.en.md +89 -0
  31. package/development/docs/maintenance.en.md +62 -0
  32. package/development/docs/materials.en.md +38 -0
  33. package/development/docs/mechanisms.en.md +227 -0
  34. package/development/docs/memory-and-learning.en.md +42 -0
  35. package/development/docs/operations.en.md +51 -0
  36. package/development/docs/records.en.md +63 -0
  37. package/development/docs/registration.en.md +63 -0
  38. package/development/skills/agent-core-app/SKILL.md +4 -0
  39. package/development/skills/agent-core-app/scripts/create-app.mjs +19 -0
  40. package/development/skills/agent-core-app/scripts/lib/knowledge.mjs +37 -1
  41. package/development/skills/agent-core-app/scripts/plan.mjs +9 -0
  42. package/development/skills/agent-core-app/scripts/resources.mjs +17 -0
  43. package/dist/cjs/diagnostics/catalog.js +6 -0
  44. package/dist/cjs/execution/cache.js +162 -0
  45. package/dist/cjs/execution/engine.js +10 -0
  46. package/dist/cjs/kits/vector-search.js +4 -7
  47. package/dist/cjs/structs/execution/cache.js +6 -0
  48. package/dist/cjs/support/model/chat-completions.js +1 -1
  49. package/dist/cjs/support/model/completion/default.js +1 -0
  50. package/dist/cjs/support/model/completion/session.js +5 -0
  51. package/dist/esm/diagnostics/catalog.d.ts +6 -0
  52. package/dist/esm/diagnostics/catalog.js +6 -0
  53. package/dist/esm/execution/cache.d.ts +20 -0
  54. package/dist/esm/execution/cache.js +156 -0
  55. package/dist/esm/execution/engine.d.ts +5 -0
  56. package/dist/esm/execution/engine.js +10 -0
  57. package/dist/esm/kits/vector-search.js +4 -7
  58. package/dist/esm/structs/execution/cache.d.ts +54 -0
  59. package/dist/esm/structs/execution/cache.js +5 -0
  60. package/dist/esm/structs/execution/engine.d.ts +6 -1
  61. package/dist/esm/structs/execution/workflow.d.ts +2 -1
  62. package/dist/esm/structs/index.d.ts +1 -0
  63. package/dist/esm/structs/kits/retrieval.d.ts +1 -0
  64. package/dist/esm/structs/kits/search.d.ts +2 -0
  65. package/dist/esm/structs/model/messages.d.ts +4 -0
  66. package/dist/esm/structs/support/model/messages.d.ts +2 -0
  67. package/dist/esm/support/context/nodes.d.ts +16 -0
  68. package/dist/esm/support/context/processing.d.ts +7 -0
  69. package/dist/esm/support/model/chat-completions.js +1 -1
  70. package/dist/esm/support/model/children.d.ts +2 -0
  71. package/dist/esm/support/model/completion/default.d.ts +1 -0
  72. package/dist/esm/support/model/completion/default.js +1 -0
  73. package/dist/esm/support/model/completion/session.d.ts +5 -0
  74. package/dist/esm/support/model/completion/session.js +5 -0
  75. package/dist/esm/support/model/preview-rules.d.ts +6 -0
  76. package/dist/esm/support/model/reply-preview.d.ts +7 -0
  77. package/dist/esm/support/model/strategies.d.ts +4 -0
  78. 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
- // One isolated snapshot per candidate serves filter, grouping and the
84
- // returned hit; filters and groupers are typed readonly, so sharing it
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?.(candidate), 'Vector grouping must be synchronous') ?? id;
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: candidate });
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')
@@ -29,4 +29,5 @@ const policy = {
29
29
  };
30
30
  },
31
31
  };
32
+ /** Frozen default policy: strictness depends only on whether the call declared functions. */
32
33
  exports.defaultCompletionPolicy = Object.freeze(policy);
@@ -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
- // One isolated snapshot per candidate serves filter, grouping and the
81
- // returned hit; filters and groupers are typed readonly, so sharing it
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?.(candidate), 'Vector grouping must be synchronous') ?? id;
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: candidate });
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
+ }
@@ -0,0 +1,5 @@
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
+ export {};