@openscout/protocol 0.2.61 → 0.2.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/actors.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ActorKind, AdvertiseScope, AgentState, MetadataMap, ScoutId } from "./common.js";
2
2
  export type AgentClass = "general" | "builder" | "reviewer" | "researcher" | "operator" | "bridge" | "system";
3
3
  export type AgentCapability = "chat" | "invoke" | "deliver" | "speak" | "listen" | "bridge" | "summarize" | "review" | "execute";
4
- export type AgentHarness = "codex" | "claude" | "native" | "worker" | "bridge" | "http";
4
+ export type AgentHarness = "codex" | "claude" | "native" | "worker" | "bridge" | "http" | "pi";
5
5
  export type WakePolicy = "manual" | "on_demand" | "keep_warm";
6
6
  export interface ActorIdentity {
7
7
  id: ScoutId;
@@ -1,5 +1,5 @@
1
1
  import type { ScoutId } from "./common.js";
2
- export type AgentIdentityDimension = "workspace" | "profile" | "harness" | "node";
2
+ export type AgentIdentityDimension = "workspace" | "profile" | "harness" | "model" | "node";
3
3
  export interface AgentIdentity {
4
4
  raw: string;
5
5
  label: string;
@@ -8,6 +8,7 @@ export interface AgentIdentity {
8
8
  workspaceQualifier?: string;
9
9
  profile?: string;
10
10
  harness?: string;
11
+ model?: string;
11
12
  }
12
13
  export interface AgentIdentityCandidate {
13
14
  agentId: ScoutId;
@@ -16,6 +17,7 @@ export interface AgentIdentityCandidate {
16
17
  workspaceQualifier?: string;
17
18
  profile?: string;
18
19
  harness?: string;
20
+ model?: string;
19
21
  aliases?: string[];
20
22
  }
21
23
  export interface AgentIdentityInput {
@@ -24,6 +26,7 @@ export interface AgentIdentityInput {
24
26
  workspaceQualifier?: string;
25
27
  profile?: string;
26
28
  harness?: string;
29
+ model?: string;
27
30
  }
28
31
  export interface AgentIdentityAlias {
29
32
  alias: string;
@@ -32,6 +35,7 @@ export interface AgentIdentityAlias {
32
35
  export declare function normalizeAgentIdentitySegment(value: string): string;
33
36
  export declare const normalizeAgentSelectorSegment: typeof normalizeAgentIdentitySegment;
34
37
  export declare const SCOUT_DISPATCHER_AGENT_ID = "scout";
38
+ export declare const OPENSCOUT_COORDINATOR_AGENT_ID = "openscout";
35
39
  export declare const BUILT_IN_AGENT_DEFINITION_IDS: ReadonlySet<string>;
36
40
  export declare function isBuiltInAgentDefinitionId(value: string | null | undefined): boolean;
37
41
  export declare function isReservedAgentDefinitionId(value: string | null | undefined): boolean;
@@ -6,6 +6,7 @@ const DIMENSION_ALIASES = {
6
6
  persona: "profile",
7
7
  harness: "harness",
8
8
  runtime: "harness",
9
+ model: "model",
9
10
  node: "node",
10
11
  host: "node",
11
12
  };
@@ -25,6 +26,7 @@ export function normalizeAgentIdentitySegment(value) {
25
26
  }
26
27
  export const normalizeAgentSelectorSegment = normalizeAgentIdentitySegment;
27
28
  export const SCOUT_DISPATCHER_AGENT_ID = "scout";
29
+ export const OPENSCOUT_COORDINATOR_AGENT_ID = "openscout";
28
30
  export const BUILT_IN_AGENT_DEFINITION_IDS = new Set([
29
31
  SCOUT_DISPATCHER_AGENT_ID,
30
32
  "builder",
@@ -54,6 +56,7 @@ function canonicalizeIdentity(input) {
54
56
  : undefined;
55
57
  const profile = input.profile ? normalizeAgentIdentitySegment(input.profile) : undefined;
56
58
  const harness = input.harness ? normalizeAgentIdentitySegment(input.harness) : undefined;
59
+ const model = input.model ? normalizeAgentIdentitySegment(input.model) : undefined;
57
60
  const nodeQualifier = input.nodeQualifier
58
61
  ? normalizeAgentIdentitySegment(input.nodeQualifier)
59
62
  : undefined;
@@ -65,6 +68,7 @@ function canonicalizeIdentity(input) {
65
68
  ...(workspaceQualifier ? { workspaceQualifier } : {}),
66
69
  ...(profile ? { profile } : {}),
67
70
  ...(harness ? { harness } : {}),
71
+ ...(model ? { model } : {}),
68
72
  ...(nodeQualifier ? { nodeQualifier } : {}),
69
73
  };
70
74
  }
@@ -96,6 +100,9 @@ function parseSegmentedIdentity(raw) {
96
100
  else if (key === "harness") {
97
101
  next.harness = value;
98
102
  }
103
+ else if (key === "model") {
104
+ next.model = value;
105
+ }
99
106
  else if (key === "node") {
100
107
  next.nodeQualifier = value;
101
108
  }
@@ -127,14 +134,60 @@ function parseSegmentedIdentity(raw) {
127
134
  }
128
135
  return constructAgentIdentity(next, { raw });
129
136
  }
137
+ function parseShorthandIdentity(raw) {
138
+ const hashIndex = raw.indexOf("#");
139
+ const questionIndex = raw.indexOf("?");
140
+ if (hashIndex === -1 && questionIndex === -1) {
141
+ return null;
142
+ }
143
+ if (raw.indexOf("#", hashIndex + 1) !== -1 || raw.indexOf("?", questionIndex + 1) !== -1) {
144
+ return null;
145
+ }
146
+ if (hashIndex !== -1 && questionIndex !== -1 && questionIndex < hashIndex) {
147
+ return null;
148
+ }
149
+ const baseEnd = Math.min(...[hashIndex, questionIndex].filter((index) => index >= 0));
150
+ const base = raw.slice(0, baseEnd);
151
+ if (!base) {
152
+ return null;
153
+ }
154
+ const parsedBase = parseSegmentedIdentity(base);
155
+ if (!parsedBase) {
156
+ return null;
157
+ }
158
+ const harnessRaw = hashIndex >= 0
159
+ ? raw.slice(hashIndex + 1, questionIndex >= 0 ? questionIndex : raw.length)
160
+ : "";
161
+ const modelRaw = questionIndex >= 0 ? raw.slice(questionIndex + 1) : "";
162
+ if ((hashIndex >= 0 && !harnessRaw) || (questionIndex >= 0 && !modelRaw)) {
163
+ return null;
164
+ }
165
+ if (harnessRaw.includes("#") || harnessRaw.includes("?") || modelRaw.includes("#") || modelRaw.includes("?")) {
166
+ return null;
167
+ }
168
+ if ((parsedBase.harness && harnessRaw) || (parsedBase.model && modelRaw)) {
169
+ return null;
170
+ }
171
+ return constructAgentIdentity({
172
+ definitionId: parsedBase.definitionId,
173
+ nodeQualifier: parsedBase.nodeQualifier,
174
+ workspaceQualifier: parsedBase.workspaceQualifier,
175
+ profile: parsedBase.profile,
176
+ harness: harnessRaw || parsedBase.harness,
177
+ model: modelRaw || parsedBase.model,
178
+ }, { raw });
179
+ }
130
180
  export function parseAgentIdentity(value) {
131
181
  const raw = trimIdentityPrefix(value);
132
182
  if (!raw) {
133
183
  return null;
134
184
  }
135
- if (raw.includes("@") || raw.includes("#")) {
185
+ if (raw.includes("@")) {
136
186
  return null;
137
187
  }
188
+ if (raw.includes("#") || raw.includes("?")) {
189
+ return parseShorthandIdentity(raw);
190
+ }
138
191
  return parseSegmentedIdentity(raw);
139
192
  }
140
193
  export function formatAgentIdentity(input, options = {}) {
@@ -153,6 +206,9 @@ export function formatAgentIdentity(input, options = {}) {
153
206
  if (canonical.harness) {
154
207
  segments.push(`harness:${canonical.harness}`);
155
208
  }
209
+ if (canonical.model) {
210
+ segments.push(`model:${canonical.model}`);
211
+ }
156
212
  if (canonical.nodeQualifier) {
157
213
  segments.push(`node:${canonical.nodeQualifier}`);
158
214
  }
@@ -171,13 +227,14 @@ export function constructAgentIdentity(input, options = {}) {
171
227
  ...(canonical.workspaceQualifier ? { workspaceQualifier: canonical.workspaceQualifier } : {}),
172
228
  ...(canonical.profile ? { profile: canonical.profile } : {}),
173
229
  ...(canonical.harness ? { harness: canonical.harness } : {}),
230
+ ...(canonical.model ? { model: canonical.model } : {}),
174
231
  };
175
232
  }
176
233
  export function extractAgentIdentities(text) {
177
234
  return extractAgentMentions(text).parsed;
178
235
  }
179
236
  export function extractAgentMentions(text) {
180
- const matches = Array.from(text.matchAll(/(^|\s)@([a-z0-9][a-z0-9._/:-]*)/gi));
237
+ const matches = Array.from(text.matchAll(/(^|[\s([{'"`])@([a-z0-9][a-z0-9._/:-]*(?:#[a-z0-9][a-z0-9._/:-]*)?(?:\?[a-z0-9][a-z0-9._/:-]*)?)(?=$|[\s)\]}",.!?:;'"`])/gi));
181
238
  const identities = new Map();
182
239
  const unparsed = [];
183
240
  for (const match of matches) {
@@ -202,6 +259,7 @@ function candidateAliases(candidate) {
202
259
  workspaceQualifier: candidate.workspaceQualifier,
203
260
  profile: candidate.profile,
204
261
  harness: candidate.harness,
262
+ model: candidate.model,
205
263
  });
206
264
  if (!canonical) {
207
265
  return [];
@@ -218,6 +276,9 @@ function candidateAliases(candidate) {
218
276
  canonical.harness
219
277
  ? formatAgentIdentity({ definitionId: canonical.definitionId, harness: canonical.harness })
220
278
  : "",
279
+ canonical.model
280
+ ? formatAgentIdentity({ definitionId: canonical.definitionId, model: canonical.model })
281
+ : "",
221
282
  canonical.nodeQualifier
222
283
  ? formatAgentIdentity({ definitionId: canonical.definitionId, nodeQualifier: canonical.nodeQualifier })
223
284
  : "",
@@ -226,7 +287,11 @@ function candidateAliases(candidate) {
226
287
  return Array.from(new Set(aliases.map((alias) => trimIdentityPrefix(alias)).filter(Boolean)));
227
288
  }
228
289
  function explicitCandidateAliases(candidate) {
229
- return Array.from(new Set((candidate.aliases ?? [])
290
+ const canonical = candidateCanonicalIdentity(candidate);
291
+ const implicitAliases = canonical?.definitionId === OPENSCOUT_COORDINATOR_AGENT_ID
292
+ ? [SCOUT_DISPATCHER_AGENT_ID]
293
+ : [];
294
+ return Array.from(new Set([...(candidate.aliases ?? []), ...implicitAliases]
230
295
  .map((alias) => normalizeAgentIdentitySegment(trimIdentityPrefix(alias)))
231
296
  .filter(Boolean)));
232
297
  }
@@ -251,6 +316,7 @@ function candidateCanonicalIdentity(candidate) {
251
316
  workspaceQualifier: candidate.workspaceQualifier,
252
317
  profile: candidate.profile,
253
318
  harness: candidate.harness,
319
+ model: candidate.model,
254
320
  });
255
321
  }
256
322
  function candidateDimensionValue(candidate, dimension) {
@@ -264,8 +330,42 @@ function candidateDimensionValue(candidate, dimension) {
264
330
  return canonical.profile;
265
331
  if (dimension === "harness")
266
332
  return canonical.harness;
333
+ if (dimension === "model")
334
+ return canonical.model;
267
335
  return canonical.nodeQualifier;
268
336
  }
337
+ function modelAliasKeys(value) {
338
+ const normalized = value ? normalizeAgentIdentitySegment(value) : "";
339
+ if (!normalized) {
340
+ return [];
341
+ }
342
+ const parts = normalized.split("-").filter(Boolean);
343
+ const aliases = new Set([normalized]);
344
+ if (parts.length > 1 && ["claude", "gpt", "openai", "anthropic"].includes(parts[0])) {
345
+ aliases.add(parts.slice(1).join("-"));
346
+ }
347
+ for (const family of ["sonnet", "opus", "haiku", "mini", "nano", "pro"]) {
348
+ const index = parts.indexOf(family);
349
+ if (index >= 0) {
350
+ aliases.add(family);
351
+ aliases.add(parts.slice(index).join("-"));
352
+ }
353
+ }
354
+ return Array.from(aliases).filter(Boolean);
355
+ }
356
+ function modelMatches(identityModel, candidate) {
357
+ if (!identityModel) {
358
+ return true;
359
+ }
360
+ return modelAliasKeys(candidate.model).includes(identityModel);
361
+ }
362
+ function definitionMatchesIdentity(identityDefinitionId, candidateDefinitionId) {
363
+ if (identityDefinitionId === candidateDefinitionId) {
364
+ return true;
365
+ }
366
+ return identityDefinitionId === SCOUT_DISPATCHER_AGENT_ID
367
+ && candidateDefinitionId === OPENSCOUT_COORDINATOR_AGENT_ID;
368
+ }
269
369
  export function agentIdentityMatches(identity, candidate) {
270
370
  const aliasKeys = identityAliasKeys(identity);
271
371
  const candidateAliasKeys = explicitCandidateAliases(candidate).map(canonicalizeAliasValue);
@@ -278,8 +378,9 @@ export function agentIdentityMatches(identity, candidate) {
278
378
  workspaceQualifier: candidate.workspaceQualifier,
279
379
  profile: candidate.profile,
280
380
  harness: candidate.harness,
381
+ model: candidate.model,
281
382
  });
282
- if (!canonical || identity.definitionId !== canonical.definitionId) {
383
+ if (!canonical || !definitionMatchesIdentity(identity.definitionId, canonical.definitionId)) {
283
384
  return false;
284
385
  }
285
386
  if (identity.nodeQualifier && identity.nodeQualifier !== canonical.nodeQualifier) {
@@ -294,6 +395,9 @@ export function agentIdentityMatches(identity, candidate) {
294
395
  if (identity.harness && identity.harness !== canonical.harness) {
295
396
  return candidateAliases(candidate).includes(trimIdentityPrefix(identity.label));
296
397
  }
398
+ if (identity.model && !modelMatches(identity.model, candidate)) {
399
+ return candidateAliases(candidate).includes(trimIdentityPrefix(identity.label));
400
+ }
297
401
  return true;
298
402
  }
299
403
  export function resolveAgentIdentity(identity, candidates) {
@@ -319,7 +423,11 @@ export function diagnoseAgentIdentity(identity, candidates) {
319
423
  if (matches.length === 0) {
320
424
  return { kind: "unknown" };
321
425
  }
322
- if (!identity.nodeQualifier && !identity.workspaceQualifier && !identity.profile && !identity.harness) {
426
+ if (!identity.nodeQualifier
427
+ && !identity.workspaceQualifier
428
+ && !identity.profile
429
+ && !identity.harness
430
+ && !identity.model) {
323
431
  const exactIdMatch = matches.find((candidate) => normalizeAgentIdentitySegment(candidate.agentId) === identity.definitionId);
324
432
  if (exactIdMatch) {
325
433
  return { kind: "resolved", match: exactIdMatch };
@@ -366,6 +474,7 @@ function buildIdentitySubset(candidate, dimensions) {
366
474
  : {}),
367
475
  ...(dimensions.includes("profile") && canonical.profile ? { profile: canonical.profile } : {}),
368
476
  ...(dimensions.includes("harness") && canonical.harness ? { harness: canonical.harness } : {}),
477
+ ...(dimensions.includes("model") && canonical.model ? { model: canonical.model } : {}),
369
478
  ...(dimensions.includes("node") && canonical.nodeQualifier ? { nodeQualifier: canonical.nodeQualifier } : {}),
370
479
  });
371
480
  }
@@ -389,7 +498,7 @@ export function formatMinimalAgentIdentity(candidate, candidates, options = {})
389
498
  if (peers.length <= 1) {
390
499
  return formatAgentIdentity({ definitionId: canonical.definitionId }, options);
391
500
  }
392
- const orderedDimensions = ["workspace", "profile", "harness", "node"];
501
+ const orderedDimensions = ["workspace", "profile", "harness", "model", "node"];
393
502
  const selectedDimensions = [];
394
503
  let remainingPeers = peers.filter((peer) => peer !== candidate);
395
504
  for (const dimension of orderedDimensions) {
package/dist/events.d.ts CHANGED
@@ -27,6 +27,9 @@ export type AgentRegisteredEvent = ControlEventBase<"agent.registered", {
27
27
  export type AgentEndpointUpsertedEvent = ControlEventBase<"agent.endpoint.upserted", {
28
28
  endpoint: AgentEndpoint;
29
29
  }>;
30
+ export type AgentEndpointDeletedEvent = ControlEventBase<"agent.endpoint.deleted", {
31
+ endpointId: string;
32
+ }>;
30
33
  export type ConversationUpsertedEvent = ControlEventBase<"conversation.upserted", {
31
34
  conversation: ConversationDefinition;
32
35
  }>;
@@ -61,4 +64,4 @@ export type CollaborationEventAppendedEvent = ControlEventBase<"collaboration.ev
61
64
  export type ScoutDispatchedEvent = ControlEventBase<"scout.dispatched", {
62
65
  dispatch: ScoutDispatchRecord;
63
66
  }>;
64
- export type ControlEvent = NodeUpsertedEvent | ActorRegisteredEvent | AgentRegisteredEvent | AgentEndpointUpsertedEvent | ConversationUpsertedEvent | BindingUpsertedEvent | MessagePostedEvent | InvocationRequestedEvent | FlightUpdatedEvent | DeliveryPlannedEvent | DeliveryAttemptedEvent | DeliveryStateChangedEvent | CollaborationUpsertedEvent | CollaborationEventAppendedEvent | ScoutDispatchedEvent;
67
+ export type ControlEvent = NodeUpsertedEvent | ActorRegisteredEvent | AgentRegisteredEvent | AgentEndpointUpsertedEvent | AgentEndpointDeletedEvent | ConversationUpsertedEvent | BindingUpsertedEvent | MessagePostedEvent | InvocationRequestedEvent | FlightUpdatedEvent | DeliveryPlannedEvent | DeliveryAttemptedEvent | DeliveryStateChangedEvent | CollaborationUpsertedEvent | CollaborationEventAppendedEvent | ScoutDispatchedEvent;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./collaboration.js";
11
11
  export * from "./messages.js";
12
12
  export * from "./invocations.js";
13
13
  export * from "./scout-dispatch.js";
14
+ export * from "./scout-delivery.js";
14
15
  export * from "./deliveries.js";
15
16
  export * from "./transports.js";
16
17
  export * from "./events.js";
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export * from "./collaboration.js";
11
11
  export * from "./messages.js";
12
12
  export * from "./invocations.js";
13
13
  export * from "./scout-dispatch.js";
14
+ export * from "./scout-delivery.js";
14
15
  export * from "./deliveries.js";
15
16
  export * from "./transports.js";
16
17
  export * from "./events.js";
@@ -0,0 +1,73 @@
1
+ import type { ConversationDefinition } from "./conversations.js";
2
+ import type { MetadataMap, ScoutId } from "./common.js";
3
+ import type { FlightRecord, InvocationExecutionPreference } from "./invocations.js";
4
+ import type { MessageRecord } from "./messages.js";
5
+ import type { ScoutCallerContext, ScoutDispatchRecord, ScoutRoutePolicy, ScoutRouteTarget } from "./scout-dispatch.js";
6
+ export type ScoutDeliverIntent = "tell" | "consult";
7
+ export type ScoutDeliverRouteKind = "dm" | "channel" | "broadcast";
8
+ export type ScoutDeliveryRemediationKind = "choose_target" | "register_target" | "wake_target" | "retry_later";
9
+ export type ScoutDeliverRejectReason = "unknown_target" | "ambiguous_target" | "invalid_target" | "missing_target";
10
+ export interface ScoutDeliverRequest {
11
+ id?: ScoutId;
12
+ caller?: ScoutCallerContext;
13
+ requesterId?: ScoutId;
14
+ requesterNodeId?: ScoutId;
15
+ body: string;
16
+ intent: ScoutDeliverIntent;
17
+ target?: ScoutRouteTarget;
18
+ targetLabel?: string;
19
+ targetAgentId?: ScoutId;
20
+ routePolicy?: ScoutRoutePolicy;
21
+ channel?: string;
22
+ replyToMessageId?: ScoutId;
23
+ speechText?: string;
24
+ ensureAwake?: boolean;
25
+ execution?: InvocationExecutionPreference;
26
+ createdAt?: number;
27
+ collaborationRecordId?: ScoutId;
28
+ messageMetadata?: MetadataMap;
29
+ invocationMetadata?: MetadataMap;
30
+ }
31
+ export interface ScoutDeliveryReceipt {
32
+ requestId: ScoutId;
33
+ routeKind: ScoutDeliverRouteKind;
34
+ requesterId: ScoutId;
35
+ requesterNodeId: ScoutId;
36
+ targetAgentId?: ScoutId;
37
+ targetLabel?: string;
38
+ conversationId: ScoutId;
39
+ messageId: ScoutId;
40
+ flightId?: ScoutId;
41
+ acceptedAt: number;
42
+ }
43
+ export interface ScoutDeliveryRemediationAction {
44
+ kind: ScoutDeliveryRemediationKind;
45
+ detail: string;
46
+ targetAgentId?: ScoutId;
47
+ targetLabel?: string;
48
+ dispatchId?: ScoutId;
49
+ }
50
+ export interface ScoutDeliverAcceptedResponse {
51
+ kind: "delivery";
52
+ accepted: true;
53
+ routeKind: ScoutDeliverRouteKind;
54
+ receipt: ScoutDeliveryReceipt;
55
+ conversation: ConversationDefinition;
56
+ message: MessageRecord;
57
+ targetAgentId?: ScoutId;
58
+ flight?: FlightRecord;
59
+ }
60
+ export interface ScoutDeliverQuestionResponse {
61
+ kind: "question";
62
+ accepted: false;
63
+ question: ScoutDispatchRecord;
64
+ remediation?: ScoutDeliveryRemediationAction;
65
+ }
66
+ export interface ScoutDeliverRejectedResponse {
67
+ kind: "rejected";
68
+ accepted: false;
69
+ reason: ScoutDeliverRejectReason;
70
+ rejection: ScoutDispatchRecord;
71
+ remediation?: ScoutDeliveryRemediationAction;
72
+ }
73
+ export type ScoutDeliverResponse = ScoutDeliverAcceptedResponse | ScoutDeliverQuestionResponse | ScoutDeliverRejectedResponse;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,9 +1,43 @@
1
- import type { AdvertiseScope, DeliveryTransport, ScoutId } from "./common.js";
2
- export type ScoutDispatchKind = "ambiguous" | "unknown" | "unparseable";
1
+ import type { WakePolicy } from "./actors.js";
2
+ import type { AdvertiseScope, DeliveryTransport, MetadataMap, ScoutId } from "./common.js";
3
+ export type ScoutDispatchKind = "ambiguous" | "unknown" | "unparseable" | "unavailable";
3
4
  export type ScoutCandidateEndpointState = "online" | "offline" | "unknown";
5
+ export type ScoutRouteTargetKind = "agent_id" | "agent_label" | "channel" | "broadcast";
6
+ export type ScoutRouteAmbiguousPolicy = "reject" | "ask";
7
+ export interface ScoutCallerContext {
8
+ actorId?: ScoutId;
9
+ nodeId?: ScoutId;
10
+ displayName?: string;
11
+ handle?: string;
12
+ currentDirectory?: string;
13
+ metadata?: MetadataMap;
14
+ }
15
+ export type ScoutRouteTarget = {
16
+ kind: "agent_id";
17
+ agentId: ScoutId;
18
+ value?: string;
19
+ } | {
20
+ kind: "agent_label";
21
+ label: string;
22
+ value?: string;
23
+ } | {
24
+ kind: "channel";
25
+ channel: string;
26
+ value?: string;
27
+ } | {
28
+ kind: "broadcast";
29
+ value?: string;
30
+ };
31
+ export interface ScoutRoutePolicy {
32
+ preferLocalNodeId?: ScoutId;
33
+ ambiguous?: ScoutRouteAmbiguousPolicy;
34
+ allowStaleDirectId?: boolean;
35
+ }
36
+ export type ScoutDispatchUnavailableReason = "manual_wake_required" | "retired" | "stale_registration" | "unknown";
4
37
  export interface ScoutDispatchCandidate {
5
38
  agentId: ScoutId;
6
39
  displayName: string;
40
+ label: string;
7
41
  authorityNodeId?: ScoutId;
8
42
  homeNodeId?: ScoutId;
9
43
  advertiseScope?: AdvertiseScope;
@@ -15,11 +49,22 @@ export interface ScoutDispatchCandidate {
15
49
  endpointState: ScoutCandidateEndpointState;
16
50
  transport?: DeliveryTransport | null;
17
51
  }
52
+ export interface ScoutDispatchUnavailableTarget {
53
+ agentId: ScoutId;
54
+ displayName: string;
55
+ reason: ScoutDispatchUnavailableReason;
56
+ detail: string;
57
+ wakePolicy?: WakePolicy | null;
58
+ endpointState: ScoutCandidateEndpointState;
59
+ transport?: DeliveryTransport | null;
60
+ projectRoot?: string | null;
61
+ }
18
62
  export interface ScoutDispatchEnvelope {
19
63
  kind: ScoutDispatchKind;
20
64
  askedLabel: string;
21
65
  detail: string;
22
66
  candidates: ScoutDispatchCandidate[];
67
+ target?: ScoutDispatchUnavailableTarget;
23
68
  dispatchedAt: number;
24
69
  dispatcherNodeId: ScoutId;
25
70
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openscout/protocol",
3
- "version": "0.2.61",
3
+ "version": "0.2.63",
4
4
  "description": "Typed protocol for the OpenScout local control plane",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./src/index.ts",
9
- "bun": "./dist/index.js",
9
+ "bun": "./src/index.ts",
10
10
  "default": "./dist/index.js"
11
11
  }
12
12
  },
package/src/actors.ts CHANGED
@@ -32,7 +32,8 @@ export type AgentHarness =
32
32
  | "native"
33
33
  | "worker"
34
34
  | "bridge"
35
- | "http";
35
+ | "http"
36
+ | "pi";
36
37
 
37
38
  export type WakePolicy = "manual" | "on_demand" | "keep_warm";
38
39
 
@@ -14,6 +14,7 @@ import {
14
14
  resolveAgentAlias,
15
15
  resolveAgentIdentity,
16
16
  resolveAgentSelector,
17
+ OPENSCOUT_COORDINATOR_AGENT_ID,
17
18
  SCOUT_DISPATCHER_AGENT_ID,
18
19
  } from "./agent-identity.js";
19
20
 
@@ -58,6 +59,23 @@ describe("agent identity grammar", () => {
58
59
  harness: "codex",
59
60
  nodeQualifier: "mini",
60
61
  });
62
+
63
+ expect(parseAgentIdentity("@lattices#codex?5.5")).toEqual({
64
+ raw: "lattices#codex?5.5",
65
+ label: "@lattices.harness:codex.model:5-5",
66
+ definitionId: "lattices",
67
+ harness: "codex",
68
+ model: "5-5",
69
+ });
70
+
71
+ expect(parseAgentIdentity("@lattices.main#claude?sonnet")).toEqual({
72
+ raw: "lattices.main#claude?sonnet",
73
+ label: "@lattices.main.harness:claude.model:sonnet",
74
+ definitionId: "lattices",
75
+ workspaceQualifier: "main",
76
+ harness: "claude",
77
+ model: "sonnet",
78
+ });
61
79
  });
62
80
 
63
81
  test("parses 3-segment positional as definitionId.workspaceQualifier.nodeQualifier", () => {
@@ -72,6 +90,8 @@ describe("agent identity grammar", () => {
72
90
  expect(parseAgentIdentity("")).toBeNull();
73
91
  expect(parseAgentIdentity("@arc.profile:")).toBeNull();
74
92
  expect(parseAgentIdentity("@arc@mini#main")).toBeNull();
93
+ expect(parseAgentIdentity("@arc#")).toBeNull();
94
+ expect(parseAgentIdentity("@arc?sonnet#claude")).toBeNull();
75
95
  expect(parseAgentIdentity("@arc.a.b.c")).toBeNull(); // 3+ positionals rejected
76
96
  });
77
97
 
@@ -81,14 +101,16 @@ describe("agent identity grammar", () => {
81
101
  workspaceQualifier: "Super Refactor",
82
102
  profile: "Dev Browser",
83
103
  harness: "Claude",
104
+ model: "GPT-5.5",
84
105
  nodeQualifier: "Mini.local",
85
106
  })).toEqual({
86
- raw: "arc.super-refactor.profile:dev-browser.harness:claude.node:mini-local",
87
- label: "@arc.super-refactor.profile:dev-browser.harness:claude.node:mini-local",
107
+ raw: "arc.super-refactor.profile:dev-browser.harness:claude.model:gpt-5-5.node:mini-local",
108
+ label: "@arc.super-refactor.profile:dev-browser.harness:claude.model:gpt-5-5.node:mini-local",
88
109
  definitionId: "arc",
89
110
  workspaceQualifier: "super-refactor",
90
111
  profile: "dev-browser",
91
112
  harness: "claude",
113
+ model: "gpt-5-5",
92
114
  nodeQualifier: "mini-local",
93
115
  });
94
116
  });
@@ -99,13 +121,14 @@ describe("agent identity grammar", () => {
99
121
  workspaceQualifier: "main",
100
122
  profile: "dev-browser",
101
123
  harness: "claude",
124
+ model: "sonnet",
102
125
  nodeQualifier: "mini",
103
- })).toBe("@arc.main.profile:dev-browser.harness:claude.node:mini");
126
+ })).toBe("@arc.main.profile:dev-browser.harness:claude.model:sonnet.node:mini");
104
127
  });
105
128
 
106
129
  test("extracts unique identities from text", () => {
107
130
  expect(extractAgentIdentities(
108
- "Compare @arc.main.profile:dev-browser with @hudson, then ping @arc.main.profile:dev-browser.",
131
+ "Compare @arc.main.profile:dev-browser with @hudson, then ping @arc.main.profile:dev-browser and @lattices#codex?5.5.",
109
132
  )).toEqual([
110
133
  {
111
134
  raw: "arc.main.profile:dev-browser",
@@ -119,6 +142,13 @@ describe("agent identity grammar", () => {
119
142
  label: "@hudson",
120
143
  definitionId: "hudson",
121
144
  },
145
+ {
146
+ raw: "lattices#codex?5.5",
147
+ label: "@lattices.harness:codex.model:5-5",
148
+ definitionId: "lattices",
149
+ harness: "codex",
150
+ model: "5-5",
151
+ },
122
152
  ]);
123
153
  });
124
154
 
@@ -202,6 +232,27 @@ describe("agent identity resolution", () => {
202
232
  harness: "codex",
203
233
  nodeQualifier: "backup-mac-mini",
204
234
  },
235
+ {
236
+ agentId: "lattices.codex-55",
237
+ definitionId: "lattices",
238
+ workspaceQualifier: "main",
239
+ harness: "codex",
240
+ model: "gpt-5.5",
241
+ },
242
+ {
243
+ agentId: "lattices.codex-54",
244
+ definitionId: "lattices",
245
+ workspaceQualifier: "main",
246
+ harness: "codex",
247
+ model: "gpt-5.4",
248
+ },
249
+ {
250
+ agentId: "lattices.sonnet",
251
+ definitionId: "lattices",
252
+ workspaceQualifier: "main",
253
+ harness: "claude",
254
+ model: "claude-sonnet-4-6",
255
+ },
205
256
  ];
206
257
 
207
258
  test("prefers explicit default aliases for bare identities", () => {
@@ -240,6 +291,16 @@ describe("agent identity resolution", () => {
240
291
  expect(resolveAgentIdentity(identity!, candidates)).toEqual(candidates[3]);
241
292
  });
242
293
 
294
+ test("resolves shorthand harness and model qualifiers", () => {
295
+ const codex = parseAgentIdentity("@lattices#codex?5.5");
296
+ expect(codex).not.toBeNull();
297
+ expect(resolveAgentIdentity(codex!, candidates)).toEqual(candidates[6]);
298
+
299
+ const sonnet = parseAgentIdentity("@lattices#claude?sonnet");
300
+ expect(sonnet).not.toBeNull();
301
+ expect(resolveAgentIdentity(sonnet!, candidates)).toEqual(candidates[8]);
302
+ });
303
+
243
304
  test("resolves aliases through the explicit alias table", () => {
244
305
  const identity = resolveAgentAlias("@huddy", [
245
306
  {
@@ -278,6 +339,7 @@ describe("agent identity resolution", () => {
278
339
  describe("scout dispatcher reservation", () => {
279
340
  test("reserves the scout definition id", () => {
280
341
  expect(SCOUT_DISPATCHER_AGENT_ID).toBe("scout");
342
+ expect(OPENSCOUT_COORDINATOR_AGENT_ID).toBe("openscout");
281
343
  expect(isReservedAgentDefinitionId("scout")).toBe(true);
282
344
  expect(isReservedAgentDefinitionId("Scout")).toBe(true);
283
345
  expect(isReservedAgentDefinitionId("SCOUT ")).toBe(true);
@@ -287,6 +349,31 @@ describe("scout dispatcher reservation", () => {
287
349
  expect(isReservedAgentDefinitionId(null)).toBe(false);
288
350
  expect(isReservedAgentDefinitionId(undefined)).toBe(false);
289
351
  });
352
+
353
+ test("resolves @scout to the stable OpenScout coordinator", () => {
354
+ const identity = parseAgentIdentity("@scout");
355
+ expect(identity).not.toBeNull();
356
+ const candidates = [
357
+ {
358
+ agentId: "openscout.main.mini",
359
+ definitionId: "openscout",
360
+ nodeQualifier: "mini",
361
+ workspaceQualifier: "main",
362
+ },
363
+ {
364
+ agentId: "ranger.main.mini",
365
+ definitionId: "ranger",
366
+ nodeQualifier: "mini",
367
+ workspaceQualifier: "main",
368
+ },
369
+ ];
370
+ const result = diagnoseAgentIdentity(identity!, candidates);
371
+ expect(result.kind).toBe("resolved");
372
+ if (result.kind === "resolved") {
373
+ expect(result.match.agentId).toBe("openscout.main.mini");
374
+ }
375
+ expect(formatMinimalAgentIdentity(candidates[0]!, candidates)).toBe("@scout");
376
+ });
290
377
  });
291
378
 
292
379
  describe("agent identity diagnosis", () => {
@@ -1,6 +1,6 @@
1
1
  import type { ScoutId } from "./common.js";
2
2
 
3
- export type AgentIdentityDimension = "workspace" | "profile" | "harness" | "node";
3
+ export type AgentIdentityDimension = "workspace" | "profile" | "harness" | "model" | "node";
4
4
 
5
5
  export interface AgentIdentity {
6
6
  raw: string;
@@ -10,6 +10,7 @@ export interface AgentIdentity {
10
10
  workspaceQualifier?: string;
11
11
  profile?: string;
12
12
  harness?: string;
13
+ model?: string;
13
14
  }
14
15
 
15
16
  export interface AgentIdentityCandidate {
@@ -19,6 +20,7 @@ export interface AgentIdentityCandidate {
19
20
  workspaceQualifier?: string;
20
21
  profile?: string;
21
22
  harness?: string;
23
+ model?: string;
22
24
  aliases?: string[];
23
25
  }
24
26
 
@@ -28,6 +30,7 @@ export interface AgentIdentityInput {
28
30
  workspaceQualifier?: string;
29
31
  profile?: string;
30
32
  harness?: string;
33
+ model?: string;
31
34
  }
32
35
 
33
36
  export interface AgentIdentityAlias {
@@ -43,6 +46,7 @@ const DIMENSION_ALIASES: Record<string, AgentIdentityDimension> = {
43
46
  persona: "profile",
44
47
  harness: "harness",
45
48
  runtime: "harness",
49
+ model: "model",
46
50
  node: "node",
47
51
  host: "node",
48
52
  };
@@ -66,6 +70,7 @@ export function normalizeAgentIdentitySegment(value: string): string {
66
70
  export const normalizeAgentSelectorSegment = normalizeAgentIdentitySegment;
67
71
 
68
72
  export const SCOUT_DISPATCHER_AGENT_ID = "scout";
73
+ export const OPENSCOUT_COORDINATOR_AGENT_ID = "openscout";
69
74
 
70
75
  export const BUILT_IN_AGENT_DEFINITION_IDS: ReadonlySet<string> = new Set([
71
76
  SCOUT_DISPATCHER_AGENT_ID,
@@ -99,6 +104,7 @@ function canonicalizeIdentity(input: AgentIdentityInput): AgentIdentityInput | n
99
104
  : undefined;
100
105
  const profile = input.profile ? normalizeAgentIdentitySegment(input.profile) : undefined;
101
106
  const harness = input.harness ? normalizeAgentIdentitySegment(input.harness) : undefined;
107
+ const model = input.model ? normalizeAgentIdentitySegment(input.model) : undefined;
102
108
  const nodeQualifier = input.nodeQualifier
103
109
  ? normalizeAgentIdentitySegment(input.nodeQualifier)
104
110
  : undefined;
@@ -112,6 +118,7 @@ function canonicalizeIdentity(input: AgentIdentityInput): AgentIdentityInput | n
112
118
  ...(workspaceQualifier ? { workspaceQualifier } : {}),
113
119
  ...(profile ? { profile } : {}),
114
120
  ...(harness ? { harness } : {}),
121
+ ...(model ? { model } : {}),
115
122
  ...(nodeQualifier ? { nodeQualifier } : {}),
116
123
  };
117
124
  }
@@ -144,6 +151,8 @@ function parseSegmentedIdentity(raw: string): AgentIdentity | null {
144
151
  next.profile = value;
145
152
  } else if (key === "harness") {
146
153
  next.harness = value;
154
+ } else if (key === "model") {
155
+ next.model = value;
147
156
  } else if (key === "node") {
148
157
  next.nodeQualifier = value;
149
158
  }
@@ -173,16 +182,70 @@ function parseSegmentedIdentity(raw: string): AgentIdentity | null {
173
182
  return constructAgentIdentity(next, { raw });
174
183
  }
175
184
 
185
+ function parseShorthandIdentity(raw: string): AgentIdentity | null {
186
+ const hashIndex = raw.indexOf("#");
187
+ const questionIndex = raw.indexOf("?");
188
+ if (hashIndex === -1 && questionIndex === -1) {
189
+ return null;
190
+ }
191
+ if (raw.indexOf("#", hashIndex + 1) !== -1 || raw.indexOf("?", questionIndex + 1) !== -1) {
192
+ return null;
193
+ }
194
+ if (hashIndex !== -1 && questionIndex !== -1 && questionIndex < hashIndex) {
195
+ return null;
196
+ }
197
+
198
+ const baseEnd = Math.min(
199
+ ...[hashIndex, questionIndex].filter((index) => index >= 0),
200
+ );
201
+ const base = raw.slice(0, baseEnd);
202
+ if (!base) {
203
+ return null;
204
+ }
205
+
206
+ const parsedBase = parseSegmentedIdentity(base);
207
+ if (!parsedBase) {
208
+ return null;
209
+ }
210
+
211
+ const harnessRaw = hashIndex >= 0
212
+ ? raw.slice(hashIndex + 1, questionIndex >= 0 ? questionIndex : raw.length)
213
+ : "";
214
+ const modelRaw = questionIndex >= 0 ? raw.slice(questionIndex + 1) : "";
215
+ if ((hashIndex >= 0 && !harnessRaw) || (questionIndex >= 0 && !modelRaw)) {
216
+ return null;
217
+ }
218
+ if (harnessRaw.includes("#") || harnessRaw.includes("?") || modelRaw.includes("#") || modelRaw.includes("?")) {
219
+ return null;
220
+ }
221
+ if ((parsedBase.harness && harnessRaw) || (parsedBase.model && modelRaw)) {
222
+ return null;
223
+ }
224
+
225
+ return constructAgentIdentity({
226
+ definitionId: parsedBase.definitionId,
227
+ nodeQualifier: parsedBase.nodeQualifier,
228
+ workspaceQualifier: parsedBase.workspaceQualifier,
229
+ profile: parsedBase.profile,
230
+ harness: harnessRaw || parsedBase.harness,
231
+ model: modelRaw || parsedBase.model,
232
+ }, { raw });
233
+ }
234
+
176
235
  export function parseAgentIdentity(value: string): AgentIdentity | null {
177
236
  const raw = trimIdentityPrefix(value);
178
237
  if (!raw) {
179
238
  return null;
180
239
  }
181
240
 
182
- if (raw.includes("@") || raw.includes("#")) {
241
+ if (raw.includes("@")) {
183
242
  return null;
184
243
  }
185
244
 
245
+ if (raw.includes("#") || raw.includes("?")) {
246
+ return parseShorthandIdentity(raw);
247
+ }
248
+
186
249
  return parseSegmentedIdentity(raw);
187
250
  }
188
251
 
@@ -207,6 +270,9 @@ export function formatAgentIdentity(
207
270
  if (canonical.harness) {
208
271
  segments.push(`harness:${canonical.harness}`);
209
272
  }
273
+ if (canonical.model) {
274
+ segments.push(`model:${canonical.model}`);
275
+ }
210
276
  if (canonical.nodeQualifier) {
211
277
  segments.push(`node:${canonical.nodeQualifier}`);
212
278
  }
@@ -231,6 +297,7 @@ export function constructAgentIdentity(
231
297
  ...(canonical.workspaceQualifier ? { workspaceQualifier: canonical.workspaceQualifier } : {}),
232
298
  ...(canonical.profile ? { profile: canonical.profile } : {}),
233
299
  ...(canonical.harness ? { harness: canonical.harness } : {}),
300
+ ...(canonical.model ? { model: canonical.model } : {}),
234
301
  };
235
302
  }
236
303
 
@@ -240,7 +307,7 @@ export function extractAgentIdentities(text: string): AgentIdentity[] {
240
307
 
241
308
  export function extractAgentMentions(text: string): { parsed: AgentIdentity[]; unparsed: string[] } {
242
309
  const matches = Array.from(
243
- text.matchAll(/(^|\s)@([a-z0-9][a-z0-9._/:-]*)/gi),
310
+ text.matchAll(/(^|[\s([{'"`])@([a-z0-9][a-z0-9._/:-]*(?:#[a-z0-9][a-z0-9._/:-]*)?(?:\?[a-z0-9][a-z0-9._/:-]*)?)(?=$|[\s)\]}",.!?:;'"`])/gi),
244
311
  );
245
312
  const identities = new Map<string, AgentIdentity>();
246
313
  const unparsed: string[] = [];
@@ -269,6 +336,7 @@ function candidateAliases(candidate: AgentIdentityCandidate): string[] {
269
336
  workspaceQualifier: candidate.workspaceQualifier,
270
337
  profile: candidate.profile,
271
338
  harness: candidate.harness,
339
+ model: candidate.model,
272
340
  });
273
341
  if (!canonical) {
274
342
  return [];
@@ -286,6 +354,9 @@ function candidateAliases(candidate: AgentIdentityCandidate): string[] {
286
354
  canonical.harness
287
355
  ? formatAgentIdentity({ definitionId: canonical.definitionId, harness: canonical.harness })
288
356
  : "",
357
+ canonical.model
358
+ ? formatAgentIdentity({ definitionId: canonical.definitionId, model: canonical.model })
359
+ : "",
289
360
  canonical.nodeQualifier
290
361
  ? formatAgentIdentity({ definitionId: canonical.definitionId, nodeQualifier: canonical.nodeQualifier })
291
362
  : "",
@@ -296,8 +367,12 @@ function candidateAliases(candidate: AgentIdentityCandidate): string[] {
296
367
  }
297
368
 
298
369
  function explicitCandidateAliases(candidate: AgentIdentityCandidate): string[] {
370
+ const canonical = candidateCanonicalIdentity(candidate);
371
+ const implicitAliases = canonical?.definitionId === OPENSCOUT_COORDINATOR_AGENT_ID
372
+ ? [SCOUT_DISPATCHER_AGENT_ID]
373
+ : [];
299
374
  return Array.from(new Set(
300
- (candidate.aliases ?? [])
375
+ [...(candidate.aliases ?? []), ...implicitAliases]
301
376
  .map((alias) => normalizeAgentIdentitySegment(trimIdentityPrefix(alias)))
302
377
  .filter(Boolean),
303
378
  ));
@@ -327,6 +402,7 @@ function candidateCanonicalIdentity(candidate: AgentIdentityCandidate): AgentIde
327
402
  workspaceQualifier: candidate.workspaceQualifier,
328
403
  profile: candidate.profile,
329
404
  harness: candidate.harness,
405
+ model: candidate.model,
330
406
  });
331
407
  }
332
408
 
@@ -342,9 +418,55 @@ function candidateDimensionValue(
342
418
  if (dimension === "workspace") return canonical.workspaceQualifier;
343
419
  if (dimension === "profile") return canonical.profile;
344
420
  if (dimension === "harness") return canonical.harness;
421
+ if (dimension === "model") return canonical.model;
345
422
  return canonical.nodeQualifier;
346
423
  }
347
424
 
425
+ function modelAliasKeys(value: string | undefined): string[] {
426
+ const normalized = value ? normalizeAgentIdentitySegment(value) : "";
427
+ if (!normalized) {
428
+ return [];
429
+ }
430
+
431
+ const parts = normalized.split("-").filter(Boolean);
432
+ const aliases = new Set<string>([normalized]);
433
+ if (parts.length > 1 && ["claude", "gpt", "openai", "anthropic"].includes(parts[0]!)) {
434
+ aliases.add(parts.slice(1).join("-"));
435
+ }
436
+
437
+ for (const family of ["sonnet", "opus", "haiku", "mini", "nano", "pro"]) {
438
+ const index = parts.indexOf(family);
439
+ if (index >= 0) {
440
+ aliases.add(family);
441
+ aliases.add(parts.slice(index).join("-"));
442
+ }
443
+ }
444
+
445
+ return Array.from(aliases).filter(Boolean);
446
+ }
447
+
448
+ function modelMatches(
449
+ identityModel: string | undefined,
450
+ candidate: AgentIdentityCandidate,
451
+ ): boolean {
452
+ if (!identityModel) {
453
+ return true;
454
+ }
455
+ return modelAliasKeys(candidate.model).includes(identityModel);
456
+ }
457
+
458
+ function definitionMatchesIdentity(
459
+ identityDefinitionId: string,
460
+ candidateDefinitionId: string,
461
+ ): boolean {
462
+ if (identityDefinitionId === candidateDefinitionId) {
463
+ return true;
464
+ }
465
+
466
+ return identityDefinitionId === SCOUT_DISPATCHER_AGENT_ID
467
+ && candidateDefinitionId === OPENSCOUT_COORDINATOR_AGENT_ID;
468
+ }
469
+
348
470
  export function agentIdentityMatches(
349
471
  identity: AgentIdentity,
350
472
  candidate: AgentIdentityCandidate,
@@ -361,8 +483,9 @@ export function agentIdentityMatches(
361
483
  workspaceQualifier: candidate.workspaceQualifier,
362
484
  profile: candidate.profile,
363
485
  harness: candidate.harness,
486
+ model: candidate.model,
364
487
  });
365
- if (!canonical || identity.definitionId !== canonical.definitionId) {
488
+ if (!canonical || !definitionMatchesIdentity(identity.definitionId, canonical.definitionId)) {
366
489
  return false;
367
490
  }
368
491
 
@@ -378,6 +501,9 @@ export function agentIdentityMatches(
378
501
  if (identity.harness && identity.harness !== canonical.harness) {
379
502
  return candidateAliases(candidate).includes(trimIdentityPrefix(identity.label));
380
503
  }
504
+ if (identity.model && !modelMatches(identity.model, candidate)) {
505
+ return candidateAliases(candidate).includes(trimIdentityPrefix(identity.label));
506
+ }
381
507
 
382
508
  return true;
383
509
  }
@@ -419,7 +545,13 @@ export function diagnoseAgentIdentity<T extends AgentIdentityCandidate>(
419
545
  return { kind: "unknown" };
420
546
  }
421
547
 
422
- if (!identity.nodeQualifier && !identity.workspaceQualifier && !identity.profile && !identity.harness) {
548
+ if (
549
+ !identity.nodeQualifier
550
+ && !identity.workspaceQualifier
551
+ && !identity.profile
552
+ && !identity.harness
553
+ && !identity.model
554
+ ) {
423
555
  const exactIdMatch = matches.find(
424
556
  (candidate) => normalizeAgentIdentitySegment(candidate.agentId) === identity.definitionId,
425
557
  );
@@ -491,6 +623,7 @@ function buildIdentitySubset(
491
623
  : {}),
492
624
  ...(dimensions.includes("profile") && canonical.profile ? { profile: canonical.profile } : {}),
493
625
  ...(dimensions.includes("harness") && canonical.harness ? { harness: canonical.harness } : {}),
626
+ ...(dimensions.includes("model") && canonical.model ? { model: canonical.model } : {}),
494
627
  ...(dimensions.includes("node") && canonical.nodeQualifier ? { nodeQualifier: canonical.nodeQualifier } : {}),
495
628
  });
496
629
  }
@@ -523,7 +656,7 @@ export function formatMinimalAgentIdentity<T extends AgentIdentityCandidate>(
523
656
  return formatAgentIdentity({ definitionId: canonical.definitionId }, options);
524
657
  }
525
658
 
526
- const orderedDimensions: AgentIdentityDimension[] = ["workspace", "profile", "harness", "node"];
659
+ const orderedDimensions: AgentIdentityDimension[] = ["workspace", "profile", "harness", "model", "node"];
527
660
  const selectedDimensions: AgentIdentityDimension[] = [];
528
661
  let remainingPeers = peers.filter((peer) => peer !== candidate);
529
662
 
package/src/events.ts CHANGED
@@ -33,6 +33,10 @@ export type AgentEndpointUpsertedEvent = ControlEventBase<"agent.endpoint.upsert
33
33
  endpoint: AgentEndpoint;
34
34
  }>;
35
35
 
36
+ export type AgentEndpointDeletedEvent = ControlEventBase<"agent.endpoint.deleted", {
37
+ endpointId: string;
38
+ }>;
39
+
36
40
  export type ConversationUpsertedEvent = ControlEventBase<"conversation.upserted", {
37
41
  conversation: ConversationDefinition;
38
42
  }>;
@@ -83,6 +87,7 @@ export type ControlEvent =
83
87
  | ActorRegisteredEvent
84
88
  | AgentRegisteredEvent
85
89
  | AgentEndpointUpsertedEvent
90
+ | AgentEndpointDeletedEvent
86
91
  | ConversationUpsertedEvent
87
92
  | BindingUpsertedEvent
88
93
  | MessagePostedEvent
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./collaboration.js";
11
11
  export * from "./messages.js";
12
12
  export * from "./invocations.js";
13
13
  export * from "./scout-dispatch.js";
14
+ export * from "./scout-delivery.js";
14
15
  export * from "./deliveries.js";
15
16
  export * from "./transports.js";
16
17
  export * from "./events.js";
@@ -0,0 +1,100 @@
1
+ import type { ConversationDefinition } from "./conversations.js";
2
+ import type { MetadataMap, ScoutId } from "./common.js";
3
+ import type { FlightRecord, InvocationExecutionPreference } from "./invocations.js";
4
+ import type { MessageRecord } from "./messages.js";
5
+ import type {
6
+ ScoutCallerContext,
7
+ ScoutDispatchRecord,
8
+ ScoutRoutePolicy,
9
+ ScoutRouteTarget,
10
+ } from "./scout-dispatch.js";
11
+
12
+ export type ScoutDeliverIntent = "tell" | "consult";
13
+
14
+ export type ScoutDeliverRouteKind = "dm" | "channel" | "broadcast";
15
+
16
+ export type ScoutDeliveryRemediationKind =
17
+ | "choose_target"
18
+ | "register_target"
19
+ | "wake_target"
20
+ | "retry_later";
21
+
22
+ export type ScoutDeliverRejectReason =
23
+ | "unknown_target"
24
+ | "ambiguous_target"
25
+ | "invalid_target"
26
+ | "missing_target";
27
+
28
+ export interface ScoutDeliverRequest {
29
+ id?: ScoutId;
30
+ caller?: ScoutCallerContext;
31
+ requesterId?: ScoutId;
32
+ requesterNodeId?: ScoutId;
33
+ body: string;
34
+ intent: ScoutDeliverIntent;
35
+ target?: ScoutRouteTarget;
36
+ targetLabel?: string;
37
+ targetAgentId?: ScoutId;
38
+ routePolicy?: ScoutRoutePolicy;
39
+ channel?: string;
40
+ replyToMessageId?: ScoutId;
41
+ speechText?: string;
42
+ ensureAwake?: boolean;
43
+ execution?: InvocationExecutionPreference;
44
+ createdAt?: number;
45
+ collaborationRecordId?: ScoutId;
46
+ messageMetadata?: MetadataMap;
47
+ invocationMetadata?: MetadataMap;
48
+ }
49
+
50
+ export interface ScoutDeliveryReceipt {
51
+ requestId: ScoutId;
52
+ routeKind: ScoutDeliverRouteKind;
53
+ requesterId: ScoutId;
54
+ requesterNodeId: ScoutId;
55
+ targetAgentId?: ScoutId;
56
+ targetLabel?: string;
57
+ conversationId: ScoutId;
58
+ messageId: ScoutId;
59
+ flightId?: ScoutId;
60
+ acceptedAt: number;
61
+ }
62
+
63
+ export interface ScoutDeliveryRemediationAction {
64
+ kind: ScoutDeliveryRemediationKind;
65
+ detail: string;
66
+ targetAgentId?: ScoutId;
67
+ targetLabel?: string;
68
+ dispatchId?: ScoutId;
69
+ }
70
+
71
+ export interface ScoutDeliverAcceptedResponse {
72
+ kind: "delivery";
73
+ accepted: true;
74
+ routeKind: ScoutDeliverRouteKind;
75
+ receipt: ScoutDeliveryReceipt;
76
+ conversation: ConversationDefinition;
77
+ message: MessageRecord;
78
+ targetAgentId?: ScoutId;
79
+ flight?: FlightRecord;
80
+ }
81
+
82
+ export interface ScoutDeliverQuestionResponse {
83
+ kind: "question";
84
+ accepted: false;
85
+ question: ScoutDispatchRecord;
86
+ remediation?: ScoutDeliveryRemediationAction;
87
+ }
88
+
89
+ export interface ScoutDeliverRejectedResponse {
90
+ kind: "rejected";
91
+ accepted: false;
92
+ reason: ScoutDeliverRejectReason;
93
+ rejection: ScoutDispatchRecord;
94
+ remediation?: ScoutDeliveryRemediationAction;
95
+ }
96
+
97
+ export type ScoutDeliverResponse =
98
+ | ScoutDeliverAcceptedResponse
99
+ | ScoutDeliverQuestionResponse
100
+ | ScoutDeliverRejectedResponse;
@@ -1,12 +1,45 @@
1
- import type { AdvertiseScope, DeliveryTransport, ScoutId } from "./common.js";
1
+ import type { WakePolicy } from "./actors.js";
2
+ import type { AdvertiseScope, DeliveryTransport, MetadataMap, ScoutId } from "./common.js";
2
3
 
3
- export type ScoutDispatchKind = "ambiguous" | "unknown" | "unparseable";
4
+ export type ScoutDispatchKind = "ambiguous" | "unknown" | "unparseable" | "unavailable";
4
5
 
5
6
  export type ScoutCandidateEndpointState = "online" | "offline" | "unknown";
6
7
 
8
+ export type ScoutRouteTargetKind = "agent_id" | "agent_label" | "channel" | "broadcast";
9
+
10
+ export type ScoutRouteAmbiguousPolicy = "reject" | "ask";
11
+
12
+ export interface ScoutCallerContext {
13
+ actorId?: ScoutId;
14
+ nodeId?: ScoutId;
15
+ displayName?: string;
16
+ handle?: string;
17
+ currentDirectory?: string;
18
+ metadata?: MetadataMap;
19
+ }
20
+
21
+ export type ScoutRouteTarget =
22
+ | { kind: "agent_id"; agentId: ScoutId; value?: string }
23
+ | { kind: "agent_label"; label: string; value?: string }
24
+ | { kind: "channel"; channel: string; value?: string }
25
+ | { kind: "broadcast"; value?: string };
26
+
27
+ export interface ScoutRoutePolicy {
28
+ preferLocalNodeId?: ScoutId;
29
+ ambiguous?: ScoutRouteAmbiguousPolicy;
30
+ allowStaleDirectId?: boolean;
31
+ }
32
+
33
+ export type ScoutDispatchUnavailableReason =
34
+ | "manual_wake_required"
35
+ | "retired"
36
+ | "stale_registration"
37
+ | "unknown";
38
+
7
39
  export interface ScoutDispatchCandidate {
8
40
  agentId: ScoutId;
9
41
  displayName: string;
42
+ label: string;
10
43
  authorityNodeId?: ScoutId;
11
44
  homeNodeId?: ScoutId;
12
45
  advertiseScope?: AdvertiseScope;
@@ -19,11 +52,23 @@ export interface ScoutDispatchCandidate {
19
52
  transport?: DeliveryTransport | null;
20
53
  }
21
54
 
55
+ export interface ScoutDispatchUnavailableTarget {
56
+ agentId: ScoutId;
57
+ displayName: string;
58
+ reason: ScoutDispatchUnavailableReason;
59
+ detail: string;
60
+ wakePolicy?: WakePolicy | null;
61
+ endpointState: ScoutCandidateEndpointState;
62
+ transport?: DeliveryTransport | null;
63
+ projectRoot?: string | null;
64
+ }
65
+
22
66
  export interface ScoutDispatchEnvelope {
23
67
  kind: ScoutDispatchKind;
24
68
  askedLabel: string;
25
69
  detail: string;
26
70
  candidates: ScoutDispatchCandidate[];
71
+ target?: ScoutDispatchUnavailableTarget;
27
72
  dispatchedAt: number;
28
73
  dispatcherNodeId: ScoutId;
29
74
  }