@animalabs/agent-framework 0.6.7 → 0.6.10

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 (54) hide show
  1. package/dist/src/agent.d.ts +5 -34
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +62 -137
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/framework.d.ts +14 -56
  6. package/dist/src/framework.d.ts.map +1 -1
  7. package/dist/src/framework.js +192 -1742
  8. package/dist/src/framework.js.map +1 -1
  9. package/dist/src/modules/workspace/index.d.ts +10 -0
  10. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  11. package/dist/src/modules/workspace/index.js +48 -2
  12. package/dist/src/modules/workspace/index.js.map +1 -1
  13. package/dist/src/modules/workspace/types.d.ts +2 -0
  14. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  15. package/dist/src/modules/workspace/types.js.map +1 -1
  16. package/dist/src/modules/workspace/watcher.d.ts +17 -0
  17. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  18. package/dist/src/modules/workspace/watcher.js +143 -7
  19. package/dist/src/modules/workspace/watcher.js.map +1 -1
  20. package/dist/src/types/agent.d.ts +15 -12
  21. package/dist/src/types/agent.d.ts.map +1 -1
  22. package/dist/test/helpers/mock-membrane.js +1 -1
  23. package/dist/test/helpers/mock-membrane.js.map +1 -1
  24. package/dist/test/mcpl-awareness-barrier.test.js +1 -228
  25. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  26. package/dist/test/refusal-single-call.test.d.ts +2 -0
  27. package/dist/test/refusal-single-call.test.d.ts.map +1 -0
  28. package/dist/test/refusal-single-call.test.js +177 -0
  29. package/dist/test/refusal-single-call.test.js.map +1 -0
  30. package/dist/test/trunk-channel-routing.test.js +5 -1
  31. package/dist/test/trunk-channel-routing.test.js.map +1 -1
  32. package/dist/test/workspace-watcher.test.js +161 -9
  33. package/dist/test/workspace-watcher.test.js.map +1 -1
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +4 -4
  36. package/src/agent.ts +67 -226
  37. package/src/framework.ts +226 -2394
  38. package/src/modules/workspace/index.ts +47 -2
  39. package/src/modules/workspace/types.ts +2 -0
  40. package/src/modules/workspace/watcher.ts +160 -7
  41. package/src/types/agent.ts +17 -12
  42. package/dist/src/canonical-json.d.ts +0 -2
  43. package/dist/src/canonical-json.d.ts.map +0 -1
  44. package/dist/src/canonical-json.js +0 -23
  45. package/dist/src/canonical-json.js.map +0 -1
  46. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts +0 -2
  47. package/dist/test/primary-summary-refusal-fallback-integration.test.d.ts.map +0 -1
  48. package/dist/test/primary-summary-refusal-fallback-integration.test.js +0 -1955
  49. package/dist/test/primary-summary-refusal-fallback-integration.test.js.map +0 -1
  50. package/dist/test/primary-summary-refusal-fallback.test.d.ts +0 -2
  51. package/dist/test/primary-summary-refusal-fallback.test.d.ts.map +0 -1
  52. package/dist/test/primary-summary-refusal-fallback.test.js +0 -753
  53. package/dist/test/primary-summary-refusal-fallback.test.js.map +0 -1
  54. package/src/canonical-json.ts +0 -22
@@ -651,6 +651,15 @@ export class WorkspaceModule implements Module {
651
651
  error: err.message,
652
652
  } as ProcessEvent);
653
653
  },
654
+ onReattach: () => {
655
+ mount.watcherError = null;
656
+ void this.initialScan(name);
657
+ this.ctx?.pushEvent({
658
+ type: 'workspace:watcher-reattached',
659
+ mount: name,
660
+ path: mount.config.path,
661
+ } as ProcessEvent);
662
+ },
654
663
  },
655
664
  );
656
665
  watcher.start();
@@ -1018,6 +1027,30 @@ export class WorkspaceModule implements Module {
1018
1027
  };
1019
1028
  }
1020
1029
 
1030
+ /**
1031
+ * Read a file's raw bytes from a mount (through the Chronicle tree, synced
1032
+ * first like the `read` tool). Public API for the framework's synthesized
1033
+ * `read_image` tool and other peer callers that need binary content.
1034
+ */
1035
+ async readBinary(
1036
+ mountPrefixedPath: string,
1037
+ ): Promise<{ data: Buffer } | { error: string }> {
1038
+ let mount: MountState;
1039
+ let relativePath: string;
1040
+ try {
1041
+ ({ mount, relativePath } = this.parsePath(mountPrefixedPath));
1042
+ } catch (error) {
1043
+ return { error: error instanceof Error ? error.message : String(error) };
1044
+ }
1045
+ const store = this.getStore();
1046
+ await this.ensureSynced(mount, relativePath);
1047
+ const entry = store.treeGet(mount.treeStateId, relativePath);
1048
+ if (!entry) return { error: `File not found: ${mountPrefixedPath}` };
1049
+ const blob = store.getBlob(entry.blobHash);
1050
+ if (!blob) return { error: `Blob not found for: ${mountPrefixedPath}` };
1051
+ return { data: blob };
1052
+ }
1053
+
1021
1054
  /**
1022
1055
  * Parse a mount-prefixed path into (mountName, relativePath).
1023
1056
  */
@@ -1662,8 +1695,20 @@ export class WorkspaceModule implements Module {
1662
1695
  mount.initialSyncDone = true;
1663
1696
  }
1664
1697
 
1665
- const prefix = relativePath ? relativePath + '/' : undefined;
1666
- const entries = store.treeList(mount.treeStateId, prefix);
1698
+ // If `path` points at a single FILE, grep just that file. Otherwise treat
1699
+ // `path` as a directory prefix (the original behaviour). Previously a file
1700
+ // path produced an empty prefix like "notes.md/" and matched nothing, so
1701
+ // grepping a specific file silently returned zero results.
1702
+ let entries: Array<{ path: string; blobHash: string }>;
1703
+ const fileNode = relativePath
1704
+ ? (store.treeGet(mount.treeStateId, relativePath) as { blobHash?: string } | null)
1705
+ : null;
1706
+ if (fileNode && fileNode.blobHash) {
1707
+ entries = [{ path: relativePath, blobHash: fileNode.blobHash }];
1708
+ } else {
1709
+ const prefix = relativePath ? relativePath + '/' : undefined;
1710
+ entries = store.treeList(mount.treeStateId, prefix);
1711
+ }
1667
1712
 
1668
1713
  for (const entry of entries) {
1669
1714
  if (fileGlob && !fileGlob.test(entry.path)) continue;
@@ -25,6 +25,8 @@ export interface MountConfig {
25
25
  watch?: 'always' | 'on-agent-action' | 'never';
26
26
  /** Debounce window in ms for watch: 'always' mode (default: 300) */
27
27
  watchDebounceMs?: number;
28
+ /** Poll interval in ms for checking watched root identity changes (default: 2000) */
29
+ watchRootPollMs?: number;
28
30
  /**
29
31
  * Simple ignore patterns (NOT full gitignore syntax). Supported forms:
30
32
  * - Exact name match: ".git", "node_modules"
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import { watch, type FSWatcher } from 'chokidar';
9
- import { mkdirSync } from 'node:fs';
9
+ import { mkdirSync, statSync } from 'node:fs';
10
+ import { basename } from 'node:path';
10
11
  import { type MountConfig } from './types.js';
11
12
 
12
13
  export type FsOp = 'created' | 'modified' | 'deleted';
@@ -30,6 +31,14 @@ export interface WatcherLifecycle {
30
31
  onReady?: () => void;
31
32
  /** Fires on any chokidar-reported error (ENOENT, EACCES, platform faults). */
32
33
  onError?: (err: Error) => void;
34
+ /** Fires after the watcher re-attaches to a replaced root inode. */
35
+ onReattach?: () => void;
36
+ }
37
+
38
+ interface RootIdentity {
39
+ dev: number;
40
+ ino: number;
41
+ birthtimeMs: number;
33
42
  }
34
43
 
35
44
  /**
@@ -42,8 +51,13 @@ export class MountWatcher {
42
51
  // create -> modify collapses to 'created' (single batch); delete always wins.
43
52
  private pendingChanges = new Map<string, FsOp>();
44
53
  private debounceTimer: ReturnType<typeof setTimeout> | null = null;
54
+ private rootPollTimer: ReturnType<typeof setInterval> | null = null;
45
55
  private suppressedPaths = new Set<string>();
46
56
  private suppressionTimers = new Map<string, ReturnType<typeof setTimeout>>();
57
+ private rootIdentity: RootIdentity | null = null;
58
+ private detached = false;
59
+ private attaching = false;
60
+ private stopped = true;
47
61
  private readonly debounceMs: number;
48
62
  private readonly config: MountConfig;
49
63
  private readonly onChangeCallback: (changes: FsChange[]) => void;
@@ -64,8 +78,14 @@ export class MountWatcher {
64
78
  * Start watching the filesystem.
65
79
  */
66
80
  start(): void {
67
- if (this.watcher) return;
81
+ if (this.watcher || this.rootPollTimer) return;
82
+
83
+ this.stopped = false;
84
+ this.attach();
85
+ this.startRootPolling();
86
+ }
68
87
 
88
+ private attach(): void {
69
89
  // Read-write mounts: ensure the path exists before chokidar attaches.
70
90
  // chokidar 4's fs.watch-based backend silently fails to fire events when
71
91
  // the watched path OR any of its parent directories don't exist at
@@ -110,21 +130,39 @@ export class MountWatcher {
110
130
  this.watcher.on('add', handleEvent('created'));
111
131
  this.watcher.on('change', handleEvent('modified'));
112
132
  this.watcher.on('unlink', handleEvent('deleted'));
133
+ this.watcher.on('unlinkDir', (dirPath) => {
134
+ if (this.isRootPath(dirPath)) {
135
+ this.detached = true;
136
+ }
137
+ });
113
138
 
114
139
  if (this.lifecycle.onReady) {
115
140
  this.watcher.once('ready', this.lifecycle.onReady);
116
141
  }
117
- if (this.lifecycle.onError) {
118
- this.watcher.on('error', (err) => {
119
- this.lifecycle.onError?.(err instanceof Error ? err : new Error(String(err)));
120
- });
121
- }
142
+ this.watcher.on('error', (err) => {
143
+ this.reportError(err);
144
+ });
145
+ this.watcher.on('raw', (event: string, rawPath: string, _details: unknown) => {
146
+ if (event !== 'rename') return;
147
+ // A rename on the watched root's path/basename is the one signal that
148
+ // survives inode reuse on Linux, where unlinkDir/error never fire.
149
+ if (this.isRootPath(rawPath) || basename(rawPath) === basename(this.config.path)) {
150
+ void this.checkRootLiveness({ force: true });
151
+ }
152
+ });
153
+
154
+ this.captureRootIdentity();
122
155
  }
123
156
 
124
157
  /**
125
158
  * Stop watching.
126
159
  */
127
160
  async stop(): Promise<void> {
161
+ this.stopped = true;
162
+ if (this.rootPollTimer) {
163
+ clearInterval(this.rootPollTimer);
164
+ this.rootPollTimer = null;
165
+ }
128
166
  if (this.debounceTimer) {
129
167
  clearTimeout(this.debounceTimer);
130
168
  this.debounceTimer = null;
@@ -140,6 +178,9 @@ export class MountWatcher {
140
178
  await this.watcher.close();
141
179
  this.watcher = null;
142
180
  }
181
+ this.rootIdentity = null;
182
+ this.detached = false;
183
+ this.attaching = false;
143
184
  }
144
185
 
145
186
  /**
@@ -219,6 +260,108 @@ export class MountWatcher {
219
260
  }, this.debounceMs);
220
261
  }
221
262
 
263
+ private startRootPolling(): void {
264
+ const pollMs = this.config.watchRootPollMs ?? 2000;
265
+ this.rootPollTimer = setInterval(() => {
266
+ void this.checkRootLiveness();
267
+ }, pollMs);
268
+ this.rootPollTimer.unref();
269
+ }
270
+
271
+ private async checkRootLiveness(opts: { force?: boolean } = {}): Promise<void> {
272
+ if (this.stopped || this.attaching) return;
273
+
274
+ let current: RootIdentity;
275
+ try {
276
+ current = this.readRootIdentity();
277
+ } catch (err) {
278
+ if (this.isErrno(err, 'ENOENT')) {
279
+ this.detached = true;
280
+ if (this.config.mode === 'read-write') {
281
+ await this.reattach();
282
+ }
283
+ return;
284
+ }
285
+ this.reportError(err);
286
+ if (opts.force) {
287
+ this.detached = true;
288
+ await this.reattach();
289
+ }
290
+ return;
291
+ }
292
+
293
+ if (!this.rootIdentity && !this.detached) {
294
+ await this.reattach();
295
+ return;
296
+ }
297
+
298
+ if (!opts.force && !this.detached && this.rootIdentity
299
+ && this.sameIdentity(current, this.rootIdentity)) {
300
+ return;
301
+ }
302
+
303
+ await this.reattach();
304
+ }
305
+
306
+ private async reattach(): Promise<void> {
307
+ if (this.attaching || this.stopped) return;
308
+
309
+ this.attaching = true;
310
+ try {
311
+ const oldWatcher = this.watcher;
312
+ this.watcher = null;
313
+ if (oldWatcher) {
314
+ await oldWatcher.close();
315
+ }
316
+ if (this.stopped) return;
317
+
318
+ this.attach();
319
+ if (!this.detached && this.rootIdentity) {
320
+ this.lifecycle.onReattach?.();
321
+ }
322
+ } catch (err) {
323
+ this.reportError(err);
324
+ } finally {
325
+ this.attaching = false;
326
+ }
327
+ }
328
+
329
+ private captureRootIdentity(): void {
330
+ try {
331
+ this.rootIdentity = this.readRootIdentity();
332
+ this.detached = false;
333
+ } catch (err) {
334
+ if (this.isErrno(err, 'ENOENT')) {
335
+ this.rootIdentity = null;
336
+ this.detached = true;
337
+ return;
338
+ }
339
+ this.rootIdentity = null;
340
+ this.detached = false;
341
+ this.reportError(err);
342
+ }
343
+ }
344
+
345
+ private readRootIdentity(): RootIdentity {
346
+ const stat = statSync(this.config.path);
347
+ return { dev: stat.dev, ino: stat.ino, birthtimeMs: stat.birthtimeMs };
348
+ }
349
+
350
+ private sameIdentity(a: RootIdentity, b: RootIdentity): boolean {
351
+ // birthtimeMs is only a secondary tripwire: sub-millisecond replacements
352
+ // can reuse both the inode and timestamp, so raw rename remains essential.
353
+ return a.dev === b.dev && a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
354
+ }
355
+
356
+ private isErrno(err: unknown, code: string): boolean {
357
+ return typeof err === 'object' && err !== null && 'code' in err
358
+ && (err as NodeJS.ErrnoException).code === code;
359
+ }
360
+
361
+ private reportError(err: unknown): void {
362
+ this.lifecycle.onError?.(err instanceof Error ? err : new Error(String(err)));
363
+ }
364
+
222
365
  private toRelative(absolutePath: string): string | null {
223
366
  const base = this.config.path.endsWith('/')
224
367
  ? this.config.path
@@ -228,4 +371,14 @@ export class MountWatcher {
228
371
  }
229
372
  return null;
230
373
  }
374
+
375
+ private isRootPath(absolutePath: string): boolean {
376
+ const root = this.config.path.endsWith('/')
377
+ ? this.config.path.slice(0, -1)
378
+ : this.config.path;
379
+ const candidate = absolutePath.endsWith('/')
380
+ ? absolutePath.slice(0, -1)
381
+ : absolutePath;
382
+ return candidate === root;
383
+ }
231
384
  }
@@ -68,6 +68,23 @@ export interface AgentConfig {
68
68
  */
69
69
  cacheTtl?: '5m' | '1h';
70
70
 
71
+ /**
72
+ * Whether to emit prompt-cache markers (cache_control) on requests.
73
+ * Default true (Anthropic API). Set false for transports that reject
74
+ * cache_control — e.g. Bedrock legacy Claude models, which fail with
75
+ * "your request did not allow prompt caching".
76
+ */
77
+ promptCaching?: boolean;
78
+
79
+ /**
80
+ * Prefill-formatter support: a synthetic user message appended after the
81
+ * conversation (e.g. chapterx's `<cmd>cat untitled.txt</cmd>` CLI-sim
82
+ * scaffold). Forwarded verbatim to membrane's prefill formatters; ignored
83
+ * by native formatting. Part of reproducing a prefill-era bot's exact
84
+ * prompting structure when migrating it into a resident.
85
+ */
86
+ prefillUserMessage?: string;
87
+
71
88
  /** Provider-specific request parameters (for example Responses reasoning
72
89
  * and server-side compaction settings). */
73
90
  providerParams?: Record<string, unknown>;
@@ -115,18 +132,6 @@ export interface AgentConfig {
115
132
  * than dropping it silently. Default true.
116
133
  */
117
134
  announceHumanTurns?: boolean;
118
- /**
119
- * Primary-lane autobiographical summary refusal fallback. Disabled by
120
- * default: when enabled, the framework may compare the refused primary
121
- * request against the latest compatible healthy request on the same branch
122
- * generation, expand newly admitted summary pairs back to exact raw
123
- * source, and issue at most one retry.
124
- */
125
- primarySummaryFallback?: {
126
- enabled?: boolean;
127
- maxNewSummaries?: number;
128
- requestBudgetTokens?: number;
129
- };
130
135
  };
131
136
  }
132
137
 
@@ -1,2 +0,0 @@
1
- export declare function canonicalJsonStringify(value: unknown): string;
2
- //# sourceMappingURL=canonical-json.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"canonical-json.d.ts","sourceRoot":"","sources":["../../src/canonical-json.ts"],"names":[],"mappings":"AAmBA,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE7D"}
@@ -1,23 +0,0 @@
1
- function normalizeCanonicalJson(value) {
2
- if (Array.isArray(value)) {
3
- return value.map((entry) => normalizeCanonicalJson(entry));
4
- }
5
- if (value && typeof value === 'object') {
6
- const withToJson = value;
7
- if (typeof withToJson.toJSON === 'function') {
8
- return normalizeCanonicalJson(withToJson.toJSON());
9
- }
10
- const normalized = {};
11
- for (const key of Object.keys(value).sort()) {
12
- const child = normalizeCanonicalJson(value[key]);
13
- if (child !== undefined)
14
- normalized[key] = child;
15
- }
16
- return normalized;
17
- }
18
- return value;
19
- }
20
- export function canonicalJsonStringify(value) {
21
- return JSON.stringify(normalizeCanonicalJson(value)) ?? 'null';
22
- }
23
- //# sourceMappingURL=canonical-json.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"canonical-json.js","sourceRoot":"","sources":["../../src/canonical-json.ts"],"names":[],"mappings":"AAAA,SAAS,sBAAsB,CAAC,KAAc;IAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7D,CAAC;IACD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,MAAM,UAAU,GAAG,KAAmC,CAAC;QACvD,IAAI,OAAO,UAAU,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAC5C,OAAO,sBAAsB,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,UAAU,GAA4B,EAAE,CAAC;QAC/C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAgC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YACvE,MAAM,KAAK,GAAG,sBAAsB,CAAE,KAAiC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC9E,IAAI,KAAK,KAAK,SAAS;gBAAE,UAAU,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACnD,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,OAAO,IAAI,CAAC,SAAS,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC;AACjE,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=primary-summary-refusal-fallback-integration.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"primary-summary-refusal-fallback-integration.test.d.ts","sourceRoot":"","sources":["../../test/primary-summary-refusal-fallback-integration.test.ts"],"names":[],"mappings":""}