@m6d/cortex-server 2.1.0 → 2.3.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/README.md +23 -9
- package/contracts/README.md +22 -8
- package/contracts/src/client-tools/index.ts +56 -0
- package/contracts/src/graph/index.ts +30 -0
- package/contracts/{runtime.ts → src/runtime/index.ts} +7 -12
- package/contracts/{wire.ts → src/wire/index.ts} +25 -13
- package/dist/contracts/{graph.d.ts → src/graph/index.d.ts} +8 -8
- package/dist/contracts/{runtime.d.ts → src/runtime/index.d.ts} +9 -43
- package/dist/contracts/{wire.d.ts → src/wire/index.d.ts} +23 -13
- package/dist/src/lib/adapters/database/index.d.ts +0 -7
- package/dist/src/lib/adapters/database/message-content.d.ts +1 -2
- package/dist/src/lib/adapters/database/mssql/index.d.ts +0 -1
- package/dist/src/lib/adapters/database/mssql/messages.d.ts +0 -1
- package/dist/src/lib/adapters/database/postgres/index.d.ts +0 -1
- package/dist/src/lib/adapters/database/postgres/messages.d.ts +0 -1
- package/dist/src/lib/ai/cc-runtime.d.ts +2 -3
- package/dist/src/lib/ai/client-tools.d.ts +84 -0
- package/dist/src/lib/ai/tools/query-graph.tool.d.ts +1 -1
- package/dist/src/lib/ai/turn-tools.d.ts +8 -6
- package/dist/src/lib/cc/client.d.ts +4 -11
- package/dist/src/lib/cc/config-cache.d.ts +0 -1
- package/dist/src/lib/cc/registry.d.ts +6 -6
- package/dist/src/lib/cc/types.d.ts +1 -1
- package/dist/src/lib/config.d.ts +9 -4
- package/dist/src/lib/graph/index.d.ts +1 -1
- package/dist/src/lib/graph/resolver.d.ts +1 -1
- package/dist/src/lib/index.d.ts +2 -1
- package/dist/src/lib/types.d.ts +2 -2
- package/dist/src/lib/ws/connections.d.ts +1 -1
- package/package.json +2 -2
- package/src/lib/adapters/database/index.ts +0 -12
- package/src/lib/adapters/database/mssql/messages.ts +1 -28
- package/src/lib/adapters/database/postgres/messages.ts +1 -22
- package/src/lib/ai/cc-runtime.ts +7 -7
- package/src/lib/ai/client-tools.ts +288 -0
- package/src/lib/ai/index.ts +60 -47
- package/src/lib/ai/tools/search-tools.tool.ts +2 -2
- package/src/lib/ai/turn-tools.ts +7 -8
- package/src/lib/cc/client.ts +1 -1
- package/src/lib/cc/format.ts +2 -6
- package/src/lib/cc/registry.ts +7 -7
- package/src/lib/cc/types.ts +1 -1
- package/src/lib/config.ts +8 -3
- package/src/lib/index.ts +4 -0
- package/src/lib/routes/chat.ts +5 -5
- package/tsconfig.json +1 -1
- package/contracts/graph.ts +0 -36
- package/contracts/interactive.ts +0 -53
- package/dist/contracts/interactive.d.ts +0 -39
- package/dist/src/lib/ai/interactive.d.ts +0 -62
- package/src/lib/ai/interactive.ts +0 -364
- /package/contracts/{graph → src/graph/clients}/embed.ts +0 -0
- /package/contracts/{graph → src/graph/clients}/neo4j.ts +0 -0
- /package/contracts/{graph → src/graph}/helpers.ts +0 -0
- /package/contracts/{graph → src/graph}/schema.ts +0 -0
- /package/contracts/{graph → src/graph}/types.ts +0 -0
- /package/contracts/{rich-text.ts → src/rich-text/index.ts} +0 -0
- /package/dist/contracts/{graph → src/graph/clients}/embed.d.ts +0 -0
- /package/dist/contracts/{graph → src/graph/clients}/neo4j.d.ts +0 -0
- /package/dist/contracts/{graph → src/graph}/helpers.d.ts +0 -0
- /package/dist/contracts/{graph → src/graph}/schema.d.ts +0 -0
- /package/dist/contracts/{graph → src/graph}/types.d.ts +0 -0
- /package/dist/contracts/{rich-text.d.ts → src/rich-text/index.d.ts} +0 -0
package/contracts/graph.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The graph contract — the knowledge-graph vocabulary, authoring types and
|
|
3
|
-
* clients that `@m6d/cortex-cli` writes with and `@m6d/cortex-server` reads
|
|
4
|
-
* with. `@m6d/cortex-server` re-exports all of it, so a project authoring
|
|
5
|
-
* domains still has exactly one import path.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export type { GraphSchema } from "./graph/schema";
|
|
9
|
-
export { GRAPH_SCHEMA, GRAPH_SCHEMA_VERSION } from "./graph/schema";
|
|
10
|
-
|
|
11
|
-
export type {
|
|
12
|
-
EndpointScalarType,
|
|
13
|
-
EndpointProperty,
|
|
14
|
-
ResponseKind,
|
|
15
|
-
AutoGenerated,
|
|
16
|
-
ConceptDef,
|
|
17
|
-
EndpointDef,
|
|
18
|
-
EndpointInput,
|
|
19
|
-
ServiceDef,
|
|
20
|
-
RuleDef,
|
|
21
|
-
DomainDef,
|
|
22
|
-
} from "./graph/types";
|
|
23
|
-
|
|
24
|
-
export {
|
|
25
|
-
defineConcept,
|
|
26
|
-
defineRule,
|
|
27
|
-
defineService,
|
|
28
|
-
defineDomain,
|
|
29
|
-
defineEndpoint,
|
|
30
|
-
} from "./graph/helpers";
|
|
31
|
-
|
|
32
|
-
export type { Neo4jConfig, Neo4jClient } from "./graph/neo4j";
|
|
33
|
-
export { createNeo4jClient } from "./graph/neo4j";
|
|
34
|
-
|
|
35
|
-
export type { EmbedFn, EmbeddingProviderConfig } from "./graph/embed";
|
|
36
|
-
export { createEmbedder } from "./graph/embed";
|
package/contracts/interactive.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The embed ↔ widget handshake for interactive tools.
|
|
3
|
-
*
|
|
4
|
-
* A page embedded by the chat widget (a hosted checkout, signing page, …)
|
|
5
|
-
* reports its outcome with:
|
|
6
|
-
*
|
|
7
|
-
* window.parent.postMessage(
|
|
8
|
-
* { type: "cortex:interactive", status: "completed", reference: "session_123" },
|
|
9
|
-
* "*",
|
|
10
|
-
* );
|
|
11
|
-
*
|
|
12
|
-
* The widget accepts the message only when the event's origin matches the
|
|
13
|
-
* tool's configured `embedOrigin`, and treats the payload as untrusted — for
|
|
14
|
-
* tools with a verify endpoint the server re-checks the reference before the
|
|
15
|
-
* agent sees a result.
|
|
16
|
-
*
|
|
17
|
-
* Like `wire.ts`, this file is compiled into the client SDKs and must stay
|
|
18
|
-
* free of runtime dependencies. It is also the spec any future SDK (Flutter)
|
|
19
|
-
* reimplements against.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
export const INTERACTIVE_MESSAGE_TYPE = "cortex:interactive";
|
|
23
|
-
|
|
24
|
-
export const INTERACTIVE_STATUSES = ["completed", "cancelled", "failed"] as const;
|
|
25
|
-
|
|
26
|
-
export type InteractiveStatus = (typeof INTERACTIVE_STATUSES)[number];
|
|
27
|
-
|
|
28
|
-
export type InteractiveHandshake = {
|
|
29
|
-
type: typeof INTERACTIVE_MESSAGE_TYPE;
|
|
30
|
-
status: InteractiveStatus;
|
|
31
|
-
/** Opaque id the tool's verify endpoint resolves (session id, envelope id, …). */
|
|
32
|
-
reference?: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Parse a `message` event into a handshake, or null when the origin doesn't
|
|
37
|
-
* match the tool's `embedOrigin` or the payload isn't a well-formed handshake.
|
|
38
|
-
* Call from the widget's `message` listener with `event.data` / `event.origin`.
|
|
39
|
-
*/
|
|
40
|
-
export function parseInteractiveHandshake(data: unknown, origin: string, embedOrigin: string) {
|
|
41
|
-
if (origin !== embedOrigin) return null;
|
|
42
|
-
if (typeof data !== "object" || data === null) return null;
|
|
43
|
-
const message = data as Record<string, unknown>;
|
|
44
|
-
if (message["type"] !== INTERACTIVE_MESSAGE_TYPE) return null;
|
|
45
|
-
const status = INTERACTIVE_STATUSES.find((known) => known === message["status"]);
|
|
46
|
-
if (!status) return null;
|
|
47
|
-
const reference = message["reference"];
|
|
48
|
-
return {
|
|
49
|
-
type: INTERACTIVE_MESSAGE_TYPE,
|
|
50
|
-
status,
|
|
51
|
-
...(typeof reference === "string" ? { reference } : {}),
|
|
52
|
-
} satisfies InteractiveHandshake;
|
|
53
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The embed ↔ widget handshake for interactive tools.
|
|
3
|
-
*
|
|
4
|
-
* A page embedded by the chat widget (a hosted checkout, signing page, …)
|
|
5
|
-
* reports its outcome with:
|
|
6
|
-
*
|
|
7
|
-
* window.parent.postMessage(
|
|
8
|
-
* { type: "cortex:interactive", status: "completed", reference: "session_123" },
|
|
9
|
-
* "*",
|
|
10
|
-
* );
|
|
11
|
-
*
|
|
12
|
-
* The widget accepts the message only when the event's origin matches the
|
|
13
|
-
* tool's configured `embedOrigin`, and treats the payload as untrusted — for
|
|
14
|
-
* tools with a verify endpoint the server re-checks the reference before the
|
|
15
|
-
* agent sees a result.
|
|
16
|
-
*
|
|
17
|
-
* Like `wire.ts`, this file is compiled into the client SDKs and must stay
|
|
18
|
-
* free of runtime dependencies. It is also the spec any future SDK (Flutter)
|
|
19
|
-
* reimplements against.
|
|
20
|
-
*/
|
|
21
|
-
export declare const INTERACTIVE_MESSAGE_TYPE = "cortex:interactive";
|
|
22
|
-
export declare const INTERACTIVE_STATUSES: readonly ["completed", "cancelled", "failed"];
|
|
23
|
-
export type InteractiveStatus = (typeof INTERACTIVE_STATUSES)[number];
|
|
24
|
-
export type InteractiveHandshake = {
|
|
25
|
-
type: typeof INTERACTIVE_MESSAGE_TYPE;
|
|
26
|
-
status: InteractiveStatus;
|
|
27
|
-
/** Opaque id the tool's verify endpoint resolves (session id, envelope id, …). */
|
|
28
|
-
reference?: string;
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Parse a `message` event into a handshake, or null when the origin doesn't
|
|
32
|
-
* match the tool's `embedOrigin` or the payload isn't a well-formed handshake.
|
|
33
|
-
* Call from the widget's `message` listener with `event.data` / `event.origin`.
|
|
34
|
-
*/
|
|
35
|
-
export declare function parseInteractiveHandshake(data: unknown, origin: string, embedOrigin: string): {
|
|
36
|
-
reference?: string | undefined;
|
|
37
|
-
type: "cortex:interactive";
|
|
38
|
-
status: "failed" | "completed" | "cancelled";
|
|
39
|
-
} | null;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { ResolvedCortexAgentConfig } from "../config";
|
|
2
|
-
import type { ChatMessage, Thread } from "../types";
|
|
3
|
-
import { ControlCenterClient } from "../cc/client";
|
|
4
|
-
import type { CcRuntime } from "../cc/registry";
|
|
5
|
-
/**
|
|
6
|
-
* The wire-shape bindings stamped on the turn's final assistant message, so a
|
|
7
|
-
* parked interactive call can be initiated and verified after any restart.
|
|
8
|
-
* Only names that actually won declaration merging are stamped — a call to a
|
|
9
|
-
* colliding consumer-owned tool must never be treated as interactive. Undefined
|
|
10
|
-
* when nothing qualifies, keeping metadata lean.
|
|
11
|
-
*/
|
|
12
|
-
export declare function interactiveToolBindings(cc: CcRuntime | undefined, takenNames: ReadonlySet<string>): {
|
|
13
|
-
[k: string]: {
|
|
14
|
-
surface: "inline" | "modal";
|
|
15
|
-
embedOrigin: string;
|
|
16
|
-
hasVerify: boolean;
|
|
17
|
-
resultDelivery: "agent" | "endpoint" | "both";
|
|
18
|
-
toolId: string;
|
|
19
|
-
};
|
|
20
|
-
} | undefined;
|
|
21
|
-
/** Null when the agent has no Control Center — every caller treats that as "skip". */
|
|
22
|
-
export declare function createCcClient(config: ResolvedCortexAgentConfig): ControlCenterClient | null;
|
|
23
|
-
/**
|
|
24
|
-
* `POST /chat/:chatId/tools/:toolCallId/initiate` — runs the interactive
|
|
25
|
-
* tool's initiate call (the tool's own endpoint) and hands the widget its
|
|
26
|
-
* embed payload. The model never sees this payload. The idempotency key is
|
|
27
|
-
* pinned to the tool call, so a reload mid-flow reuses the created session
|
|
28
|
-
* instead of opening a second one.
|
|
29
|
-
*/
|
|
30
|
-
export declare function initiateInteractiveTool(options: {
|
|
31
|
-
config: ResolvedCortexAgentConfig;
|
|
32
|
-
thread: Thread;
|
|
33
|
-
userId: string;
|
|
34
|
-
token: string;
|
|
35
|
-
toolCallId: string;
|
|
36
|
-
}): Promise<{
|
|
37
|
-
interactive: false;
|
|
38
|
-
embedUrl?: undefined;
|
|
39
|
-
surface?: undefined;
|
|
40
|
-
embedOrigin?: undefined;
|
|
41
|
-
} | {
|
|
42
|
-
interactive: true;
|
|
43
|
-
embedUrl: string;
|
|
44
|
-
surface: "inline" | "modal";
|
|
45
|
-
embedOrigin: string;
|
|
46
|
-
}>;
|
|
47
|
-
/**
|
|
48
|
-
* The trust boundary for interactive results: whatever the client submitted
|
|
49
|
-
* for a bound tool call is replaced wholesale — cancelled/failed pass through
|
|
50
|
-
* normalized, completed is re-established by the tool's verify call when it
|
|
51
|
-
* has one, and result delivery masking is applied before the model reads it.
|
|
52
|
-
* Runs before the answering turn is persisted, so both the stored transcript
|
|
53
|
-
* and the model context carry only settled results.
|
|
54
|
-
*/
|
|
55
|
-
export declare function settleInteractiveToolResults(options: {
|
|
56
|
-
messages: ChatMessage[];
|
|
57
|
-
thread: Thread;
|
|
58
|
-
userId: string;
|
|
59
|
-
token: string;
|
|
60
|
-
config: ResolvedCortexAgentConfig;
|
|
61
|
-
ccClient: ControlCenterClient | null;
|
|
62
|
-
}): Promise<void>;
|
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
import { HTTPException } from "hono/http-exception";
|
|
2
|
-
import type { InteractiveInitiateResult, MessageMetadata } from "@cortex/contracts/wire";
|
|
3
|
-
import { INTERACTIVE_STATUSES } from "@cortex/contracts/interactive";
|
|
4
|
-
import type { ResolvedCortexAgentConfig } from "@/config";
|
|
5
|
-
import type { ChatMessage, Thread } from "@/types";
|
|
6
|
-
import { ControlCenterClient } from "@/cc/client";
|
|
7
|
-
import type { CcRuntime } from "@/cc/registry";
|
|
8
|
-
import { recordRecentCcTool } from "@/cc/registry";
|
|
9
|
-
|
|
10
|
-
type InteractiveBinding = NonNullable<MessageMetadata["interactiveTools"]>[string];
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The wire-shape bindings stamped on the turn's final assistant message, so a
|
|
14
|
-
* parked interactive call can be initiated and verified after any restart.
|
|
15
|
-
* Only names that actually won declaration merging are stamped — a call to a
|
|
16
|
-
* colliding consumer-owned tool must never be treated as interactive. Undefined
|
|
17
|
-
* when nothing qualifies, keeping metadata lean.
|
|
18
|
-
*/
|
|
19
|
-
export function interactiveToolBindings(
|
|
20
|
-
cc: CcRuntime | undefined,
|
|
21
|
-
takenNames: ReadonlySet<string>,
|
|
22
|
-
) {
|
|
23
|
-
if (!cc) return undefined;
|
|
24
|
-
const entries = [...cc.interactiveTools]
|
|
25
|
-
.filter(([name]) => !takenNames.has(name))
|
|
26
|
-
.map(
|
|
27
|
-
([name, tool]) =>
|
|
28
|
-
[
|
|
29
|
-
name,
|
|
30
|
-
{ toolId: tool.toolId, ...tool.interaction } satisfies InteractiveBinding,
|
|
31
|
-
] as const,
|
|
32
|
-
);
|
|
33
|
-
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Null when the agent has no Control Center — every caller treats that as "skip". */
|
|
37
|
-
export function createCcClient(config: ResolvedCortexAgentConfig) {
|
|
38
|
-
return config.controlCenter ? new ControlCenterClient(config.controlCenter) : null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** How long initiate waits out the turn-commit race before giving up. */
|
|
42
|
-
const PENDING_CALL_RETRIES = 6;
|
|
43
|
-
const PENDING_CALL_RETRY_MS = 700;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The widget initiates the moment its stream goes idle, which can beat the
|
|
47
|
-
* turn's commit to the database — so a missing call is retried briefly before
|
|
48
|
-
* it becomes a 404. An *answered* call 404s immediately: it was settled
|
|
49
|
-
* elsewhere (another tab) and the widget should fall back to its pill.
|
|
50
|
-
*/
|
|
51
|
-
async function findPendingInteractiveCall(
|
|
52
|
-
config: ResolvedCortexAgentConfig,
|
|
53
|
-
userId: string,
|
|
54
|
-
threadId: string,
|
|
55
|
-
toolCallId: string,
|
|
56
|
-
) {
|
|
57
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
58
|
-
const stored = await config.db.messages.list(userId, threadId);
|
|
59
|
-
const messages = stored.map((row) => row.content);
|
|
60
|
-
const answered = messages.some((candidate) =>
|
|
61
|
-
candidate.parts.some(
|
|
62
|
-
(part) => part.type === "tool-result" && part.toolCallId === toolCallId,
|
|
63
|
-
),
|
|
64
|
-
);
|
|
65
|
-
if (answered) return null;
|
|
66
|
-
|
|
67
|
-
for (const message of messages) {
|
|
68
|
-
const call = message.parts.find(
|
|
69
|
-
(part) => part.type === "tool-call" && part.id === toolCallId,
|
|
70
|
-
);
|
|
71
|
-
if (call?.type === "tool-call") return { message, call };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (attempt >= PENDING_CALL_RETRIES) return null;
|
|
75
|
-
await new Promise((resolve) => setTimeout(resolve, PENDING_CALL_RETRY_MS));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* `POST /chat/:chatId/tools/:toolCallId/initiate` — runs the interactive
|
|
81
|
-
* tool's initiate call (the tool's own endpoint) and hands the widget its
|
|
82
|
-
* embed payload. The model never sees this payload. The idempotency key is
|
|
83
|
-
* pinned to the tool call, so a reload mid-flow reuses the created session
|
|
84
|
-
* instead of opening a second one.
|
|
85
|
-
*/
|
|
86
|
-
export async function initiateInteractiveTool(options: {
|
|
87
|
-
config: ResolvedCortexAgentConfig;
|
|
88
|
-
thread: Thread;
|
|
89
|
-
userId: string;
|
|
90
|
-
token: string;
|
|
91
|
-
toolCallId: string;
|
|
92
|
-
}) {
|
|
93
|
-
const { config, thread, userId, token, toolCallId } = options;
|
|
94
|
-
const pending = await findPendingInteractiveCall(config, userId, thread.id, toolCallId);
|
|
95
|
-
if (!pending) {
|
|
96
|
-
throw new HTTPException(404, { message: "No pending tool call with this id" });
|
|
97
|
-
}
|
|
98
|
-
const { message, call } = pending;
|
|
99
|
-
|
|
100
|
-
const binding = message.metadata?.interactiveTools?.[call.name];
|
|
101
|
-
const ccClient = createCcClient(config);
|
|
102
|
-
if (!binding || !ccClient) {
|
|
103
|
-
return { interactive: false } satisfies InteractiveInitiateResult;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const result = await ccClient.execute(
|
|
107
|
-
config.agentId,
|
|
108
|
-
binding.toolId,
|
|
109
|
-
{
|
|
110
|
-
input: asRecord(call.input) ?? asRecord(parseJson(call.arguments)) ?? {},
|
|
111
|
-
context: { threadId: thread.id, userId },
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
readOnly: false,
|
|
115
|
-
endUserToken: token,
|
|
116
|
-
threadId: thread.id,
|
|
117
|
-
turnKey: toolCallId,
|
|
118
|
-
stepIndex: 0,
|
|
119
|
-
callIndex: 0,
|
|
120
|
-
idempotencyKey: `${thread.id}:${toolCallId}:initiate`,
|
|
121
|
-
},
|
|
122
|
-
);
|
|
123
|
-
if (!result) {
|
|
124
|
-
throw new HTTPException(502, { message: "The tool is temporarily unavailable" });
|
|
125
|
-
}
|
|
126
|
-
if ("error" in result) {
|
|
127
|
-
throw new HTTPException(502, { message: `initiate failed: ${result.error.kind}` });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const embedUrl = asRecord(result.output)?.embedUrl;
|
|
131
|
-
if (typeof embedUrl !== "string" || !isOnOrigin(embedUrl, binding.embedOrigin)) {
|
|
132
|
-
throw new HTTPException(502, {
|
|
133
|
-
message: "initiate did not return an embedUrl on the configured origin",
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Anchor the trusted reference to this call: a verified completion may
|
|
138
|
-
// only settle against the session this initiate created, never against
|
|
139
|
-
// whichever reference the browser chooses to report.
|
|
140
|
-
if (binding.hasVerify) {
|
|
141
|
-
const reference = asRecord(result.output)?.reference;
|
|
142
|
-
if (typeof reference !== "string" || !reference) {
|
|
143
|
-
throw new HTTPException(502, {
|
|
144
|
-
message: "initiate did not return the reference its verify endpoint needs",
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
await config.db.messages.mergeInteractiveReference(
|
|
148
|
-
thread.id,
|
|
149
|
-
message.id,
|
|
150
|
-
toolCallId,
|
|
151
|
-
reference,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
interactive: true,
|
|
157
|
-
embedUrl,
|
|
158
|
-
surface: binding.surface,
|
|
159
|
-
embedOrigin: binding.embedOrigin,
|
|
160
|
-
} satisfies InteractiveInitiateResult;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* The trust boundary for interactive results: whatever the client submitted
|
|
165
|
-
* for a bound tool call is replaced wholesale — cancelled/failed pass through
|
|
166
|
-
* normalized, completed is re-established by the tool's verify call when it
|
|
167
|
-
* has one, and result delivery masking is applied before the model reads it.
|
|
168
|
-
* Runs before the answering turn is persisted, so both the stored transcript
|
|
169
|
-
* and the model context carry only settled results.
|
|
170
|
-
*/
|
|
171
|
-
export async function settleInteractiveToolResults(options: {
|
|
172
|
-
messages: ChatMessage[];
|
|
173
|
-
thread: Thread;
|
|
174
|
-
userId: string;
|
|
175
|
-
token: string;
|
|
176
|
-
config: ResolvedCortexAgentConfig;
|
|
177
|
-
ccClient: ControlCenterClient | null;
|
|
178
|
-
}) {
|
|
179
|
-
// Deliberately no early-out on a missing CC client: normalization must run
|
|
180
|
-
// for every bound result regardless — only the verify call needs CC, and
|
|
181
|
-
// without it a claimed completion fails rather than passing through.
|
|
182
|
-
const { messages, thread, userId, token, config, ccClient } = options;
|
|
183
|
-
const answering = messages.filter(
|
|
184
|
-
(message) =>
|
|
185
|
-
message.role === "assistant" &&
|
|
186
|
-
message.parts.some((part) => part.type === "tool-result"),
|
|
187
|
-
);
|
|
188
|
-
if (answering.length === 0) return;
|
|
189
|
-
|
|
190
|
-
const stored = await config.db.messages.list(userId, thread.id);
|
|
191
|
-
const storedMessages = stored.map((row) => row.content);
|
|
192
|
-
const pendingCalls = collectPendingCalls(storedMessages);
|
|
193
|
-
if (pendingCalls.size === 0) return;
|
|
194
|
-
|
|
195
|
-
const context = {
|
|
196
|
-
ccClient,
|
|
197
|
-
agentId: config.agentId,
|
|
198
|
-
threadId: thread.id,
|
|
199
|
-
userId,
|
|
200
|
-
token,
|
|
201
|
-
references: collectReferences(storedMessages),
|
|
202
|
-
};
|
|
203
|
-
for (const message of answering) {
|
|
204
|
-
await settleMessage(message, pendingCalls, context);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function settleMessage(
|
|
209
|
-
message: ChatMessage,
|
|
210
|
-
pendingCalls: Map<string, PendingInteractiveCall>,
|
|
211
|
-
context: SettleContext,
|
|
212
|
-
) {
|
|
213
|
-
for (const part of message.parts) {
|
|
214
|
-
if (part.type !== "tool-result") continue;
|
|
215
|
-
const pending = pendingCalls.get(part.toolCallId);
|
|
216
|
-
if (!pending) continue;
|
|
217
|
-
|
|
218
|
-
recordRecentCcTool(context.threadId, pending.name);
|
|
219
|
-
const settled = await settleOne(pending.binding, part.content, part.toolCallId, context);
|
|
220
|
-
part.content = JSON.stringify(settled);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
type PendingInteractiveCall = { name: string; binding: InteractiveBinding };
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* The authoritative view of what may settle: stored, unanswered tool calls
|
|
228
|
-
* whose own message carries a binding for their name, keyed by call id. The
|
|
229
|
-
* incoming message's tool-call parts are client-authored and never consulted —
|
|
230
|
-
* a fabricated call/result pair matches nothing here.
|
|
231
|
-
*/
|
|
232
|
-
function collectPendingCalls(messages: ChatMessage[]) {
|
|
233
|
-
const answered = answeredCallIds(messages);
|
|
234
|
-
const pending = new Map<string, PendingInteractiveCall>();
|
|
235
|
-
for (const message of messages) {
|
|
236
|
-
const bindings = message.metadata?.interactiveTools;
|
|
237
|
-
if (!bindings) continue;
|
|
238
|
-
for (const part of message.parts) {
|
|
239
|
-
if (part.type !== "tool-call" || answered.has(part.id)) continue;
|
|
240
|
-
const binding = bindings[part.name];
|
|
241
|
-
if (binding) pending.set(part.id, { name: part.name, binding });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return pending;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function answeredCallIds(messages: ChatMessage[]) {
|
|
248
|
-
const answered = new Set<string>();
|
|
249
|
-
for (const message of messages) {
|
|
250
|
-
for (const part of message.parts) {
|
|
251
|
-
if (part.type === "tool-result") answered.add(part.toolCallId);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return answered;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** The references initiate anchored, keyed by tool call id. */
|
|
258
|
-
function collectReferences(messages: ChatMessage[]) {
|
|
259
|
-
const references = new Map<string, string>();
|
|
260
|
-
for (const message of messages) {
|
|
261
|
-
for (const [toolCallId, reference] of Object.entries(
|
|
262
|
-
message.metadata?.interactiveReferences ?? {},
|
|
263
|
-
)) {
|
|
264
|
-
references.set(toolCallId, reference);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return references;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
type SettleContext = {
|
|
271
|
-
ccClient: ControlCenterClient | null;
|
|
272
|
-
agentId: string;
|
|
273
|
-
threadId: string;
|
|
274
|
-
userId: string;
|
|
275
|
-
token: string;
|
|
276
|
-
references: Map<string, string>;
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
async function settleOne(
|
|
280
|
-
binding: InteractiveBinding,
|
|
281
|
-
content: string | unknown[],
|
|
282
|
-
toolCallId: string,
|
|
283
|
-
context: SettleContext,
|
|
284
|
-
) {
|
|
285
|
-
const reported = parseReported(content);
|
|
286
|
-
if (reported.status === "cancelled") return { status: "cancelled" };
|
|
287
|
-
if (reported.status !== "completed") return { status: "failed" };
|
|
288
|
-
|
|
289
|
-
if (!binding.hasVerify) {
|
|
290
|
-
// A completion the server cannot re-establish is just the browser's
|
|
291
|
-
// word. Current Control Centers always configure verification, so this
|
|
292
|
-
// only fires against a foreign runtime — refuse rather than trust.
|
|
293
|
-
return { status: "failed", detail: "verification required" };
|
|
294
|
-
}
|
|
295
|
-
const anchored = context.references.get(toolCallId);
|
|
296
|
-
if (!anchored || reported.reference !== anchored) {
|
|
297
|
-
return { status: "failed", detail: "completion does not match the initiated session" };
|
|
298
|
-
}
|
|
299
|
-
if (!context.ccClient) {
|
|
300
|
-
return { status: "failed", detail: "verification unavailable" };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const result = await context.ccClient.execute(
|
|
304
|
-
context.agentId,
|
|
305
|
-
binding.toolId,
|
|
306
|
-
{
|
|
307
|
-
input: { reference: anchored },
|
|
308
|
-
phase: "verify",
|
|
309
|
-
context: { threadId: context.threadId, userId: context.userId },
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
readOnly: true,
|
|
313
|
-
endUserToken: context.token,
|
|
314
|
-
threadId: context.threadId,
|
|
315
|
-
turnKey: "verify",
|
|
316
|
-
stepIndex: 0,
|
|
317
|
-
callIndex: 0,
|
|
318
|
-
},
|
|
319
|
-
);
|
|
320
|
-
if (!result) return { status: "failed", detail: "verification unavailable" };
|
|
321
|
-
if ("error" in result) return { status: "failed", detail: result.error.kind };
|
|
322
|
-
return deliver(binding, reported.reference, result.output);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function deliver(binding: InteractiveBinding, reference: string | undefined, output: unknown) {
|
|
326
|
-
return binding.resultDelivery === "endpoint"
|
|
327
|
-
? { status: "completed", reference }
|
|
328
|
-
: { status: "completed", reference, result: output };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function parseReported(content: string | unknown[]) {
|
|
332
|
-
const record = asRecord(typeof content === "string" ? parseJson(content) : content);
|
|
333
|
-
const status = INTERACTIVE_STATUSES.find((known) => known === record?.status);
|
|
334
|
-
return {
|
|
335
|
-
status,
|
|
336
|
-
reference: typeof record?.reference === "string" ? record.reference : undefined,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function parseJson(value: string | undefined) {
|
|
341
|
-
if (value === undefined) return undefined;
|
|
342
|
-
try {
|
|
343
|
-
return JSON.parse(value) as unknown;
|
|
344
|
-
} catch {
|
|
345
|
-
return undefined;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function asRecord(value: unknown) {
|
|
350
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
351
|
-
? (value as Record<string, unknown>)
|
|
352
|
-
: undefined;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function isOnOrigin(url: string, origin: string) {
|
|
356
|
-
try {
|
|
357
|
-
const parsed = new URL(url);
|
|
358
|
-
// http(s) only: javascript:/data: URLs normalize to origin "null",
|
|
359
|
-
// which a bare equality check could otherwise be tricked into passing.
|
|
360
|
-
return ["http:", "https:"].includes(parsed.protocol) && parsed.origin === origin;
|
|
361
|
-
} catch {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|