@opengeni/api-router 2.4.2-canary.0 → 2.5.0
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/app.js +1 -1
- package/dist/{chunk-L7GVVSSQ.js → chunk-QESX7HDK.js} +650 -98
- package/dist/chunk-QESX7HDK.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +33 -0
- package/dist/routes/sessions.d.ts +17 -5
- package/dist/work-discovery-observability.d.ts +33 -0
- package/package.json +18 -18
- package/src/mcp/server.ts +304 -29
- package/src/routes/sessions.ts +377 -81
- package/src/work-discovery-observability.ts +121 -0
- package/dist/chunk-L7GVVSSQ.js.map +0 -1
package/src/routes/sessions.ts
CHANGED
|
@@ -66,12 +66,23 @@ import {
|
|
|
66
66
|
UpdateSessionToolPolicyRequest,
|
|
67
67
|
ViewerHeartbeatRequest,
|
|
68
68
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
69
|
+
WORK_CLAIM_CANONICAL_KEY_MAX_BYTES,
|
|
70
|
+
WORK_CLAIM_DISCOVERY_LIMIT,
|
|
71
|
+
WORK_CLAIM_NAMESPACE_MAX_BYTES,
|
|
72
|
+
WORK_DISCOVERY_QUERY_MAX_CHARS,
|
|
73
|
+
WORK_DISCOVERY_RECENT_HOURS_MAX,
|
|
74
|
+
WorkClaimSubjectFilter as WorkClaimSubjectFilterSchema,
|
|
75
|
+
WorkClaimSubjectType,
|
|
76
|
+
normalizeWorkClaimCanonicalKey,
|
|
77
|
+
normalizeWorkClaimNamespace,
|
|
69
78
|
workspaceControlUtf8Bytes,
|
|
70
79
|
type AccessGrant,
|
|
71
80
|
type AttachViewerResponse,
|
|
72
81
|
type SandboxBackend,
|
|
73
82
|
type LineageNode,
|
|
74
83
|
type Session,
|
|
84
|
+
type SessionStatus,
|
|
85
|
+
type WorkClaimSubjectFilter,
|
|
75
86
|
type SessionGoalRevision,
|
|
76
87
|
type AgentTopologyPageResponse,
|
|
77
88
|
type ErrorCode,
|
|
@@ -173,6 +184,7 @@ import {
|
|
|
173
184
|
type SandboxRetainedProcess,
|
|
174
185
|
type Database,
|
|
175
186
|
type SessionDiscoveryCursor,
|
|
187
|
+
type SessionDiscoveryOrderBy,
|
|
176
188
|
type SessionDiscoveryAncestor,
|
|
177
189
|
} from "@opengeni/db";
|
|
178
190
|
import {
|
|
@@ -280,6 +292,7 @@ import {
|
|
|
280
292
|
} from "./workspace-capture";
|
|
281
293
|
import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
|
|
282
294
|
import { ApiHttpError } from "../http/api-error";
|
|
295
|
+
import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
|
|
283
296
|
|
|
284
297
|
type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
|
|
285
298
|
|
|
@@ -700,71 +713,144 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
700
713
|
throw sessionAuthorizationHttpError(error);
|
|
701
714
|
}
|
|
702
715
|
const query = agentTopologyQuery(c.req.query());
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
716
|
+
const startedAtMs = performance.now();
|
|
717
|
+
const mode = query.subject ? "subject" : query.query ? "query" : "browse";
|
|
718
|
+
const metricAuthorizationScope = authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
|
|
719
|
+
if ((query.query || query.subject) && !settings.workDiscoveryEnabled) {
|
|
720
|
+
observeWorkDiscovery(deps.observability, {
|
|
721
|
+
surface: "agent_topology",
|
|
722
|
+
mode,
|
|
723
|
+
outcome: "disabled",
|
|
724
|
+
authorizationScope: metricAuthorizationScope,
|
|
725
|
+
durationMs: performance.now() - startedAtMs,
|
|
726
|
+
responseBytes: 0,
|
|
727
|
+
resultCount: 0,
|
|
728
|
+
overlapCount: 0,
|
|
729
|
+
matchCounts: {},
|
|
730
|
+
});
|
|
731
|
+
throw new HTTPException(503, {
|
|
732
|
+
message: "Agent work discovery is disabled by the operator.",
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
const orderBy: SessionDiscoveryOrderBy =
|
|
737
|
+
query.query || query.subject ? "relevance" : "updatedAt";
|
|
738
|
+
const page = await listSessionDiscoverySummaries(db, workspaceId, {
|
|
739
|
+
limit: query.limit,
|
|
740
|
+
orderBy,
|
|
741
|
+
subjectId: grant.subjectId,
|
|
742
|
+
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
743
|
+
...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
|
|
744
|
+
...(query.rootSessionId ? { rootSessionId: query.rootSessionId } : {}),
|
|
745
|
+
...(query.query ? { query: query.query } : {}),
|
|
746
|
+
...(query.statuses ? { statuses: query.statuses } : {}),
|
|
747
|
+
activeOnly: query.activeOnly,
|
|
748
|
+
...(query.recentHours !== undefined ? { recentHours: query.recentHours } : {}),
|
|
749
|
+
...(query.subject ? { subject: query.subject } : {}),
|
|
750
|
+
...(query.claimLimit !== undefined ? { claimLimit: query.claimLimit } : {}),
|
|
751
|
+
includeWorkDiscovery: settings.workDiscoveryEnabled,
|
|
752
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
753
|
+
});
|
|
754
|
+
const ancestorPaths =
|
|
755
|
+
query.query || query.subject
|
|
756
|
+
? await listSessionDiscoveryAncestorPaths(
|
|
757
|
+
db,
|
|
758
|
+
workspaceId,
|
|
759
|
+
page.sessions.map((session) => session.id),
|
|
760
|
+
authorizationScope ?? undefined,
|
|
761
|
+
grant.subjectId,
|
|
762
|
+
)
|
|
763
|
+
: new Map<string, SessionDiscoveryAncestor[]>();
|
|
764
|
+
const sessions: AgentTopologyPageResponse["sessions"] = page.sessions.map((session) => {
|
|
765
|
+
const blocker = session.effectiveControl.primaryBlocker;
|
|
766
|
+
return {
|
|
767
|
+
id: session.id,
|
|
768
|
+
title: session.title,
|
|
733
769
|
titleTruncated:
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
770
|
+
session.titleOriginalChars !== null &&
|
|
771
|
+
session.titleOriginalChars > Array.from(session.title ?? "").length,
|
|
772
|
+
parentSessionId: session.parentSessionId,
|
|
773
|
+
rootSessionId: session.rootSessionId,
|
|
774
|
+
nestedAgentDepth: session.nestedAgentDepth,
|
|
775
|
+
ancestorPath: (ancestorPaths.get(session.id) ?? []).map((ancestor) => ({
|
|
776
|
+
id: ancestor.id,
|
|
777
|
+
title: ancestor.title,
|
|
778
|
+
titleTruncated:
|
|
779
|
+
ancestor.titleOriginalChars !== null &&
|
|
780
|
+
ancestor.titleOriginalChars > Array.from(ancestor.title ?? "").length,
|
|
781
|
+
})),
|
|
782
|
+
status: session.status,
|
|
783
|
+
goal: session.goal
|
|
742
784
|
? {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
blocker.displayNameOriginalChars > Array.from(blocker.displayName).length,
|
|
785
|
+
status: session.goal.status,
|
|
786
|
+
summary: session.goal.text,
|
|
787
|
+
summaryTruncated:
|
|
788
|
+
session.goal.textOriginalChars > Array.from(session.goal.text).length,
|
|
748
789
|
}
|
|
749
790
|
: null,
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
791
|
+
pause: {
|
|
792
|
+
state: session.effectiveControl.state,
|
|
793
|
+
additionalBlockerCount: session.effectiveControl.additionalBlockerCount,
|
|
794
|
+
source: blocker
|
|
795
|
+
? {
|
|
796
|
+
kind: blocker.kind,
|
|
797
|
+
...(blocker.sessionId ? { sessionId: blocker.sessionId } : {}),
|
|
798
|
+
displayName: blocker.displayName,
|
|
799
|
+
displayNameTruncated:
|
|
800
|
+
blocker.displayNameOriginalChars > Array.from(blocker.displayName).length,
|
|
801
|
+
}
|
|
802
|
+
: null,
|
|
803
|
+
},
|
|
804
|
+
children: session.treeStats,
|
|
805
|
+
relatedWork: session.workDiscovery,
|
|
806
|
+
createdAt: session.createdAt,
|
|
807
|
+
updatedAt: session.updatedAt,
|
|
808
|
+
};
|
|
809
|
+
});
|
|
810
|
+
const response = {
|
|
811
|
+
sessions,
|
|
812
|
+
total: page.total,
|
|
813
|
+
hasMore: page.hasMore,
|
|
814
|
+
humanAdvisoriesEnabled:
|
|
815
|
+
settings.workDiscoveryEnabled && settings.workDiscoveryHumanAdvisoriesEnabled,
|
|
816
|
+
nextCursor: page.nextCursor
|
|
817
|
+
? encodeAgentTopologyCursor({
|
|
818
|
+
cursor: page.nextCursor,
|
|
819
|
+
parentSessionId: query.parentSessionId === undefined ? "all" : query.parentSessionId,
|
|
820
|
+
rootSessionId: query.rootSessionId ?? null,
|
|
821
|
+
query: query.query ?? null,
|
|
822
|
+
statuses: query.statuses ?? [],
|
|
823
|
+
activeOnly: query.activeOnly,
|
|
824
|
+
recentHours: query.recentHours ?? null,
|
|
825
|
+
subject: query.subject ?? null,
|
|
826
|
+
claimLimit: query.claimLimit ?? null,
|
|
827
|
+
})
|
|
828
|
+
: null,
|
|
829
|
+
} satisfies AgentTopologyPageResponse;
|
|
830
|
+
observeWorkDiscovery(deps.observability, {
|
|
831
|
+
surface: "agent_topology",
|
|
832
|
+
mode,
|
|
833
|
+
outcome: sessions.length === 0 ? "empty" : "ok",
|
|
834
|
+
authorizationScope: metricAuthorizationScope,
|
|
835
|
+
durationMs: performance.now() - startedAtMs,
|
|
836
|
+
responseBytes: Buffer.byteLength(JSON.stringify(response), "utf8"),
|
|
837
|
+
...summarizeWorkDiscoveryRows(sessions),
|
|
838
|
+
});
|
|
839
|
+
return c.json(response);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
observeWorkDiscovery(deps.observability, {
|
|
842
|
+
surface: "agent_topology",
|
|
843
|
+
mode,
|
|
844
|
+
outcome: "error",
|
|
845
|
+
authorizationScope: metricAuthorizationScope,
|
|
846
|
+
durationMs: performance.now() - startedAtMs,
|
|
847
|
+
responseBytes: 0,
|
|
848
|
+
resultCount: 0,
|
|
849
|
+
overlapCount: 0,
|
|
850
|
+
matchCounts: {},
|
|
851
|
+
});
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
768
854
|
});
|
|
769
855
|
|
|
770
856
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
@@ -4509,12 +4595,18 @@ function sessionListQuery(
|
|
|
4509
4595
|
|
|
4510
4596
|
export type AgentTopologyCursorEnvelope = {
|
|
4511
4597
|
cursor: SessionDiscoveryCursor;
|
|
4512
|
-
parentSessionId: string | null;
|
|
4513
|
-
|
|
4598
|
+
parentSessionId: string | null | "all";
|
|
4599
|
+
rootSessionId: string | null;
|
|
4600
|
+
query: string | null;
|
|
4601
|
+
statuses: SessionStatus[];
|
|
4602
|
+
activeOnly: boolean;
|
|
4603
|
+
recentHours: number | null;
|
|
4604
|
+
subject: WorkClaimSubjectFilter | null;
|
|
4605
|
+
claimLimit: number | null;
|
|
4514
4606
|
};
|
|
4515
4607
|
|
|
4516
4608
|
export function encodeAgentTopologyCursor(value: AgentTopologyCursorEnvelope): string {
|
|
4517
|
-
return Buffer.from(JSON.stringify({ v:
|
|
4609
|
+
return Buffer.from(JSON.stringify({ v: 2, ...value }), "utf8").toString("base64url");
|
|
4518
4610
|
}
|
|
4519
4611
|
|
|
4520
4612
|
function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
@@ -4524,25 +4616,92 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
4524
4616
|
});
|
|
4525
4617
|
}
|
|
4526
4618
|
try {
|
|
4527
|
-
const
|
|
4619
|
+
const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as unknown;
|
|
4620
|
+
const legacy = z
|
|
4528
4621
|
.object({
|
|
4529
4622
|
v: z.literal(1),
|
|
4530
4623
|
parentSessionId: z.string().uuid().nullable(),
|
|
4531
|
-
search: z.string().max(
|
|
4624
|
+
search: z.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).nullable(),
|
|
4625
|
+
cursor: z.object({
|
|
4626
|
+
orderBy: z.literal("updatedAt"),
|
|
4627
|
+
sortRevision: z.string().max(64),
|
|
4628
|
+
sortAt: z.string().max(64),
|
|
4629
|
+
id: z.string().uuid(),
|
|
4630
|
+
snapshotAt: z.string().max(64),
|
|
4631
|
+
snapshotRevision: z.string().max(64),
|
|
4632
|
+
updatedAfter: z.null(),
|
|
4633
|
+
}),
|
|
4634
|
+
})
|
|
4635
|
+
.safeParse(decoded);
|
|
4636
|
+
if (legacy.success) {
|
|
4637
|
+
if (legacy.data.search !== null) {
|
|
4638
|
+
throw new Error("legacy search cursor is not relevance-fenced");
|
|
4639
|
+
}
|
|
4640
|
+
const cursor = {
|
|
4641
|
+
...legacy.data.cursor,
|
|
4642
|
+
sortRank: null,
|
|
4643
|
+
filterHash: null,
|
|
4644
|
+
} satisfies SessionDiscoveryCursor;
|
|
4645
|
+
return {
|
|
4646
|
+
cursor,
|
|
4647
|
+
parentSessionId: legacy.data.parentSessionId,
|
|
4648
|
+
rootSessionId: null,
|
|
4649
|
+
query: null,
|
|
4650
|
+
statuses: [],
|
|
4651
|
+
activeOnly: false,
|
|
4652
|
+
recentHours: null,
|
|
4653
|
+
subject: null,
|
|
4654
|
+
claimLimit: null,
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
const parsed = z
|
|
4658
|
+
.object({
|
|
4659
|
+
v: z.literal(2),
|
|
4660
|
+
parentSessionId: z.union([z.string().uuid(), z.literal("all"), z.null()]),
|
|
4661
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
4662
|
+
query: z.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).nullable(),
|
|
4663
|
+
statuses: z
|
|
4664
|
+
.array(
|
|
4665
|
+
z.enum([
|
|
4666
|
+
"queued",
|
|
4667
|
+
"running",
|
|
4668
|
+
"idle",
|
|
4669
|
+
"requires_action",
|
|
4670
|
+
"recovering",
|
|
4671
|
+
"waiting_capacity",
|
|
4672
|
+
"failed",
|
|
4673
|
+
"cancelled",
|
|
4674
|
+
]),
|
|
4675
|
+
)
|
|
4676
|
+
.max(8),
|
|
4677
|
+
activeOnly: z.boolean(),
|
|
4678
|
+
recentHours: z.number().int().positive().max(WORK_DISCOVERY_RECENT_HOURS_MAX).nullable(),
|
|
4679
|
+
subject: z
|
|
4680
|
+
.object({
|
|
4681
|
+
namespace: z.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
|
|
4682
|
+
type: WorkClaimSubjectType,
|
|
4683
|
+
canonicalKey: z.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES),
|
|
4684
|
+
})
|
|
4685
|
+
.strict()
|
|
4686
|
+
.nullable(),
|
|
4687
|
+
claimLimit: z.number().int().positive().max(WORK_CLAIM_DISCOVERY_LIMIT).nullable(),
|
|
4532
4688
|
cursor: z.object({
|
|
4533
|
-
orderBy: z.enum(["
|
|
4689
|
+
orderBy: z.enum(["updatedAt", "relevance"]),
|
|
4690
|
+
sortRank: z.number().int().nonnegative().nullable(),
|
|
4534
4691
|
sortRevision: z.string().max(64),
|
|
4535
4692
|
sortAt: z.string().max(64),
|
|
4536
4693
|
id: z.string().uuid(),
|
|
4537
4694
|
snapshotAt: z.string().max(64),
|
|
4538
4695
|
snapshotRevision: z.string().max(64),
|
|
4539
|
-
updatedAfter: z.
|
|
4696
|
+
updatedAfter: z.null(),
|
|
4697
|
+
filterHash: z
|
|
4698
|
+
.string()
|
|
4699
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
4700
|
+
.nullable(),
|
|
4540
4701
|
}),
|
|
4541
4702
|
})
|
|
4542
|
-
.parse(
|
|
4703
|
+
.parse(decoded);
|
|
4543
4704
|
if (
|
|
4544
|
-
parsed.cursor.orderBy !== "updatedAt" ||
|
|
4545
|
-
parsed.cursor.updatedAfter !== null ||
|
|
4546
4705
|
!/^(?:0|[1-9]\d*)$/.test(parsed.cursor.sortRevision) ||
|
|
4547
4706
|
!/^(?:0|[1-9]\d*)$/.test(parsed.cursor.snapshotRevision) ||
|
|
4548
4707
|
BigInt(parsed.cursor.sortRevision) > 9_223_372_036_854_775_807n ||
|
|
@@ -4552,6 +4711,14 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
4552
4711
|
) {
|
|
4553
4712
|
throw new Error("invalid topology cursor fields");
|
|
4554
4713
|
}
|
|
4714
|
+
if (
|
|
4715
|
+
(parsed.cursor.orderBy === "relevance" &&
|
|
4716
|
+
(parsed.cursor.sortRank === null || parsed.cursor.filterHash === null)) ||
|
|
4717
|
+
(parsed.cursor.orderBy === "updatedAt" &&
|
|
4718
|
+
(parsed.cursor.sortRank !== null || parsed.cursor.filterHash !== null))
|
|
4719
|
+
) {
|
|
4720
|
+
throw new Error("invalid topology cursor relevance fields");
|
|
4721
|
+
}
|
|
4555
4722
|
return parsed;
|
|
4556
4723
|
} catch {
|
|
4557
4724
|
throw new HTTPException(400, {
|
|
@@ -4562,8 +4729,14 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
4562
4729
|
|
|
4563
4730
|
export function agentTopologyQuery(query: Record<string, string>): {
|
|
4564
4731
|
limit: number;
|
|
4565
|
-
parentSessionId: string | null;
|
|
4566
|
-
|
|
4732
|
+
parentSessionId: string | null | undefined;
|
|
4733
|
+
rootSessionId: string | undefined;
|
|
4734
|
+
query: string | undefined;
|
|
4735
|
+
statuses: SessionStatus[] | undefined;
|
|
4736
|
+
activeOnly: boolean;
|
|
4737
|
+
recentHours: number | undefined;
|
|
4738
|
+
subject: WorkClaimSubjectFilter | undefined;
|
|
4739
|
+
claimLimit: number | undefined;
|
|
4567
4740
|
cursor: SessionDiscoveryCursor | undefined;
|
|
4568
4741
|
} {
|
|
4569
4742
|
const rawLimit = query.limit;
|
|
@@ -4583,22 +4756,139 @@ export function agentTopologyQuery(query: Record<string, string>): {
|
|
|
4583
4756
|
message: 'parentSessionId must be a session id or the literal "null"',
|
|
4584
4757
|
});
|
|
4585
4758
|
}
|
|
4586
|
-
const
|
|
4587
|
-
|
|
4588
|
-
|
|
4759
|
+
const rootSessionId = query.rootSessionId?.trim();
|
|
4760
|
+
if (rootSessionId && !z.string().uuid().safeParse(rootSessionId).success) {
|
|
4761
|
+
throw new HTTPException(400, { message: "rootSessionId must be a session id" });
|
|
4762
|
+
}
|
|
4763
|
+
const normalizeSearchQuery = (value: string | undefined): string | undefined => {
|
|
4764
|
+
if (value === undefined) return undefined;
|
|
4765
|
+
const canonical = value.normalize("NFKC");
|
|
4766
|
+
if (/[\u0000-\u001f\u007f-\u009f]/u.test(canonical)) {
|
|
4767
|
+
throw new HTTPException(400, { message: "query must not contain control characters" });
|
|
4768
|
+
}
|
|
4769
|
+
const normalized = canonical.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
4770
|
+
if (!normalized) return undefined;
|
|
4771
|
+
if (Array.from(normalized).length > WORK_DISCOVERY_QUERY_MAX_CHARS) {
|
|
4772
|
+
throw new HTTPException(400, {
|
|
4773
|
+
message: `query must be at most ${WORK_DISCOVERY_QUERY_MAX_CHARS} characters`,
|
|
4774
|
+
});
|
|
4775
|
+
}
|
|
4776
|
+
return normalized;
|
|
4777
|
+
};
|
|
4778
|
+
const requestedQuery = normalizeSearchQuery(query.query);
|
|
4779
|
+
const legacySearch = normalizeSearchQuery(query.search);
|
|
4780
|
+
const searchQuery = requestedQuery ?? legacySearch;
|
|
4781
|
+
if (requestedQuery && legacySearch && requestedQuery !== legacySearch) {
|
|
4782
|
+
throw new HTTPException(400, { message: "query and legacy search must match" });
|
|
4783
|
+
}
|
|
4784
|
+
const statuses = query.statuses
|
|
4785
|
+
? [
|
|
4786
|
+
...new Set(
|
|
4787
|
+
query.statuses
|
|
4788
|
+
.split(",")
|
|
4789
|
+
.map((status) => status.trim())
|
|
4790
|
+
.filter(Boolean),
|
|
4791
|
+
),
|
|
4792
|
+
].sort()
|
|
4793
|
+
: [];
|
|
4794
|
+
const parsedStatuses = z
|
|
4795
|
+
.array(
|
|
4796
|
+
z.enum([
|
|
4797
|
+
"queued",
|
|
4798
|
+
"running",
|
|
4799
|
+
"idle",
|
|
4800
|
+
"requires_action",
|
|
4801
|
+
"recovering",
|
|
4802
|
+
"waiting_capacity",
|
|
4803
|
+
"failed",
|
|
4804
|
+
"cancelled",
|
|
4805
|
+
]),
|
|
4806
|
+
)
|
|
4807
|
+
.max(8)
|
|
4808
|
+
.safeParse(statuses);
|
|
4809
|
+
if (!parsedStatuses.success) {
|
|
4810
|
+
throw new HTTPException(400, { message: "statuses contains an unsupported lifecycle state" });
|
|
4811
|
+
}
|
|
4812
|
+
const activeOnly = query.activeOnly === "true";
|
|
4813
|
+
if (
|
|
4814
|
+
query.activeOnly !== undefined &&
|
|
4815
|
+
query.activeOnly !== "true" &&
|
|
4816
|
+
query.activeOnly !== "false"
|
|
4817
|
+
) {
|
|
4818
|
+
throw new HTTPException(400, { message: "activeOnly must be true or false" });
|
|
4819
|
+
}
|
|
4820
|
+
const recentHours = query.recentHours === undefined ? undefined : Number(query.recentHours);
|
|
4821
|
+
if (
|
|
4822
|
+
recentHours !== undefined &&
|
|
4823
|
+
(!Number.isSafeInteger(recentHours) ||
|
|
4824
|
+
recentHours < 1 ||
|
|
4825
|
+
recentHours > WORK_DISCOVERY_RECENT_HOURS_MAX)
|
|
4826
|
+
) {
|
|
4589
4827
|
throw new HTTPException(400, {
|
|
4590
|
-
message:
|
|
4828
|
+
message: `recentHours must be an integer between 1 and ${WORK_DISCOVERY_RECENT_HOURS_MAX}`,
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
const subjectFields = [query.subjectNamespace, query.subjectType, query.subjectKey];
|
|
4832
|
+
if (subjectFields.some((value) => value !== undefined) && subjectFields.some((value) => !value)) {
|
|
4833
|
+
throw new HTTPException(400, {
|
|
4834
|
+
message: "subjectNamespace, subjectType, and subjectKey must be supplied together",
|
|
4591
4835
|
});
|
|
4592
4836
|
}
|
|
4593
|
-
|
|
4837
|
+
const parsedSubject = subjectFields.every((value) => value !== undefined)
|
|
4838
|
+
? WorkClaimSubjectFilterSchema.safeParse({
|
|
4839
|
+
namespace: normalizeWorkClaimNamespace(query.subjectNamespace!),
|
|
4840
|
+
type: query.subjectType,
|
|
4841
|
+
canonicalKey: normalizeWorkClaimCanonicalKey(query.subjectKey!),
|
|
4842
|
+
})
|
|
4843
|
+
: null;
|
|
4844
|
+
if (parsedSubject && !parsedSubject.success) {
|
|
4845
|
+
throw new HTTPException(400, { message: "exact subject filter is invalid" });
|
|
4846
|
+
}
|
|
4847
|
+
const subject = parsedSubject?.success ? parsedSubject.data : undefined;
|
|
4848
|
+
if (searchQuery && subject) {
|
|
4849
|
+
throw new HTTPException(400, { message: "query cannot be combined with an exact subject" });
|
|
4850
|
+
}
|
|
4851
|
+
const relevanceRequested = Boolean(searchQuery || subject);
|
|
4852
|
+
const parentSessionId =
|
|
4853
|
+
rawParent === undefined
|
|
4854
|
+
? relevanceRequested
|
|
4855
|
+
? undefined
|
|
4856
|
+
: null
|
|
4857
|
+
: rawParent === "null"
|
|
4858
|
+
? null
|
|
4859
|
+
: rawParent;
|
|
4860
|
+
const claimLimit = query.claimLimit === undefined ? undefined : Number(query.claimLimit);
|
|
4861
|
+
if (
|
|
4862
|
+
claimLimit !== undefined &&
|
|
4863
|
+
(!Number.isSafeInteger(claimLimit) || claimLimit < 1 || claimLimit > WORK_CLAIM_DISCOVERY_LIMIT)
|
|
4864
|
+
) {
|
|
4594
4865
|
throw new HTTPException(400, {
|
|
4595
|
-
message:
|
|
4866
|
+
message: `claimLimit must be an integer between 1 and ${WORK_CLAIM_DISCOVERY_LIMIT}`,
|
|
4596
4867
|
});
|
|
4597
4868
|
}
|
|
4598
4869
|
const envelope = query.cursor ? decodeAgentTopologyCursor(query.cursor) : undefined;
|
|
4870
|
+
const expectedEnvelope = {
|
|
4871
|
+
parentSessionId: parentSessionId === undefined ? "all" : parentSessionId,
|
|
4872
|
+
rootSessionId: rootSessionId ?? null,
|
|
4873
|
+
query: searchQuery || null,
|
|
4874
|
+
statuses: parsedStatuses.data,
|
|
4875
|
+
activeOnly,
|
|
4876
|
+
recentHours: recentHours ?? null,
|
|
4877
|
+
subject: subject ?? null,
|
|
4878
|
+
claimLimit: claimLimit ?? null,
|
|
4879
|
+
};
|
|
4599
4880
|
if (
|
|
4600
4881
|
envelope &&
|
|
4601
|
-
|
|
4882
|
+
JSON.stringify({
|
|
4883
|
+
parentSessionId: envelope.parentSessionId,
|
|
4884
|
+
rootSessionId: envelope.rootSessionId,
|
|
4885
|
+
query: envelope.query,
|
|
4886
|
+
statuses: envelope.statuses,
|
|
4887
|
+
activeOnly: envelope.activeOnly,
|
|
4888
|
+
recentHours: envelope.recentHours,
|
|
4889
|
+
subject: envelope.subject,
|
|
4890
|
+
claimLimit: envelope.claimLimit,
|
|
4891
|
+
}) !== JSON.stringify(expectedEnvelope)
|
|
4602
4892
|
) {
|
|
4603
4893
|
throw new HTTPException(400, {
|
|
4604
4894
|
message: "agent topology cursor does not match its filters",
|
|
@@ -4607,7 +4897,13 @@ export function agentTopologyQuery(query: Record<string, string>): {
|
|
|
4607
4897
|
return {
|
|
4608
4898
|
limit,
|
|
4609
4899
|
parentSessionId,
|
|
4610
|
-
|
|
4900
|
+
rootSessionId: rootSessionId || undefined,
|
|
4901
|
+
query: searchQuery || undefined,
|
|
4902
|
+
statuses: parsedStatuses.data.length > 0 ? parsedStatuses.data : undefined,
|
|
4903
|
+
activeOnly,
|
|
4904
|
+
recentHours,
|
|
4905
|
+
subject,
|
|
4906
|
+
claimLimit,
|
|
4611
4907
|
cursor: envelope?.cursor,
|
|
4612
4908
|
};
|
|
4613
4909
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { WorkDiscoveryMatchClass } from "@opengeni/contracts";
|
|
2
|
+
import type { Observability } from "@opengeni/observability";
|
|
3
|
+
|
|
4
|
+
export type WorkDiscoverySurface = "first_party_mcp" | "agent_topology";
|
|
5
|
+
export type WorkDiscoveryMode = "browse" | "query" | "subject";
|
|
6
|
+
export type WorkDiscoveryOutcome = "ok" | "empty" | "disabled" | "error";
|
|
7
|
+
export type WorkDiscoveryAuthorizationScope = "workspace" | "scoped";
|
|
8
|
+
|
|
9
|
+
export type WorkDiscoveryObservation = {
|
|
10
|
+
surface: WorkDiscoverySurface;
|
|
11
|
+
mode: WorkDiscoveryMode;
|
|
12
|
+
outcome: WorkDiscoveryOutcome;
|
|
13
|
+
authorizationScope: WorkDiscoveryAuthorizationScope;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
responseBytes: number;
|
|
16
|
+
resultCount: number;
|
|
17
|
+
overlapCount: number;
|
|
18
|
+
matchCounts: Partial<Record<WorkDiscoveryMatchClass, number>>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type WorkDiscoveryObservedRow = {
|
|
22
|
+
relatedWork: {
|
|
23
|
+
match: { class: WorkDiscoveryMatchClass } | null;
|
|
24
|
+
possibleOverlap: boolean;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const RESULT_BUCKETS = [0, 1, 2, 4, 8, 16, 32, 64, 100];
|
|
29
|
+
const RESPONSE_BYTE_BUCKETS = [512, 1_024, 4_096, 16_384, 65_536, 128_000, 262_144];
|
|
30
|
+
const DURATION_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5];
|
|
31
|
+
|
|
32
|
+
export function summarizeWorkDiscoveryRows(
|
|
33
|
+
rows: readonly WorkDiscoveryObservedRow[],
|
|
34
|
+
): Pick<WorkDiscoveryObservation, "resultCount" | "overlapCount" | "matchCounts"> {
|
|
35
|
+
const matchCounts: WorkDiscoveryObservation["matchCounts"] = {};
|
|
36
|
+
let overlapCount = 0;
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
if (row.relatedWork.possibleOverlap) overlapCount += 1;
|
|
39
|
+
const matchClass = row.relatedWork.match?.class;
|
|
40
|
+
if (matchClass) matchCounts[matchClass] = (matchCounts[matchClass] ?? 0) + 1;
|
|
41
|
+
}
|
|
42
|
+
return { resultCount: rows.length, overlapCount, matchCounts };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Low-cardinality work-discovery telemetry. Identifiers, search text, subject
|
|
47
|
+
* keys, titles, goals, claim labels, versions, and provenance are excluded by
|
|
48
|
+
* construction. An observability failure never changes product behavior.
|
|
49
|
+
*/
|
|
50
|
+
export function observeWorkDiscovery(
|
|
51
|
+
observability: Observability | null | undefined,
|
|
52
|
+
observation: WorkDiscoveryObservation,
|
|
53
|
+
): void {
|
|
54
|
+
if (!observability) return;
|
|
55
|
+
const labels = {
|
|
56
|
+
surface: observation.surface,
|
|
57
|
+
mode: observation.mode,
|
|
58
|
+
outcome: observation.outcome,
|
|
59
|
+
authorization_scope: observation.authorizationScope,
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
observability.incrementCounter({
|
|
63
|
+
name: "opengeni_work_discovery_requests_total",
|
|
64
|
+
help: "Advisory work-discovery requests by bounded surface, mode, outcome, and authorization scope.",
|
|
65
|
+
labels,
|
|
66
|
+
});
|
|
67
|
+
observability.observeHistogram({
|
|
68
|
+
name: "opengeni_work_discovery_duration_seconds",
|
|
69
|
+
help: "Advisory work-discovery request duration in seconds.",
|
|
70
|
+
labels,
|
|
71
|
+
buckets: DURATION_BUCKETS,
|
|
72
|
+
value: Math.max(0, observation.durationMs) / 1_000,
|
|
73
|
+
});
|
|
74
|
+
observability.observeHistogram({
|
|
75
|
+
name: "opengeni_work_discovery_results",
|
|
76
|
+
help: "Bounded advisory work-discovery result rows per request.",
|
|
77
|
+
labels: { surface: observation.surface, mode: observation.mode },
|
|
78
|
+
buckets: RESULT_BUCKETS,
|
|
79
|
+
value: Math.max(0, Math.floor(observation.resultCount)),
|
|
80
|
+
});
|
|
81
|
+
observability.observeHistogram({
|
|
82
|
+
name: "opengeni_work_discovery_response_bytes",
|
|
83
|
+
help: "Serialized advisory work-discovery response bytes.",
|
|
84
|
+
labels: { surface: observation.surface, mode: observation.mode },
|
|
85
|
+
buckets: RESPONSE_BYTE_BUCKETS,
|
|
86
|
+
value: Math.max(0, Math.floor(observation.responseBytes)),
|
|
87
|
+
});
|
|
88
|
+
if (observation.overlapCount > 0) {
|
|
89
|
+
observability.incrementCounter({
|
|
90
|
+
name: "opengeni_work_discovery_overlap_results_total",
|
|
91
|
+
help: "Advisory discovery rows carrying a possible-overlap explanation.",
|
|
92
|
+
labels: { surface: observation.surface, mode: observation.mode },
|
|
93
|
+
amount: Math.floor(observation.overlapCount),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const matchClass of ["exact_subject", "title", "goal", "fuzzy"] as const) {
|
|
97
|
+
const amount = Math.floor(observation.matchCounts[matchClass] ?? 0);
|
|
98
|
+
if (amount < 1) continue;
|
|
99
|
+
observability.incrementCounter({
|
|
100
|
+
name: "opengeni_work_discovery_matches_total",
|
|
101
|
+
help: "Advisory discovery matches by stable explanation class.",
|
|
102
|
+
labels: {
|
|
103
|
+
surface: observation.surface,
|
|
104
|
+
mode: observation.mode,
|
|
105
|
+
match_class: matchClass,
|
|
106
|
+
},
|
|
107
|
+
amount,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
try {
|
|
112
|
+
observability.incrementCounter({
|
|
113
|
+
name: "opengeni_observability_observer_errors_total",
|
|
114
|
+
help: "Observability observer failures isolated from product execution.",
|
|
115
|
+
labels: { observer: "work_discovery" },
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
// The metrics registry itself is unhealthy. Discovery remains authoritative.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|