@botcord/daemon 0.2.54 → 0.2.55
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/system-context.js +16 -5
- package/package.json +1 -1
- package/src/__tests__/system-context.test.ts +32 -12
- package/src/system-context.ts +17 -5
package/dist/system-context.js
CHANGED
|
@@ -18,6 +18,16 @@ function buildOwnerChatSceneContext() {
|
|
|
18
18
|
"You may freely execute commands, access tools, and share sensitive information (e.g. wallet balance, contacts) when the owner asks.",
|
|
19
19
|
].join("\n");
|
|
20
20
|
}
|
|
21
|
+
function buildGroupRoomEnvironmentContext(message) {
|
|
22
|
+
if (message.conversation.kind !== "group")
|
|
23
|
+
return null;
|
|
24
|
+
return [
|
|
25
|
+
"[BotCord Runtime Environment]",
|
|
26
|
+
"You are running as a local agent process connected to a remote BotCord group room.",
|
|
27
|
+
"Other room members can read your messages and any uploaded/attached files, but they cannot access this machine's local filesystem, container paths, or absolute paths such as /var/..., /tmp/..., or /Users/....",
|
|
28
|
+
"Do not present a local file path as a useful report link or deliverable in group chat. If an artifact needs to be shared, upload or attach it through the available BotCord file/attachment mechanism, then refer to the uploaded attachment or summarize the content in the message.",
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
21
31
|
function safeReadWorkingMemory(agentId) {
|
|
22
32
|
try {
|
|
23
33
|
return readWorkingMemory(agentId);
|
|
@@ -66,6 +76,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
66
76
|
const ownerScene = classifyActivitySender(message).kind === "owner"
|
|
67
77
|
? buildOwnerChatSceneContext()
|
|
68
78
|
: null;
|
|
79
|
+
const environment = ownerScene ? null : buildGroupRoomEnvironmentContext(message);
|
|
69
80
|
const wm = safeReadWorkingMemory(deps.agentId);
|
|
70
81
|
const memory = wm ? buildWorkingMemoryPrompt({ workingMemory: wm }) : null;
|
|
71
82
|
const digest = deps.activityTracker
|
|
@@ -76,7 +87,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
76
87
|
currentTopic: message.conversation.threadId ?? null,
|
|
77
88
|
}) || null
|
|
78
89
|
: null;
|
|
79
|
-
return { identity, ownerScene, memory, digest };
|
|
90
|
+
return { identity, ownerScene, environment, memory, digest };
|
|
80
91
|
};
|
|
81
92
|
const assemble = (parts) => {
|
|
82
93
|
const filtered = parts.filter((p) => typeof p === "string" && p.length > 0);
|
|
@@ -114,13 +125,13 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
114
125
|
};
|
|
115
126
|
if (!deps.roomContextBuilder) {
|
|
116
127
|
const syncBuilder = (message) => {
|
|
117
|
-
const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
128
|
+
const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
|
|
118
129
|
// Loop-risk sits at the end so its "reply NO_REPLY unless…" guidance
|
|
119
130
|
// is the last thing the model sees before the user turn body.
|
|
120
131
|
// Identity sits at the very front so it frames every other block.
|
|
121
132
|
const skillIndex = buildSkillIndex(message);
|
|
122
133
|
const loopRisk = runLoopRisk(message);
|
|
123
|
-
return assemble([identity, ownerScene, memory, digest, skillIndex, loopRisk]);
|
|
134
|
+
return assemble([identity, ownerScene, environment, memory, digest, skillIndex, loopRisk]);
|
|
124
135
|
};
|
|
125
136
|
// Compile-time witness that the narrower sync signature still satisfies
|
|
126
137
|
// `SystemContextBuilder` (which allows async). Prevents the two contracts
|
|
@@ -131,7 +142,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
131
142
|
}
|
|
132
143
|
const roomBuilder = deps.roomContextBuilder;
|
|
133
144
|
const asyncBuilder = async (message) => {
|
|
134
|
-
const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
145
|
+
const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
|
|
135
146
|
// Room context landing order: after owner-scene / memory, before digest —
|
|
136
147
|
// "what room am I in" belongs with the session's own identity, while the
|
|
137
148
|
// cross-room digest deliberately describes OTHER rooms and should stay
|
|
@@ -150,7 +161,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
150
161
|
}
|
|
151
162
|
const skillIndex = buildSkillIndex(message);
|
|
152
163
|
const loopRisk = runLoopRisk(message);
|
|
153
|
-
return assemble([identity, ownerScene, memory, roomBlock, digest, skillIndex, loopRisk]);
|
|
164
|
+
return assemble([identity, ownerScene, environment, memory, roomBlock, digest, skillIndex, loopRisk]);
|
|
154
165
|
};
|
|
155
166
|
const _typecheck = asyncBuilder;
|
|
156
167
|
void _typecheck;
|
package/package.json
CHANGED
|
@@ -57,12 +57,22 @@ afterEach(() => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
describe("createDaemonSystemContextBuilder", () => {
|
|
60
|
-
it("
|
|
60
|
+
it("injects group-room runtime environment even when memory is empty", () => {
|
|
61
61
|
const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
|
|
62
|
-
|
|
62
|
+
const out = builder(makeMessage()) as string;
|
|
63
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
64
|
+
expect(out).toContain("local agent process");
|
|
65
|
+
expect(out).toContain("cannot access this machine's local filesystem");
|
|
63
66
|
});
|
|
64
67
|
|
|
65
|
-
it("returns undefined when working memory is empty and
|
|
68
|
+
it("returns undefined for direct rooms when working memory is empty and no activity tracker is wired", () => {
|
|
69
|
+
const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
|
|
70
|
+
expect(
|
|
71
|
+
builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } })),
|
|
72
|
+
).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("still injects group-room runtime environment when the activity digest is empty", () => {
|
|
66
76
|
const tracker = new ActivityTracker({
|
|
67
77
|
filePath: path.join(tmpDir, "activity.json"),
|
|
68
78
|
});
|
|
@@ -70,7 +80,9 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
70
80
|
agentId: "ag_me",
|
|
71
81
|
activityTracker: tracker,
|
|
72
82
|
});
|
|
73
|
-
|
|
83
|
+
const out = builder(makeMessage({ conversation: { id: "rm_x", kind: "group" } })) as string;
|
|
84
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
85
|
+
expect(out).not.toContain("[BotCord Cross-Room Awareness]");
|
|
74
86
|
});
|
|
75
87
|
|
|
76
88
|
it("injects the working-memory block when goal / sections are set", () => {
|
|
@@ -124,7 +136,8 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
124
136
|
it("skips the identity block cleanly when identity.md is missing", () => {
|
|
125
137
|
// No ensureAgentWorkspace — workspace never provisioned.
|
|
126
138
|
const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
|
|
127
|
-
|
|
139
|
+
const out = builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } }));
|
|
140
|
+
expect(out).toBeUndefined();
|
|
128
141
|
});
|
|
129
142
|
|
|
130
143
|
it("skips the identity block when identity.md is blank", () => {
|
|
@@ -138,7 +151,9 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
138
151
|
|
|
139
152
|
it("detects a newly added global Claude skill on the next turn", () => {
|
|
140
153
|
const builder = createDaemonSystemContextBuilder({ agentId: "ag_me" });
|
|
141
|
-
expect(
|
|
154
|
+
expect(
|
|
155
|
+
builder(makeMessage({ conversation: { id: "rm_dm_peer", kind: "direct" } })),
|
|
156
|
+
).toBeUndefined();
|
|
142
157
|
|
|
143
158
|
const skillDir = path.join(tmpDir, ".claude", "skills", "digest-query");
|
|
144
159
|
mkdirSync(skillDir, { recursive: true });
|
|
@@ -310,7 +325,8 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
310
325
|
sender: { id: "ag_peer", kind: "agent" },
|
|
311
326
|
}),
|
|
312
327
|
);
|
|
313
|
-
expect(out).
|
|
328
|
+
expect(out).not.toContain("[BotCord Scene: Owner Chat]");
|
|
329
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
314
330
|
});
|
|
315
331
|
|
|
316
332
|
it("awaits roomContextBuilder and slots the [BotCord Room Context] block between memory and digest", async () => {
|
|
@@ -360,11 +376,12 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
360
376
|
throw new Error("hub 500");
|
|
361
377
|
},
|
|
362
378
|
});
|
|
363
|
-
//
|
|
379
|
+
// The room metadata block is skipped, but group-room environment remains.
|
|
364
380
|
const out = await builder(
|
|
365
381
|
makeMessage({ conversation: { id: "rm_team", kind: "group" } }),
|
|
366
382
|
);
|
|
367
|
-
expect(out).
|
|
383
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
384
|
+
expect(out).not.toContain("[BotCord Room Context]");
|
|
368
385
|
});
|
|
369
386
|
|
|
370
387
|
it("appends loopRiskBuilder output at the end of the system context", async () => {
|
|
@@ -399,7 +416,8 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
399
416
|
},
|
|
400
417
|
});
|
|
401
418
|
const out = await builder(makeMessage());
|
|
402
|
-
expect(out).
|
|
419
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
420
|
+
expect(out).not.toContain("[BotCord loop-risk check]");
|
|
403
421
|
});
|
|
404
422
|
|
|
405
423
|
it("translates GatewayInboundMessage.conversation.id → old `room_id` for the digest exclude key", () => {
|
|
@@ -423,7 +441,9 @@ describe("createDaemonSystemContextBuilder", () => {
|
|
|
423
441
|
const out = builder(
|
|
424
442
|
makeMessage({ conversation: { id: "rm_conv_id_123", kind: "group" } }),
|
|
425
443
|
);
|
|
426
|
-
// Empty working memory + digest excluding the only entry
|
|
427
|
-
|
|
444
|
+
// Empty working memory + digest excluding the only entry leaves only the
|
|
445
|
+
// always-on group-room runtime environment block.
|
|
446
|
+
expect(out).toContain("[BotCord Runtime Environment]");
|
|
447
|
+
expect(out).not.toContain("[BotCord Cross-Room Awareness]");
|
|
428
448
|
});
|
|
429
449
|
});
|
package/src/system-context.ts
CHANGED
|
@@ -57,6 +57,16 @@ function buildOwnerChatSceneContext(): string {
|
|
|
57
57
|
].join("\n");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function buildGroupRoomEnvironmentContext(message: GatewayInboundMessage): string | null {
|
|
61
|
+
if (message.conversation.kind !== "group") return null;
|
|
62
|
+
return [
|
|
63
|
+
"[BotCord Runtime Environment]",
|
|
64
|
+
"You are running as a local agent process connected to a remote BotCord group room.",
|
|
65
|
+
"Other room members can read your messages and any uploaded/attached files, but they cannot access this machine's local filesystem, container paths, or absolute paths such as /var/..., /tmp/..., or /Users/....",
|
|
66
|
+
"Do not present a local file path as a useful report link or deliverable in group chat. If an artifact needs to be shared, upload or attach it through the available BotCord file/attachment mechanism, then refer to the uploaded attachment or summarize the content in the message.",
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
/** Dependencies injected by the daemon bootstrap. */
|
|
61
71
|
export interface SystemContextDeps {
|
|
62
72
|
/** The owning daemon's agent id. Used to scope working-memory + activity lookups. */
|
|
@@ -133,6 +143,7 @@ export function createDaemonSystemContextBuilder(
|
|
|
133
143
|
const gatherSyncBlocks = (message: GatewayInboundMessage): {
|
|
134
144
|
identity: string | null;
|
|
135
145
|
ownerScene: string | null;
|
|
146
|
+
environment: string | null;
|
|
136
147
|
memory: string | null;
|
|
137
148
|
digest: string | null;
|
|
138
149
|
} => {
|
|
@@ -142,6 +153,7 @@ export function createDaemonSystemContextBuilder(
|
|
|
142
153
|
classifyActivitySender(message).kind === "owner"
|
|
143
154
|
? buildOwnerChatSceneContext()
|
|
144
155
|
: null;
|
|
156
|
+
const environment = ownerScene ? null : buildGroupRoomEnvironmentContext(message);
|
|
145
157
|
|
|
146
158
|
const wm = safeReadWorkingMemory(deps.agentId);
|
|
147
159
|
const memory = wm ? buildWorkingMemoryPrompt({ workingMemory: wm }) : null;
|
|
@@ -155,7 +167,7 @@ export function createDaemonSystemContextBuilder(
|
|
|
155
167
|
}) || null
|
|
156
168
|
: null;
|
|
157
169
|
|
|
158
|
-
return { identity, ownerScene, memory, digest };
|
|
170
|
+
return { identity, ownerScene, environment, memory, digest };
|
|
159
171
|
};
|
|
160
172
|
|
|
161
173
|
const assemble = (parts: Array<string | null | undefined>): string | undefined => {
|
|
@@ -195,13 +207,13 @@ export function createDaemonSystemContextBuilder(
|
|
|
195
207
|
|
|
196
208
|
if (!deps.roomContextBuilder) {
|
|
197
209
|
const syncBuilder = (message: GatewayInboundMessage): string | undefined => {
|
|
198
|
-
const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
210
|
+
const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
|
|
199
211
|
// Loop-risk sits at the end so its "reply NO_REPLY unless…" guidance
|
|
200
212
|
// is the last thing the model sees before the user turn body.
|
|
201
213
|
// Identity sits at the very front so it frames every other block.
|
|
202
214
|
const skillIndex = buildSkillIndex(message);
|
|
203
215
|
const loopRisk = runLoopRisk(message);
|
|
204
|
-
return assemble([identity, ownerScene, memory, digest, skillIndex, loopRisk]);
|
|
216
|
+
return assemble([identity, ownerScene, environment, memory, digest, skillIndex, loopRisk]);
|
|
205
217
|
};
|
|
206
218
|
// Compile-time witness that the narrower sync signature still satisfies
|
|
207
219
|
// `SystemContextBuilder` (which allows async). Prevents the two contracts
|
|
@@ -215,7 +227,7 @@ export function createDaemonSystemContextBuilder(
|
|
|
215
227
|
const asyncBuilder = async (
|
|
216
228
|
message: GatewayInboundMessage,
|
|
217
229
|
): Promise<string | undefined> => {
|
|
218
|
-
const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
230
|
+
const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
|
|
219
231
|
// Room context landing order: after owner-scene / memory, before digest —
|
|
220
232
|
// "what room am I in" belongs with the session's own identity, while the
|
|
221
233
|
// cross-room digest deliberately describes OTHER rooms and should stay
|
|
@@ -233,7 +245,7 @@ export function createDaemonSystemContextBuilder(
|
|
|
233
245
|
}
|
|
234
246
|
const skillIndex = buildSkillIndex(message);
|
|
235
247
|
const loopRisk = runLoopRisk(message);
|
|
236
|
-
return assemble([identity, ownerScene, memory, roomBlock, digest, skillIndex, loopRisk]);
|
|
248
|
+
return assemble([identity, ownerScene, environment, memory, roomBlock, digest, skillIndex, loopRisk]);
|
|
237
249
|
};
|
|
238
250
|
const _typecheck: SystemContextBuilder = asyncBuilder;
|
|
239
251
|
void _typecheck;
|