@cohortapp/agent-sdk 2.11.10 → 2.11.11
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.11",
|
|
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": {
|
|
@@ -499,6 +499,43 @@ async function electResponderVerdict(item, deps = {}) {
|
|
|
499
499
|
* the in-process quick-reply responder; rung ≥2 requires a spawned session.
|
|
500
500
|
* @returns {Promise<{ok:boolean, path:string, reason?:string}>}
|
|
501
501
|
*/
|
|
502
|
+
// Work-verbs that mean "do a task", not "answer a question" — a DM containing one
|
|
503
|
+
// needs the full classify → session path (it may be multi-step work), so it is
|
|
504
|
+
// NEVER fast-pathed to a shallow quick reply.
|
|
505
|
+
const FASTPATH_WORK_RE = /\b(analy[sz]e|create|build|writ(e|ing)|draft|prepare|research|review|compile|generate|implement|fix|debug|refactor|deploy|migrat(e|ing)|audit|investigate|summari[sz]e|put together|pull together|set up|look into|go through|work on|take care of|handle|organi[sz]e|plan out|figure out)\b/;
|
|
506
|
+
// A DM that reads like a question / greeting / quick ask — safe to answer directly.
|
|
507
|
+
const FASTPATH_ASK_RE = /(\?|^(what|how|when|where|who|why|which|is |are |was |were |do |did |does |can |could |would |will |have |has |should |hey|hi |hello|thanks|thank you|yo |quick (q|question)|you (there|around|up)))/i;
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Decide whether a directed DM is a "plainly simple question" that can skip the
|
|
511
|
+
* LLM classifier and be answered directly. Returns a default classResult (the
|
|
512
|
+
* quick-reply shape) when it qualifies, else null (→ full classify path).
|
|
513
|
+
* Deliberately CONSERVATIVE: 1:1 DM, short, question-shaped, no work-verbs.
|
|
514
|
+
* `deps.fastPathClassify` can override (tests); `deps.disableFastPath` disables.
|
|
515
|
+
*/
|
|
516
|
+
export function fastPathClassify(item, isDm, deps = {}) {
|
|
517
|
+
if (deps && typeof deps.fastPathClassify === "function") return deps.fastPathClassify(item, isDm);
|
|
518
|
+
if (deps && deps.disableFastPath) return null;
|
|
519
|
+
if (process.env.DAEMON_DISABLE_FASTPATH === "1") return null;
|
|
520
|
+
if (!isDm) return null; // 1:1 DMs only (never group/ambient)
|
|
521
|
+
const content = String(item.content || item.subject || "").trim();
|
|
522
|
+
if (!content || content.length > 240) return null; // short only
|
|
523
|
+
if (item.has_attachments || item.attachments) return null;
|
|
524
|
+
const lc = content.toLowerCase();
|
|
525
|
+
if (FASTPATH_WORK_RE.test(lc)) return null; // work-request → full path
|
|
526
|
+
if (!FASTPATH_ASK_RE.test(content)) return null; // must look like a question/greeting
|
|
527
|
+
return {
|
|
528
|
+
priority: "high", // a directed DM is latency-sensitive → preempts backlog
|
|
529
|
+
action: "respond",
|
|
530
|
+
model: "sonnet", // fast model; a simple question does not need opus
|
|
531
|
+
summary: content.slice(0, 80),
|
|
532
|
+
category: "action_required",
|
|
533
|
+
directed_at_agent: true,
|
|
534
|
+
answerable: true, // routes to the in-process quick-reply responder
|
|
535
|
+
fast_path: true,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
502
539
|
export async function answerItem(item, service, itemId, trace_id, deps = {}, routed = null) {
|
|
503
540
|
// Resolve collaborators: real imports by default (production), injected fakes
|
|
504
541
|
// only when a test supplies them. Names match the imported symbols 1:1.
|
|
@@ -523,23 +560,36 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
|
|
|
523
560
|
// session has no idea it is discharging a planned obligation.
|
|
524
561
|
if (routed) item.execution = routed;
|
|
525
562
|
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
563
|
+
// FAST PATH: a short, directed, question-shaped DM with no work-verbs does not
|
|
564
|
+
// need a ~5-8s LLM classification to decide "answer this" — skip the classify
|
|
565
|
+
// `claude` call entirely and go straight to a quick reply. This is the single
|
|
566
|
+
// biggest reactive-latency cut after the fetch fix (the two sequential claude
|
|
567
|
+
// calls dominate processItem). CONSERVATIVE by construction: only a plain
|
|
568
|
+
// short question qualifies; work-requests and anything ambiguous fall through
|
|
569
|
+
// to the full classify → route path below, so nothing is shallow-answered.
|
|
570
|
+
let classResult = fastPathClassify(item, isDm, deps);
|
|
571
|
+
if (classResult) {
|
|
572
|
+
recordClassification(true);
|
|
573
|
+
console.log(`[daemon] fast-path DM — skipped classify (${classResult.model}): ${classResult.summary}`);
|
|
574
|
+
} else {
|
|
575
|
+
// Classify via Haiku CLI
|
|
576
|
+
classResult = await _classify({
|
|
577
|
+
sender: item.sender || "unknown",
|
|
578
|
+
sender_privilege: item.sender_privilege || item.priority_signals?.from_ceo ? "ceo" : "unknown",
|
|
579
|
+
channel: item.channel || "unknown",
|
|
580
|
+
service,
|
|
581
|
+
content: item.content || item.subject || "",
|
|
582
|
+
is_reply: item.is_reply || false,
|
|
583
|
+
thread_context: item.thread_context || null,
|
|
584
|
+
subject: item.subject || "",
|
|
585
|
+
is_dm: isDm,
|
|
586
|
+
is_group: !isDm && service === "slack",
|
|
587
|
+
agent_in_thread: item.agent_in_thread === true,
|
|
588
|
+
kind: item.kind || "message",
|
|
589
|
+
});
|
|
541
590
|
|
|
542
|
-
|
|
591
|
+
recordClassification(true);
|
|
592
|
+
}
|
|
543
593
|
|
|
544
594
|
logEvent("classifications", {
|
|
545
595
|
item_id: itemId,
|
|
@@ -862,3 +862,29 @@ test("resolveSelfMemberId reads the member cuid from the res frame's `result` (o
|
|
|
862
862
|
if (pa === undefined) delete process.env.COHORT_AGENT_ID; else process.env.COHORT_AGENT_ID = pa;
|
|
863
863
|
}
|
|
864
864
|
});
|
|
865
|
+
|
|
866
|
+
test("fastPathClassify: a short directed question skips the classifier; work-requests do not", async () => {
|
|
867
|
+
const fp = (content, isDm = true, extra = {}) => daemon.fastPathClassify({ content, ...extra }, isDm);
|
|
868
|
+
// Plain short questions → fast-path (quick reply, high priority, sonnet).
|
|
869
|
+
const q = fp("what OS is your machine on?");
|
|
870
|
+
assert.equal(q && q.fast_path, true);
|
|
871
|
+
assert.equal(q.answerable, true);
|
|
872
|
+
assert.equal(q.priority, "high");
|
|
873
|
+
assert.equal(q.action, "respond");
|
|
874
|
+
assert.ok(daemon.fastPathClassify({ content: "hey, you around?" }, true));
|
|
875
|
+
assert.ok(daemon.fastPathClassify({ content: "thanks!" }, true));
|
|
876
|
+
// Work-requests → NOT fast-pathed (fall through to full classify → session).
|
|
877
|
+
assert.equal(fp("can you analyze the Q3 report and send me the findings?"), null);
|
|
878
|
+
assert.equal(fp("please review the PR and fix the failing tests"), null);
|
|
879
|
+
assert.equal(fp("build a dashboard for the sales pipeline"), null);
|
|
880
|
+
// Non-questions, long messages, group, and non-DM → NOT fast-pathed.
|
|
881
|
+
assert.equal(fp("here are the notes from the meeting for your records"), null);
|
|
882
|
+
assert.equal(fp("x".repeat(300)), null);
|
|
883
|
+
assert.equal(fp("what OS?", false), null); // not a DM
|
|
884
|
+
assert.equal(fp(""), null);
|
|
885
|
+
// Attachments → full path.
|
|
886
|
+
assert.equal(daemon.fastPathClassify({ content: "what's this?", has_attachments: true }, true), null);
|
|
887
|
+
// Kill switch (deps.disableFastPath) forces the full classify path.
|
|
888
|
+
assert.ok(daemon.fastPathClassify({ content: "what OS?" }, true));
|
|
889
|
+
assert.equal(daemon.fastPathClassify({ content: "what OS?" }, true, { disableFastPath: true }), null);
|
|
890
|
+
});
|
|
@@ -142,7 +142,10 @@ function dm(over = {}) {
|
|
|
142
142
|
channel_id: "D77",
|
|
143
143
|
sender: "Priya Raman",
|
|
144
144
|
sender_id: "U_PRIYA",
|
|
145
|
-
|
|
145
|
+
// A work-request ("review") so this shared fixture takes the full classify →
|
|
146
|
+
// route path — the fast-path (fastPathClassify) deliberately skips the
|
|
147
|
+
// classifier only for PLAIN short questions; that path has its own test below.
|
|
148
|
+
content: "can you review and confirm the deadline for the launch plan?",
|
|
146
149
|
timestamp: new Date().toISOString(),
|
|
147
150
|
...over,
|
|
148
151
|
};
|
|
@@ -190,6 +193,20 @@ test("LADDER: a DM is journalled intake→match→route→outcome AND still quic
|
|
|
190
193
|
assert.equal(s.seen.dispatch.length, 0, "no session spawned for a rung-0 reply");
|
|
191
194
|
});
|
|
192
195
|
|
|
196
|
+
test("LADDER: a plain short question DM FAST-PATHS — quick-replies WITHOUT a classifier call", async () => {
|
|
197
|
+
const item = dm({ id: "MSG-DM-FAST", raw_ref: "slack:D91:1", channel_id: "D91", content: "what's the deadline?" });
|
|
198
|
+
writeLive(item, "slack");
|
|
199
|
+
const s = spy(RESPOND);
|
|
200
|
+
await daemon.processItem(item, "slack", s.deps);
|
|
201
|
+
|
|
202
|
+
// The whole point: the ~5-8s Haiku classify call is SKIPPED for a plain question.
|
|
203
|
+
assert.equal(s.seen.classify, 0, "fast-path: a plain directed question skips the classifier");
|
|
204
|
+
// ...but the DM is still answered via the quick-reply path, still journalled.
|
|
205
|
+
assert.deepEqual(s.seen.quick, [{ id: "MSG-DM-FAST", rung: 0 }], "the quick reply still went out at rung 0");
|
|
206
|
+
assert.equal(s.seen.dispatch.length, 0, "no session spawned");
|
|
207
|
+
assert.ok(decisions().some((r) => r.surface === "dm" && r.disposition === "react_now"), "still journalled intake→route→outcome");
|
|
208
|
+
});
|
|
209
|
+
|
|
193
210
|
// ---------------------------------------------------------------------------
|
|
194
211
|
// 2) A board task is WORK: it queues, and never reaches the classifier.
|
|
195
212
|
// ---------------------------------------------------------------------------
|