@animalabs/agent-framework 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/agent.d.ts +1 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +14 -4
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +265 -23
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +62 -7
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +333 -22
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/types.d.ts +56 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +11 -0
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +21 -0
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +2 -2
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +57 -13
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +23 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +48 -0
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +11 -2
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +18 -8
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +12 -2
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +12 -2
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +4 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +1 -1
- package/dist/test/event-gate-passthrough.test.d.ts +15 -0
- package/dist/test/event-gate-passthrough.test.d.ts.map +1 -0
- package/dist/test/event-gate-passthrough.test.js +289 -0
- package/dist/test/event-gate-passthrough.test.js.map +1 -0
- package/dist/test/explicit-prose-routing.test.js +137 -7
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/fixtures/awareness-barrier-mcpl-server.mjs +276 -0
- package/dist/test/fixtures/flaky-mcpl-server.mjs +41 -0
- package/dist/test/prose-stream-router.test.js +43 -0
- package/dist/test/prose-stream-router.test.js.map +1 -1
- package/dist/test/tool-result-spill.test.d.ts +1 -1
- package/dist/test/tool-result-spill.test.js +18 -9
- package/dist/test/tool-result-spill.test.js.map +1 -1
- package/dist/test/xml-round-persist.test.d.ts +19 -0
- package/dist/test/xml-round-persist.test.d.ts.map +1 -0
- package/dist/test/xml-round-persist.test.js +235 -0
- package/dist/test/xml-round-persist.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/agent.ts +1 -1
- package/src/framework.ts +270 -25
- package/src/gate/event-gate.ts +355 -24
- package/src/gate/types.ts +58 -0
- package/src/mcpl/prose-grammar.ts +21 -0
- package/src/mcpl/prose-stream-router.ts +48 -14
- package/src/modules/workspace/index.ts +48 -0
- package/src/modules/workspace/sync.ts +30 -7
- package/src/tool-result-history.ts +12 -2
- package/src/types/agent.ts +4 -1
- package/src/types/framework.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animalabs/agent-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"pretest": "mkdir -p dist/test/fixtures && cp test/fixtures/*.mjs dist/test/fixtures/",
|
|
34
34
|
"test": "node --test --test-force-exit dist/test/*.test.js",
|
|
35
|
+
"version": "node scripts/release-changelog.mjs && git add CHANGELOG.md",
|
|
35
36
|
"prepublishOnly": "npm run build"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|
package/src/agent.ts
CHANGED
|
@@ -67,7 +67,7 @@ export class Agent {
|
|
|
67
67
|
/** Refusal auto-rewind policy (see AgentConfig.refusalHandling). */
|
|
68
68
|
readonly refusalHandling: AgentConfig['refusalHandling'];
|
|
69
69
|
/** Prose delivery mode (see AgentConfig.proseRouting). Default 'locus'. */
|
|
70
|
-
readonly proseRouting: 'locus' | 'explicit';
|
|
70
|
+
readonly proseRouting: 'locus' | 'explicit' | 'hybrid';
|
|
71
71
|
/** Prompt-cache TTL forwarded to the provider (see AgentConfig.cacheTtl). */
|
|
72
72
|
readonly cacheTtl: NonNullable<AgentConfig['cacheTtl']>;
|
|
73
73
|
/** Emit prompt-cache markers on requests (see AgentConfig.promptCaching). */
|
package/src/framework.ts
CHANGED
|
@@ -62,7 +62,7 @@ import { computeGrant, CapabilityGrant, expandAdvertisementShorthand } from './m
|
|
|
62
62
|
import { maskNegotiatedCapabilities } from './mcpl/capability-mask.js';
|
|
63
63
|
import { HookOrchestrator } from './mcpl/hook-orchestrator.js';
|
|
64
64
|
import { PushHandler, type McplPushEvent } from './mcpl/push-handler.js';
|
|
65
|
-
import { parseProsePrefix } from './mcpl/prose-grammar.js';
|
|
65
|
+
import { parseProsePrefix, parseHybridProsePrefix } from './mcpl/prose-grammar.js';
|
|
66
66
|
import { ProseStreamRouter } from './mcpl/prose-stream-router.js';
|
|
67
67
|
import { InferenceRouter } from './mcpl/inference-router.js';
|
|
68
68
|
import { ChannelRegistry, type ChannelToolOrigin } from './mcpl/channel-registry.js';
|
|
@@ -126,6 +126,9 @@ const SILENCING_TOOLS = new Set([
|
|
|
126
126
|
'skip_reply', 'channel_publish', 'send_message', 'reply_message', 'send_dm',
|
|
127
127
|
]);
|
|
128
128
|
|
|
129
|
+
/** World-surface publication names that outrank hybrid prose envelopes. */
|
|
130
|
+
const HYBRID_PUBLICATION_TOOLS = new Set(['say', 'whisper']);
|
|
131
|
+
|
|
129
132
|
/**
|
|
130
133
|
* True when an injected message is real conversational input — something the
|
|
131
134
|
* agent might actually be replying to — rather than ambient machinery.
|
|
@@ -225,11 +228,18 @@ const isAddressedMessage = (
|
|
|
225
228
|
// envelopes can never disagree on what is a prefix.
|
|
226
229
|
|
|
227
230
|
/** One-time primer appended when an agent's proseRouting mode changes. */
|
|
228
|
-
function proseModePrimer(mode: 'explicit' | 'locus'): string {
|
|
231
|
+
function proseModePrimer(mode: 'explicit' | 'hybrid' | 'locus'): string {
|
|
229
232
|
if (mode === 'locus') {
|
|
230
233
|
return '[prose-routing] Mode change: your plain text auto-routes to the ' +
|
|
231
234
|
'conversational locus again. `>>` destination prefixes are no longer needed.';
|
|
232
235
|
}
|
|
236
|
+
if (mode === 'hybrid') {
|
|
237
|
+
return '[prose-routing] Hybrid routing enabled: unprefixed text still lands in the ' +
|
|
238
|
+
'current conversational locus. A leading `>>>destination` envelope instead routes ' +
|
|
239
|
+
'that prose to one uniquely resolved authorized Discord or Eidoverse channel. The ' +
|
|
240
|
+
'envelope remains in your memory but recipients see only its body; delivery or failure ' +
|
|
241
|
+
'is reported back to you.';
|
|
242
|
+
}
|
|
233
243
|
// Deliberately terse: a short event-style notice is classifier-safe from
|
|
234
244
|
// the user role (ablation D, 2026-07-24), while the full grammar as a user
|
|
235
245
|
// message drew a deterministic reasoning_extraction refusal (ablation A/B).
|
|
@@ -329,7 +339,7 @@ function validateUtilityArgs(
|
|
|
329
339
|
}
|
|
330
340
|
import { CheckpointManager } from './mcpl/checkpoint-manager.js';
|
|
331
341
|
import { isToolAllowed } from './mcpl/tool-policy.js';
|
|
332
|
-
import { EventGate } from './gate/event-gate.js';
|
|
342
|
+
import { EventGate, formatShadowWarning } from './gate/event-gate.js';
|
|
333
343
|
import { UsageTracker, type PersistedUsageState } from './usage/usage-tracker.js';
|
|
334
344
|
import type { SessionUsageSnapshot, UsageUpdatedEvent } from './usage/types.js';
|
|
335
345
|
import type { McplServerConnection } from './mcpl/server-connection.js';
|
|
@@ -684,6 +694,8 @@ export class AgentFramework {
|
|
|
684
694
|
/** Sticky per-TURN delivery target set by the turn's first `>>` prefix.
|
|
685
695
|
* Cleared at every non-restart turn start; survives context restarts. */
|
|
686
696
|
private proseTargetPins: Map<string, string> = new Map();
|
|
697
|
+
/** Hybrid router is fail-closed after a malformed/unresolved envelope until a new valid target. */
|
|
698
|
+
private proseHybridSuppressed: Set<string> = new Set();
|
|
687
699
|
/** Agents whose current turn requested `!` continuation — re-woken when the
|
|
688
700
|
* turn completes instead of pausing until the next external event. */
|
|
689
701
|
private proseContinuations: Set<string> = new Set();
|
|
@@ -1968,12 +1980,14 @@ export class AgentFramework {
|
|
|
1968
1980
|
/**
|
|
1969
1981
|
* Add or replace (upsert) a gate policy at runtime. Validated and persisted
|
|
1970
1982
|
* to gate.json; hot-applied in memory. Throws if no gate is configured or the
|
|
1971
|
-
* policy is invalid.
|
|
1972
|
-
*
|
|
1983
|
+
* policy is invalid. Placement: `'prepend'` puts a rule ahead of EVERYTHING
|
|
1984
|
+
* (including addressed-message wake rules — see EventGate.lintShadows);
|
|
1985
|
+
* `{before: name}` / `{after: name}` anchor it against a specific rule,
|
|
1986
|
+
* which is usually what a sampler or debounce actually wants.
|
|
1973
1987
|
*/
|
|
1974
1988
|
addGatePolicy(
|
|
1975
1989
|
rawPolicy: unknown,
|
|
1976
|
-
options?: { position?: 'append' | 'prepend' },
|
|
1990
|
+
options?: { position?: 'append' | 'prepend' | { before: string } | { after: string } },
|
|
1977
1991
|
): import('./gate/types.js').GatePolicy {
|
|
1978
1992
|
if (!this.eventGate) {
|
|
1979
1993
|
throw new Error('No EventGate configured (FrameworkConfig.gate is unset).');
|
|
@@ -2350,9 +2364,17 @@ export class AgentFramework {
|
|
|
2350
2364
|
description:
|
|
2351
2365
|
'Add or replace a wake rule (a gate.json policy) at runtime — no need to ' +
|
|
2352
2366
|
'hand-edit the file. The rule is validated and hot-applied immediately. ' +
|
|
2353
|
-
'A rule with the same `name` replaces the existing one in place
|
|
2354
|
-
'
|
|
2355
|
-
'
|
|
2367
|
+
'A rule with the same `name` replaces the existing one in place (add a ' +
|
|
2368
|
+
'position to move it). ⚠️ ORDERING IS SEMANTICS: first match wins, and a ' +
|
|
2369
|
+
'matched rule CONSUMES the event — a broad rule placed early makes every ' +
|
|
2370
|
+
'later rule unreachable for its scope, including your addressed-message ' +
|
|
2371
|
+
'(DM/mention) wake rules. Prefer `insertBefore`/`insertAfter` to place a ' +
|
|
2372
|
+
'rule relative to a specific existing rule; use `position: "prepend"` only ' +
|
|
2373
|
+
'when the rule really must beat EVERYTHING. For "additionally wake me ' +
|
|
2374
|
+
'every Nth event / at most N per hour" governors, set `passthrough: true` ' +
|
|
2375
|
+
'so non-firing matches fall through to your normal rules instead of ' +
|
|
2376
|
+
'swallowing them. The result includes a before/after probe table and any ' +
|
|
2377
|
+
'shadow warnings — READ THEM before ending the turn. Two common shapes:\n' +
|
|
2356
2378
|
'• Watch a FILE/workspace path: match on `mount` + `pathGlob`, e.g. ' +
|
|
2357
2379
|
'`{ name: "watch-notes", match: { scope: ["workspace:modified"], mount: "project", pathGlob: "notes/*.md" }, behavior: "always" }`.\n' +
|
|
2358
2380
|
'• Watch a CHANNEL: match on `source` + `channel` (and/or `tagsAny: ["chat:ambient"]`), ' +
|
|
@@ -2420,7 +2442,30 @@ export class AgentFramework {
|
|
|
2420
2442
|
position: {
|
|
2421
2443
|
type: 'string',
|
|
2422
2444
|
enum: ['append', 'prepend'],
|
|
2423
|
-
description:
|
|
2445
|
+
description:
|
|
2446
|
+
'Coarse placement. "append" (default) adds at the end; "prepend" puts the rule ' +
|
|
2447
|
+
'ahead of EVERYTHING — including addressed-message wake rules — so prefer ' +
|
|
2448
|
+
'insertBefore/insertAfter unless the rule really must beat every other rule. ' +
|
|
2449
|
+
'When replacing an existing name, giving any placement MOVES the rule there; ' +
|
|
2450
|
+
'omit all placement fields to update it in place.',
|
|
2451
|
+
},
|
|
2452
|
+
insertBefore: {
|
|
2453
|
+
type: 'string',
|
|
2454
|
+
description:
|
|
2455
|
+
'Insert the rule immediately BEFORE the named existing rule (error if absent). ' +
|
|
2456
|
+
'The safe way to order against a specific catch-all, e.g. insertBefore: "discord-ambient".',
|
|
2457
|
+
},
|
|
2458
|
+
insertAfter: {
|
|
2459
|
+
type: 'string',
|
|
2460
|
+
description: 'Insert the rule immediately AFTER the named existing rule (error if absent).',
|
|
2461
|
+
},
|
|
2462
|
+
passthrough: {
|
|
2463
|
+
type: 'boolean',
|
|
2464
|
+
description:
|
|
2465
|
+
'Observer semantics for rateLimit/passiveSample rules: when the rule matches but ' +
|
|
2466
|
+
'does not fire (below N, bucket empty), the event FALLS THROUGH to later rules ' +
|
|
2467
|
+
'instead of being consumed. Use for "additionally wake me every Nth event" ' +
|
|
2468
|
+
'governors so they never swallow DM/mention wakes.',
|
|
2424
2469
|
},
|
|
2425
2470
|
resets: {
|
|
2426
2471
|
type: 'array',
|
|
@@ -5110,6 +5155,97 @@ export class AgentFramework {
|
|
|
5110
5155
|
}
|
|
5111
5156
|
}
|
|
5112
5157
|
|
|
5158
|
+
/** Locus-preserving explicit publication. Source remains byte-identical in Chronicle. */
|
|
5159
|
+
private async deliverHybridProse(
|
|
5160
|
+
agent: Agent,
|
|
5161
|
+
rawText: string,
|
|
5162
|
+
locus: string | null,
|
|
5163
|
+
allowLocus: boolean,
|
|
5164
|
+
): Promise<void> {
|
|
5165
|
+
const lines = rawText.split('\n');
|
|
5166
|
+
const envelopes: string[] = [];
|
|
5167
|
+
let current: string[] = [];
|
|
5168
|
+
for (const line of lines) {
|
|
5169
|
+
if (line.trimStart().startsWith('>>>') && current.length > 0) {
|
|
5170
|
+
envelopes.push(current.join('\n'));
|
|
5171
|
+
current = [];
|
|
5172
|
+
}
|
|
5173
|
+
current.push(line);
|
|
5174
|
+
}
|
|
5175
|
+
if (current.length > 0) envelopes.push(current.join('\n'));
|
|
5176
|
+
|
|
5177
|
+
for (const envelope of envelopes) {
|
|
5178
|
+
if (!envelope.trim()) continue;
|
|
5179
|
+
const normalized = envelope.replace(/^\s+(?=>>>)/, '');
|
|
5180
|
+
const attempted = normalized.startsWith('>>>');
|
|
5181
|
+
const parsed = parseHybridProsePrefix(normalized);
|
|
5182
|
+
if (parsed.continueTurn) this.proseContinuations.add(agent.name);
|
|
5183
|
+
if (parsed.kind === 'private') {
|
|
5184
|
+
this.proseTargetPins.delete(agent.name);
|
|
5185
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
5186
|
+
console.error(`[prose] ${agent.name}: >>>skip_reply — ${parsed.body.length} chars kept in context (not sent)`);
|
|
5187
|
+
continue;
|
|
5188
|
+
}
|
|
5189
|
+
if (parsed.kind === 'target') {
|
|
5190
|
+
const resolved = this.channelRegistry!.resolveProseTarget(parsed.target!);
|
|
5191
|
+
if ('error' in resolved) {
|
|
5192
|
+
this.proseTargetPins.delete(agent.name);
|
|
5193
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
5194
|
+
this.bounceProse(agent, parsed.body, resolved.error, resolved.candidates, '>>>');
|
|
5195
|
+
continue;
|
|
5196
|
+
}
|
|
5197
|
+
let body = parsed.body;
|
|
5198
|
+
const usedClipboard = body.includes('{{unsent}}');
|
|
5199
|
+
if (usedClipboard) body = body.replaceAll('{{unsent}}', this.proseClipboards.get(agent.name) ?? '');
|
|
5200
|
+
this.proseTargetPins.set(agent.name, resolved.channelId);
|
|
5201
|
+
this.proseHybridSuppressed.delete(agent.name);
|
|
5202
|
+
if (!body.trim()) {
|
|
5203
|
+
console.error(`[prose] ${agent.name}: >>>${parsed.target} established target; empty body — nothing sent yet`);
|
|
5204
|
+
continue;
|
|
5205
|
+
}
|
|
5206
|
+
try {
|
|
5207
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, body, resolved.channelId);
|
|
5208
|
+
this.recordProseDelivery(agent.name, outcome);
|
|
5209
|
+
if (outcome?.delivered) {
|
|
5210
|
+
this.proseBounceStreaks.delete(agent.name);
|
|
5211
|
+
if (usedClipboard) this.proseClipboards.delete(agent.name);
|
|
5212
|
+
} else {
|
|
5213
|
+
this.proseTargetPins.delete(agent.name);
|
|
5214
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
5215
|
+
this.bounceProse(agent, parsed.body, `delivery to ${resolved.channelId} was not confirmed`, undefined, '>>>');
|
|
5216
|
+
}
|
|
5217
|
+
} catch (err) {
|
|
5218
|
+
this.proseTargetPins.delete(agent.name);
|
|
5219
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
5220
|
+
this.bounceProse(agent, parsed.body, `delivery to ${resolved.channelId} failed: ${err instanceof Error ? err.message : String(err)}`, undefined, '>>>');
|
|
5221
|
+
}
|
|
5222
|
+
continue;
|
|
5223
|
+
}
|
|
5224
|
+
if (attempted) {
|
|
5225
|
+
this.proseTargetPins.delete(agent.name);
|
|
5226
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
5227
|
+
this.bounceProse(agent, normalized, 'malformed >>> routing envelope: destination is missing', undefined, '>>>');
|
|
5228
|
+
continue;
|
|
5229
|
+
}
|
|
5230
|
+
if (this.proseHybridSuppressed.has(agent.name)) {
|
|
5231
|
+
this.recordProseSuppression(agent.name, 1);
|
|
5232
|
+
continue;
|
|
5233
|
+
}
|
|
5234
|
+
const sticky = this.proseTargetPins.get(agent.name);
|
|
5235
|
+
if (!allowLocus && !sticky) {
|
|
5236
|
+
this.recordProseSuppression(agent.name, 1);
|
|
5237
|
+
continue;
|
|
5238
|
+
}
|
|
5239
|
+
const target = sticky ?? locus;
|
|
5240
|
+
try {
|
|
5241
|
+
const outcome = await this.channelRegistry!.routeSpeech(agent.name, envelope, target);
|
|
5242
|
+
this.recordProseDelivery(agent.name, outcome);
|
|
5243
|
+
} catch (err) {
|
|
5244
|
+
console.error('hybrid locus delivery failed:', err);
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
|
|
5113
5249
|
private async deliverProseEnvelope(agent: Agent, rawText: string): Promise<void> {
|
|
5114
5250
|
const name = agent.name;
|
|
5115
5251
|
const parsed = parseProsePrefix(rawText);
|
|
@@ -5169,7 +5305,7 @@ export class AgentFramework {
|
|
|
5169
5305
|
/** Cap on consecutive bounce-triggered wakes (notices still append after). */
|
|
5170
5306
|
private static readonly PROSE_BOUNCE_WAKE_CAP = 2;
|
|
5171
5307
|
|
|
5172
|
-
private bounceProse(agent: Agent, text: string, reason: string, candidates?: string[]): void {
|
|
5308
|
+
private bounceProse(agent: Agent, text: string, reason: string, candidates?: string[], prefix: '>>' | '>>>' = '>>'): void {
|
|
5173
5309
|
const name = agent.name;
|
|
5174
5310
|
this.proseClipboards.set(name, text);
|
|
5175
5311
|
const streak = (this.proseBounceStreaks.get(name) ?? 0) + 1;
|
|
@@ -5178,8 +5314,9 @@ export class AgentFramework {
|
|
|
5178
5314
|
const notice =
|
|
5179
5315
|
`[prose-routing] Your text (${text.length} chars) was not delivered — ${reason}.${cand} ` +
|
|
5180
5316
|
'The text is retained; nothing is lost. To deliver it unchanged, reply with a ' +
|
|
5181
|
-
|
|
5182
|
-
|
|
5317
|
+
`destination plus the token {{unsent}}, e.g. "${prefix}#channel {{unsent}}" or "${prefix}@person {{unsent}}". ` +
|
|
5318
|
+
`"${prefix}skip_reply {{unsent}}" keeps it in context only.` +
|
|
5319
|
+
(prefix === '>>' ? ' The prose_help tool shows the full syntax.' : '');
|
|
5183
5320
|
try {
|
|
5184
5321
|
// Through framework.addMessage, NOT the context manager directly: while
|
|
5185
5322
|
// the turn is still streaming this defers the notice to the next tool
|
|
@@ -5381,6 +5518,8 @@ export class AgentFramework {
|
|
|
5381
5518
|
this.turnEngagedChannels.delete(agent.name);
|
|
5382
5519
|
this.turnProseDeliveries.delete(agent.name);
|
|
5383
5520
|
this.turnProseSuppressed.delete(agent.name);
|
|
5521
|
+
this.proseHybridSuppressed.delete(agent.name);
|
|
5522
|
+
if (agent.proseRouting === 'hybrid') this.proseTargetPins.delete(agent.name);
|
|
5384
5523
|
if (agent.proseRouting === 'explicit') {
|
|
5385
5524
|
// Explicit prose routing: there is no locus. The model names every
|
|
5386
5525
|
// destination in-band (`>>` prefixes); the only turn state is the
|
|
@@ -5649,7 +5788,7 @@ export class AgentFramework {
|
|
|
5649
5788
|
});
|
|
5650
5789
|
const proseStream = this.channelRegistry
|
|
5651
5790
|
? new ProseStreamRouter({
|
|
5652
|
-
mode: agent.proseRouting === 'explicit' ? 'explicit' : 'locus',
|
|
5791
|
+
mode: agent.proseRouting === 'explicit' ? 'explicit' : agent.proseRouting === 'hybrid' ? 'hybrid' : 'locus',
|
|
5653
5792
|
initialTarget: typingChannel,
|
|
5654
5793
|
resolve: (spec) => {
|
|
5655
5794
|
const r = this.channelRegistry!.resolveProseTarget(spec);
|
|
@@ -5776,9 +5915,18 @@ export class AgentFramework {
|
|
|
5776
5915
|
const rawXml = rawText && /^<(antml:)?function_calls>/.test(rawText)
|
|
5777
5916
|
? rawText
|
|
5778
5917
|
: undefined;
|
|
5918
|
+
// Persist THIS round's text only. `roundPreamble` (membrane
|
|
5919
|
+
// ≥0.5.79) is the per-round delta; the legacy `preamble` is
|
|
5920
|
+
// CUMULATIVE in XML mode — persisting it per round stored each
|
|
5921
|
+
// round's text N times and re-persisted injected
|
|
5922
|
+
// <function_results> as the agent's own words (the Evander
|
|
5923
|
+
// 2026-08-08 scaffold-leak pyramid: r1 ×3, r2 ×2).
|
|
5924
|
+
const roundText =
|
|
5925
|
+
(event.context as { roundPreamble?: string }).roundPreamble ??
|
|
5926
|
+
event.context.preamble;
|
|
5779
5927
|
assistantBlocks = [];
|
|
5780
|
-
if (
|
|
5781
|
-
assistantBlocks.push({ type: 'text', text:
|
|
5928
|
+
if (roundText) {
|
|
5929
|
+
assistantBlocks.push({ type: 'text', text: roundText });
|
|
5782
5930
|
}
|
|
5783
5931
|
for (const c of event.calls) {
|
|
5784
5932
|
assistantBlocks.push({
|
|
@@ -5820,14 +5968,28 @@ export class AgentFramework {
|
|
|
5820
5968
|
const hasSameRoundPrivateThink =
|
|
5821
5969
|
roundToolNames.includes('think') &&
|
|
5822
5970
|
requestSnapshot.sameRoundThinkTextPolicy === 'private';
|
|
5823
|
-
if (roundToolNames.some((n) =>
|
|
5971
|
+
if (roundToolNames.some((n) =>
|
|
5972
|
+
SILENCING_TOOLS.has(bareToolName(n)) ||
|
|
5973
|
+
(agent.proseRouting === 'hybrid' && HYBRID_PUBLICATION_TOOLS.has(bareToolName(n)))
|
|
5974
|
+
)) {
|
|
5824
5975
|
turnSilenced = true;
|
|
5825
5976
|
}
|
|
5826
5977
|
if (roundContent && roundContent.length > 0) {
|
|
5827
5978
|
liveProseRouting = true;
|
|
5828
5979
|
const roundSegments = splitProseSegments(assistantBlocks);
|
|
5829
5980
|
if (roundSegments.length > 0) {
|
|
5830
|
-
if (agent.proseRouting === '
|
|
5981
|
+
if (agent.proseRouting === 'hybrid') {
|
|
5982
|
+
if (turnSilenced) {
|
|
5983
|
+
this.recordProseSuppression(agent.name, roundSegments.length);
|
|
5984
|
+
} else if (!hasSameRoundPrivateThink) {
|
|
5985
|
+
const locus = resolveTurnLocus();
|
|
5986
|
+
for (const seg of roundSegments) {
|
|
5987
|
+
turnSpeechChain = turnSpeechChain
|
|
5988
|
+
.then(() => this.deliverHybridProse(agent, seg, locus, true))
|
|
5989
|
+
.catch((err) => console.error('mid-turn hybrid prose delivery failed:', err));
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
} else if (agent.proseRouting === 'explicit') {
|
|
5831
5993
|
// Explicit mode: every segment through the prose gateway.
|
|
5832
5994
|
// Silencing does not apply — unprefixed prose bounces and
|
|
5833
5995
|
// prefixed prose is deliberate; think-privacy still holds.
|
|
@@ -6157,7 +6319,15 @@ export class AgentFramework {
|
|
|
6157
6319
|
.join('\n')
|
|
6158
6320
|
.trim();
|
|
6159
6321
|
if (speechText) {
|
|
6160
|
-
if (agent.proseRouting === '
|
|
6322
|
+
if (agent.proseRouting === 'hybrid') {
|
|
6323
|
+
const locus = resolveTurnLocus();
|
|
6324
|
+
console.error(`[prose] ${agent.name}: text-only turn -> hybrid prose gateway`);
|
|
6325
|
+
try {
|
|
6326
|
+
await this.deliverHybridProse(agent, speechText, locus, true);
|
|
6327
|
+
} catch (err) {
|
|
6328
|
+
console.error('text-only hybrid prose delivery failed:', err);
|
|
6329
|
+
}
|
|
6330
|
+
} else if (agent.proseRouting === 'explicit') {
|
|
6161
6331
|
console.error(`[prose] ${agent.name}: text-only turn -> prose gateway`);
|
|
6162
6332
|
try {
|
|
6163
6333
|
await this.deliverProse(agent, speechText);
|
|
@@ -6204,7 +6374,10 @@ export class AgentFramework {
|
|
|
6204
6374
|
.filter((n): n is string => typeof n === 'string');
|
|
6205
6375
|
const silenced = liveProseRouting
|
|
6206
6376
|
? turnSilenced
|
|
6207
|
-
: turnSilenced || toolNames.some((n) =>
|
|
6377
|
+
: turnSilenced || toolNames.some((n) =>
|
|
6378
|
+
SILENCING_TOOLS.has(bareToolName(n)) ||
|
|
6379
|
+
(agent.proseRouting === 'hybrid' && HYBRID_PUBLICATION_TOOLS.has(bareToolName(n)))
|
|
6380
|
+
);
|
|
6208
6381
|
|
|
6209
6382
|
const segments = splitProseSegments(liveProseRouting ? terminalContent : response.content);
|
|
6210
6383
|
|
|
@@ -6213,7 +6386,20 @@ export class AgentFramework {
|
|
|
6213
6386
|
// the chain may still be flushing earlier rounds' posts.
|
|
6214
6387
|
await turnSpeechChain;
|
|
6215
6388
|
|
|
6216
|
-
if (agent.proseRouting === '
|
|
6389
|
+
if (agent.proseRouting === 'hybrid') {
|
|
6390
|
+
if (silenced && segments.length > 0) {
|
|
6391
|
+
this.recordProseSuppression(agent.name, segments.length);
|
|
6392
|
+
} else if (segments.length > 0) {
|
|
6393
|
+
const locus = resolveTurnLocus();
|
|
6394
|
+
for (const seg of segments) {
|
|
6395
|
+
try {
|
|
6396
|
+
await this.deliverHybridProse(agent, seg, locus, true);
|
|
6397
|
+
} catch (err) {
|
|
6398
|
+
console.error('trailing hybrid prose delivery failed:', err);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
} else if (agent.proseRouting === 'explicit') {
|
|
6217
6403
|
if (segments.length > 0) {
|
|
6218
6404
|
console.error(
|
|
6219
6405
|
`[prose] ${agent.name}: tool-call turn [${toolNames.join(', ')}] -> ${segments.length} trailing segment(s) via prose gateway`,
|
|
@@ -7075,6 +7261,9 @@ export class AgentFramework {
|
|
|
7075
7261
|
const blocks: ContentBlock[] = toolResults.map(tc => ({
|
|
7076
7262
|
type: 'tool_result' as const,
|
|
7077
7263
|
toolUseId: tc.id,
|
|
7264
|
+
// Persisted so XML replay can reconstruct the legacy <tool_name>
|
|
7265
|
+
// element byte-identically to the live injection.
|
|
7266
|
+
toolName: tc.name,
|
|
7078
7267
|
content: spilled.get(tc.id)!.text,
|
|
7079
7268
|
isError: tc.result.isError,
|
|
7080
7269
|
}));
|
|
@@ -7334,7 +7523,7 @@ export class AgentFramework {
|
|
|
7334
7523
|
/**
|
|
7335
7524
|
* Effective tool-result inline cap for an agent, with provenance. Desired
|
|
7336
7525
|
* value: resident's durable agent_settings value → residence
|
|
7337
|
-
* FrameworkConfig.toolResultInlineMaxChars → house default (
|
|
7526
|
+
* FrameworkConfig.toolResultInlineMaxChars → house default (24000).
|
|
7338
7527
|
* Effective value: min(desired, strategy bound) for EVERY source — the
|
|
7339
7528
|
* strategy's per-message safety limit is a ceiling, not a suggestion
|
|
7340
7529
|
* (Sol's #94 ruling: a durable preference must not be a durable path for
|
|
@@ -9960,6 +10149,7 @@ export class AgentFramework {
|
|
|
9960
10149
|
// range/shape checks.
|
|
9961
10150
|
const input = (call.input ?? {}) as {
|
|
9962
10151
|
name?: unknown; match?: unknown; resets?: unknown; position?: unknown;
|
|
10152
|
+
insertBefore?: unknown; insertAfter?: unknown; passthrough?: unknown;
|
|
9963
10153
|
behavior?: unknown; debounceMs?: unknown; rateLimit?: unknown; passiveSample?: unknown;
|
|
9964
10154
|
};
|
|
9965
10155
|
const behaviorSources = [
|
|
@@ -9990,17 +10180,72 @@ export class AgentFramework {
|
|
|
9990
10180
|
: input.passiveSample !== undefined ? { passive_sample: input.passiveSample }
|
|
9991
10181
|
: input.behavior;
|
|
9992
10182
|
|
|
9993
|
-
|
|
10183
|
+
// Placement: at most one of position / insertBefore / insertAfter.
|
|
10184
|
+
// With none given, a replaced rule stays where it was and a new rule
|
|
10185
|
+
// appends (addPolicy's contract).
|
|
10186
|
+
const placementSources = [
|
|
10187
|
+
input.position !== undefined ? 'position' : null,
|
|
10188
|
+
input.insertBefore !== undefined ? 'insertBefore' : null,
|
|
10189
|
+
input.insertAfter !== undefined ? 'insertAfter' : null,
|
|
10190
|
+
].filter((s): s is string => s !== null);
|
|
10191
|
+
if (placementSources.length > 1) {
|
|
10192
|
+
finish({
|
|
10193
|
+
success: false,
|
|
10194
|
+
error: `wake_add_rule: give at most one placement, got [${placementSources.join(', ')}].`,
|
|
10195
|
+
isError: true,
|
|
10196
|
+
});
|
|
10197
|
+
return;
|
|
10198
|
+
}
|
|
10199
|
+
let position: 'append' | 'prepend' | { before: string } | { after: string } | undefined;
|
|
10200
|
+
if (typeof input.insertBefore === 'string' && input.insertBefore) {
|
|
10201
|
+
position = { before: input.insertBefore };
|
|
10202
|
+
} else if (typeof input.insertAfter === 'string' && input.insertAfter) {
|
|
10203
|
+
position = { after: input.insertAfter };
|
|
10204
|
+
} else if (input.position === 'prepend' || input.position === 'append') {
|
|
10205
|
+
position = input.position;
|
|
10206
|
+
}
|
|
10207
|
+
|
|
9994
10208
|
const rawPolicy = {
|
|
9995
10209
|
name: input.name,
|
|
9996
10210
|
match: input.match ?? {},
|
|
9997
10211
|
behavior,
|
|
10212
|
+
...(input.passthrough !== undefined ? { passthrough: input.passthrough } : {}),
|
|
9998
10213
|
...(input.resets !== undefined ? { resets: input.resets } : {}),
|
|
9999
10214
|
};
|
|
10000
|
-
|
|
10215
|
+
|
|
10216
|
+
// Before/after probe table + shadow lint: the moment a rule changes the
|
|
10217
|
+
// evaluation order is the moment its real semantics must be legible
|
|
10218
|
+
// (af#105 — a prepended catch-all sampler silently disabled the
|
|
10219
|
+
// addressed-message wakes for 12h before anyone could see why).
|
|
10220
|
+
const before = new Map(
|
|
10221
|
+
this.eventGate!.probeTable().map((r) => [r.probe, r]),
|
|
10222
|
+
);
|
|
10223
|
+
const policy = this.addGatePolicy(rawPolicy, position ? { position } : undefined);
|
|
10224
|
+
const after = this.eventGate!.probeTable();
|
|
10225
|
+
const probes = after.map((r) => {
|
|
10226
|
+
const prev = before.get(r.probe);
|
|
10227
|
+
const changed = !!prev && (prev.policy !== r.policy || prev.wouldWake !== r.wouldWake);
|
|
10228
|
+
return {
|
|
10229
|
+
probe: r.probe,
|
|
10230
|
+
rule: r.policy ?? '(default)',
|
|
10231
|
+
behavior: r.behavior,
|
|
10232
|
+
wouldWake: r.wouldWake,
|
|
10233
|
+
...(changed ? { changed: true, was: `${prev!.policy ?? '(default)'} (wouldWake=${prev!.wouldWake})` } : {}),
|
|
10234
|
+
};
|
|
10235
|
+
});
|
|
10236
|
+
const shadowWarnings = this.eventGate!.lintShadows()
|
|
10237
|
+
.map((w) => formatShadowWarning(w));
|
|
10001
10238
|
finish({
|
|
10002
10239
|
success: true,
|
|
10003
|
-
data: {
|
|
10240
|
+
data: {
|
|
10241
|
+
added: policy.name,
|
|
10242
|
+
policies: this.getGatePolicyNames(),
|
|
10243
|
+
...(shadowWarnings.length > 0 ? { shadowWarnings } : {}),
|
|
10244
|
+
probes,
|
|
10245
|
+
...(probes.some((p) => 'changed' in p)
|
|
10246
|
+
? {}
|
|
10247
|
+
: { note: 'No canonical probe changed its winning rule — the new rule affects none of the common chat/heartbeat shapes, or only channel/path-specific events the probes do not model.' }),
|
|
10248
|
+
},
|
|
10004
10249
|
});
|
|
10005
10250
|
} catch (err) {
|
|
10006
10251
|
const msg = err instanceof Error ? err.message : String(err);
|