@cohortapp/agent-sdk 2.5.1 → 2.6.1
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/bin/maestro.mjs +305 -89
- package/bin/maestro.test.mjs +357 -48
- package/docs/runbooks/backup-restore.md +65 -33
- package/framework-features.json +4 -4
- package/lib/backup/policy.mjs +710 -0
- package/lib/backup/policy.test.mjs +305 -0
- package/lib/budget-escalate.mjs +133 -0
- package/lib/budget-escalate.test.mjs +232 -0
- package/lib/budget-guard.envelope.test.mjs +476 -0
- package/lib/budget-guard.mjs +853 -75
- package/lib/budget-guard.test.mjs +91 -42
- package/lib/cadences.mjs +33 -0
- package/lib/channels/orgmail/adapter.mjs +88 -3
- package/lib/channels/orgmail/adapter.test.mjs +137 -0
- package/lib/channels/repeat-suppressor.mjs +198 -0
- package/lib/channels/repeat-suppressor.test.mjs +134 -0
- package/lib/comms/receipts.mjs +297 -0
- package/lib/cost/ledger-row.mjs +333 -0
- package/lib/cost/ledger-row.test.mjs +183 -0
- package/lib/execution/drive.mjs +28 -1
- package/lib/execution/effects.mjs +191 -12
- package/lib/execution/effects.test.mjs +50 -11
- package/lib/goals/admission.mjs +13 -1
- package/lib/goals/admission.test.mjs +26 -1
- package/lib/goals/loop.mjs +13 -0
- package/lib/kpi-sensors.test.mjs +3 -0
- package/lib/mandate/cache.mjs +13 -5
- package/lib/mandate/derive.mjs +146 -21
- package/lib/mandate/derive.test.mjs +50 -6
- package/lib/mandate/model.mjs +32 -4
- package/lib/mandate/refresh.test.mjs +16 -2
- package/lib/mcp/server.test.mjs +12 -3
- package/lib/model-router/economics.mjs +107 -76
- package/lib/model-router/economics.test.mjs +64 -46
- package/lib/model-router/integration-coverage.test.mjs +39 -37
- package/lib/model-router/ledger.mjs +75 -22
- package/lib/model-router/ledger.test.mjs +35 -2
- package/lib/org/client.mjs +14 -0
- package/lib/org/cost-sync.mjs +16 -2
- package/lib/org/doctor.mjs +62 -1
- package/lib/org/doctor.test.mjs +36 -3
- package/lib/org/email-remedy.mjs +49 -0
- package/lib/org/engagement-ledger.mjs +376 -0
- package/lib/org/engagement-ledger.test.mjs +112 -0
- package/lib/org/engagement.mjs +1056 -0
- package/lib/org/engagement.test.mjs +739 -0
- package/lib/org/messaging.mjs +230 -3
- package/lib/org/messaging.test.mjs +110 -1
- package/lib/org/param-contract.mjs +56 -2
- package/lib/org/param-contract.test.mjs +26 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +5 -0
- package/lib/org/protocol.test.mjs +7 -1
- package/lib/org/tool-surface.mjs +506 -10
- package/lib/org/tool-surface.test.mjs +191 -7
- package/lib/org/ui-parity.mjs +333 -6
- package/lib/org/ui-parity.test.mjs +96 -3
- package/lib/org/work-ledger.mjs +241 -0
- package/lib/org/work-ledger.test.mjs +237 -0
- package/lib/plan/adoption-e2e.test.mjs +366 -0
- package/lib/plan/budget-enforcement.test.mjs +400 -0
- package/lib/plan/budget-runtime.mjs +215 -0
- package/lib/plan/compile.mjs +201 -5
- package/lib/plan/compile.test.mjs +19 -5
- package/lib/plan/emit.mjs +8 -0
- package/lib/plan/emit.test.mjs +18 -0
- package/lib/resource-governor.mjs +58 -12
- package/lib/resource-governor.test.mjs +41 -1
- package/lib/security/audit-engine.mjs +45 -8
- package/lib/security/audit-engine.test.mjs +35 -0
- package/lib/setup/enroll-from-cohort.mjs +14 -1
- package/lib/setup/sections/mandate.mjs +48 -7
- package/lib/setup/sections/mandate.test.mjs +17 -2
- package/lib/setup/sections/orgmail.mjs +10 -2
- package/lib/setup/state.mjs +83 -2
- package/lib/telemetry/collect.mjs +360 -20
- package/lib/telemetry/collect.test.mjs +266 -0
- package/package.json +1 -1
- package/scripts/cost/track-claude-usage.mjs +207 -48
- package/scripts/cost/track-claude-usage.test.mjs +148 -0
- package/scripts/daemon/agent-daemon.mjs +315 -17
- package/scripts/daemon/assurance-e2e.test.mjs +421 -0
- package/scripts/daemon/assurance.mjs +944 -0
- package/scripts/daemon/assurance.test.mjs +668 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +56 -0
- package/scripts/daemon/cadence-consumer.mjs +147 -9
- package/scripts/daemon/cadence-consumer.test.mjs +6 -0
- package/scripts/daemon/cadence-handlers.mjs +158 -0
- package/scripts/daemon/cadence-handlers.test.mjs +64 -0
- package/scripts/daemon/classifier.test.mjs +18 -9
- package/scripts/daemon/deliver.mjs +314 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +10 -0
- package/scripts/daemon/dispatcher.mjs +64 -6
- package/scripts/daemon/responder-cost.test.mjs +68 -0
- package/scripts/daemon/responder.mjs +351 -298
- package/scripts/local-triggers/generate-plists.test.mjs +7 -4
- package/scripts/maintenance/backup-run.mjs +415 -0
- package/scripts/maintenance/backup-to-cloud.sh +16 -116
- package/scripts/org/send-orgmail.mjs +16 -0
- package/scripts/record-receipt.sh +63 -0
- package/scripts/restore-from-backup.sh +14 -3
- package/scripts/restore-from-backup.test.mjs +8 -5
- package/scripts/send-email-threaded.py +47 -0
- package/scripts/send-sms.sh +4 -0
- package/scripts/send-whatsapp.sh +4 -0
- package/scripts/setup/init-backup.mjs +93 -38
- package/scripts/slack-send.sh +12 -0
package/lib/org/tool-surface.mjs
CHANGED
|
@@ -7,16 +7,24 @@
|
|
|
7
7
|
* - the native function-call path (`lib/tool-definitions.js` category 7 +
|
|
8
8
|
* `lib/action-executor.js` executor routing).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* 38 curated tools + 5 email tools + 5 artifact tools + 69 agent-desk tools
|
|
11
11
|
* (mail app / files / calendar / crm / books / meetings.recapFile / directory /
|
|
12
12
|
* design / call working sessions — the additive families register only when
|
|
13
13
|
* the vendored protocol carries them — see emailFamilyAvailable /
|
|
14
|
-
* artifactFamilyAvailable / deskFamilyAvailable) — NOT a
|
|
15
|
-
*
|
|
14
|
+
* artifactFamilyAvailable / deskFamilyAvailable) — NOT a one-to-one mirror of
|
|
15
|
+
* the 460-method protocol table. The long tail
|
|
16
16
|
* stays reachable through
|
|
17
17
|
* the `org_rpc` / `org_read` escape hatches, which validate against the frozen
|
|
18
18
|
* protocol table before any network I/O and are deliberately `access:"admin"`.
|
|
19
19
|
*
|
|
20
|
+
* WHAT "REACHABLE THROUGH THE ESCAPE HATCH" IS WORTH. `access:"admin"` puts
|
|
21
|
+
* org_rpc/org_read in the CEO tier ONLY (lib/tool-definitions.js: they are
|
|
22
|
+
* withheld from writeToolsLeadership and from the default lookup set). So for a
|
|
23
|
+
* leadership or default agent, "the long tail is reachable via org_rpc" is
|
|
24
|
+
* false — a capability with no curated tool simply does not exist for it. When
|
|
25
|
+
* a human-UI verb matters to an ordinary agent, it needs a CURATED tool; the
|
|
26
|
+
* escape hatch is not a parity argument, it is an operator's console.
|
|
27
|
+
*
|
|
20
28
|
* Governance parity (spec §1.5):
|
|
21
29
|
* - Tools that emit human-visible outbound content carry `outbound: true`
|
|
22
30
|
* (exactly messaging_send + email_send + email_draft_send +
|
|
@@ -76,6 +84,11 @@ import {
|
|
|
76
84
|
ESCALATION_SEVERITIES,
|
|
77
85
|
MEMORY_CLASSES,
|
|
78
86
|
} from "./param-contract.mjs";
|
|
87
|
+
// Delivery receipts. A spawned session speaks to humans through THIS surface,
|
|
88
|
+
// in its own process; the receipt is the only way the daemon that spawned it can
|
|
89
|
+
// later tell "the session answered" from "the session exited 0 and said
|
|
90
|
+
// nothing". Fail-open by construction — recordOutbound never throws.
|
|
91
|
+
import { recordOutbound } from "../comms/receipts.mjs";
|
|
79
92
|
|
|
80
93
|
/** Default org server origin when neither env nor config names one. */
|
|
81
94
|
export const DEFAULT_COHORT_BASE = "https://os.cohortapp.com";
|
|
@@ -219,11 +232,38 @@ export const ORG_TOOLS = Object.freeze([
|
|
|
219
232
|
{
|
|
220
233
|
name: "org_directory",
|
|
221
234
|
title: "Org directory",
|
|
222
|
-
description:
|
|
235
|
+
description:
|
|
236
|
+
"The org roster: members + teams from the /v1 directory read. Each AI member carries " +
|
|
237
|
+
"`presence` — {lane, detail, lastBeatMs, lastSeenMs} — which is the SAME green/amber/grey " +
|
|
238
|
+
"dot a human sees next to that colleague, from the same server-side resolver: lane " +
|
|
239
|
+
"\"online\" = available, \"indisposed\" = alive but blocked (detail says why, in the words " +
|
|
240
|
+
"the human reads), \"offline\" = genuinely dark, \"unknown\" = never beaten. Quote `lane` " +
|
|
241
|
+
"and `detail` rather than guessing from `status`, whose `ts` is client-stamped and can be " +
|
|
242
|
+
"skewed. Human members carry presence: null — that is \"not this derivation's question\", " +
|
|
243
|
+
"NOT \"offline\".",
|
|
223
244
|
input_schema: S({}),
|
|
224
245
|
access: "read",
|
|
225
246
|
binding: { kind: "read", path: "directory" },
|
|
226
247
|
},
|
|
248
|
+
{
|
|
249
|
+
name: "org_pulse",
|
|
250
|
+
title: "What needs attention",
|
|
251
|
+
description:
|
|
252
|
+
"The founder's home surface, as the human sees it (GET /v1/ops?pulse=1): `stats` " +
|
|
253
|
+
"{agentsRunning, blocked, decisionsToday, needs}, `needs` — the attention list in the " +
|
|
254
|
+
"human's own order, severity and wording (escalations, approvals, blocked colleagues) — " +
|
|
255
|
+
"and `activity`, the last 12 live-rail sentences (each carries an `age` such as 12m or 6d — a RELATIVE age, not a timestamp; the array is newest-first). Use this to answer \"what needs my " +
|
|
256
|
+
"human right now?\" and \"what just happened?\" instead of re-deriving either from raw " +
|
|
257
|
+
"board/decision reads: this IS the number on their screen, so a re-derivation can only " +
|
|
258
|
+
"disagree with it. Org-wide and PUBLIC-channel-scoped; it deliberately carries no " +
|
|
259
|
+
"per-person mentions, threads or drafts, because this plane resolves no viewer and an " +
|
|
260
|
+
"empty \"nothing mentions you\" would be a false negative. `pulse: null` means the " +
|
|
261
|
+
"derivation failed — say so, do not report zero. Also returns the plain probe fields " +
|
|
262
|
+
"(chainHead, members, openAlerts, readyTasks).",
|
|
263
|
+
input_schema: S({}),
|
|
264
|
+
access: "read",
|
|
265
|
+
binding: { kind: "read", path: "ops", query: { pulse: "1" } },
|
|
266
|
+
},
|
|
227
267
|
{
|
|
228
268
|
name: "org_snapshot",
|
|
229
269
|
title: "Org snapshot",
|
|
@@ -274,12 +314,20 @@ export const ORG_TOOLS = Object.freeze([
|
|
|
274
314
|
title: "Send org message",
|
|
275
315
|
description:
|
|
276
316
|
"Post a message into an org channel (messaging.send). Send-gate screened locally; " +
|
|
277
|
-
"the server enforces channel ACLs + pairing. An idempotencyId is auto-minted for dedup."
|
|
317
|
+
"the server enforces channel ACLs + pairing. An idempotencyId is auto-minted for dedup. " +
|
|
318
|
+
"`mentions` TAGS people — an @mention is what turns a message in a busy room into an ask " +
|
|
319
|
+
"someone is accountable for answering; without it the message is decoration. Pass member " +
|
|
320
|
+
"ids; they are normalised to the {memberId} objects hq validates.",
|
|
278
321
|
input_schema: S(
|
|
279
322
|
{
|
|
280
323
|
channelId: str("Channel id (use messaging_open_dm for a member DM)."),
|
|
281
324
|
body: str("Message body."),
|
|
282
325
|
threadRootId: str("Reply under this thread root (optional)."),
|
|
326
|
+
mentions: arr(
|
|
327
|
+
{ type: "string" },
|
|
328
|
+
"Member ids to @mention. Tag only the people who must ACT — a mention is a claim on " +
|
|
329
|
+
"someone's attention, and tagging bystanders is how a room gets muted.",
|
|
330
|
+
),
|
|
283
331
|
},
|
|
284
332
|
["channelId", "body"],
|
|
285
333
|
),
|
|
@@ -297,6 +345,244 @@ export const ORG_TOOLS = Object.freeze([
|
|
|
297
345
|
access: "write",
|
|
298
346
|
binding: { kind: "rpc", method: "channel.resolveOrCreateDm" },
|
|
299
347
|
},
|
|
348
|
+
{
|
|
349
|
+
name: "messaging_open_group",
|
|
350
|
+
title: "Open a group conversation",
|
|
351
|
+
description:
|
|
352
|
+
"Open a conversation with two or more people (channel.createConversation) and get back a " +
|
|
353
|
+
"channelId for messaging_send. One other person collapses to the deterministic 1:1 DM; two " +
|
|
354
|
+
"or more creates a GROUP_DM. Use this when several people must decide something TOGETHER — " +
|
|
355
|
+
"if they only need to know separately, DM them, and if a room already holds all of them, " +
|
|
356
|
+
"post there instead. A new room is the most expensive thing you can make: everyone in it " +
|
|
357
|
+
"carries it forever.",
|
|
358
|
+
input_schema: S(
|
|
359
|
+
{
|
|
360
|
+
memberIds: arr({ type: "string" }, "The people to put in the room (not yourself)."),
|
|
361
|
+
name: str("Optional room name — say what the room is FOR, not who is in it."),
|
|
362
|
+
},
|
|
363
|
+
["memberIds"],
|
|
364
|
+
),
|
|
365
|
+
access: "write",
|
|
366
|
+
binding: { kind: "rpc", method: "channel.createConversation" },
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "messaging_create_channel",
|
|
370
|
+
title: "Create a channel",
|
|
371
|
+
description:
|
|
372
|
+
"Create a named org channel (channel.create). Reach for this ONLY when the work is durable " +
|
|
373
|
+
"and the audience standing — a channel is permanent and sits in everyone's sidebar. For a " +
|
|
374
|
+
"one-off decision use messaging_open_group. Slug must be lowercase alphanumerics + hyphens.",
|
|
375
|
+
input_schema: S(
|
|
376
|
+
{
|
|
377
|
+
slug: str("URL-safe slug: lowercase alphanumerics and hyphens."),
|
|
378
|
+
name: str("Human name."),
|
|
379
|
+
kind: str("PUBLIC | PRIVATE (default PRIVATE)."),
|
|
380
|
+
topic: str("What this channel is for."),
|
|
381
|
+
members: arr({ type: "string" }, "Member ids to seat alongside you."),
|
|
382
|
+
},
|
|
383
|
+
["slug", "name"],
|
|
384
|
+
),
|
|
385
|
+
access: "write",
|
|
386
|
+
binding: { kind: "rpc", method: "channel.create" },
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "messaging_add_to_channel",
|
|
390
|
+
title: "Add someone to a channel",
|
|
391
|
+
description:
|
|
392
|
+
"Add a member to a channel you are already in (channel.addMember). Idempotent — re-adding " +
|
|
393
|
+
"is a no-op. Prefer this over creating a second room when the conversation is already " +
|
|
394
|
+
"happening somewhere and one more person needs to be in it.",
|
|
395
|
+
input_schema: S({ channelId: str("Channel id."), memberId: str("Who to add.") }, ["channelId", "memberId"]),
|
|
396
|
+
access: "write",
|
|
397
|
+
binding: { kind: "rpc", method: "channel.addMember" },
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "engage_colleagues",
|
|
401
|
+
title: "Bring the right people in",
|
|
402
|
+
description:
|
|
403
|
+
"Decide whether anyone actually needs to be pulled into a piece of work — and if so WHO and " +
|
|
404
|
+
"in WHICH surface — then do it. This is the judged path: it applies one threshold (engage " +
|
|
405
|
+
"only when a specific named person's action or consent is a PRECONDITION for the work to " +
|
|
406
|
+
"proceed or be safe, and you cannot supply it yourself), resolves the addressee from the org " +
|
|
407
|
+
"directory rather than guessing, picks the cheapest surface that reaches them (reply in " +
|
|
408
|
+
"place > DM > an existing room > a new room), tags them, and refuses to repeat an ask that " +
|
|
409
|
+
"is already outstanding. It will frequently decide NOBODY needs involving and say so — that " +
|
|
410
|
+
"is the tool working, not failing. Use it instead of hand-rolling a DM whenever you are " +
|
|
411
|
+
"blocked, need consent for something irreversible or external, need a required review, or " +
|
|
412
|
+
"have to tell someone a commitment to them is going to slip. Describe the work honestly: " +
|
|
413
|
+
"`selfServeExhausted` must be true for a blocked ask, because asking a human for something " +
|
|
414
|
+
"you could still work out yourself is outsourcing your own job.",
|
|
415
|
+
input_schema: S(
|
|
416
|
+
{
|
|
417
|
+
id: str("Stable id for this piece of work (used to dedupe and rate-limit)."),
|
|
418
|
+
title: str("One line naming the work."),
|
|
419
|
+
summary: str("What is going on, in the words the reader needs."),
|
|
420
|
+
state: str("in_progress | blocked | failed | done."),
|
|
421
|
+
scope: str("Subject area — used to find who owns it when nobody is named."),
|
|
422
|
+
blockerKind: str("decision | access | information | approval | capacity."),
|
|
423
|
+
blockerDetail: str("Exactly what is missing."),
|
|
424
|
+
blockerOwnerId: str("Member id, if you already know whose it is."),
|
|
425
|
+
selfServeExhausted: {
|
|
426
|
+
type: "boolean",
|
|
427
|
+
description: "True only if you have genuinely exhausted what you can do alone.",
|
|
428
|
+
},
|
|
429
|
+
irreversible: { type: "boolean", description: "The next step cannot be undone." },
|
|
430
|
+
external: { type: "boolean", description: "The next step leaves the org." },
|
|
431
|
+
financial: { type: "boolean", description: "The next step spends money." },
|
|
432
|
+
reviewRequired: { type: "boolean", description: "A policy or role requires sign-off." },
|
|
433
|
+
reviewerId: str("The required reviewer, if named."),
|
|
434
|
+
commitmentToMemberId: str("Who you promised this to."),
|
|
435
|
+
commitmentAtRisk: { type: "boolean", description: "That promise will be missed." },
|
|
436
|
+
channelId: str("Where the ask came from, so a reply lands in context."),
|
|
437
|
+
threadRootId: str("The thread it came from."),
|
|
438
|
+
requesterId: str("Who asked."),
|
|
439
|
+
question: str("The single question you need answered."),
|
|
440
|
+
options: arr({ type: "string" }, "Two or more concrete choices, if it is a decision."),
|
|
441
|
+
tried: arr({ type: "string" }, "What you already tried — this is what stops it reading as lazy."),
|
|
442
|
+
interestedParties: arr(
|
|
443
|
+
{ type: "string" },
|
|
444
|
+
"People who might like to know but whose actions do NOT change. They are recorded as " +
|
|
445
|
+
"considered-and-declined, never messaged.",
|
|
446
|
+
),
|
|
447
|
+
durable: { type: "boolean", description: "If a new room is needed, make it a named channel." },
|
|
448
|
+
},
|
|
449
|
+
["id", "title", "state"],
|
|
450
|
+
),
|
|
451
|
+
access: "write",
|
|
452
|
+
// Deliberately NOT `outbound: true`. The send-gate still runs on every
|
|
453
|
+
// message this tool produces — inside `messaging.sendMessage`, which is the
|
|
454
|
+
// one shared chokepoint — and it screens the COMPOSED BODY. Screening here
|
|
455
|
+
// instead would screen the work description, which is not what gets sent:
|
|
456
|
+
// the fallback shape stringifies the whole input blob, so the gate would be
|
|
457
|
+
// judging JSON that never reaches anybody while the real prose went
|
|
458
|
+
// unexamined. One gate, on the actual text.
|
|
459
|
+
binding: { kind: "local" },
|
|
460
|
+
},
|
|
461
|
+
// ── per-message actions ──────────────────────────────────────────────────
|
|
462
|
+
// The human long-press action sheet (react · reply · save · mark unread ·
|
|
463
|
+
// delete) had no curated counterpart: an agent could SEND into a room but
|
|
464
|
+
// could not react to, retract, bookmark or unread anything in it. The long
|
|
465
|
+
// tail was nominally reachable through org_rpc, but org_rpc is access:"admin"
|
|
466
|
+
// and lands only in the CEO tier (lib/tool-definitions.js), so for every
|
|
467
|
+
// leadership/default agent these verbs did not exist at all. They are
|
|
468
|
+
// always-on rather than desk-gated: the base `calling`/`messaging`/`channel`
|
|
469
|
+
// families have been vendored since SP3, so probing a newer method would
|
|
470
|
+
// withhold them on a protocol that carries them perfectly well.
|
|
471
|
+
{
|
|
472
|
+
name: "messaging_react",
|
|
473
|
+
title: "React to a message",
|
|
474
|
+
description:
|
|
475
|
+
"Add or remove YOUR reaction on a message (messaging.react). You must belong to the " +
|
|
476
|
+
"message's channel. Idempotent: re-adding the same emoji is a no-op, as is removing one " +
|
|
477
|
+
"you never added. Use a reaction where a reply would just be noise (acknowledging, " +
|
|
478
|
+
"voting, signalling 'seen') — it is the lightest honest signal you have.",
|
|
479
|
+
input_schema: S(
|
|
480
|
+
{
|
|
481
|
+
messageId: str("Message id."),
|
|
482
|
+
emoji: str("The emoji (or short name), e.g. \"✅\"."),
|
|
483
|
+
op: str("add (default) | remove."),
|
|
484
|
+
},
|
|
485
|
+
["messageId", "emoji"],
|
|
486
|
+
),
|
|
487
|
+
access: "write",
|
|
488
|
+
binding: { kind: "rpc", method: "messaging.react" },
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: "messaging_bookmark",
|
|
492
|
+
title: "Save a message",
|
|
493
|
+
description:
|
|
494
|
+
"Save a message to YOUR bookmarks (messaging.bookmark) — the Save row on the human " +
|
|
495
|
+
"action sheet. Private to you; it neither notifies the author nor changes the room.",
|
|
496
|
+
input_schema: S({ messageId: str("Message id.") }, ["messageId"]),
|
|
497
|
+
access: "write",
|
|
498
|
+
binding: { kind: "rpc", method: "messaging.bookmark" },
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: "messaging_delete",
|
|
502
|
+
title: "Delete your message",
|
|
503
|
+
description:
|
|
504
|
+
"Soft-delete a message you AUTHORED (messaging.delete) — the body is redacted, the " +
|
|
505
|
+
"audit trail is not. You cannot delete a colleague's message. Use this to retract " +
|
|
506
|
+
"something you posted in error; say so in the room rather than deleting silently.",
|
|
507
|
+
input_schema: S({ messageId: str("Message id (must be yours).") }, ["messageId"]),
|
|
508
|
+
access: "write",
|
|
509
|
+
binding: { kind: "rpc", method: "messaging.delete" },
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "messaging_mark_unread_from",
|
|
513
|
+
title: "Mark a room unread from here",
|
|
514
|
+
description:
|
|
515
|
+
"Move YOUR OWN read cursor in a channel back to just before a message " +
|
|
516
|
+
"(channel.markUnreadFrom), so everything from it onward recomputes as unread — the " +
|
|
517
|
+
"human 'Mark unread from here'. Affects only your cursor; nobody else's badge moves.",
|
|
518
|
+
input_schema: S(
|
|
519
|
+
{
|
|
520
|
+
channelId: str("Channel id."),
|
|
521
|
+
fromTimestamp: str("ISO datetime of the message to become the first unread one."),
|
|
522
|
+
},
|
|
523
|
+
["channelId", "fromTimestamp"],
|
|
524
|
+
),
|
|
525
|
+
access: "write",
|
|
526
|
+
binding: { kind: "rpc", method: "channel.markUnreadFrom" },
|
|
527
|
+
},
|
|
528
|
+
// ── huddles ──────────────────────────────────────────────────────────────
|
|
529
|
+
// The human surfaces collapsed audio+video onto ONE headphones control, and a
|
|
530
|
+
// calendar Join and an impromptu huddle resolve to the SAME room. The agent
|
|
531
|
+
// surface had the working-session tools (share/step/end) but no way to OPEN
|
|
532
|
+
// or ENTER the room they present into — a share tool whose callId nothing
|
|
533
|
+
// curated could produce.
|
|
534
|
+
{
|
|
535
|
+
name: "org_huddle_start",
|
|
536
|
+
title: "Start a huddle",
|
|
537
|
+
description:
|
|
538
|
+
"Open a live huddle (calling.start) — either anchored to a CHANNEL (every member of " +
|
|
539
|
+
"that room can walk in; you must belong to it) or as a direct call to named " +
|
|
540
|
+
"participantIds. Returns {callId, token, room}; feed callId to org_huddle_join for " +
|
|
541
|
+
"others, or to the share tools to present a surface. Give at least one of channelId or " +
|
|
542
|
+
"participantIds. Announce it in the room first — a huddle that opens unannounced is a " +
|
|
543
|
+
"ring nobody expected.",
|
|
544
|
+
input_schema: S(
|
|
545
|
+
{
|
|
546
|
+
channelId: str("Anchor the huddle to this channel (you must be a member)."),
|
|
547
|
+
participantIds: arr({ type: "string" }, "Member ids to ring (direct call)."),
|
|
548
|
+
kind: str("ONE_ON_ONE (default) | GROUP | CHANNEL."),
|
|
549
|
+
},
|
|
550
|
+
[],
|
|
551
|
+
),
|
|
552
|
+
access: "write",
|
|
553
|
+
binding: { kind: "rpc", method: "calling.start" },
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: "org_huddle_join",
|
|
557
|
+
title: "Join a huddle",
|
|
558
|
+
description:
|
|
559
|
+
"Join a live call you were invited to, or any huddle anchored to a channel you belong " +
|
|
560
|
+
"to (calling.join). Idempotent — rejoining returns a fresh token. Refused when the host " +
|
|
561
|
+
"has LOCKED the floor, when you were removed, or when the call has ended; relay that " +
|
|
562
|
+
"reason plainly rather than retrying.",
|
|
563
|
+
input_schema: S(
|
|
564
|
+
{
|
|
565
|
+
callId: str("Live call id."),
|
|
566
|
+
audioOnly: bool("Join without video (default false)."),
|
|
567
|
+
},
|
|
568
|
+
["callId"],
|
|
569
|
+
),
|
|
570
|
+
access: "write",
|
|
571
|
+
binding: { kind: "rpc", method: "calling.join" },
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: "org_huddle_end",
|
|
575
|
+
title: "End a huddle",
|
|
576
|
+
description:
|
|
577
|
+
"End a live huddle for EVERYONE and tear down its room (calling.end) — the close half of " +
|
|
578
|
+
"org_huddle_start, so a huddle you opened is never a room you cannot shut. Only a " +
|
|
579
|
+
"PARTICIPANT may end a call; ending one that has already ended returns CONFLICT, which " +
|
|
580
|
+
"means it is already closed, not that you failed. This ends the call for everybody, not " +
|
|
581
|
+
"just for you — say so in the room first.",
|
|
582
|
+
input_schema: S({ callId: str("Live call id.") }, ["callId"]),
|
|
583
|
+
access: "write",
|
|
584
|
+
binding: { kind: "rpc", method: "calling.end" },
|
|
585
|
+
},
|
|
300
586
|
// ── board / tasks ────────────────────────────────────────────────────────
|
|
301
587
|
{
|
|
302
588
|
name: "board_ready",
|
|
@@ -418,6 +704,50 @@ export const ORG_TOOLS = Object.freeze([
|
|
|
418
704
|
access: "write",
|
|
419
705
|
binding: { kind: "rpc", method: "board.addTaskComment" },
|
|
420
706
|
},
|
|
707
|
+
{
|
|
708
|
+
name: "work_track",
|
|
709
|
+
title: "Show your working",
|
|
710
|
+
description:
|
|
711
|
+
"Record where you are on the ask you are currently handling (board.track). The daemon " +
|
|
712
|
+
"already opens the board row when it takes the ask on and closes it when the session ends " +
|
|
713
|
+
"— you do NOT need to do either. Use this MID-WORK, when something a waiting human would " +
|
|
714
|
+
"want to know changes: you have started the substantive part (stage 'working'), you are " +
|
|
715
|
+
"stuck on something outside your control (stage 'blocked' — say what you need, and pass " +
|
|
716
|
+
"`notify` with the member id of whoever can unstick it), or the work is finished but wants " +
|
|
717
|
+
"a human's eyes (stage 'review'). Pass `channelId` and `messageId` of the message that " +
|
|
718
|
+
"asked, so this lands on the SAME row rather than opening a second one; the server dedupes " +
|
|
719
|
+
"on that pair, so a repeated call is free. Attach what you produced with `attachments`. " +
|
|
720
|
+
"Do not narrate every step — a comment per meaningful change, not per file edited.",
|
|
721
|
+
input_schema: S(
|
|
722
|
+
{
|
|
723
|
+
channelId: str("The channel the ask arrived on."),
|
|
724
|
+
messageId: str("The id of the message that asked. Identifies the board row."),
|
|
725
|
+
stage: {
|
|
726
|
+
type: "string",
|
|
727
|
+
enum: ["working", "blocked", "review"],
|
|
728
|
+
description:
|
|
729
|
+
"Where the work now stands. 'working' = started; 'blocked' = stuck and needs someone; " +
|
|
730
|
+
"'review' = finished, needs a human's eyes.",
|
|
731
|
+
},
|
|
732
|
+
note: str("What changed, in a sentence or two, for the person waiting."),
|
|
733
|
+
notify: {
|
|
734
|
+
type: "array",
|
|
735
|
+
items: { type: "string" },
|
|
736
|
+
description:
|
|
737
|
+
"Member ids to @-tag on this step — the people who genuinely need to act or know. " +
|
|
738
|
+
"Tagging everyone teaches everyone to ignore the tag.",
|
|
739
|
+
},
|
|
740
|
+
attachments: {
|
|
741
|
+
type: "array",
|
|
742
|
+
description: "Files this step produced: [{name, mimeType?, sizeBytes?, dataUrl}].",
|
|
743
|
+
items: { type: "object" },
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
["channelId", "messageId", "stage"],
|
|
747
|
+
),
|
|
748
|
+
access: "write",
|
|
749
|
+
binding: { kind: "rpc", method: "board.track" },
|
|
750
|
+
},
|
|
421
751
|
// ── decisions / approvals ────────────────────────────────────────────────
|
|
422
752
|
{
|
|
423
753
|
name: "decision_list",
|
|
@@ -505,11 +835,43 @@ export const ORG_TOOLS = Object.freeze([
|
|
|
505
835
|
title: "Search org knowledge",
|
|
506
836
|
description:
|
|
507
837
|
"Search the ACL'd shared org knowledge plane (knowledge.search). Every retrieval is " +
|
|
508
|
-
"audit-logged server-side; the server decides what this agent may see."
|
|
838
|
+
"audit-logged server-side; the server decides what this agent may see. This is the " +
|
|
839
|
+
"NARROW view — shared facts only. Prefer memory_recall, which is the same index with " +
|
|
840
|
+
"your own perspective, scope/entity/time controls and drill-down pointers.",
|
|
509
841
|
input_schema: S({ query: str("Free-text query."), limit: num("Max hits (optional).") }, ["query"]),
|
|
510
842
|
access: "read",
|
|
511
843
|
binding: { kind: "rpc", method: "knowledge.search" },
|
|
512
844
|
},
|
|
845
|
+
{
|
|
846
|
+
name: "memory_recall",
|
|
847
|
+
title: "Recall from memory",
|
|
848
|
+
description:
|
|
849
|
+
"Layered semantic recall over your unified agent memory (memory.recall) — the SAME " +
|
|
850
|
+
"recall the chat lane's colleagues use, so a seat driven from here reasons off the same " +
|
|
851
|
+
"memory as the same colleague in a channel. Read-only. Defaults to org-wide; `scope: " +
|
|
852
|
+
"\"self\"` narrows to your own perspective, `channelId` to one room, and refType+refId " +
|
|
853
|
+
"(given TOGETHER) pins one entity and wins over scope. `depth: \"specific\"` for a " +
|
|
854
|
+
"pinpoint fact, \"broad\" for context. ACL is server-side and fail-closed: the seat is " +
|
|
855
|
+
"your own key's, never a parameter, so you cannot recall another colleague's private " +
|
|
856
|
+
"view. An unkeyed embedder or an empty index returns {found:false, items:[]} — that is " +
|
|
857
|
+
"an honest \"nothing indexed\", not an error to retry.",
|
|
858
|
+
input_schema: S(
|
|
859
|
+
{
|
|
860
|
+
query: str("What to recall (2–500 chars)."),
|
|
861
|
+
scope: str("org (default) | self."),
|
|
862
|
+
channelId: str("Narrow to one channel (overrides scope)."),
|
|
863
|
+
refType: str("Entity type to pin (with refId; wins over scope)."),
|
|
864
|
+
refId: str("Entity id to pin (with refType)."),
|
|
865
|
+
since: str("ISO datetime lower bound."),
|
|
866
|
+
until: str("ISO datetime upper bound."),
|
|
867
|
+
depth: str("broad | specific."),
|
|
868
|
+
k: num("Max items, 1–25."),
|
|
869
|
+
},
|
|
870
|
+
["query"],
|
|
871
|
+
),
|
|
872
|
+
access: "read",
|
|
873
|
+
binding: { kind: "rpc", method: "memory.recall" },
|
|
874
|
+
},
|
|
513
875
|
{
|
|
514
876
|
name: "knowledge_append",
|
|
515
877
|
title: "Append org knowledge",
|
|
@@ -859,8 +1221,13 @@ export const ORG_TOOLS = Object.freeze([
|
|
|
859
1221
|
name: "email_triage",
|
|
860
1222
|
title: "Triage mail thread",
|
|
861
1223
|
description:
|
|
862
|
-
"One triage facet per call (email.triage):
|
|
863
|
-
"
|
|
1224
|
+
"One triage facet per call (email.triage): priority (P0|P1|P2) + category + tags, OR " +
|
|
1225
|
+
"assigneeMemberId, OR muted, OR done — the same one-verb-per-call rule the Inbox UI " +
|
|
1226
|
+
"applies (the server refuses combinations with BAD_REQUEST). starred / read / " +
|
|
1227
|
+
"unreadFromMessageId ride alongside the first two, but read and unreadFromMessageId are " +
|
|
1228
|
+
"mutually exclusive (both move the same read cursor). muted = no pings or badges, still " +
|
|
1229
|
+
"filed; done = cleared from the working list (false reopens); unreadFromMessageId = the " +
|
|
1230
|
+
"long-press 'Mark unread' a human gets on ONE message, leaving everything above it read.",
|
|
864
1231
|
input_schema: S(
|
|
865
1232
|
{
|
|
866
1233
|
threadId: str("Thread id."),
|
|
@@ -868,7 +1235,12 @@ export const ORG_TOOLS = Object.freeze([
|
|
|
868
1235
|
category: str("Category label (optional)."),
|
|
869
1236
|
tags: arr({ type: "string" }, "Replace the tag set (≤12; optional)."),
|
|
870
1237
|
starred: bool("Star/unstar."),
|
|
871
|
-
read: bool("Mark read/unread."),
|
|
1238
|
+
read: bool("Mark the whole thread read/unread."),
|
|
1239
|
+
unreadFromMessageId: str(
|
|
1240
|
+
"Mark unread FROM this message down (per-message granularity; exclusive with read).",
|
|
1241
|
+
),
|
|
1242
|
+
muted: bool("Mute/unmute — stays filed, stops pinging."),
|
|
1243
|
+
done: bool("Mark done (true) or reopen (false); alone in its call."),
|
|
872
1244
|
assigneeMemberId: str("Assign to this member (null to unassign)."),
|
|
873
1245
|
},
|
|
874
1246
|
["threadId"],
|
|
@@ -2448,7 +2820,54 @@ function readToFrame(r) {
|
|
|
2448
2820
|
* screenImpl?, bumpImpl? } (injectables for tests)
|
|
2449
2821
|
* @returns {Promise<object>} res frame
|
|
2450
2822
|
*/
|
|
2823
|
+
/**
|
|
2824
|
+
* Which org tools put words in front of a human, and where to find the room.
|
|
2825
|
+
*
|
|
2826
|
+
* A spawned session is a separate `claude` process: when it answers the user,
|
|
2827
|
+
* the daemon that spawned it learns nothing. That is why the daemon used to
|
|
2828
|
+
* treat exit 0 as proof of a reply and mark the ask handled forever — including
|
|
2829
|
+
* the observed case of a 241-second session whose own final text was "Nothing
|
|
2830
|
+
* was sent." A receipt written here crosses the process boundary and lets the
|
|
2831
|
+
* daemon ask the real question instead of the convenient one.
|
|
2832
|
+
*/
|
|
2833
|
+
const HUMAN_FACING_TOOLS = {
|
|
2834
|
+
messaging_send: (i) => i.channelId || i.channel,
|
|
2835
|
+
email_send: (i) => i.to || (Array.isArray(i.recipients) ? i.recipients[0] : null),
|
|
2836
|
+
email_reply: (i) => i.to || i.threadId,
|
|
2837
|
+
};
|
|
2838
|
+
|
|
2839
|
+
/**
|
|
2840
|
+
* Execute an org tool and, when it was one that speaks to a person and it
|
|
2841
|
+
* succeeded, leave a delivery receipt.
|
|
2842
|
+
*
|
|
2843
|
+
* Strictly observational: the receipt is written AFTER the frame is resolved and
|
|
2844
|
+
* only on success, it cannot alter the frame, and any failure writing it is
|
|
2845
|
+
* swallowed by `recordOutbound` itself. A bookkeeping fault must never break a
|
|
2846
|
+
* send — but the absence of a receipt is read as "the human heard nothing",
|
|
2847
|
+
* which errs toward one extra message rather than silence.
|
|
2848
|
+
*/
|
|
2451
2849
|
export async function executeOrgTool(name, input = {}, o = {}) {
|
|
2850
|
+
const frame = await executeOrgToolInner(name, input, o);
|
|
2851
|
+
try {
|
|
2852
|
+
const locate = HUMAN_FACING_TOOLS[name];
|
|
2853
|
+
if (locate && frame && frame.ok) {
|
|
2854
|
+
const channel = locate(input || {});
|
|
2855
|
+
if (channel) {
|
|
2856
|
+
recordOutbound({
|
|
2857
|
+
service: name.startsWith("email") ? "gmail" : "cohort",
|
|
2858
|
+
channel,
|
|
2859
|
+
kind: "session",
|
|
2860
|
+
via: name,
|
|
2861
|
+
chars: typeof (input && input.body) === "string" ? input.body.length : null,
|
|
2862
|
+
agentRoot: (o && o.agentRoot) || process.env.AGENT_ROOT || process.env.AGENT_DIR,
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
} catch { /* never let a receipt break a send */ }
|
|
2867
|
+
return frame;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
async function executeOrgToolInner(name, input = {}, o = {}) {
|
|
2452
2871
|
try {
|
|
2453
2872
|
const cfg = resolveOrgToolConfig(o);
|
|
2454
2873
|
// A curated tool first; else a granted integration tool from the disk cache
|
|
@@ -2507,6 +2926,78 @@ export async function executeOrgTool(name, input = {}, o = {}) {
|
|
|
2507
2926
|
case "org_whoami":
|
|
2508
2927
|
return whoami(cfg, { fetchImpl: o.fetchImpl, env: o.env || process.env });
|
|
2509
2928
|
|
|
2929
|
+
case "engage_colleagues": {
|
|
2930
|
+
// The judged path. `engage` loads the directory + rooms + this agent's
|
|
2931
|
+
// engagement history, applies the threshold, and either sends or records
|
|
2932
|
+
// why it didn't. Both outcomes come back as an OK frame carrying the
|
|
2933
|
+
// reasoning: "nobody needed involving" is an answer, not an error, and
|
|
2934
|
+
// returning it as an error would teach the model to retry until it
|
|
2935
|
+
// manages to interrupt somebody.
|
|
2936
|
+
const { engage } = await import("./engagement.mjs");
|
|
2937
|
+
const i = input || {};
|
|
2938
|
+
const work = {
|
|
2939
|
+
id: String(i.id || ""),
|
|
2940
|
+
title: String(i.title || ""),
|
|
2941
|
+
summary: i.summary ? String(i.summary) : "",
|
|
2942
|
+
scope: i.scope ? String(i.scope) : "",
|
|
2943
|
+
state: String(i.state || "in_progress"),
|
|
2944
|
+
durable: i.durable === true,
|
|
2945
|
+
selfServeExhausted: i.selfServeExhausted === true,
|
|
2946
|
+
...(i.blockerKind || i.blockerDetail || i.blockerOwnerId
|
|
2947
|
+
? {
|
|
2948
|
+
blocker: {
|
|
2949
|
+
kind: String(i.blockerKind || "information"),
|
|
2950
|
+
scope: i.scope ? String(i.scope) : "",
|
|
2951
|
+
detail: i.blockerDetail ? String(i.blockerDetail) : "",
|
|
2952
|
+
ownerId: i.blockerOwnerId ? String(i.blockerOwnerId) : null,
|
|
2953
|
+
},
|
|
2954
|
+
}
|
|
2955
|
+
: {}),
|
|
2956
|
+
risk: { irreversible: i.irreversible === true, external: i.external === true, financial: i.financial === true },
|
|
2957
|
+
...(i.reviewRequired || i.reviewerId
|
|
2958
|
+
? { review: { required: i.reviewRequired === true, reviewerId: i.reviewerId ? String(i.reviewerId) : null } }
|
|
2959
|
+
: {}),
|
|
2960
|
+
...(i.commitmentToMemberId
|
|
2961
|
+
? { commitment: { toMemberId: String(i.commitmentToMemberId), atRisk: i.commitmentAtRisk === true } }
|
|
2962
|
+
: {}),
|
|
2963
|
+
origin: {
|
|
2964
|
+
channelId: i.channelId ? String(i.channelId) : null,
|
|
2965
|
+
threadRootId: i.threadRootId ? String(i.threadRootId) : null,
|
|
2966
|
+
requesterId: i.requesterId ? String(i.requesterId) : null,
|
|
2967
|
+
},
|
|
2968
|
+
question: i.question ? String(i.question) : "",
|
|
2969
|
+
options: Array.isArray(i.options) ? i.options.map(String) : [],
|
|
2970
|
+
tried: Array.isArray(i.tried) ? i.tried.map(String) : [],
|
|
2971
|
+
interestedParties: Array.isArray(i.interestedParties) ? i.interestedParties.map(String) : [],
|
|
2972
|
+
};
|
|
2973
|
+
if (!work.id || !work.title) return errFrame("BAD_REQUEST", "engage_colleagues: id and title are required");
|
|
2974
|
+
// Own member id, so the judgement never routes the ask back to this
|
|
2975
|
+
// seat. Unresolvable is survivable — `resolveTargets` only uses it to
|
|
2976
|
+
// exclude self and to find a supervisor edge.
|
|
2977
|
+
const envv = o.env || process.env;
|
|
2978
|
+
const me = String(envv.COHORT_AGENT_ID || envv.COHORT_MEMBER_ID || "");
|
|
2979
|
+
const run = await engage(work, {
|
|
2980
|
+
cfg: { org: { cohort: { enabled: true, base: cfg.base, token: cfg.token, orgId: cfg.orgId, agentId: me } } },
|
|
2981
|
+
agentRoot: cfg.agentRoot,
|
|
2982
|
+
me,
|
|
2983
|
+
fetchImpl: o.fetchImpl,
|
|
2984
|
+
});
|
|
2985
|
+
const d = run.decision || {};
|
|
2986
|
+
return okFrame({
|
|
2987
|
+
engaged: run.engaged,
|
|
2988
|
+
outcome: run.outcome,
|
|
2989
|
+
surface: run.surface,
|
|
2990
|
+
channelId: run.channelId,
|
|
2991
|
+
trigger: d.trigger || null,
|
|
2992
|
+
reasonCode: d.reasonCode || null,
|
|
2993
|
+
reason: d.reason || null,
|
|
2994
|
+
targets: (d.targets || []).map((t) => ({ memberId: t.memberId, name: t.name, rung: t.rung, why: t.why })),
|
|
2995
|
+
consideredNotEngaged: d.consideredNotEngaged || [],
|
|
2996
|
+
why: d.why || [],
|
|
2997
|
+
error: run.error || null,
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
|
|
2510
3001
|
case "org_events_tail": {
|
|
2511
3002
|
const q = {};
|
|
2512
3003
|
if (input && input.cursor != null) q.cursor = input.cursor;
|
|
@@ -2562,7 +3053,12 @@ export async function executeOrgTool(name, input = {}, o = {}) {
|
|
|
2562
3053
|
default: {
|
|
2563
3054
|
const binding = def.binding || {};
|
|
2564
3055
|
if (binding.kind === "read") {
|
|
2565
|
-
|
|
3056
|
+
// `binding.query` lets a curated tool PIN a read's query params (e.g.
|
|
3057
|
+
// org_pulse → ops?pulse=1). The alternative — telling every agent to
|
|
3058
|
+
// pass ?pulse=1 through org_read — is not reachable at all below the
|
|
3059
|
+
// CEO tier, since org_read is access:"admin". Same encoder org_read
|
|
3060
|
+
// uses, so one read path cannot encode two ways.
|
|
3061
|
+
return readToFrame(await read(withQuery(binding.path, binding.query), callOpts));
|
|
2566
3062
|
}
|
|
2567
3063
|
if (binding.kind === "rpc") {
|
|
2568
3064
|
// SP5: a granted integration tool proxies to integration.invokeTool
|