@vellumai/assistant 0.10.3 → 0.10.4-staging.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.
Files changed (239) hide show
  1. package/openapi.yaml +73 -56
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  4. package/src/__tests__/assistant-stream-state.test.ts +3 -76
  5. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
  6. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  7. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  8. package/src/__tests__/channel-guardian.test.ts +82 -32
  9. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  10. package/src/__tests__/channel-reply-delivery.test.ts +6 -2
  11. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +148 -0
  13. package/src/__tests__/consult-deadline.test.ts +60 -0
  14. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  15. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  16. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  17. package/src/__tests__/contacts-write.test.ts +0 -2
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +98 -7
  20. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  21. package/src/__tests__/conversation-error.test.ts +18 -0
  22. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  23. package/src/__tests__/conversation-title-service.test.ts +222 -201
  24. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  25. package/src/__tests__/delete-propagation.test.ts +5 -3
  26. package/src/__tests__/dm-backfill.test.ts +6 -4
  27. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  28. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  29. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  30. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  31. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  32. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  33. package/src/__tests__/helpers/mock-logger.ts +1 -0
  34. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  35. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  36. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  37. package/src/__tests__/invite-routes-http.test.ts +34 -0
  38. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  39. package/src/__tests__/list-messages-page-latest.test.ts +173 -4
  40. package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
  41. package/src/__tests__/non-member-access-request.test.ts +15 -13
  42. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  43. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  44. package/src/__tests__/persona-resolver.test.ts +75 -45
  45. package/src/__tests__/plugin-bootstrap.test.ts +13 -5
  46. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  47. package/src/__tests__/provider-usage-tracking.test.ts +1 -1
  48. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  49. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  50. package/src/__tests__/reaction-persistence.test.ts +51 -4
  51. package/src/__tests__/relay-server.test.ts +88 -31
  52. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  53. package/src/__tests__/settings-routes.test.ts +32 -0
  54. package/src/__tests__/slack-block-formatting.test.ts +1 -38
  55. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  56. package/src/__tests__/stt-hints.test.ts +6 -3
  57. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  58. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  59. package/src/__tests__/subagent-role-registry.test.ts +17 -4
  60. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  61. package/src/__tests__/subagent-tools.test.ts +398 -3
  62. package/src/__tests__/thread-backfill.test.ts +3 -3
  63. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
  64. package/src/__tests__/tool-start-timestamp.test.ts +4 -3
  65. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  66. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  67. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  68. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  69. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  70. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  71. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  72. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  73. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
  74. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  75. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  76. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  77. package/src/agent/loop-exclusive-tool.test.ts +19 -15
  78. package/src/agent/loop-native-web-search.test.ts +200 -0
  79. package/src/agent/loop.ts +108 -1
  80. package/src/api/responses/conversation-message.ts +9 -0
  81. package/src/approvals/guardian-request-resolvers.ts +16 -4
  82. package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
  83. package/src/calls/guardian-dispatch.ts +14 -11
  84. package/src/calls/inbound-trust-reader.ts +7 -1
  85. package/src/calls/relay-access-wait.ts +6 -6
  86. package/src/calls/relay-server.ts +22 -2
  87. package/src/calls/relay-setup-router.ts +10 -10
  88. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  89. package/src/cli/commands/contacts.ts +10 -7
  90. package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
  91. package/src/cli/commands/memory/worker.ts +97 -30
  92. package/src/cli/commands/plugins.ts +3 -146
  93. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
  94. package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
  95. package/src/cli/lib/publish-plugin.ts +231 -1
  96. package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
  97. package/src/config/bundled-skills/subagent/SKILL.md +16 -1
  98. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  99. package/src/config/call-site-defaults.ts +0 -6
  100. package/src/config/llm-resolver.ts +0 -3
  101. package/src/config/schemas/call-site-catalog.ts +0 -7
  102. package/src/config/schemas/heartbeat.ts +2 -5
  103. package/src/config/schemas/llm.ts +3 -12
  104. package/src/config/schemas/memory-lifecycle.ts +1 -1
  105. package/src/config/seed-inference-profiles.ts +76 -35
  106. package/src/config/sync-gated-profiles.ts +0 -3
  107. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  108. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  109. package/src/contacts/contact-store.ts +27 -237
  110. package/src/contacts/contacts-write.ts +18 -58
  111. package/src/contacts/gateway-channel-read.ts +51 -0
  112. package/src/contacts/member-write-relay.ts +25 -31
  113. package/src/contacts/types.ts +3 -15
  114. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  115. package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
  116. package/src/daemon/conversation-agent-loop.ts +68 -61
  117. package/src/daemon/conversation-error.ts +7 -10
  118. package/src/daemon/conversation-tool-setup.ts +0 -10
  119. package/src/daemon/conversation.ts +10 -0
  120. package/src/daemon/external-plugins-bootstrap.ts +8 -2
  121. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  122. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  123. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  124. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  125. package/src/daemon/handlers/config-channels.ts +14 -29
  126. package/src/daemon/lifecycle.ts +16 -4
  127. package/src/daemon/message-types/surfaces.ts +2 -0
  128. package/src/heartbeat/heartbeat-service.ts +5 -0
  129. package/src/home/relationship-state-writer.ts +5 -0
  130. package/src/memory/__tests__/embedding-cache.test.ts +136 -0
  131. package/src/memory/compaction-ledger-store.ts +107 -0
  132. package/src/memory/conversation-crud.ts +136 -61
  133. package/src/memory/conversation-title-service.ts +173 -24
  134. package/src/memory/embedding-backend.ts +8 -1
  135. package/src/memory/embedding-cache.ts +139 -0
  136. package/src/memory/jobs-worker.ts +75 -29
  137. package/src/memory/memory-retrospective-job.ts +5 -0
  138. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
  139. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  140. package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
  141. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
  142. package/src/memory/schema/contacts.ts +6 -2
  143. package/src/memory/schema/conversations.ts +39 -0
  144. package/src/memory/steps.ts +1090 -367
  145. package/src/memory/worker-control.ts +104 -18
  146. package/src/memory/worker-process.ts +17 -0
  147. package/src/messaging/channel-binding-metadata.ts +31 -0
  148. package/src/messaging/channel-binding-schema.ts +51 -0
  149. package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
  150. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
  151. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
  152. package/src/messaging/providers/a2a/deliver.ts +5 -1
  153. package/src/messaging/providers/a2a/transport.ts +10 -0
  154. package/src/messaging/providers/callback-routing.ts +48 -0
  155. package/src/messaging/providers/channel-transport.ts +55 -0
  156. package/src/messaging/providers/index.ts +65 -241
  157. package/src/messaging/providers/slack/binding-metadata.ts +62 -0
  158. package/src/messaging/providers/slack/transport.ts +92 -0
  159. package/src/messaging/providers/telegram-bot/transport.ts +51 -0
  160. package/src/messaging/providers/whatsapp/transport.ts +38 -0
  161. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  162. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  163. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  164. package/src/notifications/destination-resolver.ts +7 -23
  165. package/src/notifications/emit-signal.ts +5 -11
  166. package/src/plugins/defaults/index.ts +0 -35
  167. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
  168. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
  169. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
  170. package/src/plugins/disabled-state.ts +31 -0
  171. package/src/plugins/registry.ts +55 -12
  172. package/src/prompts/persona-resolver.ts +43 -11
  173. package/src/providers/call-site-routing.ts +41 -0
  174. package/src/providers/provider-send-message.ts +6 -0
  175. package/src/providers/ratelimit.ts +6 -0
  176. package/src/providers/registry.ts +1 -1
  177. package/src/providers/retry.ts +6 -0
  178. package/src/providers/types.ts +13 -0
  179. package/src/providers/usage-tracking.ts +6 -0
  180. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  181. package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
  182. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  183. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
  184. package/src/runtime/access-request-helper.ts +1 -2
  185. package/src/runtime/actor-trust-resolver.ts +44 -17
  186. package/src/runtime/anchored-guardian.test.ts +7 -54
  187. package/src/runtime/anchored-guardian.ts +4 -53
  188. package/src/runtime/assistant-stream-state.ts +12 -74
  189. package/src/runtime/channel-reply-delivery.ts +3 -8
  190. package/src/runtime/guardian-vellum-migration.ts +18 -16
  191. package/src/runtime/invite-redemption-service.ts +25 -10
  192. package/src/runtime/local-actor-identity.test.ts +108 -0
  193. package/src/runtime/local-actor-identity.ts +27 -20
  194. package/src/runtime/member-verdict-cache.ts +0 -0
  195. package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
  196. package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
  197. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  198. package/src/runtime/routes/contact-routes.ts +40 -25
  199. package/src/runtime/routes/conversation-list-routes.ts +1 -29
  200. package/src/runtime/routes/conversation-routes.ts +27 -7
  201. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  202. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  203. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  204. package/src/runtime/routes/settings-routes.ts +8 -3
  205. package/src/runtime/services/conversation-serializer.ts +6 -49
  206. package/src/runtime/slack-block-formatting.ts +0 -15
  207. package/src/runtime/trust-verdict-consumer.ts +36 -41
  208. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  209. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  210. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
  211. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  212. package/src/subagent/index.ts +1 -1
  213. package/src/subagent/manager.ts +245 -33
  214. package/src/subagent/types.ts +8 -1
  215. package/src/tools/registry.ts +10 -3
  216. package/src/tools/subagent/consult-deadline.ts +49 -0
  217. package/src/tools/subagent/spawn.ts +234 -5
  218. package/src/util/logger.ts +9 -0
  219. package/src/util/platform.ts +14 -0
  220. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  221. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  222. package/src/workspace/migrations/registry.ts +2 -0
  223. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  224. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  225. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  226. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
  227. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  228. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  229. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  230. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  231. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  232. package/src/plugins/defaults/advisor/config.ts +0 -21
  233. package/src/plugins/defaults/advisor/consult.ts +0 -197
  234. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  235. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  236. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  237. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  238. package/src/plugins/defaults/advisor/package.json +0 -14
  239. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
@@ -4,10 +4,9 @@
4
4
  *
5
5
  * The gateway resolves a per-actor verdict from its ACL DB and stamps it onto
6
6
  * inbound `sourceMetadata`. These pure mappers turn that verdict into the same
7
- * {@link TrustContext} / {@link ResolvedMember} the local resolver would have
8
- * produced — ACL + identity only. INFO fields (notes, userFile, contactType,
9
- * interactionCount) are never carried on the wire; the consumer re-joins them
10
- * locally by contactId.
7
+ * {@link TrustContext} the local resolver would have produced — ACL + identity
8
+ * only. INFO fields (notes, userFile, contactType, interactionCount) are never
9
+ * carried on the wire; the consumer re-joins them locally by contactId.
11
10
  */
12
11
 
13
12
  import type { TrustVerdict } from "@vellumai/gateway-client";
@@ -18,12 +17,12 @@ import type {
18
17
  ChannelPolicy,
19
18
  ChannelStatus,
20
19
  ContactChannel,
20
+ ContactRole,
21
21
  ContactWithChannels,
22
22
  } from "../contacts/types.js";
23
23
  import type { TrustContext } from "../daemon/trust-context.js";
24
24
  import type { ActorTrustContext } from "./actor-trust-resolver.js";
25
25
  import { toTrustContext } from "./actor-trust-resolver.js";
26
- import type { ResolvedMember } from "./routes/inbound-stages/acl-enforcement.js";
27
26
 
28
27
  export interface TrustVerdictTransport {
29
28
  sourceChannel: ChannelId;
@@ -66,7 +65,7 @@ export function actorTrustContextFromVerdict(
66
65
  // deny/escalate. Null for memberless verdicts. Text path is unaffected:
67
66
  // toTrustContext derives the same member fields trustContextFromVerdict
68
67
  // already stamps.
69
- memberRecord: resolvedMemberFromVerdict(verdict),
68
+ memberRecord: memberRecordFromVerdict(verdict),
70
69
  trustClass: verdict.trustClass,
71
70
  actorMetadata: {
72
71
  identifier,
@@ -99,11 +98,11 @@ export function trustContextFromVerdict(
99
98
  // Stamp the verdict's ACL member fields onto the context so downstream turn
100
99
  // assembly reads member status/policy from the verdict rather than a local
101
100
  // re-resolution. The contact ID anchors the local info-only join.
102
- const member = resolvedMemberFromVerdict(verdict);
101
+ const member = verdictMemberFromVerdict(verdict);
103
102
  if (member) {
104
- context.requesterContactId = member.contact.id;
105
- context.memberStatus = channelStatusToMemberStatus(member.channel.status);
106
- context.memberPolicy = member.channel.policy;
103
+ context.requesterContactId = member.contactId;
104
+ context.memberStatus = channelStatusToMemberStatus(member.status);
105
+ context.memberPolicy = member.policy;
107
106
  }
108
107
 
109
108
  return context;
@@ -111,7 +110,7 @@ export function trustContextFromVerdict(
111
110
 
112
111
  /**
113
112
  * True when the verdict carries a member identity (contactId or channelId),
114
- * regardless of whether that member resolves to a usable {@link ResolvedMember}.
113
+ * regardless of whether that member resolves to a usable {@link VerdictMember}.
115
114
  */
116
115
  export function verdictHasMemberIdentity(verdict: TrustVerdict): boolean {
117
116
  return !!(verdict.contactId || verdict.channelId);
@@ -119,13 +118,13 @@ export function verdictHasMemberIdentity(verdict: TrustVerdict): boolean {
119
118
 
120
119
  /**
121
120
  * True when the verdict claims a member identity but that member can't be
122
- * synthesized (partial/mixed-version verdict). Such a verdict is unusable —
121
+ * resolved (partial/mixed-version verdict). Such a verdict is unusable —
123
122
  * callers fall back to local resolution.
124
123
  */
125
124
  export function verdictMemberUnresolvable(verdict: TrustVerdict): boolean {
126
125
  return (
127
126
  verdictHasMemberIdentity(verdict) &&
128
- resolvedMemberFromVerdict(verdict) === null
127
+ verdictMemberFromVerdict(verdict) === null
129
128
  );
130
129
  }
131
130
 
@@ -167,8 +166,8 @@ export interface VerdictMember {
167
166
  /**
168
167
  * Extract the narrow {@link VerdictMember} ACL view from a gateway verdict.
169
168
  *
170
- * Mirrors {@link resolvedMemberFromVerdict}'s guards (contactId/channelId
171
- * present + known status/policy enums), failing closed to null otherwise.
169
+ * Guards on contactId/channelId presence + known status/policy enums, failing
170
+ * closed to null otherwise.
172
171
  */
173
172
  export function verdictMemberFromVerdict(
174
173
  verdict: TrustVerdict,
@@ -190,38 +189,26 @@ export function verdictMemberFromVerdict(
190
189
  }
191
190
 
192
191
  /**
193
- * Build a synthetic {@link ResolvedMember} from a gateway verdict.
192
+ * Build the voice-path {@link ActorTrustContext.memberRecord} from a gateway
193
+ * verdict's narrow ACL view.
194
194
  *
195
195
  * ACL + identity only; info fields are placeholders, re-joined locally by
196
- * contactId. Returns null for memberless verdicts.
196
+ * contactId. Returns null for memberless/unresolvable verdicts.
197
197
  */
198
- export function resolvedMemberFromVerdict(
198
+ function memberRecordFromVerdict(
199
199
  verdict: TrustVerdict,
200
- ): ResolvedMember | null {
201
- if (!verdict.contactId || !verdict.channelId) return null;
202
- // Member verdict requires valid known status+policy enums, else null
203
- // (fail-closed): a partial/mixed-version verdict (absent OR
204
- // present-but-unknown ACL value) must not synthesize an active/allow channel
205
- // that would skip ingress ACL gates.
206
- if (!verdict.status || !verdict.policy) return null;
207
- if (!isChannelStatus(verdict.status) || !isChannelPolicy(verdict.policy)) {
208
- return null;
209
- }
200
+ ): ActorTrustContext["memberRecord"] {
201
+ const member = verdictMemberFromVerdict(verdict);
202
+ if (!member) return null;
210
203
 
211
204
  const channel: ContactChannel = {
212
- id: verdict.channelId,
213
- contactId: verdict.contactId,
205
+ id: member.channelId,
206
+ contactId: member.contactId,
214
207
  type: verdict.type ?? "",
215
208
  address: verdict.address ?? "",
216
209
  isPrimary: false,
217
210
  externalChatId: verdict.externalChatId ?? null,
218
- status: verdict.status,
219
- policy: verdict.policy,
220
- verifiedAt: verdict.verifiedAt ?? null,
221
- verifiedVia: verdict.verifiedVia ?? null,
222
211
  inviteId: null,
223
- revokedReason: null,
224
- blockedReason: null,
225
212
  lastSeenAt: null,
226
213
  interactionCount: 0,
227
214
  lastInteraction: null,
@@ -229,20 +216,28 @@ export function resolvedMemberFromVerdict(
229
216
  createdAt: 0,
230
217
  };
231
218
 
219
+ const role: ContactRole =
220
+ verdict.trustClass === "guardian" ? "guardian" : "contact";
221
+
232
222
  const contact: ContactWithChannels = {
233
- id: verdict.contactId,
234
- displayName: verdict.memberDisplayName ?? "",
223
+ id: member.contactId,
224
+ displayName: member.displayName ?? "",
235
225
  notes: null,
226
+ role,
236
227
  lastInteraction: null,
237
228
  interactionCount: 0,
238
229
  createdAt: 0,
239
230
  updatedAt: 0,
240
- role: verdict.trustClass === "guardian" ? "guardian" : "contact",
241
231
  contactType: "human",
242
- principalId: verdict.guardianPrincipalId ?? null,
243
232
  userFile: null,
244
233
  channels: [channel],
245
234
  };
246
235
 
247
- return { contact, channel };
236
+ return {
237
+ contact,
238
+ channel,
239
+ status: member.status,
240
+ policy: member.policy,
241
+ role,
242
+ };
248
243
  }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { advisorRequestText, buildAdvisorSystem } from "../consult-prompt.js";
4
+
5
+ describe("buildAdvisorSystem", () => {
6
+ test("includes the senior-advisor framing", () => {
7
+ const prompt = buildAdvisorSystem(null);
8
+ expect(prompt).toContain("senior advisor");
9
+ });
10
+
11
+ test("embeds the parent prompt inside <agent_system_prompt> when provided", () => {
12
+ const prompt = buildAdvisorSystem("You are a coding agent.");
13
+ expect(prompt).toContain(
14
+ "<agent_system_prompt>\nYou are a coding agent.\n</agent_system_prompt>",
15
+ );
16
+ });
17
+
18
+ test("omits the <agent_system_prompt> block when no parent prompt is given", () => {
19
+ const prompt = buildAdvisorSystem(null);
20
+ expect(prompt).not.toContain("<agent_system_prompt>");
21
+ });
22
+ });
23
+
24
+ describe("advisorRequestText", () => {
25
+ test("is non-empty and asks for focused strategic guidance", () => {
26
+ const text = advisorRequestText();
27
+ expect(text.length).toBeGreaterThan(0);
28
+ expect(text).toContain("focused strategic guidance");
29
+ });
30
+
31
+ test("imposes no length cap", () => {
32
+ // The request must not constrain how much the advisor writes.
33
+ expect(advisorRequestText()).not.toContain("words");
34
+ });
35
+ });
@@ -1,11 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import type { ContentBlock, Message } from "../../../../providers/types.js";
4
- import { toAdvisorMessages } from "../transcript.js";
3
+ import type { ContentBlock, Message } from "../../providers/types.js";
4
+ import { sanitizeConsultTranscript } from "../consult-transcript.js";
5
5
 
6
6
  const text = (t: string): ContentBlock => ({ type: "text", text: t });
7
7
 
8
- describe("toAdvisorMessages", () => {
8
+ describe("sanitizeConsultTranscript", () => {
9
9
  test("drops thinking and redacted-thinking blocks", () => {
10
10
  const messages: Message[] = [
11
11
  { role: "user", content: [text("do the task")] },
@@ -18,12 +18,34 @@ describe("toAdvisorMessages", () => {
18
18
  ],
19
19
  },
20
20
  ];
21
- const out = toAdvisorMessages(messages);
21
+ const out = sanitizeConsultTranscript(messages);
22
22
  expect(out).toHaveLength(2);
23
23
  expect(out[1].content).toEqual([text("here is my answer")]);
24
24
  });
25
25
 
26
- test("strips the pending advisor tool_use from the final assistant turn", () => {
26
+ test("drops a file block", () => {
27
+ const messages: Message[] = [
28
+ {
29
+ role: "user",
30
+ content: [
31
+ text("read this"),
32
+ {
33
+ type: "file",
34
+ source: {
35
+ type: "base64",
36
+ media_type: "application/pdf",
37
+ data: "z",
38
+ filename: "f.pdf",
39
+ },
40
+ },
41
+ ],
42
+ },
43
+ ];
44
+ const out = sanitizeConsultTranscript(messages);
45
+ expect(out[0].content).toEqual([text("read this")]);
46
+ });
47
+
48
+ test("strips the pending tool_use from the final assistant turn", () => {
27
49
  const messages: Message[] = [
28
50
  { role: "user", content: [text("task")] },
29
51
  {
@@ -34,7 +56,7 @@ describe("toAdvisorMessages", () => {
34
56
  ],
35
57
  },
36
58
  ];
37
- const out = toAdvisorMessages(messages);
59
+ const out = sanitizeConsultTranscript(messages);
38
60
  expect(out[1].content).toEqual([text("let me consult the advisor")]);
39
61
  });
40
62
 
@@ -51,7 +73,7 @@ describe("toAdvisorMessages", () => {
51
73
  },
52
74
  { role: "assistant", content: [text("done")] },
53
75
  ];
54
- const out = toAdvisorMessages(messages);
76
+ const out = sanitizeConsultTranscript(messages);
55
77
  expect(out[1].content[0]).toEqual({
56
78
  type: "tool_use",
57
79
  id: "a",
@@ -87,7 +109,7 @@ describe("toAdvisorMessages", () => {
87
109
  },
88
110
  { role: "assistant", content: [text("found it; here is the answer")] },
89
111
  ];
90
- const out = toAdvisorMessages(messages);
112
+ const out = sanitizeConsultTranscript(messages);
91
113
  const flat = out.flatMap((m) => m.content);
92
114
  expect(flat.some((b) => b.type === "server_tool_use")).toBe(false);
93
115
  expect(flat.some((b) => b.type === "web_search_tool_result")).toBe(false);
@@ -103,7 +125,7 @@ describe("toAdvisorMessages", () => {
103
125
  const messages: Message[] = [
104
126
  { role: "user", content: [text("look at this"), img] },
105
127
  ];
106
- const out = toAdvisorMessages(messages);
128
+ const out = sanitizeConsultTranscript(messages);
107
129
  expect(out[0].content).toEqual([text("look at this"), img]);
108
130
  });
109
131
 
@@ -136,7 +158,7 @@ describe("toAdvisorMessages", () => {
136
158
  ],
137
159
  },
138
160
  ];
139
- const out = toAdvisorMessages(messages);
161
+ const out = sanitizeConsultTranscript(messages);
140
162
  expect(out[0].content[0]).toEqual({
141
163
  type: "tool_result",
142
164
  tool_use_id: "a",
@@ -144,4 +166,19 @@ describe("toAdvisorMessages", () => {
144
166
  contentBlocks: [img],
145
167
  });
146
168
  });
169
+
170
+ test("drops messages that are empty after sanitization", () => {
171
+ const messages: Message[] = [
172
+ { role: "user", content: [text("task")] },
173
+ {
174
+ role: "assistant",
175
+ content: [{ type: "thinking", thinking: "secret", signature: "sig" }],
176
+ },
177
+ { role: "assistant", content: [text("answer")] },
178
+ ];
179
+ const out = sanitizeConsultTranscript(messages);
180
+ expect(out).toHaveLength(2);
181
+ expect(out[0].content).toEqual([text("task")]);
182
+ expect(out[1].content).toEqual([text("answer")]);
183
+ });
147
184
  });
@@ -1,47 +1,18 @@
1
1
  /**
2
- * Prompt fragments for the advisor plugin:
3
- * - `appendSteering` / `stripSteering` — the executor-facing steering block the
4
- * `pre-model-call` hook injects so the model reaches for the advisor tool at
5
- * the right times.
6
- * - `buildAdvisorSystem` / `advisorRequestText` — the advisor-facing framing for
7
- * the consult itself.
2
+ * Advice-framing prompt fragments for the advisor consult:
3
+ * - `buildAdvisorSystem` — the advisor-facing system prompt; frames the role and,
4
+ * for context, embeds the executor's own system prompt.
5
+ * - `advisorRequestText` — the final user turn appended to the transcript asking
6
+ * for guidance.
8
7
  */
9
8
 
10
- /** Idempotency marker; the steering block is appended at the end of the prompt. */
11
- export const STEERING_MARKER = "<!-- advisor:steering -->";
12
-
13
- const STEERING_BODY = `You have an \`advisor\` tool backed by a stronger reviewer model. It takes NO parameters — calling it forwards your entire conversation automatically (the task, every tool call, every result). Call advisor BEFORE you start building or implementing: once you understand what's being asked, consult it to shape the plan — it can lay out the plan when you don't have one yet, or pressure-test and sharpen a plan you've already drafted. Orient yourself first (read the relevant files, understand the task), then call advisor before you commit to an approach and start producing work. Also call it when you get stuck, when you're weighing a change in direction, and once before you declare the task done. Give its guidance serious weight; only override it when primary-source evidence contradicts a specific claim, and say so when you do.`;
14
-
15
- const ADVISOR_STEERING = `${STEERING_MARKER}\n${STEERING_BODY}`;
16
-
17
- /** Append the steering block to the executor's system prompt (idempotent). */
18
- export function appendSteering(systemPrompt: string | null): string | null {
19
- if (systemPrompt === null) return null;
20
- if (systemPrompt.includes(STEERING_MARKER)) return systemPrompt;
21
- return `${systemPrompt}\n\n${ADVISOR_STEERING}`;
22
- }
23
-
24
- /** Remove a previously-appended steering block, recovering the original prompt. */
25
- export function stripSteering(systemPrompt: string | null): string | null {
26
- if (systemPrompt === null) return null;
27
- const idx = systemPrompt.indexOf(STEERING_MARKER);
28
- if (idx === -1) return systemPrompt;
29
- return systemPrompt.slice(0, idx).trimEnd();
30
- }
31
-
32
9
  /**
33
10
  * System prompt for the advisor sub-call. Frames the advisor's role and, for
34
11
  * context, quotes the executor's own system prompt (as the advisor tool does —
35
12
  * the advisor sees the system prompt as context about the executor's task).
36
- *
37
- * `runtimeContext`, when present, carries the agent's situational context that
38
- * lives outside its system prompt — available tools and skills, workspace /
39
- * project context, and recalled memory (see `buildAdvisorContext`) — so the
40
- * advisor can ground its recommendations in what the agent can actually do.
41
13
  */
42
14
  export function buildAdvisorSystem(
43
15
  originalSystemPrompt: string | null,
44
- runtimeContext?: string | null,
45
16
  ): string {
46
17
  const base = `You are a senior advisor consulted by another AI agent working on a task — most often at the planning stage, before it starts building, but sometimes partway through. The entire conversation above is the agent's working context: its task or goal, every tool call it has made, and every result it has seen. The agent has paused to consult you because you bring a second, independent perspective it cannot get from inside its own reasoning loop. Your job is to maximize its odds of completing the task correctly and efficiently.
47
18
 
@@ -64,16 +35,23 @@ Write as much as the guidance genuinely needs, and no more.`;
64
35
  if (originalSystemPrompt) {
65
36
  prompt += `\n\nFor context, the agent is operating under this system prompt:\n<agent_system_prompt>\n${originalSystemPrompt}\n</agent_system_prompt>`;
66
37
  }
67
- if (runtimeContext) {
68
- prompt += `\n\nThe agent's runtime context — the tools and skills available to it, the loaded workspace/project context, and relevant memory — follows. Ground your recommendations in what the agent can actually do and what is around it; reference specific tools, skills, files, or memory where relevant.\n<agent_runtime_context>\n${runtimeContext}\n</agent_runtime_context>`;
69
- }
70
38
  return prompt;
71
39
  }
72
40
 
73
41
  /**
74
42
  * The final user turn appended to the transcript for the advisor sub-call. Asks
75
43
  * for guidance; imposes no length limit — the advisor decides how much to say.
44
+ *
45
+ * `agentRequest` is the executing agent's own `objective` from the
46
+ * `subagent_spawn` call — the agent's framing of what it wants weighed in on.
47
+ * It is included verbatim because (a) the agent naturally states the task there,
48
+ * and (b) the inherited transcript can be thin (e.g. a wake turn whose task
49
+ * lives in memory rather than a user message), so the request text is often the
50
+ * advisor's clearest signal of what is actually being asked.
76
51
  */
77
- export function advisorRequestText(): string {
78
- return `Review the conversation above — the task, the tool calls, and their results — and give focused strategic guidance on how to proceed.`;
52
+ export function advisorRequestText(agentRequest?: string): string {
53
+ const base = `Review the conversation above — the task, the tool calls, and their results — and give focused strategic guidance on how to proceed.`;
54
+ const trimmed = agentRequest?.trim();
55
+ if (!trimmed) return base;
56
+ return `${base}\n\nThe agent described what it wants your input on:\n<agent_request>\n${trimmed}\n</agent_request>\nTreat this as the agent's framing of the task. If it conflicts with the transcript above, say so; if the transcript is sparse, rely on it.`;
79
57
  }
@@ -1,6 +1,10 @@
1
1
  /**
2
- * Convert a captured executor transcript into the message list sent to the
3
- * advisor sub-call.
2
+ * Sanitize an inherited parent transcript before it is injected into a
3
+ * tool-less advisor consult subagent.
4
+ *
5
+ * `Conversation.injectInheritedContext` injects the parent's messages VERBATIM
6
+ * with no sanitization, so the advisor path must run this over the parent
7
+ * messages before they are injected.
4
8
  *
5
9
  * Strips blocks the advisor shouldn't (or can't) replay:
6
10
  * - thinking / redacted-thinking (the advisor tool drops thinking),
@@ -16,13 +20,17 @@
16
20
  * Visual tasks depend on it; the advisor profile is expected to be vision-capable.
17
21
  *
18
22
  * It also strips the *pending* client tool calls from the final assistant turn:
19
- * at capture time (a `post-model-call` before tools run) the last assistant
20
- * message carries the `advisor` tool_use with no matching `tool_result` yet, so
21
- * sending it would be a dangling call. Earlier, completed `tool_use` /
22
- * `tool_result` pairs are preserved intact.
23
+ * at capture time the last assistant message can carry a `tool_use` with no
24
+ * matching `tool_result` yet, so sending it would be a dangling call. Earlier,
25
+ * completed `tool_use` / `tool_result` pairs are preserved intact.
26
+ *
27
+ * In-flight-turn parity — ensuring the assistant's just-written plan from the
28
+ * current turn is included while its dangling tool_use is stripped — is handled
29
+ * by the consumer (the wiring that calls this before injection); this function
30
+ * only strips a dangling `tool_use` IF it appears on the final assistant turn.
23
31
  */
24
32
 
25
- import type { ContentBlock, Message } from "../../../providers/types.js";
33
+ import type { ContentBlock, Message } from "../providers/types.js";
26
34
 
27
35
  /** Drop disallowed blocks; recursively sanitize tool_result content. `null` = drop. */
28
36
  function sanitize(block: ContentBlock): ContentBlock | null {
@@ -51,7 +59,9 @@ function sanitize(block: ContentBlock): ContentBlock | null {
51
59
  }
52
60
  }
53
61
 
54
- export function toAdvisorMessages(messages: ReadonlyArray<Message>): Message[] {
62
+ export function sanitizeConsultTranscript(
63
+ messages: ReadonlyArray<Message>,
64
+ ): Message[] {
55
65
  const out: Message[] = [];
56
66
  const lastIndex = messages.length - 1;
57
67
 
@@ -1,4 +1,4 @@
1
- export { mergeSkillIds } from "./manager.js";
1
+ export { mergeSkillIds, SubagentAbortedError } from "./manager.js";
2
2
  export type { SubagentRole } from "./types.js";
3
3
  export { SUBAGENT_ROLE_REGISTRY, TERMINAL_STATUSES } from "./types.js";
4
4