Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/disabled-console-receipt.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Under `proseRouting: "disabled"`, the turn-end receipt now reads `[delivered] plain speech → no channel — N plain-speech segment(s) kept in the transcript (…)` instead of `[delivered] nothing — N plain-speech segment(s) suppressed (…)`, which read to a resident talking with its operator through a console or Web UI as if its replies had reached no one (#225).
5 changes: 3 additions & 2 deletions docs/disabled-prose-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ want every external utterance to be an explicit tool action.
- Ambient traffic and channel opens cannot establish a publication locus.
- Outgoing prose streaming and typing indicators are disabled.
- Explicit publish tools (Discord/Portal sends, Eidoverse `say`, etc.) are unchanged.
- Authored prose remains in Chronicle. At turn end, a private `[delivered] nothing`
receipt records how many prose segments were suppressed.
- Authored prose remains in Chronicle. An ordinary turn that wrote plain speech
ends with a private `[delivered] plain speech → no channel` receipt, which
records how many segments stayed in the transcript instead of going to a channel.
- The mode injects no model-visible primer.
- Sleep announcements remain private unless the resident sends one explicitly.

Expand Down
17 changes: 12 additions & 5 deletions src/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13483,7 +13483,7 @@ export class AgentFramework {
if (kept > 0) notes.push(`${kept} plain-speech segment(s) kept private (skip_reply or a private think)`);
if (suppressed > 0) {
notes.push(agent.proseRouting === 'disabled'
? `${suppressed} plain-speech segment(s) suppressed (proseRouting=disabled — publish only with an explicit send tool)`
? `${suppressed} plain-speech segment(s) kept in the transcript (proseRouting=disabled — post to a channel only with an explicit send tool)`
: `${suppressed} plain-speech segment(s) suppressed`);
}
const suppressedNote = notes.join(' · ');
Expand All @@ -13492,10 +13492,17 @@ export class AgentFramework {
// since the turn held it may have had such an attempt: its state is
// unknown now.
const uncertain = unconfirmed.length > 0 || dismissedAtRisk.length > 0 || letGo.length > 0;
// In disabled mode, routing posts no plain speech to any channel, and the
// words stay in the transcript. The receipt says only that. Who watched
// the turn's stream is for the host to know, and an explicit send
// reports itself in its tool result. Disabled mode holds no drafts, so
// it is never uncertain.
const text =
shown.length > 0
? `[delivered] plain speech → ${shown.join(' · ')}${suppressedNote ? ` · ${suppressedNote}` : ''}`
: `[delivered] nothing${uncertain ? ' confirmed' : ''} — ${suppressedNote}`;
: agent.proseRouting === 'disabled'
? `[delivered] plain speech → no channel — ${suppressedNote}`
: `[delivered] nothing${uncertain ? ' confirmed' : ''} — ${suppressedNote}`;
try {
const mid = agent.getContextManager().addMessage(
'user',
Expand Down Expand Up @@ -16346,9 +16353,9 @@ export class AgentFramework {
// receipt. Queued deliveries and holds settled before final
// dispatch, even when the last response is empty, so the receipt
// and the continuation see the last rounds' deliveries;
// trailing/text-only segments were awaited in-loop. Locus and
// hybrid modes only; explicit-mode envelopes acknowledge
// themselves through the prose gateway.
// trailing/text-only segments were awaited in-loop. Every mode
// but explicit, whose envelopes acknowledge themselves through
// the prose gateway.
if (!trigger?.suppressProse && turnProseRouting !== 'explicit') {
this.appendProseDeliveryReceipt(agent, streamRouting);
}
Expand Down
3 changes: 2 additions & 1 deletion src/types/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ export interface AgentConfig {
* that publication envelope through the same authorized resolver.
* See docs/hybrid-prose-routing.md.
* - 'disabled': generated prose is never published. Only explicit tools may
* send externally; prose remains in Chronicle with a private suppression receipt.
* send externally; prose remains in Chronicle, and a turn that wrote plain
* speech ends with a private `[delivered] plain speech → no channel` receipt.
*/
proseRouting?: 'locus' | 'explicit' | 'hybrid' | 'disabled';

Expand Down
6 changes: 3 additions & 3 deletions test/explicit-prose-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2202,8 +2202,8 @@ describe('explicit prose routing', () => {
const texts = framework.getAgent('assistant')!.getContextManager().getAllMessages()
.flatMap(m => m.content).filter(b => b.type === 'text').map(b => (b as { text: string }).text);
assert.ok(texts.includes(authored), 'authored prose remains in Chronicle');
assert.ok(texts.some(t => t.includes('[delivered] nothing') && t.includes('proseRouting=disabled')),
'private suppression receipt explains the unsent prose');
assert.ok(texts.includes('[delivered] plain speech → no channel — 1 plain-speech segment(s) kept in the transcript (proseRouting=disabled — post to a channel only with an explicit send tool)'),
'the private receipt says the prose went to no channel and stays in the transcript');

await framework.stop();
});
Expand All @@ -2227,7 +2227,7 @@ describe('explicit prose routing', () => {
assert.equal(module.calls[0]!.name, 'say');
const texts = framework.getAgent('assistant')!.getContextManager().getAllMessages()
.flatMap(m => m.content).filter(b => b.type === 'text').map(b => (b as { text: string }).text);
assert.ok(texts.some(t => t.includes('[delivered] nothing') && t.includes('2 plain-speech segment(s) suppressed')),
assert.ok(texts.some(t => t.includes('[delivered] plain speech → no channel') && t.includes('2 plain-speech segment(s) kept in the transcript')),
'one receipt accounts for mid-turn and trailing prose');

await framework.stop();
Expand Down
2 changes: 1 addition & 1 deletion test/prose-drafts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ describe('held prose drafts, end to end', () => {
createMockResponse([text('More of it.')]),
]);
assert.deepEqual(h.drafts(), []);
assert.ok(h.texts().some((t) => t.startsWith('[delivered] nothing — 2 plain-speech segment(s) suppressed (proseRouting=disabled')));
assert.ok(h.texts().some((t) => t.startsWith('[delivered] plain speech → no channel — 2 plain-speech segment(s) kept in the transcript (proseRouting=disabled')));
} finally {
await h.close();
}
Expand Down
Loading