@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/dist/src/framework.js
CHANGED
|
@@ -13,7 +13,7 @@ import { computeGrant, CapabilityGrant, expandAdvertisementShorthand } from './m
|
|
|
13
13
|
import { maskNegotiatedCapabilities } from './mcpl/capability-mask.js';
|
|
14
14
|
import { HookOrchestrator } from './mcpl/hook-orchestrator.js';
|
|
15
15
|
import { PushHandler } from './mcpl/push-handler.js';
|
|
16
|
-
import { parseProsePrefix } from './mcpl/prose-grammar.js';
|
|
16
|
+
import { parseProsePrefix, parseHybridProsePrefix } from './mcpl/prose-grammar.js';
|
|
17
17
|
import { ProseStreamRouter } from './mcpl/prose-stream-router.js';
|
|
18
18
|
import { InferenceRouter } from './mcpl/inference-router.js';
|
|
19
19
|
import { ChannelRegistry } from './mcpl/channel-registry.js';
|
|
@@ -64,6 +64,8 @@ function sniffImageMediaType(data) {
|
|
|
64
64
|
const SILENCING_TOOLS = new Set([
|
|
65
65
|
'skip_reply', 'channel_publish', 'send_message', 'reply_message', 'send_dm',
|
|
66
66
|
]);
|
|
67
|
+
/** World-surface publication names that outrank hybrid prose envelopes. */
|
|
68
|
+
const HYBRID_PUBLICATION_TOOLS = new Set(['say', 'whisper']);
|
|
67
69
|
/**
|
|
68
70
|
* True when an injected message is real conversational input — something the
|
|
69
71
|
* agent might actually be replying to — rather than ambient machinery.
|
|
@@ -159,6 +161,13 @@ function proseModePrimer(mode) {
|
|
|
159
161
|
return '[prose-routing] Mode change: your plain text auto-routes to the ' +
|
|
160
162
|
'conversational locus again. `>>` destination prefixes are no longer needed.';
|
|
161
163
|
}
|
|
164
|
+
if (mode === 'hybrid') {
|
|
165
|
+
return '[prose-routing] Hybrid routing enabled: unprefixed text still lands in the ' +
|
|
166
|
+
'current conversational locus. A leading `>>>destination` envelope instead routes ' +
|
|
167
|
+
'that prose to one uniquely resolved authorized Discord or Eidoverse channel. The ' +
|
|
168
|
+
'envelope remains in your memory but recipients see only its body; delivery or failure ' +
|
|
169
|
+
'is reported back to you.';
|
|
170
|
+
}
|
|
162
171
|
// Deliberately terse: a short event-style notice is classifier-safe from
|
|
163
172
|
// the user role (ablation D, 2026-07-24), while the full grammar as a user
|
|
164
173
|
// message drew a deterministic reasoning_extraction refusal (ablation A/B).
|
|
@@ -249,7 +258,7 @@ function validateUtilityArgs(input, schema) {
|
|
|
249
258
|
}
|
|
250
259
|
import { CheckpointManager } from './mcpl/checkpoint-manager.js';
|
|
251
260
|
import { isToolAllowed } from './mcpl/tool-policy.js';
|
|
252
|
-
import { EventGate } from './gate/event-gate.js';
|
|
261
|
+
import { EventGate, formatShadowWarning } from './gate/event-gate.js';
|
|
253
262
|
import { UsageTracker } from './usage/usage-tracker.js';
|
|
254
263
|
import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
|
|
255
264
|
import { DEFAULT_DISCORD_AWARENESS_EMOJI, DiscordAwarenessOutbox, defaultDiscordAwarenessOutboxPath, extractDiscordAwarenessRefs, } from './recovery/discord-awareness-outbox.js';
|
|
@@ -459,6 +468,8 @@ export class AgentFramework {
|
|
|
459
468
|
/** Sticky per-TURN delivery target set by the turn's first `>>` prefix.
|
|
460
469
|
* Cleared at every non-restart turn start; survives context restarts. */
|
|
461
470
|
proseTargetPins = new Map();
|
|
471
|
+
/** Hybrid router is fail-closed after a malformed/unresolved envelope until a new valid target. */
|
|
472
|
+
proseHybridSuppressed = new Set();
|
|
462
473
|
/** Agents whose current turn requested `!` continuation — re-woken when the
|
|
463
474
|
* turn completes instead of pausing until the next external event. */
|
|
464
475
|
proseContinuations = new Set();
|
|
@@ -1570,8 +1581,10 @@ export class AgentFramework {
|
|
|
1570
1581
|
/**
|
|
1571
1582
|
* Add or replace (upsert) a gate policy at runtime. Validated and persisted
|
|
1572
1583
|
* to gate.json; hot-applied in memory. Throws if no gate is configured or the
|
|
1573
|
-
* policy is invalid.
|
|
1574
|
-
*
|
|
1584
|
+
* policy is invalid. Placement: `'prepend'` puts a rule ahead of EVERYTHING
|
|
1585
|
+
* (including addressed-message wake rules — see EventGate.lintShadows);
|
|
1586
|
+
* `{before: name}` / `{after: name}` anchor it against a specific rule,
|
|
1587
|
+
* which is usually what a sampler or debounce actually wants.
|
|
1575
1588
|
*/
|
|
1576
1589
|
addGatePolicy(rawPolicy, options) {
|
|
1577
1590
|
if (!this.eventGate) {
|
|
@@ -1904,9 +1917,17 @@ export class AgentFramework {
|
|
|
1904
1917
|
name: 'wake_add_rule',
|
|
1905
1918
|
description: 'Add or replace a wake rule (a gate.json policy) at runtime — no need to ' +
|
|
1906
1919
|
'hand-edit the file. The rule is validated and hot-applied immediately. ' +
|
|
1907
|
-
'A rule with the same `name` replaces the existing one in place
|
|
1908
|
-
'
|
|
1909
|
-
'
|
|
1920
|
+
'A rule with the same `name` replaces the existing one in place (add a ' +
|
|
1921
|
+
'position to move it). ⚠️ ORDERING IS SEMANTICS: first match wins, and a ' +
|
|
1922
|
+
'matched rule CONSUMES the event — a broad rule placed early makes every ' +
|
|
1923
|
+
'later rule unreachable for its scope, including your addressed-message ' +
|
|
1924
|
+
'(DM/mention) wake rules. Prefer `insertBefore`/`insertAfter` to place a ' +
|
|
1925
|
+
'rule relative to a specific existing rule; use `position: "prepend"` only ' +
|
|
1926
|
+
'when the rule really must beat EVERYTHING. For "additionally wake me ' +
|
|
1927
|
+
'every Nth event / at most N per hour" governors, set `passthrough: true` ' +
|
|
1928
|
+
'so non-firing matches fall through to your normal rules instead of ' +
|
|
1929
|
+
'swallowing them. The result includes a before/after probe table and any ' +
|
|
1930
|
+
'shadow warnings — READ THEM before ending the turn. Two common shapes:\n' +
|
|
1910
1931
|
'• Watch a FILE/workspace path: match on `mount` + `pathGlob`, e.g. ' +
|
|
1911
1932
|
'`{ name: "watch-notes", match: { scope: ["workspace:modified"], mount: "project", pathGlob: "notes/*.md" }, behavior: "always" }`.\n' +
|
|
1912
1933
|
'• Watch a CHANNEL: match on `source` + `channel` (and/or `tagsAny: ["chat:ambient"]`), ' +
|
|
@@ -1973,7 +1994,27 @@ export class AgentFramework {
|
|
|
1973
1994
|
position: {
|
|
1974
1995
|
type: 'string',
|
|
1975
1996
|
enum: ['append', 'prepend'],
|
|
1976
|
-
description: '
|
|
1997
|
+
description: 'Coarse placement. "append" (default) adds at the end; "prepend" puts the rule ' +
|
|
1998
|
+
'ahead of EVERYTHING — including addressed-message wake rules — so prefer ' +
|
|
1999
|
+
'insertBefore/insertAfter unless the rule really must beat every other rule. ' +
|
|
2000
|
+
'When replacing an existing name, giving any placement MOVES the rule there; ' +
|
|
2001
|
+
'omit all placement fields to update it in place.',
|
|
2002
|
+
},
|
|
2003
|
+
insertBefore: {
|
|
2004
|
+
type: 'string',
|
|
2005
|
+
description: 'Insert the rule immediately BEFORE the named existing rule (error if absent). ' +
|
|
2006
|
+
'The safe way to order against a specific catch-all, e.g. insertBefore: "discord-ambient".',
|
|
2007
|
+
},
|
|
2008
|
+
insertAfter: {
|
|
2009
|
+
type: 'string',
|
|
2010
|
+
description: 'Insert the rule immediately AFTER the named existing rule (error if absent).',
|
|
2011
|
+
},
|
|
2012
|
+
passthrough: {
|
|
2013
|
+
type: 'boolean',
|
|
2014
|
+
description: 'Observer semantics for rateLimit/passiveSample rules: when the rule matches but ' +
|
|
2015
|
+
'does not fire (below N, bucket empty), the event FALLS THROUGH to later rules ' +
|
|
2016
|
+
'instead of being consumed. Use for "additionally wake me every Nth event" ' +
|
|
2017
|
+
'governors so they never swallow DM/mention wakes.',
|
|
1977
2018
|
},
|
|
1978
2019
|
resets: {
|
|
1979
2020
|
type: 'array',
|
|
@@ -4307,6 +4348,98 @@ export class AgentFramework {
|
|
|
4307
4348
|
await this.deliverProseEnvelope(agent, envelope.replace(/^\s+(?=>>)/, ''));
|
|
4308
4349
|
}
|
|
4309
4350
|
}
|
|
4351
|
+
/** Locus-preserving explicit publication. Source remains byte-identical in Chronicle. */
|
|
4352
|
+
async deliverHybridProse(agent, rawText, locus, allowLocus) {
|
|
4353
|
+
const lines = rawText.split('\n');
|
|
4354
|
+
const envelopes = [];
|
|
4355
|
+
let current = [];
|
|
4356
|
+
for (const line of lines) {
|
|
4357
|
+
if (line.trimStart().startsWith('>>>') && current.length > 0) {
|
|
4358
|
+
envelopes.push(current.join('\n'));
|
|
4359
|
+
current = [];
|
|
4360
|
+
}
|
|
4361
|
+
current.push(line);
|
|
4362
|
+
}
|
|
4363
|
+
if (current.length > 0)
|
|
4364
|
+
envelopes.push(current.join('\n'));
|
|
4365
|
+
for (const envelope of envelopes) {
|
|
4366
|
+
if (!envelope.trim())
|
|
4367
|
+
continue;
|
|
4368
|
+
const normalized = envelope.replace(/^\s+(?=>>>)/, '');
|
|
4369
|
+
const attempted = normalized.startsWith('>>>');
|
|
4370
|
+
const parsed = parseHybridProsePrefix(normalized);
|
|
4371
|
+
if (parsed.continueTurn)
|
|
4372
|
+
this.proseContinuations.add(agent.name);
|
|
4373
|
+
if (parsed.kind === 'private') {
|
|
4374
|
+
this.proseTargetPins.delete(agent.name);
|
|
4375
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
4376
|
+
console.error(`[prose] ${agent.name}: >>>skip_reply — ${parsed.body.length} chars kept in context (not sent)`);
|
|
4377
|
+
continue;
|
|
4378
|
+
}
|
|
4379
|
+
if (parsed.kind === 'target') {
|
|
4380
|
+
const resolved = this.channelRegistry.resolveProseTarget(parsed.target);
|
|
4381
|
+
if ('error' in resolved) {
|
|
4382
|
+
this.proseTargetPins.delete(agent.name);
|
|
4383
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
4384
|
+
this.bounceProse(agent, parsed.body, resolved.error, resolved.candidates, '>>>');
|
|
4385
|
+
continue;
|
|
4386
|
+
}
|
|
4387
|
+
let body = parsed.body;
|
|
4388
|
+
const usedClipboard = body.includes('{{unsent}}');
|
|
4389
|
+
if (usedClipboard)
|
|
4390
|
+
body = body.replaceAll('{{unsent}}', this.proseClipboards.get(agent.name) ?? '');
|
|
4391
|
+
this.proseTargetPins.set(agent.name, resolved.channelId);
|
|
4392
|
+
this.proseHybridSuppressed.delete(agent.name);
|
|
4393
|
+
if (!body.trim()) {
|
|
4394
|
+
console.error(`[prose] ${agent.name}: >>>${parsed.target} established target; empty body — nothing sent yet`);
|
|
4395
|
+
continue;
|
|
4396
|
+
}
|
|
4397
|
+
try {
|
|
4398
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, body, resolved.channelId);
|
|
4399
|
+
this.recordProseDelivery(agent.name, outcome);
|
|
4400
|
+
if (outcome?.delivered) {
|
|
4401
|
+
this.proseBounceStreaks.delete(agent.name);
|
|
4402
|
+
if (usedClipboard)
|
|
4403
|
+
this.proseClipboards.delete(agent.name);
|
|
4404
|
+
}
|
|
4405
|
+
else {
|
|
4406
|
+
this.proseTargetPins.delete(agent.name);
|
|
4407
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
4408
|
+
this.bounceProse(agent, parsed.body, `delivery to ${resolved.channelId} was not confirmed`, undefined, '>>>');
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
catch (err) {
|
|
4412
|
+
this.proseTargetPins.delete(agent.name);
|
|
4413
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
4414
|
+
this.bounceProse(agent, parsed.body, `delivery to ${resolved.channelId} failed: ${err instanceof Error ? err.message : String(err)}`, undefined, '>>>');
|
|
4415
|
+
}
|
|
4416
|
+
continue;
|
|
4417
|
+
}
|
|
4418
|
+
if (attempted) {
|
|
4419
|
+
this.proseTargetPins.delete(agent.name);
|
|
4420
|
+
this.proseHybridSuppressed.add(agent.name);
|
|
4421
|
+
this.bounceProse(agent, normalized, 'malformed >>> routing envelope: destination is missing', undefined, '>>>');
|
|
4422
|
+
continue;
|
|
4423
|
+
}
|
|
4424
|
+
if (this.proseHybridSuppressed.has(agent.name)) {
|
|
4425
|
+
this.recordProseSuppression(agent.name, 1);
|
|
4426
|
+
continue;
|
|
4427
|
+
}
|
|
4428
|
+
const sticky = this.proseTargetPins.get(agent.name);
|
|
4429
|
+
if (!allowLocus && !sticky) {
|
|
4430
|
+
this.recordProseSuppression(agent.name, 1);
|
|
4431
|
+
continue;
|
|
4432
|
+
}
|
|
4433
|
+
const target = sticky ?? locus;
|
|
4434
|
+
try {
|
|
4435
|
+
const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target);
|
|
4436
|
+
this.recordProseDelivery(agent.name, outcome);
|
|
4437
|
+
}
|
|
4438
|
+
catch (err) {
|
|
4439
|
+
console.error('hybrid locus delivery failed:', err);
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4310
4443
|
async deliverProseEnvelope(agent, rawText) {
|
|
4311
4444
|
const name = agent.name;
|
|
4312
4445
|
const parsed = parseProsePrefix(rawText);
|
|
@@ -4365,7 +4498,7 @@ export class AgentFramework {
|
|
|
4365
4498
|
}
|
|
4366
4499
|
/** Cap on consecutive bounce-triggered wakes (notices still append after). */
|
|
4367
4500
|
static PROSE_BOUNCE_WAKE_CAP = 2;
|
|
4368
|
-
bounceProse(agent, text, reason, candidates) {
|
|
4501
|
+
bounceProse(agent, text, reason, candidates, prefix = '>>') {
|
|
4369
4502
|
const name = agent.name;
|
|
4370
4503
|
this.proseClipboards.set(name, text);
|
|
4371
4504
|
const streak = (this.proseBounceStreaks.get(name) ?? 0) + 1;
|
|
@@ -4373,8 +4506,9 @@ export class AgentFramework {
|
|
|
4373
4506
|
const cand = candidates?.length ? ` Known channels it could mean: ${candidates.join(', ')}.` : '';
|
|
4374
4507
|
const notice = `[prose-routing] Your text (${text.length} chars) was not delivered — ${reason}.${cand} ` +
|
|
4375
4508
|
'The text is retained; nothing is lost. To deliver it unchanged, reply with a ' +
|
|
4376
|
-
|
|
4377
|
-
|
|
4509
|
+
`destination plus the token {{unsent}}, e.g. "${prefix}#channel {{unsent}}" or "${prefix}@person {{unsent}}". ` +
|
|
4510
|
+
`"${prefix}skip_reply {{unsent}}" keeps it in context only.` +
|
|
4511
|
+
(prefix === '>>' ? ' The prose_help tool shows the full syntax.' : '');
|
|
4378
4512
|
try {
|
|
4379
4513
|
// Through framework.addMessage, NOT the context manager directly: while
|
|
4380
4514
|
// the turn is still streaming this defers the notice to the next tool
|
|
@@ -4563,6 +4697,9 @@ export class AgentFramework {
|
|
|
4563
4697
|
this.turnEngagedChannels.delete(agent.name);
|
|
4564
4698
|
this.turnProseDeliveries.delete(agent.name);
|
|
4565
4699
|
this.turnProseSuppressed.delete(agent.name);
|
|
4700
|
+
this.proseHybridSuppressed.delete(agent.name);
|
|
4701
|
+
if (agent.proseRouting === 'hybrid')
|
|
4702
|
+
this.proseTargetPins.delete(agent.name);
|
|
4566
4703
|
if (agent.proseRouting === 'explicit') {
|
|
4567
4704
|
// Explicit prose routing: there is no locus. The model names every
|
|
4568
4705
|
// destination in-band (`>>` prefixes); the only turn state is the
|
|
@@ -4801,7 +4938,7 @@ export class AgentFramework {
|
|
|
4801
4938
|
});
|
|
4802
4939
|
const proseStream = this.channelRegistry
|
|
4803
4940
|
? new ProseStreamRouter({
|
|
4804
|
-
mode: agent.proseRouting === 'explicit' ? 'explicit' : 'locus',
|
|
4941
|
+
mode: agent.proseRouting === 'explicit' ? 'explicit' : agent.proseRouting === 'hybrid' ? 'hybrid' : 'locus',
|
|
4805
4942
|
initialTarget: typingChannel,
|
|
4806
4943
|
resolve: (spec) => {
|
|
4807
4944
|
const r = this.channelRegistry.resolveProseTarget(spec);
|
|
@@ -4917,9 +5054,17 @@ export class AgentFramework {
|
|
|
4917
5054
|
const rawXml = rawText && /^<(antml:)?function_calls>/.test(rawText)
|
|
4918
5055
|
? rawText
|
|
4919
5056
|
: undefined;
|
|
5057
|
+
// Persist THIS round's text only. `roundPreamble` (membrane
|
|
5058
|
+
// ≥0.5.79) is the per-round delta; the legacy `preamble` is
|
|
5059
|
+
// CUMULATIVE in XML mode — persisting it per round stored each
|
|
5060
|
+
// round's text N times and re-persisted injected
|
|
5061
|
+
// <function_results> as the agent's own words (the Evander
|
|
5062
|
+
// 2026-08-08 scaffold-leak pyramid: r1 ×3, r2 ×2).
|
|
5063
|
+
const roundText = event.context.roundPreamble ??
|
|
5064
|
+
event.context.preamble;
|
|
4920
5065
|
assistantBlocks = [];
|
|
4921
|
-
if (
|
|
4922
|
-
assistantBlocks.push({ type: 'text', text:
|
|
5066
|
+
if (roundText) {
|
|
5067
|
+
assistantBlocks.push({ type: 'text', text: roundText });
|
|
4923
5068
|
}
|
|
4924
5069
|
for (const c of event.calls) {
|
|
4925
5070
|
assistantBlocks.push({
|
|
@@ -4956,14 +5101,28 @@ export class AgentFramework {
|
|
|
4956
5101
|
const roundToolNames = event.calls.map((c) => c.name);
|
|
4957
5102
|
const hasSameRoundPrivateThink = roundToolNames.includes('think') &&
|
|
4958
5103
|
requestSnapshot.sameRoundThinkTextPolicy === 'private';
|
|
4959
|
-
if (roundToolNames.some((n) => SILENCING_TOOLS.has(bareToolName(n))
|
|
5104
|
+
if (roundToolNames.some((n) => SILENCING_TOOLS.has(bareToolName(n)) ||
|
|
5105
|
+
(agent.proseRouting === 'hybrid' && HYBRID_PUBLICATION_TOOLS.has(bareToolName(n))))) {
|
|
4960
5106
|
turnSilenced = true;
|
|
4961
5107
|
}
|
|
4962
5108
|
if (roundContent && roundContent.length > 0) {
|
|
4963
5109
|
liveProseRouting = true;
|
|
4964
5110
|
const roundSegments = splitProseSegments(assistantBlocks);
|
|
4965
5111
|
if (roundSegments.length > 0) {
|
|
4966
|
-
if (agent.proseRouting === '
|
|
5112
|
+
if (agent.proseRouting === 'hybrid') {
|
|
5113
|
+
if (turnSilenced) {
|
|
5114
|
+
this.recordProseSuppression(agent.name, roundSegments.length);
|
|
5115
|
+
}
|
|
5116
|
+
else if (!hasSameRoundPrivateThink) {
|
|
5117
|
+
const locus = resolveTurnLocus();
|
|
5118
|
+
for (const seg of roundSegments) {
|
|
5119
|
+
turnSpeechChain = turnSpeechChain
|
|
5120
|
+
.then(() => this.deliverHybridProse(agent, seg, locus, true))
|
|
5121
|
+
.catch((err) => console.error('mid-turn hybrid prose delivery failed:', err));
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
else if (agent.proseRouting === 'explicit') {
|
|
4967
5126
|
// Explicit mode: every segment through the prose gateway.
|
|
4968
5127
|
// Silencing does not apply — unprefixed prose bounces and
|
|
4969
5128
|
// prefixed prose is deliberate; think-privacy still holds.
|
|
@@ -5243,7 +5402,17 @@ export class AgentFramework {
|
|
|
5243
5402
|
.join('\n')
|
|
5244
5403
|
.trim();
|
|
5245
5404
|
if (speechText) {
|
|
5246
|
-
if (agent.proseRouting === '
|
|
5405
|
+
if (agent.proseRouting === 'hybrid') {
|
|
5406
|
+
const locus = resolveTurnLocus();
|
|
5407
|
+
console.error(`[prose] ${agent.name}: text-only turn -> hybrid prose gateway`);
|
|
5408
|
+
try {
|
|
5409
|
+
await this.deliverHybridProse(agent, speechText, locus, true);
|
|
5410
|
+
}
|
|
5411
|
+
catch (err) {
|
|
5412
|
+
console.error('text-only hybrid prose delivery failed:', err);
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
else if (agent.proseRouting === 'explicit') {
|
|
5247
5416
|
console.error(`[prose] ${agent.name}: text-only turn -> prose gateway`);
|
|
5248
5417
|
try {
|
|
5249
5418
|
await this.deliverProse(agent, speechText);
|
|
@@ -5292,13 +5461,30 @@ export class AgentFramework {
|
|
|
5292
5461
|
.filter((n) => typeof n === 'string');
|
|
5293
5462
|
const silenced = liveProseRouting
|
|
5294
5463
|
? turnSilenced
|
|
5295
|
-
: turnSilenced || toolNames.some((n) => SILENCING_TOOLS.has(bareToolName(n))
|
|
5464
|
+
: turnSilenced || toolNames.some((n) => SILENCING_TOOLS.has(bareToolName(n)) ||
|
|
5465
|
+
(agent.proseRouting === 'hybrid' && HYBRID_PUBLICATION_TOOLS.has(bareToolName(n))));
|
|
5296
5466
|
const segments = splitProseSegments(liveProseRouting ? terminalContent : response.content);
|
|
5297
5467
|
// Preserve in-channel ordering: everything enqueued live must
|
|
5298
5468
|
// land before the trailing prose. Awaited even when silenced —
|
|
5299
5469
|
// the chain may still be flushing earlier rounds' posts.
|
|
5300
5470
|
await turnSpeechChain;
|
|
5301
|
-
if (agent.proseRouting === '
|
|
5471
|
+
if (agent.proseRouting === 'hybrid') {
|
|
5472
|
+
if (silenced && segments.length > 0) {
|
|
5473
|
+
this.recordProseSuppression(agent.name, segments.length);
|
|
5474
|
+
}
|
|
5475
|
+
else if (segments.length > 0) {
|
|
5476
|
+
const locus = resolveTurnLocus();
|
|
5477
|
+
for (const seg of segments) {
|
|
5478
|
+
try {
|
|
5479
|
+
await this.deliverHybridProse(agent, seg, locus, true);
|
|
5480
|
+
}
|
|
5481
|
+
catch (err) {
|
|
5482
|
+
console.error('trailing hybrid prose delivery failed:', err);
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
else if (agent.proseRouting === 'explicit') {
|
|
5302
5488
|
if (segments.length > 0) {
|
|
5303
5489
|
console.error(`[prose] ${agent.name}: tool-call turn [${toolNames.join(', ')}] -> ${segments.length} trailing segment(s) via prose gateway`);
|
|
5304
5490
|
for (const seg of segments) {
|
|
@@ -6082,6 +6268,9 @@ export class AgentFramework {
|
|
|
6082
6268
|
const blocks = toolResults.map(tc => ({
|
|
6083
6269
|
type: 'tool_result',
|
|
6084
6270
|
toolUseId: tc.id,
|
|
6271
|
+
// Persisted so XML replay can reconstruct the legacy <tool_name>
|
|
6272
|
+
// element byte-identically to the live injection.
|
|
6273
|
+
toolName: tc.name,
|
|
6085
6274
|
content: spilled.get(tc.id).text,
|
|
6086
6275
|
isError: tc.result.isError,
|
|
6087
6276
|
}));
|
|
@@ -6316,7 +6505,7 @@ export class AgentFramework {
|
|
|
6316
6505
|
/**
|
|
6317
6506
|
* Effective tool-result inline cap for an agent, with provenance. Desired
|
|
6318
6507
|
* value: resident's durable agent_settings value → residence
|
|
6319
|
-
* FrameworkConfig.toolResultInlineMaxChars → house default (
|
|
6508
|
+
* FrameworkConfig.toolResultInlineMaxChars → house default (24000).
|
|
6320
6509
|
* Effective value: min(desired, strategy bound) for EVERY source — the
|
|
6321
6510
|
* strategy's per-message safety limit is a ceiling, not a suggestion
|
|
6322
6511
|
* (Sol's #94 ruling: a durable preference must not be a durable path for
|
|
@@ -8520,17 +8709,70 @@ export class AgentFramework {
|
|
|
8520
8709
|
: input.rateLimit !== undefined ? { rate_limit: input.rateLimit }
|
|
8521
8710
|
: input.passiveSample !== undefined ? { passive_sample: input.passiveSample }
|
|
8522
8711
|
: input.behavior;
|
|
8523
|
-
|
|
8712
|
+
// Placement: at most one of position / insertBefore / insertAfter.
|
|
8713
|
+
// With none given, a replaced rule stays where it was and a new rule
|
|
8714
|
+
// appends (addPolicy's contract).
|
|
8715
|
+
const placementSources = [
|
|
8716
|
+
input.position !== undefined ? 'position' : null,
|
|
8717
|
+
input.insertBefore !== undefined ? 'insertBefore' : null,
|
|
8718
|
+
input.insertAfter !== undefined ? 'insertAfter' : null,
|
|
8719
|
+
].filter((s) => s !== null);
|
|
8720
|
+
if (placementSources.length > 1) {
|
|
8721
|
+
finish({
|
|
8722
|
+
success: false,
|
|
8723
|
+
error: `wake_add_rule: give at most one placement, got [${placementSources.join(', ')}].`,
|
|
8724
|
+
isError: true,
|
|
8725
|
+
});
|
|
8726
|
+
return;
|
|
8727
|
+
}
|
|
8728
|
+
let position;
|
|
8729
|
+
if (typeof input.insertBefore === 'string' && input.insertBefore) {
|
|
8730
|
+
position = { before: input.insertBefore };
|
|
8731
|
+
}
|
|
8732
|
+
else if (typeof input.insertAfter === 'string' && input.insertAfter) {
|
|
8733
|
+
position = { after: input.insertAfter };
|
|
8734
|
+
}
|
|
8735
|
+
else if (input.position === 'prepend' || input.position === 'append') {
|
|
8736
|
+
position = input.position;
|
|
8737
|
+
}
|
|
8524
8738
|
const rawPolicy = {
|
|
8525
8739
|
name: input.name,
|
|
8526
8740
|
match: input.match ?? {},
|
|
8527
8741
|
behavior,
|
|
8742
|
+
...(input.passthrough !== undefined ? { passthrough: input.passthrough } : {}),
|
|
8528
8743
|
...(input.resets !== undefined ? { resets: input.resets } : {}),
|
|
8529
8744
|
};
|
|
8530
|
-
|
|
8745
|
+
// Before/after probe table + shadow lint: the moment a rule changes the
|
|
8746
|
+
// evaluation order is the moment its real semantics must be legible
|
|
8747
|
+
// (af#105 — a prepended catch-all sampler silently disabled the
|
|
8748
|
+
// addressed-message wakes for 12h before anyone could see why).
|
|
8749
|
+
const before = new Map(this.eventGate.probeTable().map((r) => [r.probe, r]));
|
|
8750
|
+
const policy = this.addGatePolicy(rawPolicy, position ? { position } : undefined);
|
|
8751
|
+
const after = this.eventGate.probeTable();
|
|
8752
|
+
const probes = after.map((r) => {
|
|
8753
|
+
const prev = before.get(r.probe);
|
|
8754
|
+
const changed = !!prev && (prev.policy !== r.policy || prev.wouldWake !== r.wouldWake);
|
|
8755
|
+
return {
|
|
8756
|
+
probe: r.probe,
|
|
8757
|
+
rule: r.policy ?? '(default)',
|
|
8758
|
+
behavior: r.behavior,
|
|
8759
|
+
wouldWake: r.wouldWake,
|
|
8760
|
+
...(changed ? { changed: true, was: `${prev.policy ?? '(default)'} (wouldWake=${prev.wouldWake})` } : {}),
|
|
8761
|
+
};
|
|
8762
|
+
});
|
|
8763
|
+
const shadowWarnings = this.eventGate.lintShadows()
|
|
8764
|
+
.map((w) => formatShadowWarning(w));
|
|
8531
8765
|
finish({
|
|
8532
8766
|
success: true,
|
|
8533
|
-
data: {
|
|
8767
|
+
data: {
|
|
8768
|
+
added: policy.name,
|
|
8769
|
+
policies: this.getGatePolicyNames(),
|
|
8770
|
+
...(shadowWarnings.length > 0 ? { shadowWarnings } : {}),
|
|
8771
|
+
probes,
|
|
8772
|
+
...(probes.some((p) => 'changed' in p)
|
|
8773
|
+
? {}
|
|
8774
|
+
: { 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.' }),
|
|
8775
|
+
},
|
|
8534
8776
|
});
|
|
8535
8777
|
}
|
|
8536
8778
|
catch (err) {
|