@ziggs-ai/agent-sdk 0.1.12 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +41 -2
- package/dist/AgentHost.d.ts +201 -0
- package/dist/AgentHost.js +652 -0
- package/dist/adapters/AnthropicAdapter.d.ts +39 -0
- package/dist/adapters/AnthropicAdapter.js +200 -0
- package/dist/adapters/McpAdapter.d.ts +199 -0
- package/dist/adapters/McpAdapter.js +468 -0
- package/dist/adapters/OpenAIAdapter.d.ts +26 -0
- package/dist/adapters/OpenAIAdapter.js +158 -0
- package/dist/adapters/index.js +2 -0
- package/dist/agent/Agent.d.ts +78 -0
- package/dist/agent/Agent.js +58 -0
- package/dist/agent/ClaudeSdkAgent.d.ts +38 -0
- package/dist/agent/ClaudeSdkAgent.js +322 -0
- package/dist/agent/claudeCognition.d.ts +67 -0
- package/dist/agent/claudeCognition.js +103 -0
- package/dist/agent/claudeSdkProtocol.d.ts +11 -0
- package/dist/agent/claudeSdkTools.d.ts +19 -0
- package/dist/agent/claudeSdkTools.js +57 -0
- package/dist/agent/hostedAgentConnection.d.ts +15 -0
- package/dist/agent/hostedAgentConnection.js +29 -0
- package/dist/agent/toolManagerToSdkMcp.d.ts +12 -0
- package/dist/agent/toolManagerToSdkMcp.js +123 -0
- package/dist/brain/index.d.ts +50 -0
- package/dist/brain/index.js +45 -0
- package/dist/cognition/validateContext.d.ts +11 -0
- package/dist/cognition/validateContext.js +117 -0
- package/dist/context/applyEffects.d.ts +2 -0
- package/dist/context/applyEffects.js +89 -0
- package/dist/context/batch.d.ts +2 -0
- package/dist/context/batch.js +18 -0
- package/dist/context/classifyEnvelope.d.ts +1 -0
- package/dist/context/classifyEnvelope.js +58 -0
- package/dist/context/routingLabels.d.ts +4 -0
- package/dist/context/routingLabels.js +48 -0
- package/dist/defineAgent.d.ts +14 -0
- package/dist/defineAgent.js +68 -0
- package/dist/formatters/AgreementFormatter.d.ts +18 -0
- package/dist/formatters/AgreementFormatter.js +130 -0
- package/dist/formatters/HistoryFormatter.d.ts +26 -0
- package/dist/formatters/HistoryFormatter.js +173 -0
- package/dist/formatters/index.js +2 -0
- package/{src/index.ts → dist/index.d.ts} +7 -53
- package/dist/index.js +66 -0
- package/dist/ingress/normalizeIncoming.d.ts +65 -0
- package/dist/ingress/normalizeIncoming.js +88 -0
- package/dist/memory/MemoryStore.d.ts +55 -0
- package/dist/memory/MemoryStore.js +78 -0
- package/dist/pricing/fleetDefaults.d.ts +46 -0
- package/dist/pricing/fleetDefaults.js +168 -0
- package/dist/pricing/fleetEvalFree.d.ts +7 -0
- package/dist/pricing/fleetEvalFree.js +24 -0
- package/dist/pricing/fleetFreeTierA.gen.d.ts +6 -0
- package/{src/pricing/fleetFreeTierA.gen.ts → dist/pricing/fleetFreeTierA.gen.js} +5 -6
- package/dist/pricing/fleetTierByAgentId.gen.d.ts +7 -0
- package/dist/pricing/fleetTierByAgentId.gen.js +768 -0
- package/dist/runtime/AgentMachine.d.ts +143 -0
- package/dist/runtime/AgentMachine.js +400 -0
- package/dist/runtime/PromptBuilder.d.ts +96 -0
- package/dist/runtime/PromptBuilder.js +439 -0
- package/{src/runtime/bootstrapCtx.ts → dist/runtime/bootstrapCtx.d.ts} +1 -22
- package/dist/runtime/bootstrapCtx.js +37 -0
- package/dist/runtime/buildOutcome.d.ts +68 -0
- package/dist/runtime/buildOutcome.js +464 -0
- package/dist/runtime/defaults.d.ts +54 -0
- package/dist/runtime/defaults.js +47 -0
- package/dist/runtime/deriveRespondsToProposals.d.ts +9 -0
- package/dist/runtime/deriveRespondsToProposals.js +38 -0
- package/dist/runtime/kickoff.d.ts +15 -0
- package/dist/runtime/kickoff.js +112 -0
- package/dist/runtime/runMechanical.d.ts +25 -0
- package/dist/runtime/runMechanical.js +74 -0
- package/dist/runtime/runTurn.d.ts +63 -0
- package/dist/runtime/runTurn.js +800 -0
- package/dist/runtime/validateWorkflow.d.ts +17 -0
- package/dist/runtime/validateWorkflow.js +209 -0
- package/dist/server/ConnectionPool.d.ts +68 -0
- package/dist/server/ConnectionPool.js +166 -0
- package/dist/server/EventQueue.d.ts +33 -0
- package/dist/server/EventQueue.js +98 -0
- package/dist/server/InboxCatchUp.d.ts +24 -0
- package/dist/server/InboxCatchUp.js +214 -0
- package/dist/server/OutboxBuffer.d.ts +33 -0
- package/dist/server/OutboxBuffer.js +68 -0
- package/dist/server/SeenMessages.d.ts +13 -0
- package/dist/server/SeenMessages.js +31 -0
- package/dist/server/StateTimers.d.ts +34 -0
- package/dist/server/StateTimers.js +70 -0
- package/dist/server/ZiggsEffectHandler.d.ts +79 -0
- package/dist/server/ZiggsEffectHandler.js +319 -0
- package/dist/server/agreements/AgreementService.d.ts +50 -0
- package/dist/server/agreements/AgreementService.js +85 -0
- package/dist/server/createHealthServer.d.ts +7 -0
- package/dist/server/createHealthServer.js +75 -0
- package/dist/server/proactive/ProactiveTrigger.d.ts +33 -0
- package/dist/server/proactive/ProactiveTrigger.js +71 -0
- package/dist/server/runLauncher.d.ts +28 -0
- package/dist/server/runLauncher.js +93 -0
- package/dist/server/tasks/TaskService.d.ts +25 -0
- package/dist/server/tasks/TaskService.js +103 -0
- package/dist/server/tasks/index.js +1 -0
- package/dist/server/telemetryIngest.d.ts +24 -0
- package/dist/server/telemetryIngest.js +62 -0
- package/dist/server/tools/index.d.ts +24 -0
- package/{src/server/tools/index.ts → dist/server/tools/index.js} +4 -24
- package/dist/server/tools/tier1/protocolRunner.d.ts +39 -0
- package/dist/server/tools/tier1/protocolRunner.js +288 -0
- package/dist/server/tools/tier1/protocolTools.d.ts +12 -0
- package/dist/server/tools/tier1/protocolTools.js +37 -0
- package/dist/server/tools/tier2/connectionTools.d.ts +4 -0
- package/dist/server/tools/tier2/connectionTools.js +63 -0
- package/dist/server/tools/tier2/contextTools.d.ts +4 -0
- package/dist/server/tools/tier2/contextTools.js +46 -0
- package/dist/server/tools/tier2/discoveryTools.d.ts +4 -0
- package/dist/server/tools/tier2/discoveryTools.js +25 -0
- package/dist/server/tools/tier2/marketplaceTools.d.ts +3 -0
- package/dist/server/tools/tier2/marketplaceTools.js +22 -0
- package/dist/server/tools/tier2/paymentTools.d.ts +12 -0
- package/dist/server/tools/tier2/paymentTools.js +165 -0
- package/dist/server/ziggsconnect/ZiggsConnectClient.d.ts +35 -0
- package/dist/server/ziggsconnect/ZiggsConnectClient.js +95 -0
- package/dist/server/ziggscontext/ZiggsContextClient.d.ts +53 -0
- package/dist/server/ziggscontext/ZiggsContextClient.js +124 -0
- package/dist/server/ziggspay/ZiggsPayClient.d.ts +82 -0
- package/dist/server/ziggspay/ZiggsPayClient.js +203 -0
- package/dist/shared/ids.d.ts +1 -0
- package/dist/shared/ids.js +3 -0
- package/dist/shared/runtimeLog.d.ts +18 -0
- package/dist/shared/runtimeLog.js +68 -0
- package/dist/shared/types.d.ts +22 -0
- package/dist/shared/types.js +23 -0
- package/dist/tasks/protocolRegistry.d.ts +16 -0
- package/dist/tasks/protocolRegistry.js +20 -0
- package/dist/tools/ToolManager.d.ts +12 -0
- package/dist/tools/ToolManager.js +74 -0
- package/dist/tools/ToolProvider.d.ts +6 -0
- package/dist/tools/ToolProvider.js +21 -0
- package/dist/tools/defineTool.d.ts +27 -0
- package/dist/tools/defineTool.js +62 -0
- package/{src/tools/index.ts → dist/tools/index.d.ts} +0 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/recordReport.d.ts +41 -0
- package/dist/tools/recordReport.js +30 -0
- package/dist/types.d.ts +466 -0
- package/dist/types.js +7 -0
- package/dist/utils/jsonExtractor.d.ts +2 -0
- package/dist/utils/jsonExtractor.js +111 -0
- package/package.json +14 -6
- package/src/AgentHost.ts +0 -814
- package/src/adapters/AnthropicAdapter.ts +0 -238
- package/src/adapters/McpAdapter.ts +0 -678
- package/src/adapters/OpenAIAdapter.ts +0 -194
- package/src/agent/Agent.ts +0 -112
- package/src/agent/ClaudeSdkAgent.ts +0 -401
- package/src/agent/claudeCognition.ts +0 -139
- package/src/agent/claudeSdkTools.ts +0 -81
- package/src/agent/hostedAgentConnection.ts +0 -41
- package/src/agent/toolManagerToSdkMcp.ts +0 -136
- package/src/cognition/validateContext.ts +0 -95
- package/src/context/applyEffects.ts +0 -80
- package/src/context/batch.ts +0 -19
- package/src/context/classifyEnvelope.ts +0 -38
- package/src/context/routingLabels.ts +0 -46
- package/src/defineAgent.ts +0 -77
- package/src/formatters/AgreementFormatter.ts +0 -111
- package/src/formatters/HistoryFormatter.ts +0 -172
- package/src/ingress/normalizeIncoming.ts +0 -162
- package/src/memory/MemoryStore.ts +0 -104
- package/src/pricing/fleetDefaults.ts +0 -225
- package/src/pricing/fleetEvalFree.ts +0 -24
- package/src/pricing/fleetTierByAgentId.gen.ts +0 -775
- package/src/runtime/AgentMachine.ts +0 -484
- package/src/runtime/PromptBuilder.ts +0 -522
- package/src/runtime/buildOutcome.ts +0 -563
- package/src/runtime/defaults.ts +0 -86
- package/src/runtime/deriveRespondsToProposals.ts +0 -35
- package/src/runtime/kickoff.ts +0 -117
- package/src/runtime/runMechanical.ts +0 -106
- package/src/runtime/runTurn.ts +0 -870
- package/src/runtime/validateWorkflow.ts +0 -221
- package/src/server/ConnectionPool.ts +0 -206
- package/src/server/EventQueue.ts +0 -133
- package/src/server/InboxCatchUp.ts +0 -291
- package/src/server/OutboxBuffer.ts +0 -90
- package/src/server/SeenMessages.ts +0 -27
- package/src/server/StateTimers.ts +0 -85
- package/src/server/ZiggsEffectHandler.ts +0 -427
- package/src/server/agreements/AgreementService.ts +0 -118
- package/src/server/createHealthServer.ts +0 -85
- package/src/server/proactive/ProactiveTrigger.ts +0 -83
- package/src/server/runLauncher.ts +0 -146
- package/src/server/tasks/TaskService.ts +0 -111
- package/src/server/telemetryIngest.ts +0 -91
- package/src/server/tools/tier1/protocolRunner.ts +0 -353
- package/src/server/tools/tier1/protocolTools.ts +0 -114
- package/src/server/tools/tier2/connectionTools.ts +0 -75
- package/src/server/tools/tier2/contextTools.ts +0 -63
- package/src/server/tools/tier2/discoveryTools.ts +0 -34
- package/src/server/tools/tier2/marketplaceTools.ts +0 -27
- package/src/server/tools/tier2/paymentTools.ts +0 -170
- package/src/server/ziggsconnect/ZiggsConnectClient.ts +0 -126
- package/src/server/ziggscontext/ZiggsContextClient.ts +0 -129
- package/src/server/ziggspay/ZiggsPayClient.ts +0 -193
- package/src/shared/ids.ts +0 -3
- package/src/shared/runtimeLog.ts +0 -72
- package/src/shared/types.ts +0 -29
- package/src/tasks/protocolRegistry.ts +0 -25
- package/src/tools/ToolManager.ts +0 -95
- package/src/tools/ToolProvider.ts +0 -19
- package/src/tools/defineTool.ts +0 -90
- package/src/tools/recordReport.ts +0 -82
- package/src/types.ts +0 -483
- package/src/utils/jsonExtractor.ts +0 -100
- /package/{src/adapters/index.ts → dist/adapters/index.d.ts} +0 -0
- /package/{src/agent/claudeSdkProtocol.ts → dist/agent/claudeSdkProtocol.js} +0 -0
- /package/{src/formatters/index.ts → dist/formatters/index.d.ts} +0 -0
- /package/{src/server/tasks/index.ts → dist/server/tasks/index.d.ts} +0 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
+
import { AgentHost } from '../AgentHost.js';
|
|
5
|
+
import { thinkingDefaults } from '../runtime/defaults.js';
|
|
6
|
+
/**
|
|
7
|
+
* Typed failure from the Ziggs MCP gateway (ZIG-702). `code` is the stable
|
|
8
|
+
* machine-readable reason — e.g. `GRANT_REVOKED`, `GRANT_EXPIRED`,
|
|
9
|
+
* `GRANT_HOLDER_MISMATCH`, `GRANT_CONNECTION_MISMATCH` — so agent code can
|
|
10
|
+
* branch ("revoked → tell the user and stop retrying" vs "network blip →
|
|
11
|
+
* retry") instead of parsing transport-error prose.
|
|
12
|
+
*/
|
|
13
|
+
export class ZiggsGatewayError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
status;
|
|
16
|
+
constructor(code, message, status) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'ZiggsGatewayError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.status = status;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Wrap fetch so structured gateway denials (401/403 with a `code` body)
|
|
25
|
+
* surface as ZiggsGatewayError instead of a generic transport failure.
|
|
26
|
+
*/
|
|
27
|
+
export function wrapGatewayFetch(baseFetch = fetch) {
|
|
28
|
+
return (async (input, init) => {
|
|
29
|
+
const response = await baseFetch(input, init);
|
|
30
|
+
if (response.status === 401 || response.status === 403) {
|
|
31
|
+
const body = (await response
|
|
32
|
+
.clone()
|
|
33
|
+
.json()
|
|
34
|
+
.catch(() => null));
|
|
35
|
+
if (body && typeof body.code === 'string') {
|
|
36
|
+
throw new ZiggsGatewayError(body.code, typeof body.message === 'string' ? body.message : body.code, response.status);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return response;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Resolve the gateway URL + auth headers for a `type: 'ziggs'` transport. */
|
|
43
|
+
export function resolveZiggsGatewayTarget(config, defaults = {}) {
|
|
44
|
+
if (!config.connectionId) {
|
|
45
|
+
throw new Error('ziggs MCP transport requires a connectionId');
|
|
46
|
+
}
|
|
47
|
+
if (!config.grantId) {
|
|
48
|
+
throw new Error('ziggs MCP transport requires a grantId');
|
|
49
|
+
}
|
|
50
|
+
const operatorKey = config.operatorKey || process.env.ZIGGS_OPERATOR_KEY;
|
|
51
|
+
if (!operatorKey) {
|
|
52
|
+
throw new Error('ziggs MCP transport requires an operatorKey (or ZIGGS_OPERATOR_KEY)');
|
|
53
|
+
}
|
|
54
|
+
const agentId = config.agentId || defaults.agentId;
|
|
55
|
+
if (!agentId) {
|
|
56
|
+
throw new Error('ziggs MCP transport requires an agentId to impersonate (X-Agent-Id)');
|
|
57
|
+
}
|
|
58
|
+
const baseUrl = (config.baseUrl ||
|
|
59
|
+
process.env.BACKEND_URL ||
|
|
60
|
+
process.env.ZIGGS_BACKEND_URL ||
|
|
61
|
+
'http://localhost:3000').replace(/\/$/, '');
|
|
62
|
+
return {
|
|
63
|
+
url: new URL(`${baseUrl}/connections/${encodeURIComponent(config.connectionId)}/mcp`),
|
|
64
|
+
headers: {
|
|
65
|
+
authorization: `Bearer ${operatorKey}`,
|
|
66
|
+
'x-agent-id': agentId,
|
|
67
|
+
'x-grant-id': config.grantId,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the per-user gateway URL + headers (ZIG-771). No connectionId/grantId —
|
|
73
|
+
* the gateway resolves the invoking user's own connection + grant from
|
|
74
|
+
* `x-on-behalf-of-user` + the impersonated agent.
|
|
75
|
+
*/
|
|
76
|
+
export function resolvePerUserGatewayTarget(config, agentId, userId, onBehalfOfAgreementId) {
|
|
77
|
+
const operatorKey = config.operatorKey || process.env.ZIGGS_OPERATOR_KEY;
|
|
78
|
+
if (!operatorKey) {
|
|
79
|
+
throw new Error('ziggs-per-user MCP transport requires an operatorKey (or ZIGGS_OPERATOR_KEY)');
|
|
80
|
+
}
|
|
81
|
+
const baseUrl = (config.baseUrl ||
|
|
82
|
+
process.env.BACKEND_URL ||
|
|
83
|
+
process.env.ZIGGS_BACKEND_URL ||
|
|
84
|
+
'http://localhost:3000').replace(/\/$/, '');
|
|
85
|
+
return {
|
|
86
|
+
url: new URL(`${baseUrl}/connections/mcp/relay`),
|
|
87
|
+
headers: {
|
|
88
|
+
authorization: `Bearer ${operatorKey}`,
|
|
89
|
+
'x-agent-id': agentId,
|
|
90
|
+
'x-on-behalf-of-user': userId,
|
|
91
|
+
// ZIG-831: when this call runs under a delegation, userId is the delegating
|
|
92
|
+
// agent (senderId), not the end-user who owns the connection. The gateway
|
|
93
|
+
// uses this agreement to resolve the delegation root's user.
|
|
94
|
+
...(onBehalfOfAgreementId ? { 'x-on-behalf-of-agreement': onBehalfOfAgreementId } : {}),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Provider `tools` array cap (OpenAI's is 128). */
|
|
99
|
+
const DEFAULT_MAX_TOOLS = 128;
|
|
100
|
+
/** Headroom reserved for control actions (wait/respond) + future additions. */
|
|
101
|
+
const CONTROL_TOOL_RESERVE = 4;
|
|
102
|
+
/**
|
|
103
|
+
* How many MCP tools may be bound to the LLM, given the provider cap and the
|
|
104
|
+
* number of extra (non-MCP) tools the agent also carries. Always >= 1 so a
|
|
105
|
+
* pathological config still binds something rather than nothing. ZIG-759.
|
|
106
|
+
*/
|
|
107
|
+
export function mcpToolBudget(maxTools, extraToolCount) {
|
|
108
|
+
return Math.max(1, maxTools - CONTROL_TOOL_RESERVE - extraToolCount);
|
|
109
|
+
}
|
|
110
|
+
export function buildTransport(config, defaults = {}) {
|
|
111
|
+
if (config.type === 'stdio') {
|
|
112
|
+
return new StdioClientTransport({
|
|
113
|
+
command: config.command,
|
|
114
|
+
args: config.args ?? [],
|
|
115
|
+
env: config.env,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (config.type === 'ziggs') {
|
|
119
|
+
const target = resolveZiggsGatewayTarget(config, defaults);
|
|
120
|
+
return new StreamableHTTPClientTransport(target.url, {
|
|
121
|
+
requestInit: { headers: target.headers },
|
|
122
|
+
fetch: wrapGatewayFetch(),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (config.type === 'ziggs-per-user') {
|
|
126
|
+
// Per-user transports are built lazily per invoking user in createMcpAgent
|
|
127
|
+
// (each needs a different x-on-behalf-of-user header), not here.
|
|
128
|
+
throw new Error('ziggs-per-user transport is created per invoking user, not via buildTransport');
|
|
129
|
+
}
|
|
130
|
+
return new StreamableHTTPClientTransport(new URL(config.url), config.headers && Object.keys(config.headers).length > 0
|
|
131
|
+
? { requestInit: { headers: config.headers } }
|
|
132
|
+
: undefined);
|
|
133
|
+
}
|
|
134
|
+
function mcpContentToResult(content) {
|
|
135
|
+
const text = content
|
|
136
|
+
.map(c => (c.type === 'text' ? c.text ?? '' : JSON.stringify(c)))
|
|
137
|
+
.join('\n');
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(text);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return text;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function mapMcpTools(callTool, tools) {
|
|
146
|
+
return tools.map(tool => ({
|
|
147
|
+
schema: {
|
|
148
|
+
type: 'function',
|
|
149
|
+
function: {
|
|
150
|
+
name: tool.name,
|
|
151
|
+
description: tool.description ?? '',
|
|
152
|
+
parameters: tool.inputSchema ?? { type: 'object', properties: {} },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
handler: async (args, context) => {
|
|
156
|
+
const result = await callTool(tool.name, args, context);
|
|
157
|
+
if (result.isError) {
|
|
158
|
+
throw new Error(mcpContentToResult(result.content));
|
|
159
|
+
}
|
|
160
|
+
return mcpContentToResult(result.content);
|
|
161
|
+
},
|
|
162
|
+
isGenericFallback: true,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
function buildDefaultWorkflow(description, mcpTools) {
|
|
166
|
+
const dflt = thinkingDefaults({ initial: 'idle' });
|
|
167
|
+
// Bind each MCP tool as an explicit action so runTurn includes them in the
|
|
168
|
+
// LLM API call and the decision schema. Without this, allowedToolNames is
|
|
169
|
+
// empty and the tools array passed to the LLM is also empty.
|
|
170
|
+
const toolActions = Object.fromEntries(mcpTools.map(t => [t.name, {
|
|
171
|
+
tool: t.name,
|
|
172
|
+
prompt: { when: t.description || `Use ${t.name}` },
|
|
173
|
+
}]));
|
|
174
|
+
return {
|
|
175
|
+
id: 'mcp-agent',
|
|
176
|
+
description: description || 'MCP-backed agent',
|
|
177
|
+
initial: 'idle',
|
|
178
|
+
states: {
|
|
179
|
+
idle: {
|
|
180
|
+
kind: 'thinking',
|
|
181
|
+
prompt: {
|
|
182
|
+
role: 'You are a helpful agent powered by an MCP server.',
|
|
183
|
+
goal: description || 'Help the user by calling the available tools.',
|
|
184
|
+
constraints: 'Use your tools to answer the user\'s request. After gathering results, use "respond" to send the answer. Use "wait" only when no action is needed.',
|
|
185
|
+
},
|
|
186
|
+
actions: {
|
|
187
|
+
...dflt.actions,
|
|
188
|
+
...toolActions,
|
|
189
|
+
respond: {
|
|
190
|
+
prompt: {
|
|
191
|
+
instruction: 'Send your final answer to the user.',
|
|
192
|
+
when: 'You have gathered enough information and are ready to reply to the user.',
|
|
193
|
+
format: 'args: { receiverId: "<userId>", message: "<your answer>" }',
|
|
194
|
+
},
|
|
195
|
+
produces: 'message-sent',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
transitions: [
|
|
199
|
+
{ to: 'idle', when: (o) => o.kind === 'wait' },
|
|
200
|
+
{ to: 'idle', when: (o) => o.kind === 'error' },
|
|
201
|
+
{ to: 'done', when: (o) => o.kind === 'message-sent' },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
done: {
|
|
205
|
+
kind: 'parked',
|
|
206
|
+
transitions: [
|
|
207
|
+
{ to: 'idle', when: (o) => o.kind === 'message-received' },
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Max chars for a single tool description bound to the LLM. Verbose MCP servers
|
|
215
|
+
* embed whole manuals in their descriptions (PostHog's `execute-sql` alone is
|
|
216
|
+
* ~30K chars of HogQL docs; its 146 tools total ~54K tokens of descriptions) and
|
|
217
|
+
* that full block rides every turn. Keep the lead — what the tool does + how to
|
|
218
|
+
* call it — and drop the embedded reference; agents fetch detail via dedicated
|
|
219
|
+
* schema/help tools at call time. Tunable; big enough to preserve real guidance.
|
|
220
|
+
* ZIG-759.
|
|
221
|
+
*/
|
|
222
|
+
export const MAX_TOOL_DESC_CHARS = 2000;
|
|
223
|
+
/** Truncate one description at a sentence/newline boundary near the cap. */
|
|
224
|
+
export function trimDescription(desc) {
|
|
225
|
+
if (!desc || desc.length <= MAX_TOOL_DESC_CHARS)
|
|
226
|
+
return desc;
|
|
227
|
+
const slice = desc.slice(0, MAX_TOOL_DESC_CHARS);
|
|
228
|
+
const boundary = Math.max(slice.lastIndexOf('\n'), slice.lastIndexOf('. '));
|
|
229
|
+
const head = boundary > MAX_TOOL_DESC_CHARS * 0.6 ? slice.slice(0, boundary + 1) : slice;
|
|
230
|
+
return `${head.trimEnd()} […description truncated; use the server's schema/help tools for full detail]`;
|
|
231
|
+
}
|
|
232
|
+
/** Trim oversized descriptions across the tool set (cost control, ZIG-759). */
|
|
233
|
+
function trimToolDescriptions(tools, agentId) {
|
|
234
|
+
let trimmedCount = 0;
|
|
235
|
+
let savedChars = 0;
|
|
236
|
+
const out = tools.map((t) => {
|
|
237
|
+
const nextDesc = trimDescription(t.description);
|
|
238
|
+
if (nextDesc === t.description)
|
|
239
|
+
return t;
|
|
240
|
+
trimmedCount += 1;
|
|
241
|
+
savedChars += (t.description?.length ?? 0) - (nextDesc?.length ?? 0);
|
|
242
|
+
return { ...t, description: nextDesc };
|
|
243
|
+
});
|
|
244
|
+
if (trimmedCount > 0) {
|
|
245
|
+
console.warn(`[McpAdapter] ${agentId}: trimmed ${trimmedCount} oversized tool description(s) to ${MAX_TOOL_DESC_CHARS} chars, saving ~${Math.round(savedChars / 4 / 1000)}K tokens/turn. (ZIG-759)`);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Keep only tools whose name matches one of the allowlist substrings
|
|
251
|
+
* (case-insensitive). No allowlist → pass through unchanged. Applied before the
|
|
252
|
+
* maxTools cap so a targeted agent binds exactly the tools it needs. ZIG-759.
|
|
253
|
+
*/
|
|
254
|
+
function filterToolsByAllowlist(tools, allowedTools, agentId) {
|
|
255
|
+
if (!allowedTools || allowedTools.length === 0)
|
|
256
|
+
return tools;
|
|
257
|
+
const patterns = allowedTools.map((p) => p.toLowerCase());
|
|
258
|
+
const kept = tools.filter((t) => patterns.some((p) => t.name.toLowerCase().includes(p)));
|
|
259
|
+
console.warn(`[McpAdapter] ${agentId}: tool allowlist active — bound ${kept.length}/${tools.length} tools matching [${allowedTools.join(', ')}]${kept.length === 0 ? ' ⚠️ NONE MATCHED — check patterns against the server\'s tool names' : ': ' + kept.map((t) => t.name).join(', ')}.`);
|
|
260
|
+
return kept;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Cap the tool set bound to the LLM at the provider limit (OpenAI 128) —
|
|
264
|
+
* truncate and log rather than crash — and trim oversized descriptions so the
|
|
265
|
+
* bound schema doesn't ship a manual every turn. ZIG-759 Phase 1. Shared by the
|
|
266
|
+
* boot-time (single-client) and per-user paths. When `allowedTools` is set the
|
|
267
|
+
* server's tools are narrowed by name first, so the cap rarely bites.
|
|
268
|
+
*/
|
|
269
|
+
function capTools(tools, agentId, maxTools, extraToolCount, allowedTools) {
|
|
270
|
+
const allowed = filterToolsByAllowlist(tools, allowedTools, agentId);
|
|
271
|
+
const trimmed = trimToolDescriptions(allowed, agentId);
|
|
272
|
+
const budget = mcpToolBudget(maxTools, extraToolCount);
|
|
273
|
+
if (trimmed.length <= budget)
|
|
274
|
+
return trimmed;
|
|
275
|
+
console.warn(`[McpAdapter] ${agentId}: MCP server exposes ${trimmed.length} tools, over the ${maxTools}-tool LLM cap; binding the first ${budget}, dropping ${trimmed.length - budget}. (ZIG-759 Phase 2 will select per query.)`);
|
|
276
|
+
return trimmed.slice(0, budget);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Wraps any MCP server as a Ziggs AgentHost.
|
|
280
|
+
*
|
|
281
|
+
* 1. Opens a connection to the MCP server (stdio, HTTP, or the Ziggs gateway).
|
|
282
|
+
* 2. Calls tools/list and maps each tool to a Ziggs ToolDefinition.
|
|
283
|
+
* 3. Returns an AgentHost pre-loaded with those tools, ready to connectAsync().
|
|
284
|
+
*
|
|
285
|
+
* Usage (local stdio server):
|
|
286
|
+
* const { host, closeMcp } = await createMcpAgent({
|
|
287
|
+
* transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] },
|
|
288
|
+
* agentId: 'my-github-agent',
|
|
289
|
+
* name: 'GitHub Agent',
|
|
290
|
+
* description: 'Wraps the GitHub MCP server',
|
|
291
|
+
* });
|
|
292
|
+
* await host.connectAsync();
|
|
293
|
+
*
|
|
294
|
+
* Usage (OAuth-protected remote server, brokered through Ziggs — the user
|
|
295
|
+
* connected the server and issued this agent a grant; the agent never sees
|
|
296
|
+
* the user's token):
|
|
297
|
+
* const { host, closeMcp } = await createMcpAgent({
|
|
298
|
+
* transport: { type: 'ziggs', connectionId: 'con_abc', grantId: 'cgr_xyz' },
|
|
299
|
+
* agentId: 'my-notion-agent',
|
|
300
|
+
* name: 'Notion Agent',
|
|
301
|
+
* description: 'Works in the owner\'s Notion via their MCP connection',
|
|
302
|
+
* });
|
|
303
|
+
* await host.connectAsync();
|
|
304
|
+
*
|
|
305
|
+
* process.on('SIGTERM', async () => {
|
|
306
|
+
* host.disconnect();
|
|
307
|
+
* await host.drain();
|
|
308
|
+
* await closeMcp();
|
|
309
|
+
* });
|
|
310
|
+
*/
|
|
311
|
+
export async function createMcpAgent(options) {
|
|
312
|
+
const { transport: transportConfig, extraTools = [], workflow, maxTools = DEFAULT_MAX_TOOLS, allowedTools, toolSchema, ...hostOptions } = options;
|
|
313
|
+
// ── Per-user path (ZIG-771): no boot connection; one MCP client per invoking
|
|
314
|
+
// user, each carrying that user's on-behalf-of header for an isolated session.
|
|
315
|
+
if (transportConfig.type === 'ziggs-per-user') {
|
|
316
|
+
const agentId = transportConfig.agentId || hostOptions.agentId;
|
|
317
|
+
if (!agentId) {
|
|
318
|
+
throw new Error('ziggs-per-user MCP transport requires an agentId to impersonate');
|
|
319
|
+
}
|
|
320
|
+
if (!toolSchema || toolSchema.length === 0) {
|
|
321
|
+
throw new Error('ziggs-per-user MCP transport requires toolSchema (no boot connection to discover tools)');
|
|
322
|
+
}
|
|
323
|
+
const tools = capTools(toolSchema, agentId, maxTools, extraTools.length, allowedTools);
|
|
324
|
+
const clients = new Map();
|
|
325
|
+
const clientFor = (userId, onBehalfOfAgreementId) => {
|
|
326
|
+
// Key by user + agreement: the gateway may resolve a different end-user
|
|
327
|
+
// per agreement (delegation), so sessions must not be shared across them.
|
|
328
|
+
const cacheKey = `${userId}|${onBehalfOfAgreementId ?? ''}`;
|
|
329
|
+
let p = clients.get(cacheKey);
|
|
330
|
+
if (!p) {
|
|
331
|
+
p = (async () => {
|
|
332
|
+
const target = resolvePerUserGatewayTarget(transportConfig, agentId, userId, onBehalfOfAgreementId);
|
|
333
|
+
const transport = new StreamableHTTPClientTransport(target.url, {
|
|
334
|
+
requestInit: { headers: target.headers },
|
|
335
|
+
fetch: wrapGatewayFetch(),
|
|
336
|
+
});
|
|
337
|
+
const c = new Client({ name: 'ziggs-mcp-agent', version: '1.0.0' });
|
|
338
|
+
await c.connect(transport);
|
|
339
|
+
return c;
|
|
340
|
+
})().catch((err) => {
|
|
341
|
+
// Don't cache a failed connect (e.g. NO_USER_CONNECTION before the
|
|
342
|
+
// user consents) — the next turn must be able to retry.
|
|
343
|
+
clients.delete(cacheKey);
|
|
344
|
+
throw err;
|
|
345
|
+
});
|
|
346
|
+
clients.set(cacheKey, p);
|
|
347
|
+
}
|
|
348
|
+
return p;
|
|
349
|
+
};
|
|
350
|
+
// When the invoking user hasn't connected this server, open a connection
|
|
351
|
+
// request targeting THEM (X-On-Behalf-Of-User) so the consent prompt lands
|
|
352
|
+
// in their inbox. The backend dedups pending requests, so retrying every
|
|
353
|
+
// turn is safe. ZIG-771.
|
|
354
|
+
const backendBase = (transportConfig.baseUrl ||
|
|
355
|
+
process.env.BACKEND_URL ||
|
|
356
|
+
process.env.ZIGGS_BACKEND_URL ||
|
|
357
|
+
'http://localhost:3000').replace(/\/$/, '');
|
|
358
|
+
// App URL for the user-facing connect link (ZIG-790): api.ziggsai.com → ziggsai.com.
|
|
359
|
+
const appUrl = backendBase.includes('localhost')
|
|
360
|
+
? 'http://localhost:1234'
|
|
361
|
+
: backendBase.replace(/\/\/api\./, '//');
|
|
362
|
+
const serverLabel = transportConfig.serverUrl
|
|
363
|
+
? new URL(transportConfig.serverUrl).host
|
|
364
|
+
: 'the external service';
|
|
365
|
+
// Open a connection request for this user; returns its id so the caller can
|
|
366
|
+
// deep-link the user straight to it on the Connections page (ZIG-790).
|
|
367
|
+
const triggerConnectionRequest = async (userId) => {
|
|
368
|
+
if (!transportConfig.serverUrl)
|
|
369
|
+
return null;
|
|
370
|
+
const operatorKey = transportConfig.operatorKey || process.env.ZIGGS_OPERATOR_KEY;
|
|
371
|
+
const res = await fetch(`${backendBase}/connections/mcp/requests`, {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
headers: {
|
|
374
|
+
'content-type': 'application/json',
|
|
375
|
+
authorization: `Bearer ${operatorKey}`,
|
|
376
|
+
'x-agent-id': agentId,
|
|
377
|
+
'x-on-behalf-of-user': userId,
|
|
378
|
+
},
|
|
379
|
+
body: JSON.stringify({
|
|
380
|
+
serverUrl: transportConfig.serverUrl,
|
|
381
|
+
tools: tools.map(t => t.name),
|
|
382
|
+
}),
|
|
383
|
+
});
|
|
384
|
+
const body = (await res.json().catch(() => null));
|
|
385
|
+
return body?.request?.requestId ?? null;
|
|
386
|
+
};
|
|
387
|
+
const caller = async (name, args, context) => {
|
|
388
|
+
const userId = typeof context.senderId === 'string' ? context.senderId : undefined;
|
|
389
|
+
if (!userId) {
|
|
390
|
+
throw new Error('per-user MCP tool call requires an invoking user (context.senderId is missing)');
|
|
391
|
+
}
|
|
392
|
+
// ZIG-831: for a delegated wake, senderId is the delegating agent, not the
|
|
393
|
+
// connection-owning end-user; pass the agreement so the gateway resolves
|
|
394
|
+
// the delegation root's user.
|
|
395
|
+
const onBehalfOfAgreementId = typeof context.onBehalfOfAgreementId === 'string' ? context.onBehalfOfAgreementId : undefined;
|
|
396
|
+
// ZIG-832 (B): per-user resolution is intermittently flaky even when the
|
|
397
|
+
// connection is valid and resolvable — the same on-behalf call fails with
|
|
398
|
+
// NO_USER_CONNECTION on one wake and succeeds on the next. clientFor never
|
|
399
|
+
// caches a failed connect, so each attempt re-resolves from scratch. Retry
|
|
400
|
+
// a few times (with backoff) before surfacing the connect prompt, so a
|
|
401
|
+
// transient miss no longer fails the whole fetch step.
|
|
402
|
+
const MAX_ATTEMPTS = 3;
|
|
403
|
+
let lastErr;
|
|
404
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
405
|
+
try {
|
|
406
|
+
const c = await clientFor(userId, onBehalfOfAgreementId);
|
|
407
|
+
return (await c.callTool({ name, arguments: args }));
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
lastErr = err;
|
|
411
|
+
const resolutionMiss = err instanceof ZiggsGatewayError && err.code === 'NO_USER_CONNECTION';
|
|
412
|
+
const transient = resolutionMiss || !(err instanceof ZiggsGatewayError);
|
|
413
|
+
if (attempt < MAX_ATTEMPTS && transient) {
|
|
414
|
+
console.warn(`[McpAdapter] per-user ${name} attempt ${attempt}/${MAX_ATTEMPTS} failed (${err.message}); retrying with a fresh connection`);
|
|
415
|
+
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (lastErr instanceof ZiggsGatewayError &&
|
|
422
|
+
lastErr.code === 'NO_USER_CONNECTION' &&
|
|
423
|
+
transportConfig.serverUrl) {
|
|
424
|
+
const requestId = await triggerConnectionRequest(userId).catch(() => null);
|
|
425
|
+
const link = `${appUrl}/app/settings/connections${requestId ? `?request=${encodeURIComponent(requestId)}` : ''}`;
|
|
426
|
+
return {
|
|
427
|
+
content: [
|
|
428
|
+
{
|
|
429
|
+
type: 'text',
|
|
430
|
+
text: `ACTION NEEDED: the user has not connected ${serverLabel}. Ask them to approve the connection here: ${link} — then retry.`,
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
throw lastErr;
|
|
436
|
+
};
|
|
437
|
+
const host = new AgentHost({
|
|
438
|
+
...hostOptions,
|
|
439
|
+
workflow: workflow ?? buildDefaultWorkflow(hostOptions.description ?? '', tools),
|
|
440
|
+
tools: [...mapMcpTools(caller, tools), ...extraTools],
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
host,
|
|
444
|
+
closeMcp: async () => {
|
|
445
|
+
await Promise.all([...clients.values()].map(p => p.then(c => c.close()).catch(() => { })));
|
|
446
|
+
},
|
|
447
|
+
toolNames: tools.map(t => t.name),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
// ── Single-client path (stdio / http / brokered ziggs): discover tools at boot.
|
|
451
|
+
const transport = buildTransport(transportConfig, { agentId: hostOptions.agentId });
|
|
452
|
+
const client = new Client({ name: 'ziggs-mcp-agent', version: '1.0.0' });
|
|
453
|
+
await client.connect(transport);
|
|
454
|
+
const rawTools = capTools((await client.listTools()).tools, hostOptions.agentId, maxTools, extraTools.length, allowedTools);
|
|
455
|
+
const caller = (name, args) => client.callTool({ name, arguments: args });
|
|
456
|
+
const mcpTools = mapMcpTools(caller, rawTools);
|
|
457
|
+
const resolvedWorkflow = workflow ?? buildDefaultWorkflow(hostOptions.description ?? '', rawTools);
|
|
458
|
+
const host = new AgentHost({
|
|
459
|
+
...hostOptions,
|
|
460
|
+
workflow: resolvedWorkflow,
|
|
461
|
+
tools: [...mcpTools, ...extraTools],
|
|
462
|
+
});
|
|
463
|
+
return {
|
|
464
|
+
host,
|
|
465
|
+
closeMcp: () => client.close(),
|
|
466
|
+
toolNames: rawTools.map((t) => t.name),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import type { ToolDefinition } from '../tools/defineTool.js';
|
|
3
|
+
interface ChatResult {
|
|
4
|
+
content: string | null;
|
|
5
|
+
tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] | null;
|
|
6
|
+
message: OpenAI.Chat.ChatCompletionMessage;
|
|
7
|
+
usage: OpenAI.CompletionUsage | undefined;
|
|
8
|
+
}
|
|
9
|
+
interface AdapterOptions {
|
|
10
|
+
key?: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
}
|
|
13
|
+
/** OpenAI completion-token cap varies by model family (e.g. gpt-4o-mini max 16384). */
|
|
14
|
+
export declare function maxCompletionTokensForModel(model: string): number;
|
|
15
|
+
export declare class OpenAIAdapter {
|
|
16
|
+
private client;
|
|
17
|
+
model: string;
|
|
18
|
+
constructor({ key, model }: AdapterOptions);
|
|
19
|
+
chat(prompt: string, tools?: ToolDefinition[], temperature?: number, systemHints?: OpenAI.Chat.ChatCompletionMessageParam[], options?: Record<string, unknown>): Promise<ChatResult>;
|
|
20
|
+
chatMessages(messages: OpenAI.Chat.ChatCompletionMessageParam[], tools?: ToolDefinition[], options?: Record<string, unknown>): Promise<ChatResult>;
|
|
21
|
+
chatMessagesStream(messages: OpenAI.Chat.ChatCompletionMessageParam[], onChunk: (text: string) => void): Promise<{
|
|
22
|
+
text: string;
|
|
23
|
+
usage: OpenAI.CompletionUsage | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
+
/** OpenAI completion-token cap varies by model family (e.g. gpt-4o-mini max 16384). */
|
|
4
|
+
export function maxCompletionTokensForModel(model) {
|
|
5
|
+
const m = model.toLowerCase();
|
|
6
|
+
if (m.includes('gpt-4o'))
|
|
7
|
+
return 16384;
|
|
8
|
+
return 25000;
|
|
9
|
+
}
|
|
10
|
+
export class OpenAIAdapter {
|
|
11
|
+
client;
|
|
12
|
+
model;
|
|
13
|
+
constructor({ key, model = 'gpt-5.4' }) {
|
|
14
|
+
this.client = new OpenAI({ apiKey: key });
|
|
15
|
+
this.model = model;
|
|
16
|
+
}
|
|
17
|
+
async chat(prompt, tools = [], temperature = 0.2, systemHints = [], options = {}) {
|
|
18
|
+
const messages = [
|
|
19
|
+
{ role: 'system', content: 'Respond with valid JSON.' },
|
|
20
|
+
...systemHints,
|
|
21
|
+
{ role: 'user', content: prompt },
|
|
22
|
+
];
|
|
23
|
+
return this.chatMessages(messages, tools, { ...options, temperature });
|
|
24
|
+
}
|
|
25
|
+
async chatMessages(messages, tools = [], options = {}) {
|
|
26
|
+
const openAITools = tools.length > 0
|
|
27
|
+
? tools.map(t => {
|
|
28
|
+
const fn = (t.schema.function || t.schema);
|
|
29
|
+
const tAny = t;
|
|
30
|
+
const usageWhen = tAny['usage']
|
|
31
|
+
? ` Use when: ${tAny['usage']['when']}`
|
|
32
|
+
: '';
|
|
33
|
+
return {
|
|
34
|
+
type: 'function',
|
|
35
|
+
function: {
|
|
36
|
+
name: fn['name'],
|
|
37
|
+
description: (fn['description'] || '') + usageWhen,
|
|
38
|
+
parameters: fn['parameters'],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
})
|
|
42
|
+
: undefined;
|
|
43
|
+
try {
|
|
44
|
+
const { usage: _u, response_format: _explicitResponseFormat, ...restOptions } = options;
|
|
45
|
+
const responseFormat = openAITools
|
|
46
|
+
? undefined
|
|
47
|
+
: (_explicitResponseFormat ?? { type: 'json_object' });
|
|
48
|
+
// OpenAI 400s response_format:{type:'json_object'} unless the word "json"
|
|
49
|
+
// appears somewhere in the messages. Callers are expected to mention it
|
|
50
|
+
// in their system prompt, but that's an implicit cross-file contract —
|
|
51
|
+
// guarantee it here instead of trusting every call site to remember.
|
|
52
|
+
let finalMessages = messages;
|
|
53
|
+
if (responseFormat?.type === 'json_object' &&
|
|
54
|
+
!messages.some(m => {
|
|
55
|
+
const c = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
|
|
56
|
+
return /json/i.test(c);
|
|
57
|
+
})) {
|
|
58
|
+
finalMessages = [...messages, { role: 'system', content: 'Respond with valid JSON.' }];
|
|
59
|
+
}
|
|
60
|
+
// Verbose LLM request/response logging — enabled for debugging.
|
|
61
|
+
// Comment out again to reduce log noise.
|
|
62
|
+
const divider = '─'.repeat(80);
|
|
63
|
+
console.log(`\n${divider}`);
|
|
64
|
+
console.log(`📤 LLM REQUEST → ${this.model} (${finalMessages.length} msg${finalMessages.length === 1 ? '' : 's'})`);
|
|
65
|
+
console.log(divider);
|
|
66
|
+
for (const m of finalMessages) {
|
|
67
|
+
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
68
|
+
const len = content?.length ?? 0;
|
|
69
|
+
console.log(`\n[${m.role.toUpperCase()}] ${len} chars`);
|
|
70
|
+
console.log(content ?? '');
|
|
71
|
+
}
|
|
72
|
+
console.log(`${divider}\n`);
|
|
73
|
+
const res = await this.client.chat.completions.create({
|
|
74
|
+
model: this.model,
|
|
75
|
+
messages: finalMessages,
|
|
76
|
+
stream: false,
|
|
77
|
+
// Reasoning models (gpt-5.x/o-series) need a high cap for hidden
|
|
78
|
+
// reasoning tokens; gpt-4o family is capped lower by the API.
|
|
79
|
+
max_completion_tokens: maxCompletionTokensForModel(this.model),
|
|
80
|
+
response_format: responseFormat,
|
|
81
|
+
...(openAITools && { tools: openAITools }),
|
|
82
|
+
...restOptions,
|
|
83
|
+
});
|
|
84
|
+
const msg = res.choices[0].message;
|
|
85
|
+
const finishReason = res.choices[0].finish_reason;
|
|
86
|
+
const content = msg.content?.trim() ?? null;
|
|
87
|
+
const toolCalls = msg.tool_calls?.length ? msg.tool_calls : null;
|
|
88
|
+
// Verbose LLM response logging — enabled for debugging.
|
|
89
|
+
// Comment out again to reduce log noise.
|
|
90
|
+
console.log(`\n${divider}`);
|
|
91
|
+
console.log(`📥 LLM RESPONSE ← ${this.model} finish=${finishReason}`);
|
|
92
|
+
console.log(divider);
|
|
93
|
+
if (content) {
|
|
94
|
+
console.log(`\n[CONTENT] ${content.length} chars`);
|
|
95
|
+
console.log(content);
|
|
96
|
+
}
|
|
97
|
+
if (toolCalls) {
|
|
98
|
+
console.log(`\n[TOOL CALLS] ${toolCalls.length}`);
|
|
99
|
+
for (const tc of toolCalls) {
|
|
100
|
+
console.log(` → ${tc.function.name}(${tc.function.arguments})`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!content && !toolCalls)
|
|
104
|
+
console.log('\n(no content, no tool calls)');
|
|
105
|
+
if (res.usage) {
|
|
106
|
+
const u = res.usage;
|
|
107
|
+
console.log(`\n[USAGE] prompt=${u.prompt_tokens} completion=${u.completion_tokens}${u.completion_tokens_details?.reasoning_tokens != null ? ` (reasoning=${u.completion_tokens_details.reasoning_tokens})` : ''} total=${u.total_tokens}`);
|
|
108
|
+
}
|
|
109
|
+
console.log(`${divider}\n`);
|
|
110
|
+
if (!content && !toolCalls && finishReason === 'length') {
|
|
111
|
+
const u = res.usage;
|
|
112
|
+
throw new Error(`LLM returned no usable output — completion token budget exhausted before any content ` +
|
|
113
|
+
`(finish_reason=length, reasoning_tokens=${u?.completion_tokens_details?.reasoning_tokens ?? 'unknown'})`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
content,
|
|
117
|
+
tool_calls: toolCalls,
|
|
118
|
+
message: msg,
|
|
119
|
+
usage: res.usage,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const err = error;
|
|
124
|
+
runtimeLog.error('OpenAIAdapter', `LLM API call failed: ${err.message}`, {
|
|
125
|
+
model: this.model,
|
|
126
|
+
error: err.message,
|
|
127
|
+
status: err.status || 'unknown',
|
|
128
|
+
code: err.code || 'unknown',
|
|
129
|
+
});
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async chatMessagesStream(messages, onChunk) {
|
|
134
|
+
const stream = await this.client.chat.completions.create({
|
|
135
|
+
model: this.model,
|
|
136
|
+
messages,
|
|
137
|
+
stream: true,
|
|
138
|
+
// Without this, a streamed completion never returns a usage block — the
|
|
139
|
+
// streamed reply (often the largest output) would bill tokens that are
|
|
140
|
+
// counted nowhere (ZIG-813). The usage arrives on a final chunk whose
|
|
141
|
+
// `choices` is empty.
|
|
142
|
+
stream_options: { include_usage: true },
|
|
143
|
+
max_completion_tokens: maxCompletionTokensForModel(this.model),
|
|
144
|
+
});
|
|
145
|
+
let text = '';
|
|
146
|
+
let usage;
|
|
147
|
+
for await (const chunk of stream) {
|
|
148
|
+
if (chunk.usage)
|
|
149
|
+
usage = chunk.usage;
|
|
150
|
+
const delta = chunk.choices[0]?.delta?.content;
|
|
151
|
+
if (delta) {
|
|
152
|
+
text += delta;
|
|
153
|
+
onChunk(delta);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { text, usage };
|
|
157
|
+
}
|
|
158
|
+
}
|