@openscout/protocol 0.2.16 → 0.2.17

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.
@@ -0,0 +1,508 @@
1
+ import type { ScoutId } from "./common.js";
2
+
3
+ export type AgentIdentityDimension = "workspace" | "profile" | "harness" | "node";
4
+
5
+ export interface AgentIdentity {
6
+ raw: string;
7
+ label: string;
8
+ definitionId: ScoutId;
9
+ nodeQualifier?: string;
10
+ workspaceQualifier?: string;
11
+ profile?: string;
12
+ harness?: string;
13
+ }
14
+
15
+ export interface AgentIdentityCandidate {
16
+ agentId: ScoutId;
17
+ definitionId?: ScoutId;
18
+ nodeQualifier?: string;
19
+ workspaceQualifier?: string;
20
+ profile?: string;
21
+ harness?: string;
22
+ aliases?: string[];
23
+ }
24
+
25
+ export interface AgentIdentityInput {
26
+ definitionId: ScoutId;
27
+ nodeQualifier?: string;
28
+ workspaceQualifier?: string;
29
+ profile?: string;
30
+ harness?: string;
31
+ }
32
+
33
+ export interface AgentIdentityAlias {
34
+ alias: string;
35
+ target: AgentIdentityInput;
36
+ }
37
+
38
+ const DIMENSION_ALIASES: Record<string, AgentIdentityDimension> = {
39
+ workspace: "workspace",
40
+ worktree: "workspace",
41
+ branch: "workspace",
42
+ profile: "profile",
43
+ persona: "profile",
44
+ harness: "harness",
45
+ runtime: "harness",
46
+ node: "node",
47
+ host: "node",
48
+ };
49
+
50
+ function trimIdentityPrefix(value: string): string {
51
+ return value
52
+ .trim()
53
+ .replace(/^@+/, "")
54
+ .replace(/[.,!?;)\]]+$/, "");
55
+ }
56
+
57
+ export function normalizeAgentIdentitySegment(value: string): string {
58
+ return value
59
+ .trim()
60
+ .toLowerCase()
61
+ .replace(/[^a-z0-9]+/g, "-")
62
+ .replace(/-+/g, "-")
63
+ .replace(/^-+|-+$/g, "");
64
+ }
65
+
66
+ export const normalizeAgentSelectorSegment = normalizeAgentIdentitySegment;
67
+
68
+ function normalizeDimensionKey(value: string): AgentIdentityDimension | null {
69
+ return DIMENSION_ALIASES[normalizeAgentIdentitySegment(value)] ?? null;
70
+ }
71
+
72
+ function canonicalizeIdentity(input: AgentIdentityInput): AgentIdentityInput | null {
73
+ const definitionId = normalizeAgentIdentitySegment(input.definitionId);
74
+ const workspaceQualifier = input.workspaceQualifier
75
+ ? normalizeAgentIdentitySegment(input.workspaceQualifier)
76
+ : undefined;
77
+ const profile = input.profile ? normalizeAgentIdentitySegment(input.profile) : undefined;
78
+ const harness = input.harness ? normalizeAgentIdentitySegment(input.harness) : undefined;
79
+ const nodeQualifier = input.nodeQualifier
80
+ ? normalizeAgentIdentitySegment(input.nodeQualifier)
81
+ : undefined;
82
+
83
+ if (!definitionId) {
84
+ return null;
85
+ }
86
+
87
+ return {
88
+ definitionId,
89
+ ...(workspaceQualifier ? { workspaceQualifier } : {}),
90
+ ...(profile ? { profile } : {}),
91
+ ...(harness ? { harness } : {}),
92
+ ...(nodeQualifier ? { nodeQualifier } : {}),
93
+ };
94
+ }
95
+
96
+ function parseSegmentedIdentity(raw: string): AgentIdentity | null {
97
+ const parts = raw.split(".").filter(Boolean);
98
+ const definitionPart = parts.shift();
99
+ if (!definitionPart) {
100
+ return null;
101
+ }
102
+
103
+ const next: AgentIdentityInput = {
104
+ definitionId: definitionPart,
105
+ };
106
+
107
+ let sawPositionalWorkspace = false;
108
+ for (const part of parts) {
109
+ if (part.includes(":")) {
110
+ const [rawKey, rawValue = ""] = part.split(":", 2);
111
+ const key = normalizeDimensionKey(rawKey);
112
+ const value = normalizeAgentIdentitySegment(rawValue);
113
+ if (!key || !value) {
114
+ return null;
115
+ }
116
+
117
+ if (key === "workspace") {
118
+ next.workspaceQualifier = value;
119
+ } else if (key === "profile") {
120
+ next.profile = value;
121
+ } else if (key === "harness") {
122
+ next.harness = value;
123
+ } else if (key === "node") {
124
+ next.nodeQualifier = value;
125
+ }
126
+ continue;
127
+ }
128
+
129
+ if (sawPositionalWorkspace) {
130
+ return null;
131
+ }
132
+
133
+ const value = normalizeAgentIdentitySegment(part);
134
+ if (!value) {
135
+ return null;
136
+ }
137
+
138
+ next.workspaceQualifier = value;
139
+ sawPositionalWorkspace = true;
140
+ }
141
+
142
+ return constructAgentIdentity(next, { raw });
143
+ }
144
+
145
+ export function parseAgentIdentity(value: string): AgentIdentity | null {
146
+ const raw = trimIdentityPrefix(value);
147
+ if (!raw) {
148
+ return null;
149
+ }
150
+
151
+ if (raw.includes("@") || raw.includes("#")) {
152
+ return null;
153
+ }
154
+
155
+ return parseSegmentedIdentity(raw);
156
+ }
157
+
158
+ export function formatAgentIdentity(
159
+ input: AgentIdentityInput,
160
+ options: { includeSigil?: boolean } = {},
161
+ ): string {
162
+ const canonical = canonicalizeIdentity(input);
163
+ if (!canonical) {
164
+ return options.includeSigil === false ? "" : "@";
165
+ }
166
+
167
+ const prefix = options.includeSigil === false ? "" : "@";
168
+ const segments = [`${prefix}${canonical.definitionId}`];
169
+
170
+ if (canonical.workspaceQualifier) {
171
+ segments.push(canonical.workspaceQualifier);
172
+ }
173
+ if (canonical.profile) {
174
+ segments.push(`profile:${canonical.profile}`);
175
+ }
176
+ if (canonical.harness) {
177
+ segments.push(`harness:${canonical.harness}`);
178
+ }
179
+ if (canonical.nodeQualifier) {
180
+ segments.push(`node:${canonical.nodeQualifier}`);
181
+ }
182
+
183
+ return segments.join(".");
184
+ }
185
+
186
+ export function constructAgentIdentity(
187
+ input: AgentIdentityInput,
188
+ options: { raw?: string } = {},
189
+ ): AgentIdentity | null {
190
+ const canonical = canonicalizeIdentity(input);
191
+ if (!canonical) {
192
+ return null;
193
+ }
194
+
195
+ return {
196
+ raw: options.raw ?? formatAgentIdentity(canonical, { includeSigil: false }),
197
+ label: formatAgentIdentity(canonical),
198
+ definitionId: canonical.definitionId,
199
+ ...(canonical.nodeQualifier ? { nodeQualifier: canonical.nodeQualifier } : {}),
200
+ ...(canonical.workspaceQualifier ? { workspaceQualifier: canonical.workspaceQualifier } : {}),
201
+ ...(canonical.profile ? { profile: canonical.profile } : {}),
202
+ ...(canonical.harness ? { harness: canonical.harness } : {}),
203
+ };
204
+ }
205
+
206
+ export function extractAgentIdentities(text: string): AgentIdentity[] {
207
+ const matches = Array.from(
208
+ text.matchAll(/(^|\s)@([a-z0-9][a-z0-9._/:-]*)/gi),
209
+ );
210
+ const identities = new Map<string, AgentIdentity>();
211
+
212
+ for (const match of matches) {
213
+ const candidate = parseAgentIdentity(match[2] ?? "");
214
+ if (!candidate) {
215
+ continue;
216
+ }
217
+
218
+ identities.set(candidate.label, candidate);
219
+ }
220
+
221
+ return Array.from(identities.values());
222
+ }
223
+
224
+ function candidateAliases(candidate: AgentIdentityCandidate): string[] {
225
+ const canonical = canonicalizeIdentity({
226
+ definitionId: candidate.definitionId || candidate.agentId,
227
+ nodeQualifier: candidate.nodeQualifier,
228
+ workspaceQualifier: candidate.workspaceQualifier,
229
+ profile: candidate.profile,
230
+ harness: candidate.harness,
231
+ });
232
+ if (!canonical) {
233
+ return [];
234
+ }
235
+
236
+ const aliases = [
237
+ canonical.definitionId,
238
+ formatAgentIdentity(canonical),
239
+ canonical.workspaceQualifier
240
+ ? formatAgentIdentity({ definitionId: canonical.definitionId, workspaceQualifier: canonical.workspaceQualifier })
241
+ : "",
242
+ canonical.profile
243
+ ? formatAgentIdentity({ definitionId: canonical.definitionId, profile: canonical.profile })
244
+ : "",
245
+ canonical.harness
246
+ ? formatAgentIdentity({ definitionId: canonical.definitionId, harness: canonical.harness })
247
+ : "",
248
+ canonical.nodeQualifier
249
+ ? formatAgentIdentity({ definitionId: canonical.definitionId, nodeQualifier: canonical.nodeQualifier })
250
+ : "",
251
+ ...((candidate.aliases ?? []).map((alias) => alias.trim()).filter(Boolean)),
252
+ ];
253
+
254
+ return Array.from(new Set(aliases.map((alias) => trimIdentityPrefix(alias)).filter(Boolean)));
255
+ }
256
+
257
+ function explicitCandidateAliases(candidate: AgentIdentityCandidate): string[] {
258
+ return Array.from(new Set(
259
+ (candidate.aliases ?? [])
260
+ .map((alias) => normalizeAgentIdentitySegment(trimIdentityPrefix(alias)))
261
+ .filter(Boolean),
262
+ ));
263
+ }
264
+
265
+ function canonicalizeAliasValue(value: string): string {
266
+ const parsed = parseAgentIdentity(value.startsWith("@") ? value : `@${value}`);
267
+ if (parsed) {
268
+ return trimIdentityPrefix(parsed.label);
269
+ }
270
+
271
+ return normalizeAgentIdentitySegment(value);
272
+ }
273
+
274
+ function identityAliasKeys(identity: AgentIdentity): string[] {
275
+ return Array.from(new Set([
276
+ trimIdentityPrefix(identity.label),
277
+ trimIdentityPrefix(identity.raw),
278
+ trimIdentityPrefix(formatAgentIdentity(identity, { includeSigil: false })),
279
+ ].filter(Boolean)));
280
+ }
281
+
282
+ function candidateCanonicalIdentity(candidate: AgentIdentityCandidate): AgentIdentity | null {
283
+ return constructAgentIdentity({
284
+ definitionId: candidate.definitionId || candidate.agentId,
285
+ nodeQualifier: candidate.nodeQualifier,
286
+ workspaceQualifier: candidate.workspaceQualifier,
287
+ profile: candidate.profile,
288
+ harness: candidate.harness,
289
+ });
290
+ }
291
+
292
+ function candidateDimensionValue(
293
+ candidate: AgentIdentityCandidate,
294
+ dimension: AgentIdentityDimension,
295
+ ): string | undefined {
296
+ const canonical = candidateCanonicalIdentity(candidate);
297
+ if (!canonical) {
298
+ return undefined;
299
+ }
300
+
301
+ if (dimension === "workspace") return canonical.workspaceQualifier;
302
+ if (dimension === "profile") return canonical.profile;
303
+ if (dimension === "harness") return canonical.harness;
304
+ return canonical.nodeQualifier;
305
+ }
306
+
307
+ export function agentIdentityMatches(
308
+ identity: AgentIdentity,
309
+ candidate: AgentIdentityCandidate,
310
+ ): boolean {
311
+ const aliasKeys = identityAliasKeys(identity);
312
+ const candidateAliasKeys = explicitCandidateAliases(candidate).map(canonicalizeAliasValue);
313
+ if (aliasKeys.some((key) => candidateAliasKeys.includes(key))) {
314
+ return true;
315
+ }
316
+
317
+ const canonical = canonicalizeIdentity({
318
+ definitionId: candidate.definitionId || candidate.agentId,
319
+ nodeQualifier: candidate.nodeQualifier,
320
+ workspaceQualifier: candidate.workspaceQualifier,
321
+ profile: candidate.profile,
322
+ harness: candidate.harness,
323
+ });
324
+ if (!canonical || identity.definitionId !== canonical.definitionId) {
325
+ return false;
326
+ }
327
+
328
+ if (identity.nodeQualifier && identity.nodeQualifier !== canonical.nodeQualifier) {
329
+ return candidateAliases(candidate).includes(trimIdentityPrefix(identity.label));
330
+ }
331
+ if (identity.workspaceQualifier && identity.workspaceQualifier !== canonical.workspaceQualifier) {
332
+ return candidateAliases(candidate).includes(trimIdentityPrefix(identity.label));
333
+ }
334
+ if (identity.profile && identity.profile !== canonical.profile) {
335
+ return candidateAliases(candidate).includes(trimIdentityPrefix(identity.label));
336
+ }
337
+ if (identity.harness && identity.harness !== canonical.harness) {
338
+ return candidateAliases(candidate).includes(trimIdentityPrefix(identity.label));
339
+ }
340
+
341
+ return true;
342
+ }
343
+
344
+ export function resolveAgentIdentity<T extends AgentIdentityCandidate>(
345
+ identity: AgentIdentity,
346
+ candidates: T[],
347
+ ): T | null {
348
+ const aliasKeys = identityAliasKeys(identity);
349
+ const exactAliasMatches = candidates.filter((candidate) => {
350
+ const candidateAliasKeys = explicitCandidateAliases(candidate).map(canonicalizeAliasValue);
351
+ return aliasKeys.some((key) => candidateAliasKeys.includes(key));
352
+ });
353
+ if (exactAliasMatches.length === 1) {
354
+ return exactAliasMatches[0];
355
+ }
356
+ if (exactAliasMatches.length > 1) {
357
+ return null;
358
+ }
359
+
360
+ const matches = candidates.filter((candidate) => agentIdentityMatches(identity, candidate));
361
+ if (matches.length === 1) {
362
+ return matches[0];
363
+ }
364
+
365
+ if (!identity.nodeQualifier && !identity.workspaceQualifier && !identity.profile && !identity.harness) {
366
+ return matches.find((candidate) => normalizeAgentIdentitySegment(candidate.agentId) === identity.definitionId)
367
+ ?? matches[0]
368
+ ?? null;
369
+ }
370
+
371
+ return null;
372
+ }
373
+
374
+ export function constructAgentAlias(input: {
375
+ alias: string;
376
+ target: AgentIdentityInput;
377
+ }): AgentIdentityAlias | null {
378
+ const alias = normalizeAgentIdentitySegment(trimIdentityPrefix(input.alias));
379
+ const target = canonicalizeIdentity(input.target);
380
+ if (!alias || !target) {
381
+ return null;
382
+ }
383
+
384
+ return { alias, target };
385
+ }
386
+
387
+ export function formatAgentAlias(alias: AgentIdentityAlias, options: { includeSigil?: boolean } = {}): string {
388
+ const prefix = options.includeSigil === false ? "" : "@";
389
+ return `${prefix}${normalizeAgentIdentitySegment(alias.alias)}`;
390
+ }
391
+
392
+ export function resolveAgentAlias(
393
+ value: string,
394
+ aliases: AgentIdentityAlias[],
395
+ ): AgentIdentity | null {
396
+ const aliasKey = normalizeAgentIdentitySegment(trimIdentityPrefix(value));
397
+ if (!aliasKey) {
398
+ return null;
399
+ }
400
+
401
+ const matches = aliases.filter((alias) => normalizeAgentIdentitySegment(alias.alias) === aliasKey);
402
+ if (matches.length !== 1) {
403
+ return null;
404
+ }
405
+
406
+ return constructAgentIdentity(matches[0].target);
407
+ }
408
+
409
+ function identitiesDifferOnDimension(
410
+ left: AgentIdentityCandidate,
411
+ right: AgentIdentityCandidate,
412
+ dimension: AgentIdentityDimension,
413
+ ): boolean {
414
+ return candidateDimensionValue(left, dimension) !== candidateDimensionValue(right, dimension);
415
+ }
416
+
417
+ function buildIdentitySubset(
418
+ candidate: AgentIdentityCandidate,
419
+ dimensions: AgentIdentityDimension[],
420
+ ): AgentIdentity | null {
421
+ const canonical = candidateCanonicalIdentity(candidate);
422
+ if (!canonical) {
423
+ return null;
424
+ }
425
+
426
+ return constructAgentIdentity({
427
+ definitionId: canonical.definitionId,
428
+ ...(dimensions.includes("workspace") && canonical.workspaceQualifier
429
+ ? { workspaceQualifier: canonical.workspaceQualifier }
430
+ : {}),
431
+ ...(dimensions.includes("profile") && canonical.profile ? { profile: canonical.profile } : {}),
432
+ ...(dimensions.includes("harness") && canonical.harness ? { harness: canonical.harness } : {}),
433
+ ...(dimensions.includes("node") && canonical.nodeQualifier ? { nodeQualifier: canonical.nodeQualifier } : {}),
434
+ });
435
+ }
436
+
437
+ export function formatMinimalAgentIdentity<T extends AgentIdentityCandidate>(
438
+ candidate: T,
439
+ candidates: T[],
440
+ options: { includeSigil?: boolean } = {},
441
+ ): string {
442
+ const canonical = candidateCanonicalIdentity(candidate);
443
+ if (!canonical) {
444
+ return options.includeSigil === false ? "" : "@";
445
+ }
446
+
447
+ const peers = candidates.filter((peer) => {
448
+ const peerCanonical = candidateCanonicalIdentity(peer);
449
+ return peerCanonical && peerCanonical.definitionId === canonical.definitionId;
450
+ });
451
+
452
+ const aliasMatches = explicitCandidateAliases(candidate)
453
+ .map((alias) => ({ alias, identity: parseAgentIdentity(`@${alias}`) }))
454
+ .filter((entry): entry is { alias: string; identity: AgentIdentity } => Boolean(entry.identity))
455
+ .filter((entry) => resolveAgentIdentity(entry.identity, candidates) === candidate)
456
+ .sort((left, right) => left.alias.length - right.alias.length || left.alias.localeCompare(right.alias));
457
+ if (aliasMatches.length > 0) {
458
+ return formatAgentIdentity({ definitionId: aliasMatches[0].alias }, options);
459
+ }
460
+
461
+ if (peers.length <= 1) {
462
+ return formatAgentIdentity({ definitionId: canonical.definitionId }, options);
463
+ }
464
+
465
+ const orderedDimensions: AgentIdentityDimension[] = ["workspace", "profile", "harness", "node"];
466
+ const selectedDimensions: AgentIdentityDimension[] = [];
467
+ let remainingPeers = peers.filter((peer) => peer !== candidate);
468
+
469
+ for (const dimension of orderedDimensions) {
470
+ const value = candidateDimensionValue(candidate, dimension);
471
+ if (!value) {
472
+ continue;
473
+ }
474
+ const splitsRemainingPeers = remainingPeers.some((peer) => identitiesDifferOnDimension(candidate, peer, dimension));
475
+ if (!splitsRemainingPeers) {
476
+ continue;
477
+ }
478
+
479
+ selectedDimensions.push(dimension);
480
+ remainingPeers = remainingPeers.filter((peer) => (
481
+ !identitiesDifferOnDimension(candidate, peer, dimension)
482
+ ));
483
+ const current = buildIdentitySubset(candidate, selectedDimensions);
484
+ if (current && resolveAgentIdentity(current, candidates)) {
485
+ return formatAgentIdentity(current, options);
486
+ }
487
+ }
488
+
489
+ return formatAgentIdentity(canonical, options);
490
+ }
491
+
492
+ export type AgentAddress = AgentIdentity;
493
+ export type AgentAddressCandidate = AgentIdentityCandidate;
494
+ export type AgentSelector = AgentIdentity;
495
+ export type AgentSelectorCandidate = AgentIdentityCandidate;
496
+
497
+ export const parseAgentAddress = parseAgentIdentity;
498
+ export const formatAgentAddress = formatAgentIdentity;
499
+ export const constructAgentAddress = constructAgentIdentity;
500
+ export const extractAgentAddresses = extractAgentIdentities;
501
+ export const agentAddressMatches = agentIdentityMatches;
502
+ export const resolveAgentAddress = resolveAgentIdentity;
503
+
504
+ export const parseAgentSelector = parseAgentIdentity;
505
+ export const formatAgentSelector = formatAgentIdentity;
506
+ export const extractAgentSelectors = extractAgentIdentities;
507
+ export const agentSelectorMatches = agentIdentityMatches;
508
+ export const resolveAgentSelector = resolveAgentIdentity;
@@ -0,0 +1 @@
1
+ export * from "./agent-address.js";