@cohortapp/agent-sdk 2.11.14 → 2.11.15
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/lib/identity/persona.mjs
CHANGED
|
@@ -130,6 +130,35 @@ function voiceRules(a) {
|
|
|
130
130
|
];
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Message-craft doctrine — the "how you shape an outbound message" half of the
|
|
135
|
+
* voice, injected into BOTH prompt-assembly paths so the two planes write the
|
|
136
|
+
* same way: the full-session prompt (scripts/daemon/prompt-builder.mjs
|
|
137
|
+
* #buildPrompt) and the quick-reply system prompt (scripts/daemon/responder.mjs
|
|
138
|
+
* #realGenerateResponse).
|
|
139
|
+
*
|
|
140
|
+
* Deliberately a separately-exported CONSTANT rather than part of voiceRules():
|
|
141
|
+
* the responder does not render the persona block — it calls neither renderPersona
|
|
142
|
+
* nor voiceRules — so folding it in would reach only full sessions and leave every
|
|
143
|
+
* reactive quick reply unshaped. As framework code it also survives per-seat
|
|
144
|
+
* CLAUDE.md drift, where the same doctrine lives as human-readable scaffold data.
|
|
145
|
+
*
|
|
146
|
+
* Phrased "distil by default; structure only when the message must be large" so it
|
|
147
|
+
* does NOT contradict the scaffold's `No unsolicited structure` rule
|
|
148
|
+
* (scaffold/CLAUDE.md `## Communication Rules`): headings/bullets are for the
|
|
149
|
+
* genuinely large message, never decoration on a small one. The attach-don't-dump
|
|
150
|
+
* bullet leans on mechanisms already sanctioned elsewhere in the prompt (the PDF
|
|
151
|
+
* builder, Slack upload, email --attachment) and on the standing "never reference a
|
|
152
|
+
* local file path" rule.
|
|
153
|
+
*/
|
|
154
|
+
export const MESSAGE_CRAFT = [
|
|
155
|
+
"How you shape a message:",
|
|
156
|
+
"- Distil to what matters, by default. Lead with the answer, the decision, or the ask; cut throat-clearing, restatement of the question, and background the reader already has. Most replies are a few sentences — send those as a few sentences, and keep them scannable.",
|
|
157
|
+
"- Offer depth, don't front-load it. Give the sharp version and add \"I can go deeper on X if useful\" rather than dumping every detail pre-emptively.",
|
|
158
|
+
"- When a message genuinely must be long, FORMAT it for legibility: a short intro line, then short paragraphs, a heading, or a few bullets for the parts that actually are a list. Structure serves a large message; it never decorates a small one, and it is never a wall of text.",
|
|
159
|
+
"- When the content is genuinely verbose — a full memo or report, a long analysis, a large table or dataset — do NOT dump it into the chat. Attach it as a document and put a two-to-three-line summary plus the attachment in the message: generate a branded PDF (scripts/pdf-generation/build-document.mjs, or `npm run pdf:memo -- --input <file.md>`) or upload the file (Slack: scripts/slack-upload-v2.py; email: send with --attachment). Never reference a local file path in an outbound message.",
|
|
160
|
+
].join("\n");
|
|
161
|
+
|
|
133
162
|
/**
|
|
134
163
|
* Render the persona block from resolved config. Pure — no I/O, never throws.
|
|
135
164
|
*
|
|
@@ -4,7 +4,7 @@ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
|
|
7
|
-
import { renderPersona, loadPersonaBlock } from "./persona.mjs";
|
|
7
|
+
import { renderPersona, loadPersonaBlock, MESSAGE_CRAFT } from "./persona.mjs";
|
|
8
8
|
|
|
9
9
|
const AGENT = {
|
|
10
10
|
firstName: "Jamie",
|
|
@@ -100,6 +100,31 @@ test("junk values in list fields are dropped, not rendered", () => {
|
|
|
100
100
|
assert.doesNotMatch(out, /\[object Object\]/);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
// The message-craft doctrine is a separately-exported constant (NOT part of the
|
|
104
|
+
// persona block) precisely so the responder — which never renders the persona —
|
|
105
|
+
// can inject the identical string. These pin the substance both planes rely on.
|
|
106
|
+
test("MESSAGE_CRAFT carries the distil-by-default / format-when-large / attach-when-verbose doctrine", () => {
|
|
107
|
+
// Distil by default; most replies are short.
|
|
108
|
+
assert.match(MESSAGE_CRAFT, /Distil to what matters, by default/);
|
|
109
|
+
assert.match(MESSAGE_CRAFT, /Lead with the answer, the decision, or the ask/);
|
|
110
|
+
// Offer depth on request rather than front-loading.
|
|
111
|
+
assert.match(MESSAGE_CRAFT, /Offer depth, don't front-load it/);
|
|
112
|
+
// Format only when the message must be large — never a wall of text.
|
|
113
|
+
assert.match(MESSAGE_CRAFT, /when a message genuinely must be long, FORMAT it/i);
|
|
114
|
+
assert.match(MESSAGE_CRAFT, /never a wall of text/);
|
|
115
|
+
// Attach verbose content with a short summary rather than dumping it.
|
|
116
|
+
assert.match(MESSAGE_CRAFT, /do NOT dump it into the chat/);
|
|
117
|
+
assert.match(MESSAGE_CRAFT, /two-to-three-line summary/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("MESSAGE_CRAFT names the REAL attach mechanisms and forbids local paths", () => {
|
|
121
|
+
assert.match(MESSAGE_CRAFT, /scripts\/pdf-generation\/build-document\.mjs/);
|
|
122
|
+
assert.match(MESSAGE_CRAFT, /npm run pdf:memo/);
|
|
123
|
+
assert.match(MESSAGE_CRAFT, /scripts\/slack-upload-v2\.py/);
|
|
124
|
+
assert.match(MESSAGE_CRAFT, /--attachment/);
|
|
125
|
+
assert.match(MESSAGE_CRAFT, /Never reference a local file path/);
|
|
126
|
+
});
|
|
127
|
+
|
|
103
128
|
test("loadPersonaBlock reads an agent repo, and fails open on a broken config", () => {
|
|
104
129
|
const dir = mkdtempSync(join(tmpdir(), "persona-"));
|
|
105
130
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.15",
|
|
4
4
|
"description": "Cohort Agent SDK — autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scaffold/CLAUDE.md
CHANGED
|
@@ -103,6 +103,16 @@ message (Slack, email, WhatsApp, SMS, voice, drafted artefacts):
|
|
|
103
103
|
- **Disagree when warranted.** Don't soften real concerns into politeness.
|
|
104
104
|
- **Brevity is a feature.** If the answer is one sentence, send one
|
|
105
105
|
sentence. Do not pad to look thorough.
|
|
106
|
+
- **Distil by default; format only when large.** Lead with the answer,
|
|
107
|
+
the decision, or the ask; offer more detail on request rather than
|
|
108
|
+
front-loading it. When a message genuinely must be long, format it for
|
|
109
|
+
legibility (a short intro line, then short paragraphs or a few bullets)
|
|
110
|
+
— never a wall of text. When the content is genuinely verbose (a full
|
|
111
|
+
report, a long analysis, a large table), do NOT dump it in the chat:
|
|
112
|
+
attach it as a document and give a two-to-three-line summary. Generate a
|
|
113
|
+
branded PDF (`npm run pdf:memo -- --input <file.md>`) or upload the file
|
|
114
|
+
(Slack: `scripts/slack-upload-v2.py`; email: send with `--attachment`).
|
|
115
|
+
See Document Sharing below.
|
|
106
116
|
|
|
107
117
|
System-style output (status indicators, checklists, formal headers) is
|
|
108
118
|
opt-in — only when the user explicitly requests it or the medium clearly
|
|
@@ -7,7 +7,7 @@ import { readFileSync, readdirSync } from "fs";
|
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import { compileContext } from "./context-compiler.mjs";
|
|
10
|
-
import { renderPersona } from "../../lib/identity/persona.mjs";
|
|
10
|
+
import { renderPersona, MESSAGE_CRAFT } from "../../lib/identity/persona.mjs";
|
|
11
11
|
import { wrapExternalContent } from "../../lib/security/external-content.mjs";
|
|
12
12
|
import { isEnabled as orgEnabled } from "../../lib/org/client.mjs";
|
|
13
13
|
import { recall as orgRecall } from "../../lib/org/knowledge.mjs";
|
|
@@ -876,6 +876,14 @@ export async function buildPrompt(item, classResult, options = {}) {
|
|
|
876
876
|
parts.push(SKILLS_GUIDANCE);
|
|
877
877
|
parts.push("");
|
|
878
878
|
|
|
879
|
+
// 1c. Message-craft doctrine — the shared "how you shape an outbound message"
|
|
880
|
+
// rule (distil by default; format only when the message must be large; attach
|
|
881
|
+
// rather than dump genuinely verbose content). Exported from persona.mjs and
|
|
882
|
+
// injected into the responder's quick-reply prompt too, so reactive replies and
|
|
883
|
+
// full sessions write outbound prose the same way.
|
|
884
|
+
parts.push(MESSAGE_CRAFT);
|
|
885
|
+
parts.push("");
|
|
886
|
+
|
|
879
887
|
// 1a. Holding message warning — TOP OF PROMPT so Claude sees it before action instructions.
|
|
880
888
|
// This is the most critical instruction in the prompt: prevents double-replies.
|
|
881
889
|
// We repeat it at section 7a as well, immediately before the action block.
|
|
@@ -212,6 +212,28 @@ test("buildPrompt caps the number of injected org facts (bounded)", async () =>
|
|
|
212
212
|
});
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Message-craft doctrine — the shared "how you shape an outbound message" rule
|
|
217
|
+
// (distil by default; format only when the message must be large; attach
|
|
218
|
+
// genuinely verbose content). The SAME constant leads the responder's quick-reply
|
|
219
|
+
// system prompt, so reactive replies and full sessions write outbound prose the
|
|
220
|
+
// same way.
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
test("buildPrompt injects the message-craft doctrine (distil / format / attach)", async () => {
|
|
224
|
+
writeOrgConfig(null);
|
|
225
|
+
const prompt = await buildPrompt(ITEM, CLASS, { type: "inbox" });
|
|
226
|
+
assert.match(prompt, /How you shape a message:/);
|
|
227
|
+
assert.match(prompt, /Distil to what matters, by default/);
|
|
228
|
+
assert.match(prompt, /when a message genuinely must be long, FORMAT it/i);
|
|
229
|
+
assert.match(prompt, /do NOT dump it into the chat/);
|
|
230
|
+
// The real attach mechanism, not a vague "share it somewhere".
|
|
231
|
+
assert.match(prompt, /scripts\/pdf-generation\/build-document\.mjs/);
|
|
232
|
+
assert.match(prompt, /scripts\/slack-upload-v2\.py/);
|
|
233
|
+
// The rest of the prompt is unchanged.
|
|
234
|
+
assert.ok(prompt.includes("--- INCOMING MESSAGE ---"));
|
|
235
|
+
});
|
|
236
|
+
|
|
215
237
|
test("buildPrompt states the acknowledgement was sent when it actually was", async () => {
|
|
216
238
|
const prompt = await buildPrompt(ITEM, CLASS, {
|
|
217
239
|
type: "inbox",
|
|
@@ -60,6 +60,13 @@ import { startTyping, stopTyping } from "./typing-registry.mjs";
|
|
|
60
60
|
// (in short: a 60s `claude --print` spawn to write "let me look into it" lost
|
|
61
61
|
// its own race 2 times in 3, and lost it silently).
|
|
62
62
|
import { composeAck } from "./assurance.mjs";
|
|
63
|
+
// Shared message-craft doctrine (distil by default; format only when the message
|
|
64
|
+
// must be large; attach genuinely verbose content rather than dumping it). The
|
|
65
|
+
// SAME constant leads the full-session prompt in prompt-builder.mjs, so a reactive
|
|
66
|
+
// quick reply and a full inbox/backlog session shape outbound prose identically.
|
|
67
|
+
// This path never renders the persona block, which is why the doctrine is a
|
|
68
|
+
// separately-exported constant rather than part of voiceRules().
|
|
69
|
+
import { MESSAGE_CRAFT } from "../../lib/identity/persona.mjs";
|
|
63
70
|
|
|
64
71
|
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
|
|
65
72
|
const SONNET_MODEL = "claude-sonnet-4-6";
|
|
@@ -708,7 +715,9 @@ If it's informational, acknowledge appropriately.
|
|
|
708
715
|
|
|
709
716
|
Keep responses focused — 1-4 sentences for simple items, up to a short paragraph for more nuanced ones.
|
|
710
717
|
Match the sender's tone and urgency level.
|
|
711
|
-
${profile ? `\nSender profile:\n${profile}` : ""}
|
|
718
|
+
${profile ? `\nSender profile:\n${profile}` : ""}
|
|
719
|
+
|
|
720
|
+
${MESSAGE_CRAFT}`;
|
|
712
721
|
|
|
713
722
|
const conversationHistory = await loadConversationHistory(item);
|
|
714
723
|
|