@animalabs/agent-framework 0.22.4 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -3
- package/dist/src/agent.d.ts +36 -10
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +76 -32
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/index.d.ts +1 -1
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +34 -2
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +72 -16
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +3 -2
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +47 -7
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +12 -7
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +59 -11
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/config-checks.d.ts +2 -2
- package/dist/src/config-checks.d.ts.map +1 -1
- package/dist/src/config-checks.js +6 -2
- package/dist/src/config-checks.js.map +1 -1
- package/dist/src/framework.d.ts +744 -120
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3546 -906
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +13 -6
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -0
- package/dist/src/held-writes.d.ts.map +1 -0
- package/dist/src/held-writes.js +56 -0
- package/dist/src/held-writes.js.map +1 -0
- package/dist/src/index.d.ts +7 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +4 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/capability-grant.js +1 -1
- package/dist/src/mcpl/capability-grant.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +95 -18
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +578 -150
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +2 -2
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +6 -7
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +5 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +4 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +6 -7
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +7 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +9 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +227 -0
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
- package/dist/src/mcpl/modern-connection.js +1088 -0
- package/dist/src/mcpl/modern-connection.js.map +1 -0
- package/dist/src/mcpl/prefix-rule.d.ts +18 -0
- package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
- package/dist/src/mcpl/prefix-rule.js +25 -0
- package/dist/src/mcpl/prefix-rule.js.map +1 -0
- package/dist/src/mcpl/protocol-family.d.ts +179 -0
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
- package/dist/src/mcpl/protocol-family.js +697 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -0
- package/dist/src/mcpl/push-coalescer.d.ts +38 -19
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +52 -23
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +66 -15
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +243 -76
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +63 -18
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +117 -32
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +326 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +995 -212
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +25 -5
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +51 -12
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.js +232 -0
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +56 -13
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +206 -70
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +66 -20
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +2 -1
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +2 -1
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +16 -6
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +65 -6
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +3 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts +42 -2
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +268 -65
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +27 -5
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +366 -76
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/refusal.d.ts +14 -0
- package/dist/src/modules/discord/refusal.d.ts.map +1 -0
- package/dist/src/modules/discord/refusal.js +14 -0
- package/dist/src/modules/discord/refusal.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +17 -7
- package/dist/src/modules/discord/types.d.ts.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js +6 -2
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +25 -2
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +45 -5
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +2 -1
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +19 -9
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +214 -42
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +667 -113
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +240 -8
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +554 -24
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +10 -1
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +42 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +130 -25
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +112 -39
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +100 -29
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/tool-args.d.ts +40 -0
- package/dist/src/tool-args.d.ts.map +1 -0
- package/dist/src/tool-args.js +207 -0
- package/dist/src/tool-args.js.map +1 -0
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +9 -2
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/types/agent.d.ts +27 -17
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +8 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +17 -2
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +54 -10
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +2 -2
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/agent.ts +99 -36
- package/src/code-execution/index.ts +1 -0
- package/src/code-execution/py-runner.ts +104 -17
- package/src/code-execution/runtime-py.ts +47 -7
- package/src/code-execution/tool-definition.ts +65 -11
- package/src/config-checks.ts +4 -2
- package/src/framework.ts +3633 -956
- package/src/gate/event-gate.ts +14 -6
- package/src/held-writes.ts +59 -0
- package/src/index.ts +19 -4
- package/src/mcpl/capability-grant.ts +1 -1
- package/src/mcpl/channel-registry.ts +613 -160
- package/src/mcpl/convert-block.ts +15 -7
- package/src/mcpl/index.ts +14 -1
- package/src/mcpl/inference-router.ts +8 -7
- package/src/mcpl/log-value.ts +10 -0
- package/src/mcpl/modern-connection.ts +1182 -0
- package/src/mcpl/prefix-rule.ts +32 -0
- package/src/mcpl/protocol-family.ts +691 -0
- package/src/mcpl/push-coalescer.ts +67 -29
- package/src/mcpl/reference-fetcher.ts +288 -82
- package/src/mcpl/references.ts +123 -33
- package/src/mcpl/server-connection.ts +1050 -221
- package/src/mcpl/server-registry.ts +50 -12
- package/src/mcpl/tool-result-normalize.ts +294 -0
- package/src/mcpl/transport.ts +204 -62
- package/src/mcpl/types.ts +67 -20
- package/src/mcpl/visible-content.ts +2 -1
- package/src/module-registry.ts +68 -13
- package/src/modules/api/index.ts +3 -2
- package/src/modules/discord/discord-js-client.ts +288 -72
- package/src/modules/discord/index.ts +380 -77
- package/src/modules/discord/refusal.ts +13 -0
- package/src/modules/discord/types.ts +17 -9
- package/src/modules/health/index.ts +6 -2
- package/src/modules/index.ts +1 -1
- package/src/modules/workspace/disk-agreement.ts +46 -5
- package/src/modules/workspace/effects.ts +18 -7
- package/src/modules/workspace/index.ts +722 -124
- package/src/modules/workspace/observe.ts +591 -22
- package/src/modules/workspace/types.ts +10 -1
- package/src/modules/workspace/watcher.ts +126 -25
- package/src/silent-heartbeat.ts +184 -59
- package/src/tool-args.ts +213 -0
- package/src/tool-presentation.ts +9 -2
- package/src/types/agent.ts +27 -17
- package/src/types/events.ts +8 -0
- package/src/types/framework.ts +17 -2
- package/src/types/module.ts +55 -10
- package/src/types/trace.ts +2 -2
|
@@ -45,7 +45,16 @@ export interface MountConfig {
|
|
|
45
45
|
* `^\..+\.sw[px]$`, `~$` and `\.subl.*\.tmp$`.
|
|
46
46
|
*/
|
|
47
47
|
ignore?: string[];
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Whether to follow symlinks (default: false). Without it, no symlink on a
|
|
50
|
+
* path is followed: not one at the path itself, and not one among the
|
|
51
|
+
* directories on its way beneath the mount's root, wherever it leads. A path
|
|
52
|
+
* with a symlink among its directories is refused naming the link, and for
|
|
53
|
+
* a link inside the mount, its target. A mount root that is
|
|
54
|
+
* itself a symlink is still the root. With it, a symlink on a path is
|
|
55
|
+
* followed while what it reaches stays inside the mount. Either way, a
|
|
56
|
+
* listing or a sync's walk descends into no symlinked directory.
|
|
57
|
+
*/
|
|
49
58
|
followSymlinks?: boolean;
|
|
50
59
|
/** Maximum file size in bytes (default: 5MB) */
|
|
51
60
|
maxFileSize?: number;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { watch, type FSWatcher } from 'chokidar';
|
|
9
|
-
import { mkdirSync, statSync } from 'node:fs';
|
|
10
|
-
import { basename, sep } from 'node:path';
|
|
9
|
+
import { closeSync, constants, fstatSync, mkdirSync, openSync, statSync, type Stats } from 'node:fs';
|
|
10
|
+
import { basename, resolve, sep } from 'node:path';
|
|
11
11
|
import { passedOverAt } from './observe.js';
|
|
12
12
|
import { type MountConfig } from './types.js';
|
|
13
13
|
|
|
@@ -43,6 +43,8 @@ interface RootIdentity {
|
|
|
43
43
|
birthtimeMs: number;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
const identityOf = (stat: Stats): RootIdentity => ({ dev: stat.dev, ino: stat.ino, birthtimeMs: stat.birthtimeMs });
|
|
47
|
+
|
|
46
48
|
/**
|
|
47
49
|
* Manages filesystem watching for a single mount.
|
|
48
50
|
* Handles debouncing, ignore patterns, and write suppression.
|
|
@@ -58,11 +60,20 @@ export class MountWatcher {
|
|
|
58
60
|
private suppressedPaths = new Set<string>();
|
|
59
61
|
private suppressionTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
60
62
|
private rootIdentity: RootIdentity | null = null;
|
|
63
|
+
// The root as last attached, held open on Linux (see holdRoot).
|
|
64
|
+
private rootFd: number | null = null;
|
|
61
65
|
private detached = false;
|
|
62
66
|
private attaching = false;
|
|
63
67
|
private stopped = true;
|
|
64
68
|
private readonly debounceMs: number;
|
|
65
69
|
private readonly config: MountConfig;
|
|
70
|
+
/**
|
|
71
|
+
* The root as the watcher makes, holds, watches and checks it, and as each
|
|
72
|
+
* event's path is read against: the configured path resolved (a link in it
|
|
73
|
+
* kept), since chokidar hands back the paths it reports normalized, and
|
|
74
|
+
* those of a root configured as `./m` matched none of them.
|
|
75
|
+
*/
|
|
76
|
+
private readonly root: string;
|
|
66
77
|
private readonly onChangeCallback: (changes: FsChange[]) => void;
|
|
67
78
|
private readonly lifecycle: WatcherLifecycle;
|
|
68
79
|
|
|
@@ -74,6 +85,7 @@ export class MountWatcher {
|
|
|
74
85
|
private readonly others: readonly string[] = [],
|
|
75
86
|
) {
|
|
76
87
|
this.config = config;
|
|
88
|
+
this.root = resolve(config.path);
|
|
77
89
|
this.debounceMs = config.watchDebounceMs ?? 300;
|
|
78
90
|
this.onChangeCallback = onChange;
|
|
79
91
|
this.lifecycle = lifecycle;
|
|
@@ -100,7 +112,7 @@ export class MountWatcher {
|
|
|
100
112
|
// directories a user asked us only to read would be surprising.
|
|
101
113
|
if (this.config.mode === 'read-write') {
|
|
102
114
|
try {
|
|
103
|
-
mkdirSync(this.
|
|
115
|
+
mkdirSync(this.root, { recursive: true });
|
|
104
116
|
} catch (err) {
|
|
105
117
|
this.lifecycle.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
106
118
|
}
|
|
@@ -108,7 +120,7 @@ export class MountWatcher {
|
|
|
108
120
|
|
|
109
121
|
const ignore = this.config.ignore ?? [];
|
|
110
122
|
|
|
111
|
-
this.watcher = watch(this.
|
|
123
|
+
this.watcher = watch(this.root, {
|
|
112
124
|
// What a walk passes over by design, by the path relative to the mount:
|
|
113
125
|
// another mount's directory, the mount's ignore list and editors' swap
|
|
114
126
|
// and backup files. chokidar 4
|
|
@@ -165,13 +177,19 @@ export class MountWatcher {
|
|
|
165
177
|
this.watcher.on('error', (err) => {
|
|
166
178
|
this.reportError(err);
|
|
167
179
|
});
|
|
168
|
-
this.watcher.on('raw', (event: string, rawPath: string
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
this.watcher.on('raw', (event: string, rawPath: string | null) => {
|
|
181
|
+
// libuv reports the root's own move as a rename under the root's name,
|
|
182
|
+
// and its removal too once nothing holds it open. An entry beneath the
|
|
183
|
+
// root with that name comes the same way, as does, on macOS, an
|
|
184
|
+
// FSEvents event for the root that carries no modification flags, such
|
|
185
|
+
// as its making just before the watch. A held root tells these apart
|
|
186
|
+
// (checkRootLiveness), so the rename runs the check at once. A root
|
|
187
|
+
// that isn't held can be made again with the removed one's identity,
|
|
188
|
+
// so there the rename forces a re-attach. A rename without a name, as
|
|
189
|
+
// libuv gives for a file whose path it can't read, is passed over.
|
|
190
|
+
if (event !== 'rename' || typeof rawPath !== 'string') return;
|
|
191
|
+
if (basename(rawPath) !== basename(this.root)) return;
|
|
192
|
+
void this.checkRootLiveness({ force: this.rootFd === null });
|
|
175
193
|
});
|
|
176
194
|
|
|
177
195
|
this.captureRootIdentity();
|
|
@@ -196,6 +214,8 @@ export class MountWatcher {
|
|
|
196
214
|
this.suppressionTimers.clear();
|
|
197
215
|
this.suppressedPaths.clear();
|
|
198
216
|
this.pendingChanges.clear();
|
|
217
|
+
// Before the close, so a close that fails still lets go of the root.
|
|
218
|
+
this.releaseRoot();
|
|
199
219
|
|
|
200
220
|
if (this.watcher) {
|
|
201
221
|
await this.watcher.close();
|
|
@@ -291,6 +311,15 @@ export class MountWatcher {
|
|
|
291
311
|
this.rootPollTimer.unref();
|
|
292
312
|
}
|
|
293
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Whether the root is still the directory the watcher attached to, checked
|
|
316
|
+
* every `watchRootPollMs` and at a rename under the root's name. A root
|
|
317
|
+
* replaced at its path, or whose held directory has been removed, is
|
|
318
|
+
* attached to again, and with `force` so is one that looks unchanged. A
|
|
319
|
+
* root that's gone is let go of: a read-write mount makes it again and
|
|
320
|
+
* attaches to it, and a read-only one waits, detached, until a check finds
|
|
321
|
+
* it back.
|
|
322
|
+
*/
|
|
294
323
|
private async checkRootLiveness(opts: { force?: boolean } = {}): Promise<void> {
|
|
295
324
|
if (this.stopped || this.attaching) return;
|
|
296
325
|
|
|
@@ -298,8 +327,10 @@ export class MountWatcher {
|
|
|
298
327
|
try {
|
|
299
328
|
current = this.readRootIdentity();
|
|
300
329
|
} catch (err) {
|
|
301
|
-
|
|
330
|
+
// A held root that has been removed is gone, whatever the path's error.
|
|
331
|
+
if (this.isErrno(err, 'ENOENT') || this.heldRootRemoved()) {
|
|
302
332
|
this.detached = true;
|
|
333
|
+
this.releaseRoot();
|
|
303
334
|
if (this.config.mode === 'read-write') {
|
|
304
335
|
await this.reattach();
|
|
305
336
|
}
|
|
@@ -319,7 +350,7 @@ export class MountWatcher {
|
|
|
319
350
|
}
|
|
320
351
|
|
|
321
352
|
if (!opts.force && !this.detached && this.rootIdentity
|
|
322
|
-
&& this.sameIdentity(current, this.rootIdentity)) {
|
|
353
|
+
&& this.sameIdentity(current, this.rootIdentity) && !this.heldRootRemoved()) {
|
|
323
354
|
return;
|
|
324
355
|
}
|
|
325
356
|
|
|
@@ -333,8 +364,15 @@ export class MountWatcher {
|
|
|
333
364
|
try {
|
|
334
365
|
const oldWatcher = this.watcher;
|
|
335
366
|
this.watcher = null;
|
|
367
|
+
this.releaseRoot();
|
|
336
368
|
if (oldWatcher) {
|
|
337
|
-
|
|
369
|
+
try {
|
|
370
|
+
await oldWatcher.close();
|
|
371
|
+
} catch (err) {
|
|
372
|
+
// chokidar removes its listeners before it closes, so the old
|
|
373
|
+
// instance reports nothing more, and the root is attached to again.
|
|
374
|
+
this.reportError(err);
|
|
375
|
+
}
|
|
338
376
|
}
|
|
339
377
|
if (this.stopped) return;
|
|
340
378
|
|
|
@@ -351,9 +389,11 @@ export class MountWatcher {
|
|
|
351
389
|
|
|
352
390
|
private captureRootIdentity(): void {
|
|
353
391
|
try {
|
|
354
|
-
this.rootIdentity = this.
|
|
392
|
+
this.rootIdentity = this.holdRoot();
|
|
355
393
|
this.detached = false;
|
|
356
394
|
} catch (err) {
|
|
395
|
+
// What holdRoot opened before it failed isn't held.
|
|
396
|
+
this.releaseRoot();
|
|
357
397
|
if (this.isErrno(err, 'ENOENT')) {
|
|
358
398
|
this.rootIdentity = null;
|
|
359
399
|
this.detached = true;
|
|
@@ -365,14 +405,79 @@ export class MountWatcher {
|
|
|
365
405
|
}
|
|
366
406
|
}
|
|
367
407
|
|
|
408
|
+
/**
|
|
409
|
+
* The root's identity, with the root held open on Linux until the watcher
|
|
410
|
+
* lets go of it: at a re-attach, when the root is found gone, or at the
|
|
411
|
+
* stop. The held directory then says itself whether it has been removed
|
|
412
|
+
* (heldRootRemoved). A removed directory's inode number can otherwise go
|
|
413
|
+
* to the next directory made, as ext4 gives it, and one held open isn't
|
|
414
|
+
* given out. Elsewhere, or where the root can't be opened as a directory,
|
|
415
|
+
* as without read permission, at the process's file limit or when the
|
|
416
|
+
* path isn't a directory, it isn't held, and its identity is read from its
|
|
417
|
+
* path. So too where the root's link count reads 0 as it's opened: if
|
|
418
|
+
* that's the file system's count for a directory in place, a later check
|
|
419
|
+
* would take the root for removed and re-attach at every poll.
|
|
420
|
+
*/
|
|
421
|
+
private holdRoot(): RootIdentity {
|
|
422
|
+
if (process.platform !== 'linux') return this.readRootIdentity();
|
|
423
|
+
let fd: number;
|
|
424
|
+
try {
|
|
425
|
+
// A directory only, so a FIFO at the path fails at once rather than
|
|
426
|
+
// blocking until a writer opens it.
|
|
427
|
+
fd = openSync(this.root, constants.O_RDONLY | constants.O_DIRECTORY);
|
|
428
|
+
} catch {
|
|
429
|
+
return this.readRootIdentity();
|
|
430
|
+
}
|
|
431
|
+
this.rootFd = fd;
|
|
432
|
+
const stats = fstatSync(fd);
|
|
433
|
+
if (stats.nlink === 0) {
|
|
434
|
+
this.releaseRoot();
|
|
435
|
+
return this.readRootIdentity();
|
|
436
|
+
}
|
|
437
|
+
return identityOf(stats);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Whether the directory the watcher holds has been removed, which its link
|
|
442
|
+
* count says whatever has since been made at its path. On a file system
|
|
443
|
+
* whose inode numbers a server gives out, the hold can't keep a root made
|
|
444
|
+
* again from taking the removed one's number, and a removal by another
|
|
445
|
+
* client shows once this one revalidates the directory's cached
|
|
446
|
+
* attributes. A held directory that can't be read, as a stale one on a
|
|
447
|
+
* network file system, counts as removed.
|
|
448
|
+
*/
|
|
449
|
+
private heldRootRemoved(): boolean {
|
|
450
|
+
if (this.rootFd === null) return false;
|
|
451
|
+
try {
|
|
452
|
+
return fstatSync(this.rootFd).nlink === 0;
|
|
453
|
+
} catch {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private releaseRoot(): void {
|
|
459
|
+
if (this.rootFd === null) return;
|
|
460
|
+
const fd = this.rootFd;
|
|
461
|
+
this.rootFd = null;
|
|
462
|
+
try {
|
|
463
|
+
closeSync(fd);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
// Linux frees the descriptor whatever close returns, so the error is
|
|
466
|
+
// reported and what called this goes on.
|
|
467
|
+
this.reportError(err);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
368
471
|
private readRootIdentity(): RootIdentity {
|
|
369
|
-
|
|
370
|
-
return { dev: stat.dev, ino: stat.ino, birthtimeMs: stat.birthtimeMs };
|
|
472
|
+
return identityOf(statSync(this.root));
|
|
371
473
|
}
|
|
372
474
|
|
|
373
475
|
private sameIdentity(a: RootIdentity, b: RootIdentity): boolean {
|
|
374
|
-
// birthtimeMs
|
|
375
|
-
//
|
|
476
|
+
// birthtimeMs tells a root made again from a removed one whose inode
|
|
477
|
+
// number it took, unless both were made within one tick of the clock or
|
|
478
|
+
// the file system keeps no birth time. A held root's removal shows in its
|
|
479
|
+
// link count instead, and where the root isn't held the rename's force
|
|
480
|
+
// covers those.
|
|
376
481
|
return a.dev === b.dev && a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
|
|
377
482
|
}
|
|
378
483
|
|
|
@@ -389,9 +494,7 @@ export class MountWatcher {
|
|
|
389
494
|
// Chokidar hands back platform-separated absolute paths; compare with
|
|
390
495
|
// the platform separator and normalize the result to the '/'-separated
|
|
391
496
|
// logical form the rest of the workspace uses. No-op on POSIX.
|
|
392
|
-
const base = this.
|
|
393
|
-
? this.config.path
|
|
394
|
-
: this.config.path + sep;
|
|
497
|
+
const base = this.root.endsWith(sep) ? this.root : this.root + sep;
|
|
395
498
|
if (absolutePath.startsWith(base)) {
|
|
396
499
|
return absolutePath.slice(base.length).split(sep).join('/');
|
|
397
500
|
}
|
|
@@ -399,9 +502,7 @@ export class MountWatcher {
|
|
|
399
502
|
}
|
|
400
503
|
|
|
401
504
|
private isRootPath(absolutePath: string): boolean {
|
|
402
|
-
const root = this.
|
|
403
|
-
? this.config.path.slice(0, -1)
|
|
404
|
-
: this.config.path;
|
|
505
|
+
const root = this.root.endsWith(sep) ? this.root.slice(0, -1) : this.root;
|
|
405
506
|
const candidate = absolutePath.endsWith(sep)
|
|
406
507
|
? absolutePath.slice(0, -1)
|
|
407
508
|
: absolutePath;
|
package/src/silent-heartbeat.ts
CHANGED
|
@@ -1,40 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Request-only rows: user turns a request carries before a stored response
|
|
3
|
+
* that the store never holds, rendered again by every later compile.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* after whatever the window held before it — often the previous assistant
|
|
6
|
-
* message, since a suppressed tick leaves no delivery receipt behind. The
|
|
7
|
-
* wire formatter merges consecutive same-role messages, so without a turn
|
|
8
|
-
* between them the tick would read as an unprompted continuation of the
|
|
9
|
-
* earlier response and one assistant message would carry signed thinking
|
|
10
|
-
* from two separate responses (a provider 400 when it is the latest
|
|
11
|
-
* assistant message; see `Agent.addAssistantResponse`).
|
|
5
|
+
* Two kinds exist, and both are anchored to the response they prompted:
|
|
12
6
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* message.
|
|
7
|
+
* - A silent heartbeat tick stores no prompt row (#216): its reply is written
|
|
8
|
+
* straight after whatever the window held before it — often the previous
|
|
9
|
+
* assistant message, since a suppressed tick leaves no delivery receipt
|
|
10
|
+
* behind. Every row a tick writes is stamped with `metadata.silentHeartbeat`,
|
|
11
|
+
* and a separator carrying the tick's instruction is rendered before the
|
|
12
|
+
* first of the tick's rows that survives compilation.
|
|
13
|
+
* - A compile that ends on the agent's own message gets a trailing
|
|
14
|
+
* `[Continue]` turn (some models reject a trailing assistant message). The
|
|
15
|
+
* reply it prompts is stored straight after that message, so the reply's
|
|
16
|
+
* rows are stamped `metadata.promptedBy` (`RequestOnlyPrompt`), and the turn
|
|
17
|
+
* is rendered before the first of them that survives. A reply follows the
|
|
18
|
+
* prompt when what it was sent ends on that `[Continue]`, or on the tool
|
|
19
|
+
* results of a reply that followed it (`toolLoopPrompt`): a stream start
|
|
20
|
+
* and each round of a stream alike. A reply sent anything else after the
|
|
21
|
+
* results (a message that arrived meanwhile) follows that, and carries no
|
|
22
|
+
* prompt.
|
|
23
|
+
*
|
|
24
|
+
* Without the row, the wire formatter merges consecutive same-role messages:
|
|
25
|
+
* the reply would read as an unprompted continuation of the earlier response,
|
|
26
|
+
* and one assistant message would carry signed thinking from two separate
|
|
27
|
+
* responses (a provider 400 when it is the latest assistant message; see
|
|
28
|
+
* `Agent.addAssistantResponse`). And the request that minted the reply held
|
|
29
|
+
* the row: a provider that binds signed thinking to the prefix it was minted
|
|
30
|
+
* under (context-manager #155) refuses that thinking once a later request
|
|
31
|
+
* drops it. Each row's text is read from the stamp (what the request that
|
|
32
|
+
* minted the reply rendered, so a later change to the constant changes no
|
|
33
|
+
* stored reply's prefix) and its position is a function of the stamped rows
|
|
34
|
+
* alone, so every compile renders it in the same place (the request that
|
|
35
|
+
* minted the reply ends on that same turn), which keeps the prompt-cache
|
|
36
|
+
* prefix and the binding stable — unlike a per-compile injection that
|
|
37
|
+
* re-anchors to the newest user message, or a system prompt that changes for
|
|
38
|
+
* one turn.
|
|
21
39
|
*
|
|
22
40
|
* Known limit: compiled messages carry no store provenance, so a stored row
|
|
23
41
|
* is recognised only by structural identity the provider made unique
|
|
24
|
-
* (tool_use ids, thinking signatures). A
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* message's source rows.
|
|
42
|
+
* (tool_use ids, thinking signatures). A reply with neither — unsigned and
|
|
43
|
+
* tool-free — gets no row and still merges as before. Without signed thinking
|
|
44
|
+
* that merge carries no provider-rejection risk; it is only a misreading.
|
|
45
|
+
* Reply text is deliberately never used as identity: identical replies ("all
|
|
46
|
+
* quiet") would collide, and an ordinary reply with the same text could take
|
|
47
|
+
* another reply's row and rewrite cached history. Exact matching for every
|
|
48
|
+
* row would need the compile result to expose each message's source rows.
|
|
32
49
|
*/
|
|
33
50
|
import type { ContentBlock, NormalizedMessage } from '@animalabs/membrane';
|
|
34
51
|
|
|
35
|
-
/** Request-only separator
|
|
36
|
-
*
|
|
37
|
-
|
|
52
|
+
/** Request-only separator a silent tick's request ends on. It carries the
|
|
53
|
+
* tick's instruction, so the tick's own request reads it as its prompt, and
|
|
54
|
+
* every later compile renders it again before the tick's first surviving row.
|
|
55
|
+
* Constant on purpose: it carries no time or id. Each tick's rows record the
|
|
56
|
+
* text their request rendered (`SilentHeartbeatStamp.separator`), so changing
|
|
57
|
+
* this constant changes only ticks that open afterwards. */
|
|
58
|
+
export const SILENT_HEARTBEAT_SEPARATOR =
|
|
59
|
+
'[silent heartbeat] Scheduled private self-check. Review pending matters privately. ' +
|
|
60
|
+
'Do not narrate or publish plain prose; use an explicit send tool only if you deliberately choose to contact someone.';
|
|
61
|
+
|
|
62
|
+
/** The separator ticks rendered before their rows recorded its text: rows
|
|
63
|
+
* stamped without `separator` still render it, byte for byte. */
|
|
64
|
+
const LEGACY_SILENT_HEARTBEAT_SEPARATOR = '[heartbeat tick]';
|
|
65
|
+
|
|
66
|
+
/** Request-only turn a compile that ends on the agent's own message gets. */
|
|
67
|
+
export const CONTINUE_ROW = '[Continue]';
|
|
38
68
|
|
|
39
69
|
/** Identity of one authenticated silent heartbeat tick. */
|
|
40
70
|
export interface SilentHeartbeatTick {
|
|
@@ -50,20 +80,63 @@ export interface SilentHeartbeatTick {
|
|
|
50
80
|
* (tool_result rows are stored as `user`). */
|
|
51
81
|
export interface SilentHeartbeatStamp extends SilentHeartbeatTick {
|
|
52
82
|
agentName: string;
|
|
83
|
+
/** The separator text the tick's request rendered, which later compiles
|
|
84
|
+
* render again. Absent on rows stamped before it was recorded; they render
|
|
85
|
+
* the bare marker those requests carried. */
|
|
86
|
+
separator?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One tick's key in an index or a rendered window: the server and its event
|
|
90
|
+
* id together, since two servers' events can share an id. */
|
|
91
|
+
export function tickKey(tick: SilentHeartbeatTick): string {
|
|
92
|
+
return JSON.stringify([tick.serverId, tick.eventId]);
|
|
53
93
|
}
|
|
54
94
|
|
|
55
95
|
/** The tick stamp on a stored row's metadata, if any. */
|
|
56
96
|
export function silentHeartbeatOf(metadata: unknown): SilentHeartbeatStamp | undefined {
|
|
57
97
|
const stamp = (metadata as { silentHeartbeat?: unknown } | undefined)?.silentHeartbeat;
|
|
58
98
|
if (!stamp || typeof stamp !== 'object') return undefined;
|
|
59
|
-
const { eventId, serverId, agentName } = stamp as Record<string, unknown>;
|
|
99
|
+
const { eventId, serverId, agentName, separator } = stamp as Record<string, unknown>;
|
|
60
100
|
return typeof eventId === 'string' && typeof serverId === 'string' && typeof agentName === 'string'
|
|
61
|
-
? { eventId, serverId, agentName }
|
|
101
|
+
? { eventId, serverId, agentName, ...(typeof separator === 'string' ? { separator } : {}) }
|
|
62
102
|
: undefined;
|
|
63
103
|
}
|
|
64
104
|
|
|
105
|
+
/** A request-only turn a request ended on before the agent's reply, as
|
|
106
|
+
* `metadata.promptedBy` on the rows of each reply that follows it: `turn`
|
|
107
|
+
* names the one prompt, and `text` is what the request rendered. */
|
|
108
|
+
export interface RequestOnlyPrompt {
|
|
109
|
+
turn: string;
|
|
110
|
+
text: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The stamps on a row a turn stores after a request-only turn
|
|
114
|
+
* (`silentHeartbeatOf`, `promptedByOf` read them back). A type alias, so a
|
|
115
|
+
* value fits `MessageMetadata`'s index signature without a cast, and a
|
|
116
|
+
* misspelled key fails to compile. */
|
|
117
|
+
export type RequestOnlyStamps = {
|
|
118
|
+
silentHeartbeat?: SilentHeartbeatStamp;
|
|
119
|
+
promptedBy?: RequestOnlyPrompt;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** The request-only prompt a stored row replies to, if any. */
|
|
123
|
+
export function promptedByOf(metadata: unknown): RequestOnlyPrompt | undefined {
|
|
124
|
+
const prompt = (metadata as { promptedBy?: unknown } | undefined)?.promptedBy;
|
|
125
|
+
if (!prompt || typeof prompt !== 'object') return undefined;
|
|
126
|
+
const { turn, text } = prompt as Record<string, unknown>;
|
|
127
|
+
return typeof turn === 'string' && typeof text === 'string' ? { turn, text } : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
65
130
|
export function silentHeartbeatSeparatorTurn(): NormalizedMessage {
|
|
66
|
-
return
|
|
131
|
+
return requestOnlyTurn(SILENT_HEARTBEAT_SEPARATOR);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function continueTurn(): NormalizedMessage {
|
|
135
|
+
return requestOnlyTurn(CONTINUE_ROW);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function requestOnlyTurn(text: string): NormalizedMessage {
|
|
139
|
+
return { participant: 'user', content: [{ type: 'text', text }] };
|
|
67
140
|
}
|
|
68
141
|
|
|
69
142
|
/**
|
|
@@ -76,7 +149,7 @@ export function silentHeartbeatSeparatorTurn(): NormalizedMessage {
|
|
|
76
149
|
function identityKeys(content: ContentBlock[]): string[] {
|
|
77
150
|
const keys: string[] = [];
|
|
78
151
|
for (const block of content) {
|
|
79
|
-
if (block.type === 'tool_use' && typeof block.id === 'string' && block.id) keys.push(
|
|
152
|
+
if (block.type === 'tool_use' && typeof block.id === 'string' && block.id) keys.push(toolUseKey(block.id));
|
|
80
153
|
else if (block.type === 'thinking' && typeof block.signature === 'string' && block.signature) {
|
|
81
154
|
keys.push(`thinking:${block.signature}`);
|
|
82
155
|
} else if (block.type === 'redacted_thinking' && typeof block.data === 'string' && block.data) {
|
|
@@ -86,56 +159,108 @@ function identityKeys(content: ContentBlock[]): string[] {
|
|
|
86
159
|
return keys;
|
|
87
160
|
}
|
|
88
161
|
|
|
162
|
+
function toolUseKey(id: string): string {
|
|
163
|
+
return `tool_use:${id}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
89
166
|
export interface StoredRowLike {
|
|
90
167
|
participant: string;
|
|
91
168
|
content: ContentBlock[];
|
|
92
169
|
metadata?: unknown;
|
|
93
170
|
}
|
|
94
171
|
|
|
95
|
-
export interface
|
|
96
|
-
/** Identity key of every assistant row this agent stored during a
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
172
|
+
export interface RequestOnlyRowIndex {
|
|
173
|
+
/** Identity key of every assistant row this agent stored during a silent
|
|
174
|
+
* tick → the tick (`tickKey`), and the separator text to render before
|
|
175
|
+
* its first surviving row. */
|
|
176
|
+
ticks: Map<string, { tick: string; text: string }>;
|
|
177
|
+
/** Identity key of every assistant row this agent stored in reply to a
|
|
178
|
+
* request-only prompt → that prompt. */
|
|
179
|
+
prompts: Map<string, RequestOnlyPrompt>;
|
|
100
180
|
}
|
|
101
181
|
|
|
102
|
-
/** Index the rows
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
182
|
+
/** Index the agent's own rows that follow request-only turns: the rows of its
|
|
183
|
+
* silent ticks, and the rows of each reply to a `[Continue]`. The two are
|
|
184
|
+
* independent, so a row can be both. Only stamped rows are keyed, so an
|
|
185
|
+
* agent's history walk costs a metadata read per row. */
|
|
186
|
+
export function indexRequestOnlyRows(stored: readonly StoredRowLike[], agentName: string): RequestOnlyRowIndex {
|
|
187
|
+
const ticks = new Map<string, { tick: string; text: string }>();
|
|
188
|
+
const prompts = new Map<string, RequestOnlyPrompt>();
|
|
106
189
|
for (const row of stored) {
|
|
107
|
-
const stamp = silentHeartbeatOf(row.metadata);
|
|
108
|
-
if (!stamp || stamp.agentName !== agentName) continue;
|
|
109
|
-
started.add(stamp.eventId);
|
|
110
190
|
if (row.participant !== agentName) continue;
|
|
111
|
-
|
|
191
|
+
const stamp = silentHeartbeatOf(row.metadata);
|
|
192
|
+
const ownTick = stamp !== undefined && stamp.agentName === agentName;
|
|
193
|
+
const prompt = promptedByOf(row.metadata);
|
|
194
|
+
if (!ownTick && !prompt) continue;
|
|
195
|
+
const keys = identityKeys(row.content);
|
|
196
|
+
if (ownTick) {
|
|
197
|
+
const tick = { tick: tickKey(stamp), text: stamp.separator ?? LEGACY_SILENT_HEARTBEAT_SEPARATOR };
|
|
198
|
+
for (const key of keys) ticks.set(key, tick);
|
|
199
|
+
}
|
|
200
|
+
if (prompt) for (const key of keys) prompts.set(key, prompt);
|
|
112
201
|
}
|
|
113
|
-
return {
|
|
202
|
+
return { ticks, prompts };
|
|
114
203
|
}
|
|
115
204
|
|
|
116
205
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
206
|
+
* The prompt a reply continues when what it is sent ends on `last`: if
|
|
207
|
+
* `last` holds tool results, the prompt of the reply that called those tools
|
|
208
|
+
* (this reply continues that reply's tool loop); otherwise none. Read from
|
|
209
|
+
* the stored stamps, so it doesn't depend on anything held in memory. A
|
|
210
|
+
* request that ends on its own `[Continue]` mints a new prompt instead.
|
|
211
|
+
*/
|
|
212
|
+
export function toolLoopPrompt(
|
|
213
|
+
last: NormalizedMessage | undefined,
|
|
214
|
+
index: RequestOnlyRowIndex,
|
|
215
|
+
): RequestOnlyPrompt | undefined {
|
|
216
|
+
if (!last) return undefined;
|
|
217
|
+
for (const block of last.content) {
|
|
218
|
+
if (block.type !== 'tool_result') continue;
|
|
219
|
+
const prompt = index.prompts.get(toolUseKey(block.toolUseId));
|
|
220
|
+
if (prompt) return prompt;
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface RenderedRequestOnlyRows {
|
|
226
|
+
messages: NormalizedMessage[];
|
|
227
|
+
/** The ticks (`tickKey`) whose separator this window renders. A tick none
|
|
228
|
+
* of whose rows survive compilation renders none. */
|
|
229
|
+
separatedTicks: Set<string>;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Insert the request-only turns into a compiled window, each immediately
|
|
234
|
+
* before the first of its rows that survives compilation (the first row when
|
|
235
|
+
* it is rendered, otherwise whichever later row a strategy kept): a tick's
|
|
236
|
+
* separator, and the prompt each reply to `[Continue]` followed, after any
|
|
237
|
+
* separator, as the reply's request had them. Rows folded into summaries
|
|
120
238
|
* simply do not match; ordinary messages pass through untouched.
|
|
121
239
|
*/
|
|
122
|
-
export function
|
|
240
|
+
export function renderRequestOnlyRows(
|
|
123
241
|
messages: NormalizedMessage[],
|
|
124
|
-
|
|
242
|
+
index: RequestOnlyRowIndex,
|
|
125
243
|
agentName: string,
|
|
126
|
-
):
|
|
127
|
-
|
|
128
|
-
const
|
|
244
|
+
): RenderedRequestOnlyRows {
|
|
245
|
+
const separatedTicks = new Set<string>();
|
|
246
|
+
const promptedTurns = new Set<string>();
|
|
247
|
+
if (index.ticks.size === 0 && index.prompts.size === 0) return { messages, separatedTicks };
|
|
129
248
|
const out: NormalizedMessage[] = [];
|
|
130
249
|
for (const message of messages) {
|
|
131
250
|
if (message.participant === agentName) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
251
|
+
const keys = identityKeys(message.content);
|
|
252
|
+
const tick = keys.map((key) => index.ticks.get(key)).find((t) => t !== undefined);
|
|
253
|
+
if (tick !== undefined && !separatedTicks.has(tick.tick)) {
|
|
254
|
+
separatedTicks.add(tick.tick);
|
|
255
|
+
out.push(requestOnlyTurn(tick.text));
|
|
256
|
+
}
|
|
257
|
+
const prompt = keys.map((key) => index.prompts.get(key)).find((p) => p !== undefined);
|
|
258
|
+
if (prompt !== undefined && !promptedTurns.has(prompt.turn)) {
|
|
259
|
+
promptedTurns.add(prompt.turn);
|
|
260
|
+
out.push(requestOnlyTurn(prompt.text));
|
|
136
261
|
}
|
|
137
262
|
}
|
|
138
263
|
out.push(message);
|
|
139
264
|
}
|
|
140
|
-
return out;
|
|
265
|
+
return { messages: out, separatedTicks };
|
|
141
266
|
}
|