Skip to content

Commit 6797c81

Browse files
feat(honcho): add observed_peer/observer_peer params to conclusion writes
Closes the write-side symmetry gap. The read tools shipped in 1e70994 already accept literal peer names via query_peer_conclusions; create_conclusion(s) now match. New optional params on create_conclusion and create_conclusions: - observed_peer: literal peer name being observed (overrides 'observed' alias) - observer_peer: literal peer name doing the observing (defaults to AI peer) Enables cell-B writes (user evaluating an AI peer) and arbitrary multi-agent edges (Claude observing Codex, etc.) without breaking the existing 'observed: user|self' alias contract for the common cases. Resolution lives in a single helper, resolveConclusionPeers, used by both create handlers. Literal names take precedence over aliases when provided; alias-only callers see identical behavior. Response payload now reports the resolved (observer, observed) edge for traceability. The bulk handler also reuses cached peer objects (userPeer / aiPeer from the session block) when resolved names match — only fetches fresh peer objects for genuinely arbitrary edges.
1 parent 792b3cc commit 6797c81

2 files changed

Lines changed: 135 additions & 30 deletions

File tree

plugins/honcho/dist/mcp-server.js

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29216,6 +29216,23 @@ var ENV_SHADOW_MAP = {
2921629216
};
2921729217
var OBSERVED_USER = "user";
2921829218
var OBSERVED_SELF = "self";
29219+
function resolveConclusionPeers(args, observed, observationMode, config2) {
29220+
const observedLiteral = args?.observed_peer;
29221+
const observerLiteral = args?.observer_peer;
29222+
if (observedLiteral || observerLiteral) {
29223+
return {
29224+
observerName: observerLiteral ?? config2.aiPeer,
29225+
observedName: observedLiteral ?? config2.peerName
29226+
};
29227+
}
29228+
if (observed === OBSERVED_SELF) {
29229+
return { observerName: config2.aiPeer, observedName: config2.aiPeer };
29230+
}
29231+
return {
29232+
observerName: observationMode === "unified" ? config2.peerName : config2.aiPeer,
29233+
observedName: config2.peerName
29234+
};
29235+
}
2921929236
var DANGEROUS_FIELDS = new Set(["workspace", "endpoint.environment", "endpoint.baseUrl"]);
2922029237
var SESSION_AFFECTING_FIELDS = new Set([
2922129238
"workspace",
@@ -29678,6 +29695,14 @@ For either target, the test before writing is: "would future-me find this useful
2967829695
enum: [OBSERVED_USER, OBSERVED_SELF],
2967929696
description: "'user' (default) saves a conclusion about the user. 'self' saves to the AI peer's own working log \u2014 retrievable across sessions via query_conclusions. Write liberally; consolidation prunes.",
2968029697
default: OBSERVED_USER
29698+
},
29699+
observed_peer: {
29700+
type: "string",
29701+
description: "Literal peer name being observed (escape hatch for multi-agent setups). Overrides the 'observed' alias when provided. Use to write about a specific agent (e.g. observed_peer='codex' for cell-B writes when paired with observer_peer=<userName>)."
29702+
},
29703+
observer_peer: {
29704+
type: "string",
29705+
description: "Literal peer name doing the observing. Defaults to the AI peer (caller). Pass the user peer name to write a cell-B conclusion (user's evaluation of an agent)."
2968129706
}
2968229707
},
2968329708
required: ["content"]
@@ -29967,6 +29992,14 @@ For either target, the test before writing is: "would future-me find this useful
2996729992
enum: [OBSERVED_USER, OBSERVED_SELF],
2996829993
description: "'user' (default) saves conclusions about the user. 'self' saves conclusions about the AI peer (Claude).",
2996929994
default: OBSERVED_USER
29995+
},
29996+
observed_peer: {
29997+
type: "string",
29998+
description: "Literal peer name being observed (escape hatch for multi-agent / cell-B writes). Overrides the 'observed' alias when provided."
29999+
},
30000+
observer_peer: {
30001+
type: "string",
30002+
description: "Literal peer name doing the observing. Defaults to the AI peer (caller). Pass user peer name for cell-B writes."
2997030003
}
2997130004
},
2997230005
required: ["contents"]
@@ -30156,15 +30189,19 @@ For either target, the test before writing is: "would future-me find this useful
3015630189
};
3015730190
}
3015830191
const observed = args?.observed ?? OBSERVED_USER;
30159-
const observationMode = getObservationMode(config2);
30160-
const { scopePeer, observedPeerName } = observed === OBSERVED_SELF ? { scopePeer: await honcho.peer(config2.aiPeer), observedPeerName: config2.aiPeer } : {
30161-
scopePeer: await honcho.peer(observationMode === "unified" ? config2.peerName : config2.aiPeer),
30162-
observedPeerName: config2.peerName
30163-
};
30164-
const conclusionScope = scopePeer.conclusionsOf(observedPeerName);
30165-
const created = await conclusionScope.create(contents.map((content) => ({ content })));
30192+
const { observerName, observedName } = resolveConclusionPeers(args, observed, getObservationMode(config2), config2);
30193+
const observerPeer = await honcho.peer(observerName);
30194+
const created = await observerPeer.conclusionsOf(observedName).create(contents.map((content) => ({ content })));
3016630195
return {
30167-
content: [{ type: "text", text: JSON.stringify({ success: true, created: created.length, observed }) }]
30196+
content: [{
30197+
type: "text",
30198+
text: JSON.stringify({
30199+
success: true,
30200+
created: created.length,
30201+
observer: observerName,
30202+
observed: observedName
30203+
})
30204+
}]
3016830205
};
3016930206
} catch (error3) {
3017030207
return {
@@ -30325,16 +30362,19 @@ For either target, the test before writing is: "would future-me find this useful
3032530362
case "create_conclusion": {
3032630363
const content = args?.content;
3032730364
const observed = args?.observed ?? OBSERVED_USER;
30328-
const { writePeer, observedPeerName } = observed === OBSERVED_SELF ? { writePeer: aiPeer ?? await honcho.peer(config2.aiPeer), observedPeerName: config2.aiPeer } : { writePeer: activePeer, observedPeerName: config2.peerName };
30329-
const conclusions = await writePeer.conclusionsOf(observedPeerName).create({
30365+
const { observerName, observedName } = resolveConclusionPeers(args, observed, observationMode, config2);
30366+
const writePeer = observerName === config2.peerName && activePeer === userPeer ? userPeer : observerName === config2.aiPeer && aiPeer ? aiPeer : await honcho.peer(observerName);
30367+
const conclusions = await writePeer.conclusionsOf(observedName).create({
3033030368
content,
3033130369
sessionId: session.id
3033230370
});
30371+
const tag = observed === OBSERVED_SELF ? "self-" : "";
30372+
const edge = observerName !== config2.aiPeer && observerName !== config2.peerName ? ` (observer=${observerName}, observed=${observedName})` : "";
3033330373
return {
3033430374
content: [
3033530375
{
3033630376
type: "text",
30337-
text: `Saved ${observed === OBSERVED_SELF ? "self-" : ""}conclusion: ${conclusions[0]?.content || content}`
30377+
text: `Saved ${tag}conclusion${edge}: ${conclusions[0]?.content || content}`
3033830378
}
3033930379
]
3034030380
};

plugins/honcho/src/mcp/server.ts

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,37 @@ const OBSERVED_USER = "user" as const;
5555
const OBSERVED_SELF = "self" as const;
5656
type ObservedTarget = typeof OBSERVED_USER | typeof OBSERVED_SELF;
5757

58+
/**
59+
* Resolve (observer, observed) peer names for a conclusion write.
60+
*
61+
* Literal peer-name args (observed_peer / observer_peer) take precedence over
62+
* the alias-based routing — this is the escape hatch for multi-agent setups
63+
* and cell-B writes (user evaluating an AI peer). When neither literal is
64+
* provided, falls back to the OBSERVED_USER/OBSERVED_SELF alias semantics.
65+
*/
66+
function resolveConclusionPeers(
67+
args: Record<string, unknown> | undefined,
68+
observed: ObservedTarget,
69+
observationMode: "unified" | "directional",
70+
config: { peerName: string; aiPeer: string }
71+
): { observerName: string; observedName: string } {
72+
const observedLiteral = args?.observed_peer as string | undefined;
73+
const observerLiteral = args?.observer_peer as string | undefined;
74+
if (observedLiteral || observerLiteral) {
75+
return {
76+
observerName: observerLiteral ?? config.aiPeer,
77+
observedName: observedLiteral ?? config.peerName,
78+
};
79+
}
80+
if (observed === OBSERVED_SELF) {
81+
return { observerName: config.aiPeer, observedName: config.aiPeer };
82+
}
83+
return {
84+
observerName: observationMode === "unified" ? config.peerName : config.aiPeer,
85+
observedName: config.peerName,
86+
};
87+
}
88+
5889
// Fields that require confirm=true to change
5990
const DANGEROUS_FIELDS = new Set(["workspace", "endpoint.environment", "endpoint.baseUrl"]);
6091

@@ -600,6 +631,14 @@ export async function runMcpServer(): Promise<void> {
600631
description: "'user' (default) saves a conclusion about the user. 'self' saves to the AI peer's own working log — retrievable across sessions via query_conclusions. Write liberally; consolidation prunes.",
601632
default: OBSERVED_USER,
602633
},
634+
observed_peer: {
635+
type: "string",
636+
description: "Literal peer name being observed (escape hatch for multi-agent setups). Overrides the 'observed' alias when provided. Use to write about a specific agent (e.g. observed_peer='codex' for cell-B writes when paired with observer_peer=<userName>).",
637+
},
638+
observer_peer: {
639+
type: "string",
640+
description: "Literal peer name doing the observing. Defaults to the AI peer (caller). Pass the user peer name to write a cell-B conclusion (user's evaluation of an agent).",
641+
},
603642
},
604643
required: ["content"],
605644
},
@@ -889,6 +928,14 @@ export async function runMcpServer(): Promise<void> {
889928
description: "'user' (default) saves conclusions about the user. 'self' saves conclusions about the AI peer (Claude).",
890929
default: OBSERVED_USER,
891930
},
931+
observed_peer: {
932+
type: "string",
933+
description: "Literal peer name being observed (escape hatch for multi-agent / cell-B writes). Overrides the 'observed' alias when provided.",
934+
},
935+
observer_peer: {
936+
type: "string",
937+
description: "Literal peer name doing the observing. Defaults to the AI peer (caller). Pass user peer name for cell-B writes.",
938+
},
892939
},
893940
required: ["contents"],
894941
},
@@ -1099,21 +1146,26 @@ export async function runMcpServer(): Promise<void> {
10991146
};
11001147
}
11011148
const observed: ObservedTarget = (args?.observed as ObservedTarget) ?? OBSERVED_USER;
1102-
// Self-conclusions: aiPeer observes itself, regardless of observation mode.
1103-
// User-conclusions: routed by observation mode (unified=user→user, directional=ai→user).
1104-
const observationMode = getObservationMode(config);
1105-
const { scopePeer, observedPeerName } = observed === OBSERVED_SELF
1106-
? { scopePeer: await honcho.peer(config.aiPeer), observedPeerName: config.aiPeer }
1107-
: {
1108-
scopePeer: await honcho.peer(observationMode === "unified" ? config.peerName : config.aiPeer),
1109-
observedPeerName: config.peerName,
1110-
};
1111-
const conclusionScope = scopePeer.conclusionsOf(observedPeerName);
1112-
const created = await conclusionScope.create(
1149+
const { observerName, observedName } = resolveConclusionPeers(
1150+
args,
1151+
observed,
1152+
getObservationMode(config),
1153+
config
1154+
);
1155+
const observerPeer = await honcho.peer(observerName);
1156+
const created = await observerPeer.conclusionsOf(observedName).create(
11131157
contents.map((content) => ({ content }))
11141158
);
11151159
return {
1116-
content: [{ type: "text", text: JSON.stringify({ success: true, created: created.length, observed }) }],
1160+
content: [{
1161+
type: "text",
1162+
text: JSON.stringify({
1163+
success: true,
1164+
created: created.length,
1165+
observer: observerName,
1166+
observed: observedName,
1167+
}),
1168+
}],
11171169
};
11181170
} catch (error) {
11191171
return {
@@ -1305,23 +1357,36 @@ export async function runMcpServer(): Promise<void> {
13051357
case "create_conclusion": {
13061358
const content = args?.content as string;
13071359
const observed: ObservedTarget = (args?.observed as ObservedTarget) ?? OBSERVED_USER;
1360+
const { observerName, observedName } = resolveConclusionPeers(
1361+
args,
1362+
observed,
1363+
observationMode,
1364+
config
1365+
);
13081366

1309-
// Self-conclusions: aiPeer observes itself. User-conclusions: existing
1310-
// observation-mode routing (activePeer.conclusionsOf(userPeerName)).
1311-
const { writePeer, observedPeerName } = observed === OBSERVED_SELF
1312-
? { writePeer: aiPeer ?? await honcho.peer(config.aiPeer), observedPeerName: config.aiPeer }
1313-
: { writePeer: activePeer, observedPeerName: config.peerName };
1367+
// Reuse cached peer objects when the resolved names match the
1368+
// session's active peers; otherwise fetch fresh peer objects for
1369+
// arbitrary multi-agent edges.
1370+
const writePeer = observerName === config.peerName && activePeer === userPeer
1371+
? userPeer
1372+
: observerName === config.aiPeer && aiPeer
1373+
? aiPeer
1374+
: await honcho.peer(observerName);
13141375

1315-
const conclusions = await writePeer.conclusionsOf(observedPeerName).create({
1376+
const conclusions = await writePeer.conclusionsOf(observedName).create({
13161377
content,
13171378
sessionId: session.id,
13181379
});
13191380

1381+
const tag = observed === OBSERVED_SELF ? "self-" : "";
1382+
const edge = observerName !== config.aiPeer && observerName !== config.peerName
1383+
? ` (observer=${observerName}, observed=${observedName})`
1384+
: "";
13201385
return {
13211386
content: [
13221387
{
13231388
type: "text",
1324-
text: `Saved ${observed === OBSERVED_SELF ? "self-" : ""}conclusion: ${conclusions[0]?.content || content}`,
1389+
text: `Saved ${tag}conclusion${edge}: ${conclusions[0]?.content || content}`,
13251390
},
13261391
],
13271392
};

0 commit comments

Comments
 (0)