@ziggs-ai/agent-sdk 0.33.0 → 0.33.2
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 +24 -0
- package/README.md +7 -3
- package/dist/Agent.d.ts +10 -1
- package/dist/Agent.js +18 -7
- package/dist/mcp/createMcpAgent.js +5 -23
- package/dist/platform/ConnectionPool.d.ts +8 -4
- package/dist/platform/ConnectionPool.js +17 -6
- package/dist/platform/inboxLoop.d.ts +14 -3
- package/dist/platform/inboxLoop.js +53 -14
- package/dist/platform/tools/tier1/protocolRunner.js +16 -60
- package/dist/platform/tools/tier2/connectionTools.js +2 -1
- package/dist/runtime/AgentMachine.d.ts +10 -0
- package/dist/runtime/AgentMachine.js +31 -38
- package/dist/runtime/validateWorkflow.js +19 -11
- package/dist/shared/backendUrl.d.ts +11 -0
- package/dist/shared/backendUrl.js +17 -0
- package/dist/tools/capabilityAdapter.js +2 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,30 @@
|
|
|
12
12
|
|
|
13
13
|
## Unreleased
|
|
14
14
|
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Hosted Claude agents can discover and invoke the access explanation tool named by their recovery instructions.
|
|
18
|
+
- `disconnect(timeoutMs)` is bounded and resolves once the inbox host lease is
|
|
19
|
+
released: an idle long-poll is cancelled at once, a pass in flight gets up to
|
|
20
|
+
the bound to acknowledge what it finished, and a pass that outlasts the bound
|
|
21
|
+
loses the lease anyway so the next host is handed what was not acknowledged.
|
|
22
|
+
`drain()` waits on the event queue only. A pool no longer refuses to start a
|
|
23
|
+
replacement host with "retry after its current delivery completes": the
|
|
24
|
+
replacement starts once the previous host's lease is released, and
|
|
25
|
+
`shutdown(timeoutMs)` spends one budget across stopping intake and draining.
|
|
26
|
+
|
|
27
|
+
- `agreement_subcontract` sends the terms the model stated. With no end date
|
|
28
|
+
and no task count the server writes a standing sub-agreement, as it does
|
|
29
|
+
over MCP; the SDK used to turn that into one count-bound task, and to reuse
|
|
30
|
+
an open sub-agreement for the same worker instead of creating one.
|
|
31
|
+
- The backend address is read one way across the SDK: explicit config, then
|
|
32
|
+
`ZIGGS_BACKEND_URL`, then `BACKEND_URL`, then what `configureApiClient` set.
|
|
33
|
+
`createMcpAgent`'s per-user connect request no longer falls back to
|
|
34
|
+
`http://localhost:3000` when only `configureApiClient` is set.
|
|
35
|
+
- A tool (mechanical) state whose tool throws, with no transition for the
|
|
36
|
+
error, now returns the agent to its initial state, as a thinking state
|
|
37
|
+
already did. `validateWorkflow` warns a tool state with no error transition.
|
|
38
|
+
|
|
15
39
|
### Added
|
|
16
40
|
|
|
17
41
|
- The Claude brain asks the backend's wake gate before spending a turn on chat
|
package/README.md
CHANGED
|
@@ -47,14 +47,18 @@ const agent = createAgent({
|
|
|
47
47
|
await agent.connectAsync();
|
|
48
48
|
|
|
49
49
|
// When your process shuts down:
|
|
50
|
-
// agent.disconnect();
|
|
50
|
+
// await agent.disconnect();
|
|
51
51
|
// await agent.drain();
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
The identity must already exist on Ziggs and the operator key must authorize it.
|
|
55
55
|
`connectAsync()` starts outbound delivery; network failures retry inside the
|
|
56
|
-
inbox loop. It is safe to call again while connected. `disconnect()`
|
|
57
|
-
polling and clears state timers
|
|
56
|
+
inbox loop. It is safe to call again while connected. `disconnect(timeoutMs)`
|
|
57
|
+
stops new polling and clears state timers, lets the inbox pass in flight
|
|
58
|
+
acknowledge what it finished, then releases the inbox host lease; a pass that
|
|
59
|
+
outlasts the bound (30 s by default) loses the lease anyway, and the next host
|
|
60
|
+
is handed what was not acknowledged. `drain()` waits for queued and active
|
|
61
|
+
wakes.
|
|
58
62
|
|
|
59
63
|
The Claude brain’s default wake timeout is five minutes; the Ziggs brain’s is
|
|
60
64
|
two minutes. `tickTimeoutMs` overrides either. Legacy `cognition: 'fsm' |
|
package/dist/Agent.d.ts
CHANGED
|
@@ -418,7 +418,16 @@ export declare class Agent {
|
|
|
418
418
|
wakeTaskAssignment(taskId: string, options?: {
|
|
419
419
|
fromInbox?: boolean;
|
|
420
420
|
}): Promise<void>;
|
|
421
|
-
|
|
421
|
+
/**
|
|
422
|
+
* Stop taking work. Timers and polls stop at once; the inbox loop cancels
|
|
423
|
+
* an idle long-poll and lets a pass already handed to this host finish and
|
|
424
|
+
* acknowledge, then releases the host lease — within `timeoutMs`. A pass
|
|
425
|
+
* that outlasts the bound does not keep the lease: it is released anyway
|
|
426
|
+
* and the next host is handed what was not acknowledged. Resolves once the
|
|
427
|
+
* lease is released either way. In-flight wakes keep running in the event
|
|
428
|
+
* queue; `drain()` is what waits for those.
|
|
429
|
+
*/
|
|
430
|
+
disconnect(timeoutMs?: number): Promise<void>;
|
|
422
431
|
/**
|
|
423
432
|
* Waits for in-flight ticks to finish. Call before process.exit() on SIGTERM.
|
|
424
433
|
* A timeout is returned, not thrown — the caller names it (lab verdict, fleet
|
package/dist/Agent.js
CHANGED
|
@@ -12,7 +12,7 @@ import { applyBrainToAgentOptions, isClaudeCognition, } from './brain/index.js';
|
|
|
12
12
|
import { CLAUDE_SDK_TOOLS } from './brain/claudeSdkTools.js';
|
|
13
13
|
import { resolveClaudeModel } from './brain/claudeCognition.js';
|
|
14
14
|
import { CHAT_TOOLS } from './platform/tools/tier2/chatTools.js';
|
|
15
|
-
import { contextReadTool, openTool } from './platform/tools/tier2/contextTools.js';
|
|
15
|
+
import { contextReadTool, openTool, accessExplainTool } from './platform/tools/tier2/contextTools.js';
|
|
16
16
|
import { hostedModelTools } from './brain/hostedToolSurface.js';
|
|
17
17
|
import { EventQueue } from './platform/EventQueue.js';
|
|
18
18
|
import { StateTimers } from './platform/StateTimers.js';
|
|
@@ -33,7 +33,7 @@ import { ArtifactsClient, ConnectionsClient, ContextReadClient, TelemetryClient,
|
|
|
33
33
|
import { normalizeNeeds, unmetNeeds, unmetNeedsMessage, } from './tools/needs.js';
|
|
34
34
|
import { TaskService } from './platform/tasks/TaskService.js';
|
|
35
35
|
import { AgreementService } from './platform/agreements/AgreementService.js';
|
|
36
|
-
import { runInboxLoop } from './platform/inboxLoop.js';
|
|
36
|
+
import { INBOX_STOP_TIMEOUT_MS, runInboxLoop } from './platform/inboxLoop.js';
|
|
37
37
|
import { requestExactMatch } from './platform/requestMatch.js';
|
|
38
38
|
import { SeenMessages } from './platform/SeenMessages.js';
|
|
39
39
|
import { runtimeLog } from './shared/runtimeLog.js';
|
|
@@ -99,12 +99,13 @@ function createToolManager(options) {
|
|
|
99
99
|
// model reaches it through tool_write; an agent that can be handed a task
|
|
100
100
|
// with a check-back rule but cannot arm a wake would have to busy-wait, and
|
|
101
101
|
// there is nothing to busy-wait with.
|
|
102
|
-
// open / context_read are the hosted twins of the MCP everyday set.
|
|
102
|
+
// open / context_read are the hosted twins of the MCP everyday set.
|
|
103
|
+
// access_explain must also be available for protocol and artifact recovery calls. Skip
|
|
103
104
|
// them when the agent already brought the same tools, so the shadow warning
|
|
104
105
|
// does not fire on a duplicate registration.
|
|
105
106
|
const ownNames = new Set(ownTools.map((t) => toolName(t)));
|
|
106
107
|
const everydayContext = isClaudeCognition(options.cognition)
|
|
107
|
-
? [contextReadTool, openTool].filter((t) => !ownNames.has(toolName(t)))
|
|
108
|
+
? [contextReadTool, openTool, accessExplainTool].filter((t) => !ownNames.has(toolName(t)))
|
|
108
109
|
: [];
|
|
109
110
|
const platformTools = [...taskTools, ...CHAT_TOOLS, ...browseTools, ...claudeSdkTools, ...everydayContext, ...WAKE_TOOLS];
|
|
110
111
|
warnOnShadowedPlatformTools(platformTools, ownTools, options.agentId);
|
|
@@ -1544,13 +1545,23 @@ export class Agent {
|
|
|
1544
1545
|
chatId: sessionId,
|
|
1545
1546
|
}, laneKey);
|
|
1546
1547
|
}
|
|
1547
|
-
|
|
1548
|
+
/**
|
|
1549
|
+
* Stop taking work. Timers and polls stop at once; the inbox loop cancels
|
|
1550
|
+
* an idle long-poll and lets a pass already handed to this host finish and
|
|
1551
|
+
* acknowledge, then releases the host lease — within `timeoutMs`. A pass
|
|
1552
|
+
* that outlasts the bound does not keep the lease: it is released anyway
|
|
1553
|
+
* and the next host is handed what was not acknowledged. Resolves once the
|
|
1554
|
+
* lease is released either way. In-flight wakes keep running in the event
|
|
1555
|
+
* queue; `drain()` is what waits for those.
|
|
1556
|
+
*/
|
|
1557
|
+
async disconnect(timeoutMs = INBOX_STOP_TIMEOUT_MS) {
|
|
1548
1558
|
this.started = false;
|
|
1549
1559
|
this.pollRunner?.stop();
|
|
1550
1560
|
this.stateTimers.clearAll();
|
|
1551
|
-
const
|
|
1561
|
+
const loop = this.inboxLoop;
|
|
1552
1562
|
this.inboxLoop = null;
|
|
1553
|
-
|
|
1563
|
+
if (loop)
|
|
1564
|
+
await loop.stop(timeoutMs);
|
|
1554
1565
|
}
|
|
1555
1566
|
/**
|
|
1556
1567
|
* Waits for in-flight ticks to finish. Call before process.exit() on SIGTERM.
|
|
@@ -2,7 +2,8 @@ import { resolveWebAppOrigin, connectionsSettingsAppUrl, laneHeader } from '@zig
|
|
|
2
2
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
3
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
4
4
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
|
-
import { ConnectionsClient
|
|
5
|
+
import { ConnectionsClient } from '@ziggs-ai/api-client';
|
|
6
|
+
import { resolveBackendUrl } from '../shared/backendUrl.js';
|
|
6
7
|
import { createAgent } from '../Agent.js';
|
|
7
8
|
import { laneFromToolContext } from '../tools/toolCreds.js';
|
|
8
9
|
import { thinkingDefaults } from '../runtime/defaults.js';
|
|
@@ -58,15 +59,7 @@ export function resolveZiggsGatewayTarget(config, defaults = {}) {
|
|
|
58
59
|
if (!agentId) {
|
|
59
60
|
throw new Error('ziggs MCP transport requires an agentId to impersonate (X-Agent-Id)');
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
// the resolution every other SDK call uses — not `localhost:3000`. A deployment
|
|
63
|
-
// that sets none of BACKEND_URL / ZIGGS_BACKEND_URL would otherwise have every
|
|
64
|
-
// gateway call silently point at localhost inside its own container: the
|
|
65
|
-
// requests never reach the backend, and the agent reports it as unreachable.
|
|
66
|
-
const baseUrl = (config.baseUrl ||
|
|
67
|
-
process.env.ZIGGS_BACKEND_URL ||
|
|
68
|
-
process.env.BACKEND_URL ||
|
|
69
|
-
getBackendUrl()).replace(/\/$/, '');
|
|
62
|
+
const baseUrl = resolveBackendUrl(config.baseUrl);
|
|
70
63
|
return {
|
|
71
64
|
url: new URL(`${baseUrl}/connections/${encodeURIComponent(config.connectionId)}/mcp`),
|
|
72
65
|
headers: {
|
|
@@ -92,15 +85,7 @@ export function resolvePerUserGatewayTarget(config, agentId, userId, onBehalfOfA
|
|
|
92
85
|
if (!operatorKey) {
|
|
93
86
|
throw new Error('ziggs-per-user MCP transport requires an operatorKey (or ZIGGS_OPERATOR_KEY)');
|
|
94
87
|
}
|
|
95
|
-
|
|
96
|
-
// the resolution every other SDK call uses — not `localhost:3000`. A deployment
|
|
97
|
-
// that sets none of BACKEND_URL / ZIGGS_BACKEND_URL would otherwise have every
|
|
98
|
-
// gateway call silently point at localhost inside its own container: the
|
|
99
|
-
// requests never reach the backend, and the agent reports it as unreachable.
|
|
100
|
-
const baseUrl = (config.baseUrl ||
|
|
101
|
-
process.env.ZIGGS_BACKEND_URL ||
|
|
102
|
-
process.env.BACKEND_URL ||
|
|
103
|
-
getBackendUrl()).replace(/\/$/, '');
|
|
88
|
+
const baseUrl = resolveBackendUrl(config.baseUrl);
|
|
104
89
|
return {
|
|
105
90
|
url: new URL(`${baseUrl}/connections/mcp/relay`),
|
|
106
91
|
headers: {
|
|
@@ -384,10 +369,7 @@ export async function createMcpAgent(options) {
|
|
|
384
369
|
// request targeting THEM (X-On-Behalf-Of-User) so the consent prompt lands
|
|
385
370
|
// in their inbox. The backend dedups pending requests, so retrying every
|
|
386
371
|
// turn is safe.
|
|
387
|
-
const backendBase = (transportConfig.baseUrl
|
|
388
|
-
process.env.BACKEND_URL ||
|
|
389
|
-
process.env.ZIGGS_BACKEND_URL ||
|
|
390
|
-
'http://localhost:3000').replace(/\/$/, '');
|
|
372
|
+
const backendBase = resolveBackendUrl(transportConfig.baseUrl);
|
|
391
373
|
// App URL for the user-facing connect link: api.ziggsai.com → ziggsai.com.
|
|
392
374
|
const appUrl = resolveWebAppOrigin(undefined, backendBase);
|
|
393
375
|
const serverLabel = transportConfig.serverUrl
|
|
@@ -17,6 +17,7 @@ export declare class ConnectionPool {
|
|
|
17
17
|
private readonly manager;
|
|
18
18
|
private readonly configs;
|
|
19
19
|
private readonly hosts;
|
|
20
|
+
private stopTimeoutMs;
|
|
20
21
|
private readonly responseListeners;
|
|
21
22
|
/**
|
|
22
23
|
* Register N agent configs. Nothing connects until `connectAll()`. A config
|
|
@@ -45,12 +46,15 @@ export declare class ConnectionPool {
|
|
|
45
46
|
* is already live and polling the same inboxes, so that window is where one
|
|
46
47
|
* delivery gets handled twice: two hosts, one request, two answers.
|
|
47
48
|
*
|
|
48
|
-
* `disconnect()`
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* `disconnect()` stops the inbox loop and timers, lets the pass in flight
|
|
50
|
+
* acknowledge what it finished, and releases the host lease — all inside
|
|
51
|
+
* the bound; in-flight ticks live in each host's event queue, which is what
|
|
52
|
+
* `drain()` waits on. One budget covers both, so a fleet on a 120s stop
|
|
53
|
+
* clock is not asked to wait twice.
|
|
51
54
|
*
|
|
52
55
|
* A host that cannot finish inside `timeoutMs` is left behind rather than
|
|
53
|
-
* holding up the rest — the caller is on a shutdown clock of its own.
|
|
56
|
+
* holding up the rest — the caller is on a shutdown clock of its own. Its
|
|
57
|
+
* lease is released regardless, so the replacement can host at once.
|
|
54
58
|
*/
|
|
55
59
|
shutdown(timeoutMs?: number): Promise<void>;
|
|
56
60
|
/**
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { ConnectionManager } from '@ziggs-ai/api-client';
|
|
17
17
|
import { createAgent } from '../Agent.js';
|
|
18
|
+
import { INBOX_STOP_TIMEOUT_MS } from './inboxLoop.js';
|
|
18
19
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
19
20
|
export class ConnectionPool {
|
|
20
21
|
manager = new ConnectionManager();
|
|
@@ -23,6 +24,9 @@ export class ConnectionPool {
|
|
|
23
24
|
// open/close callbacks and does not expose them, and re-reading one through
|
|
24
25
|
// `startAgent` after a stop would reconnect the agent we are shutting down.
|
|
25
26
|
hosts = new Map();
|
|
27
|
+
// How long a stopping host may spend acknowledging its pass in flight before
|
|
28
|
+
// its lease is released anyway. `shutdown()` sets it to the caller's budget.
|
|
29
|
+
stopTimeoutMs = INBOX_STOP_TIMEOUT_MS;
|
|
26
30
|
// Listeners enqueued by requestFrom — drained on outbound send-message effects.
|
|
27
31
|
responseListeners = new Map();
|
|
28
32
|
/**
|
|
@@ -64,7 +68,9 @@ export class ConnectionPool {
|
|
|
64
68
|
runtimeLog.debug('ConnectionPool', `connected "${agentId}"`);
|
|
65
69
|
return host;
|
|
66
70
|
}, async (host) => {
|
|
67
|
-
|
|
71
|
+
// Resolves once the host's lease is released, so a replacement
|
|
72
|
+
// started next cannot share the inbox with a host still acking.
|
|
73
|
+
await host.disconnect(this.stopTimeoutMs);
|
|
68
74
|
this.hosts.delete(agentId);
|
|
69
75
|
runtimeLog.debug('ConnectionPool', `disconnected "${agentId}"`);
|
|
70
76
|
}, metaByAgentId.get(agentId));
|
|
@@ -108,19 +114,24 @@ export class ConnectionPool {
|
|
|
108
114
|
* is already live and polling the same inboxes, so that window is where one
|
|
109
115
|
* delivery gets handled twice: two hosts, one request, two answers.
|
|
110
116
|
*
|
|
111
|
-
* `disconnect()`
|
|
112
|
-
*
|
|
113
|
-
*
|
|
117
|
+
* `disconnect()` stops the inbox loop and timers, lets the pass in flight
|
|
118
|
+
* acknowledge what it finished, and releases the host lease — all inside
|
|
119
|
+
* the bound; in-flight ticks live in each host's event queue, which is what
|
|
120
|
+
* `drain()` waits on. One budget covers both, so a fleet on a 120s stop
|
|
121
|
+
* clock is not asked to wait twice.
|
|
114
122
|
*
|
|
115
123
|
* A host that cannot finish inside `timeoutMs` is left behind rather than
|
|
116
|
-
* holding up the rest — the caller is on a shutdown clock of its own.
|
|
124
|
+
* holding up the rest — the caller is on a shutdown clock of its own. Its
|
|
125
|
+
* lease is released regardless, so the replacement can host at once.
|
|
117
126
|
*/
|
|
118
127
|
async shutdown(timeoutMs = 30_000) {
|
|
128
|
+
const deadline = Date.now() + timeoutMs;
|
|
119
129
|
const hosts = [...this.hosts.values()];
|
|
130
|
+
this.stopTimeoutMs = timeoutMs;
|
|
120
131
|
await this.manager.stopAll();
|
|
121
132
|
await Promise.all(hosts.map(async (host) => {
|
|
122
133
|
try {
|
|
123
|
-
const result = await host.drain(
|
|
134
|
+
const result = await host.drain(Math.max(0, deadline - Date.now()));
|
|
124
135
|
if (result?.timedOut) {
|
|
125
136
|
runtimeLog.warn('ConnectionPool', `drain timed out after ${timeoutMs}ms: ${result.queuedEvents} event(s) queued, ${result.busyLanes.length} busy lane(s)`);
|
|
126
137
|
}
|
|
@@ -66,12 +66,21 @@ export declare function chatsInWakeOrder(chats: InboxChatNews[]): InboxChatNews[
|
|
|
66
66
|
* Reused each iteration of {@link runInboxLoop}; also usable as a single catch-up
|
|
67
67
|
* read (`waitSeconds: 0`) at bootstrap.
|
|
68
68
|
*/
|
|
69
|
-
export declare function pollInboxOnce(deps: InboxLoopDeps, waitSeconds?: number,
|
|
69
|
+
export declare function pollInboxOnce(deps: InboxLoopDeps, waitSeconds?: number, opts?: {
|
|
70
70
|
forceAck?: boolean;
|
|
71
|
+
signal?: AbortSignal;
|
|
71
72
|
}): Promise<InboxPassResult>;
|
|
72
73
|
export interface InboxLoopHandle {
|
|
73
|
-
/**
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Stop reading. An idle long-poll is cancelled at once; a pass already
|
|
76
|
+
* handed to the host runs on so it can acknowledge what it finished, and
|
|
77
|
+
* the host lease is released after that. A pass still running when
|
|
78
|
+
* `timeoutMs` is up does not hold the lease: it is released anyway, and the
|
|
79
|
+
* next host is handed whatever was not acknowledged. Resolves `true` when
|
|
80
|
+
* the pass finished inside the bound, `false` when the lease was released
|
|
81
|
+
* out from under it.
|
|
82
|
+
*/
|
|
83
|
+
stop(timeoutMs?: number): Promise<boolean>;
|
|
75
84
|
/** True while the loop is running. */
|
|
76
85
|
readonly running: boolean;
|
|
77
86
|
/**
|
|
@@ -90,6 +99,8 @@ export interface InboxLoopHandle {
|
|
|
90
99
|
}
|
|
91
100
|
/** Count held windows so retries can back off without discarding mail. */
|
|
92
101
|
export declare function heldPassAfter(prev: number, pass: Pick<InboxPassResult, 'acked' | 'failures' | 'truncated'>): number;
|
|
102
|
+
/** How long `stop()` waits for a pass in flight before releasing the lease anyway. */
|
|
103
|
+
export declare const INBOX_STOP_TIMEOUT_MS = 30000;
|
|
93
104
|
/**
|
|
94
105
|
* Run {@link pollInboxOnce} forever (until stopped). Each iteration long-polls
|
|
95
106
|
* for `waitSeconds`; a passing envelope returns instantly, an empty one holds.
|
|
@@ -130,11 +130,17 @@ async function deliverMessagesForChat(deps, news, refs, reader, attempt, handled
|
|
|
130
130
|
* Reused each iteration of {@link runInboxLoop}; also usable as a single catch-up
|
|
131
131
|
* read (`waitSeconds: 0`) at bootstrap.
|
|
132
132
|
*/
|
|
133
|
-
export async function pollInboxOnce(deps, waitSeconds = 0,
|
|
133
|
+
export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
134
134
|
const label = deps.label ?? 'inboxLoop';
|
|
135
135
|
const inbox = inboxClient(deps);
|
|
136
136
|
const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
137
|
-
const envelope = await inbox.getInbox(
|
|
137
|
+
const envelope = await inbox.getInbox({
|
|
138
|
+
...(waitSeconds > 0 ? { waitSeconds } : {}),
|
|
139
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
140
|
+
});
|
|
141
|
+
// A response may race shutdown. Do not enqueue another wake after stopping;
|
|
142
|
+
// an already-running pass keeps its lease through its final acknowledgement.
|
|
143
|
+
opts.signal?.throwIfAborted();
|
|
138
144
|
let messagesDelivered = 0;
|
|
139
145
|
let resourceEvents = 0;
|
|
140
146
|
let tasksDelivered = 0;
|
|
@@ -370,8 +376,31 @@ const ERROR_BACKOFF_MAX_MS = 30_000;
|
|
|
370
376
|
export function heldPassAfter(prev, pass) {
|
|
371
377
|
return !pass.acked && (pass.failures > 0 || pass.truncated) ? prev + 1 : 0;
|
|
372
378
|
}
|
|
373
|
-
function sleep(ms) {
|
|
374
|
-
return new Promise((resolve) =>
|
|
379
|
+
function sleep(ms, signal) {
|
|
380
|
+
return new Promise((resolve) => {
|
|
381
|
+
const finish = () => {
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
signal?.removeEventListener('abort', finish);
|
|
384
|
+
resolve();
|
|
385
|
+
};
|
|
386
|
+
const timer = setTimeout(finish, ms);
|
|
387
|
+
if (signal?.aborted)
|
|
388
|
+
finish();
|
|
389
|
+
else
|
|
390
|
+
signal?.addEventListener('abort', finish, { once: true });
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/** How long `stop()` waits for a pass in flight before releasing the lease anyway. */
|
|
394
|
+
export const INBOX_STOP_TIMEOUT_MS = 30_000;
|
|
395
|
+
/** Resolves `true` when `promise` settles within `ms`, `false` otherwise. The timer never outlives the answer. */
|
|
396
|
+
function withinMs(promise, ms) {
|
|
397
|
+
return new Promise((resolve) => {
|
|
398
|
+
const timer = setTimeout(() => resolve(false), Math.max(0, ms));
|
|
399
|
+
promise.then(() => true, () => true).then((settled) => {
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
resolve(settled);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
375
404
|
}
|
|
376
405
|
/**
|
|
377
406
|
* Run {@link pollInboxOnce} forever (until stopped). Each iteration long-polls
|
|
@@ -394,17 +423,22 @@ export function runInboxLoop(deps, opts = {}) {
|
|
|
394
423
|
let lastPassAt = null;
|
|
395
424
|
let lastAttemptAt = null;
|
|
396
425
|
let first = true;
|
|
426
|
+
const shutdown = new AbortController();
|
|
427
|
+
// One DELETE, whichever gets there first: the loop's own exit, or a stop
|
|
428
|
+
// that ran out of patience with the pass in flight.
|
|
429
|
+
let released = null;
|
|
430
|
+
const releaseOnce = () => (released ??= releaseInboxHostBestEffort(deps));
|
|
397
431
|
const main = (async () => {
|
|
398
432
|
while (running) {
|
|
399
433
|
try {
|
|
400
434
|
lastAttemptAt = Date.now();
|
|
401
|
-
const pass = await pollInboxOnce(deps, first ? 0 : waitSeconds);
|
|
435
|
+
const pass = await pollInboxOnce(deps, first ? 0 : waitSeconds, { signal: shutdown.signal });
|
|
402
436
|
lastPassAt = Date.now();
|
|
403
437
|
first = false;
|
|
404
438
|
consecutiveErrors = 0;
|
|
405
439
|
heldPasses = heldPassAfter(heldPasses, pass);
|
|
406
440
|
if (heldPasses)
|
|
407
|
-
await sleep(Math.min(ERROR_BACKOFF_MAX_MS, ERROR_BACKOFF_BASE_MS * 2 ** Math.min(heldPasses - 1, 5)));
|
|
441
|
+
await sleep(Math.min(ERROR_BACKOFF_MAX_MS, ERROR_BACKOFF_BASE_MS * 2 ** Math.min(heldPasses - 1, 5)), shutdown.signal);
|
|
408
442
|
}
|
|
409
443
|
catch (err) {
|
|
410
444
|
if (!running)
|
|
@@ -419,17 +453,17 @@ export function runInboxLoop(deps, opts = {}) {
|
|
|
419
453
|
if (throttleMs !== null) {
|
|
420
454
|
consecutiveErrors = 0;
|
|
421
455
|
warn(label, `inbox loop throttled: ${err?.message ?? err} — waiting ${Math.round(throttleMs)}ms as instructed`);
|
|
422
|
-
await sleep(throttleMs);
|
|
456
|
+
await sleep(throttleMs, shutdown.signal);
|
|
423
457
|
continue;
|
|
424
458
|
}
|
|
425
459
|
const backoff = Math.min(ERROR_BACKOFF_MAX_MS, ERROR_BACKOFF_BASE_MS * 2 ** (consecutiveErrors - 1));
|
|
426
460
|
const jitter = backoff * (0.5 + (consecutiveErrors % 7) / 14); // deterministic-ish, no Math.random
|
|
427
461
|
warn(label, `inbox loop pass failed (#${consecutiveErrors}): ${err?.message ?? err} — retrying in ~${Math.round(jitter)}ms`);
|
|
428
|
-
await sleep(jitter);
|
|
462
|
+
await sleep(jitter, shutdown.signal);
|
|
429
463
|
}
|
|
430
464
|
}
|
|
431
465
|
})();
|
|
432
|
-
const done = main;
|
|
466
|
+
const done = main.finally(releaseOnce);
|
|
433
467
|
return {
|
|
434
468
|
get running() {
|
|
435
469
|
return running;
|
|
@@ -440,12 +474,17 @@ export function runInboxLoop(deps, opts = {}) {
|
|
|
440
474
|
get lastAttemptAt() {
|
|
441
475
|
return lastAttemptAt;
|
|
442
476
|
},
|
|
443
|
-
async stop() {
|
|
477
|
+
async stop(timeoutMs = INBOX_STOP_TIMEOUT_MS) {
|
|
444
478
|
running = false;
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
479
|
+
// Cancel an idle long-poll; a pass already handed to the host keeps
|
|
480
|
+
// going so it can acknowledge what it finished before the lease goes.
|
|
481
|
+
shutdown.abort();
|
|
482
|
+
const finished = await withinMs(done, timeoutMs);
|
|
483
|
+
if (!finished) {
|
|
484
|
+
warn(label, `inbox pass still running after ${timeoutMs}ms — releasing the host lease now; what it has not acknowledged is redelivered to the next host`);
|
|
485
|
+
await releaseOnce();
|
|
486
|
+
}
|
|
487
|
+
return finished;
|
|
449
488
|
},
|
|
450
489
|
};
|
|
451
490
|
}
|
|
@@ -57,7 +57,7 @@ cachedAgreements) {
|
|
|
57
57
|
case 'agreement-propose':
|
|
58
58
|
return proposeAgreement(services, payload, chatId, agents, actorId, writeKeys);
|
|
59
59
|
case 'agreement-subcontract':
|
|
60
|
-
return subcontractAgreement(services, payload, namedSubcontractChat(payload, chatId),
|
|
60
|
+
return subcontractAgreement(services, payload, namedSubcontractChat(payload, chatId), actorId, writeKeys);
|
|
61
61
|
case 'agreement-respond':
|
|
62
62
|
if (!payload.agreementId || !payload.action)
|
|
63
63
|
return null;
|
|
@@ -322,9 +322,6 @@ function agIsProvider(a, agentId) {
|
|
|
322
322
|
function agIsParty(a, agentId) {
|
|
323
323
|
return partyActorIds(a.parties).includes(agentId);
|
|
324
324
|
}
|
|
325
|
-
function agActive(a) {
|
|
326
|
-
return !a.status || !TERMINAL_STATUS.has(String(a.status));
|
|
327
|
-
}
|
|
328
325
|
// Spawnable = actually ACTIVE. A freshly-subcontracted sub is `draft` until its
|
|
329
326
|
// counterparty auto-approves; spawning under a draft (or citing an agreement the
|
|
330
327
|
// assignee isn't a party on) 400s and cascades into a bogus worker "connection
|
|
@@ -339,66 +336,25 @@ function namedSubcontractChat(payload, wakeChatId) {
|
|
|
339
336
|
return named.trim();
|
|
340
337
|
return wakeChatId;
|
|
341
338
|
}
|
|
342
|
-
|
|
339
|
+
/**
|
|
340
|
+
* A sub-agreement under the agreement this wake works for, with exactly the
|
|
341
|
+
* terms the model stated. When it states neither an end date nor a task count
|
|
342
|
+
* the server writes a standing sub-agreement, the same as over MCP — the
|
|
343
|
+
* shared tool description says so.
|
|
344
|
+
*
|
|
345
|
+
* This used to rewrite such terms to one count-bound task, and to reuse any
|
|
346
|
+
* open sub-agreement for the same worker. Both served a backend rule that
|
|
347
|
+
* closed a parent once every child had finished; that rule is gone (a parent
|
|
348
|
+
* now closes only by its own quota, expiry or an explicit close), so the
|
|
349
|
+
* rewrite only made a hosted agent's sub-deal end after one task where the same
|
|
350
|
+
* request over MCP stays open. A crash replay is still covered by the
|
|
351
|
+
* wake-derived idempotency key.
|
|
352
|
+
*/
|
|
353
|
+
async function subcontractAgreement(services, payload, chatId, actingAgentId, writeKeys = null) {
|
|
343
354
|
if (!payload.description || !payload.executorId)
|
|
344
355
|
return null;
|
|
345
|
-
// b1: idempotent subcontract — if a non-terminal sub-agreement for
|
|
346
|
-
// this executor already exists under the same parent, reuse it instead of
|
|
347
|
-
// creating a duplicate (which bloats the chain and blocks the fulfill cascade).
|
|
348
|
-
if (chatId) {
|
|
349
|
-
const parent = payload.parentAgreementId;
|
|
350
|
-
const existing = (await agreementsInChat(services, chatId, cachedAgreements)).find((a) => agActive(a) && agIsProvider(a, payload.executorId) &&
|
|
351
|
-
(!parent || a.parentAgreementId === parent || a.rootAgreementId === parent));
|
|
352
|
-
if (existing?.agreementId)
|
|
353
|
-
return existing;
|
|
354
|
-
}
|
|
355
356
|
const priced = applyFleetPricingForProtocol('agreement-subcontract', payload, actingAgentId);
|
|
356
357
|
const { operation: _op, ...fields } = priced;
|
|
357
|
-
// a subcontract must carry a working close condition, or it hangs
|
|
358
|
-
// 'active' forever — and because the backend only auto-fulfills a parent hire
|
|
359
|
-
// once every child sub-agreement is terminal (the fulfill cascade), one
|
|
360
|
-
// never-closing child leaves the whole engagement open. Deciding lifecycle is
|
|
361
|
-
// left to the model, which under load (context bloat, competing wakes) emits
|
|
362
|
-
// incomplete terms — e.g. time-bound with no expiresAt and no maxExecutions —
|
|
363
|
-
// that never reach fulfillment. Make the common single-delegated-task case
|
|
364
|
-
// deterministic: if the caller gave neither a real quota nor a real deadline,
|
|
365
|
-
// treat it as one count-bound execution so completing that task closes the
|
|
366
|
-
// sub — which then cascades the parent closed. A genuinely durable subcontract
|
|
367
|
-
// still opts out by setting maxExecutions>1 or a time-bound expiresAt.
|
|
368
|
-
const f = fields;
|
|
369
|
-
// repair incomplete terms instead of clobbering them. The model
|
|
370
|
-
// often emits a real quota or deadline but gets the envelope wrong — a
|
|
371
|
-
// numeric-string maxExecutions, or a quota/deadline with the lifecycle field
|
|
372
|
-
// missing or mismatched. The old check treated all of those as "no close
|
|
373
|
-
// condition" and forced count-bound/1, silently discarding the stated intent
|
|
374
|
-
// (e.g. maxExecutions=2 → 1, so the sub fulfills one task early and the next
|
|
375
|
-
// spawn under it fails).
|
|
376
|
-
if (typeof f.maxExecutions === 'string' && f.maxExecutions.trim() !== '' && Number.isFinite(Number(f.maxExecutions))) {
|
|
377
|
-
f.maxExecutions = Number(f.maxExecutions);
|
|
378
|
-
}
|
|
379
|
-
const hasQuota = typeof f.maxExecutions === 'number' && f.maxExecutions > 0;
|
|
380
|
-
const hasDeadline = typeof f.expiresAt === 'string' && f.expiresAt.length > 0;
|
|
381
|
-
if (f.lifecycle === 'count-bound' && hasQuota) {
|
|
382
|
-
// coherent as stated
|
|
383
|
-
}
|
|
384
|
-
else if (f.lifecycle === 'time-bound' && hasDeadline) {
|
|
385
|
-
// coherent as stated
|
|
386
|
-
}
|
|
387
|
-
else if (hasQuota) {
|
|
388
|
-
f.lifecycle = 'count-bound'; // quota given, lifecycle missing/mismatched — keep the quota
|
|
389
|
-
}
|
|
390
|
-
else if (hasDeadline) {
|
|
391
|
-
f.lifecycle = 'time-bound'; // deadline given, lifecycle missing/mismatched — keep the deadline
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
// no usable close condition at all — the deterministic single-task default
|
|
395
|
-
f.lifecycle = 'count-bound';
|
|
396
|
-
f.maxExecutions = 1;
|
|
397
|
-
}
|
|
398
|
-
// Wake-derived idempotency key (delivery law): a crash-replay that slips past
|
|
399
|
-
// the b1 reuse check above (e.g. no chatId, or the sub not yet visible in the
|
|
400
|
-
// snapshot) resolves to the already-created sub-agreement server-side instead
|
|
401
|
-
// of minting a duplicate.
|
|
402
358
|
const idempotencyKey = writeKeys?.agreementSubcontractKey({
|
|
403
359
|
executorId: payload.executorId,
|
|
404
360
|
description: payload.description,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { connectionProxyCapability, requestConnectionCapability, ConnectionsClient, } from '@ziggs-ai/api-client';
|
|
2
2
|
import { defineTool } from '../../../tools/defineTool.js';
|
|
3
3
|
import { toolFromCapability } from '../../../tools/capabilityAdapter.js';
|
|
4
|
+
import { resolveBackendUrl } from '../../../shared/backendUrl.js';
|
|
4
5
|
// connection_proxy / connection_request are shared capability
|
|
5
6
|
// definitions (api-client). connection_list_grants stays SDK-only: the MCP
|
|
6
7
|
// surface folds connection listing into ziggs_connection_list (
|
|
@@ -19,7 +20,7 @@ export const connectionListGrantsTool = defineTool('connection_list_grants', {
|
|
|
19
20
|
const operatorKey = ctx?.['operatorKey'];
|
|
20
21
|
if (!operatorKey)
|
|
21
22
|
throw new Error('operatorKey missing from tool context');
|
|
22
|
-
const baseUrl =
|
|
23
|
+
const baseUrl = resolveBackendUrl();
|
|
23
24
|
const client = new ConnectionsClient(operatorKey, ctx?.['agentId'] || undefined, baseUrl);
|
|
24
25
|
try {
|
|
25
26
|
const grants = await client.listGrants({ connectionId: args['connectionId'] });
|
|
@@ -159,6 +159,16 @@ export declare class AgentMachine {
|
|
|
159
159
|
*/
|
|
160
160
|
private _parkAtInitial;
|
|
161
161
|
_step(outcome: Outcome, depth?: number): Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* A turn threw. Route the error through the state's transitions; if none
|
|
164
|
+
* takes it, recover to workflow.initial so the next event is not judged by
|
|
165
|
+
* transitions written for this state.
|
|
166
|
+
*
|
|
167
|
+
* One method for thinking and tool (mechanical) states. The recovery used to
|
|
168
|
+
* live only in the thinking copy, so a tool state that threw with no error
|
|
169
|
+
* route stayed put and judged the next event as itself.
|
|
170
|
+
*/
|
|
171
|
+
_recoverFromTurnError(error: unknown, kind: 'thinking' | 'mechanical'): Promise<void>;
|
|
162
172
|
/**
|
|
163
173
|
* Unmatched thinking outcome: go back to workflow.initial when that state is
|
|
164
174
|
* parked. Used to require an error outcome, so a tool-result /
|
|
@@ -243,24 +243,7 @@ export class AgentMachine {
|
|
|
243
243
|
await this._step(turnOutcome, depth + 1);
|
|
244
244
|
}
|
|
245
245
|
catch (error) {
|
|
246
|
-
|
|
247
|
-
const errOutcome = {
|
|
248
|
-
kind: 'error',
|
|
249
|
-
source: 'unknown',
|
|
250
|
-
cause: error?.message || String(error),
|
|
251
|
-
retryable: false,
|
|
252
|
-
};
|
|
253
|
-
applyOutcomeToCtx(this.ctx, errOutcome);
|
|
254
|
-
this._running = false;
|
|
255
|
-
this._notify();
|
|
256
|
-
const fallback = this._evaluateTransitions(errOutcome);
|
|
257
|
-
if (fallback && fallback !== this.state) {
|
|
258
|
-
this.state = fallback;
|
|
259
|
-
this._notify();
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
|
|
263
|
-
}
|
|
246
|
+
await this._recoverFromTurnError(error, 'mechanical');
|
|
264
247
|
}
|
|
265
248
|
return;
|
|
266
249
|
}
|
|
@@ -346,27 +329,37 @@ export class AgentMachine {
|
|
|
346
329
|
await this._step(turnOutcome, depth + 1);
|
|
347
330
|
}
|
|
348
331
|
catch (error) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
332
|
+
await this._recoverFromTurnError(error, 'thinking');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* A turn threw. Route the error through the state's transitions; if none
|
|
337
|
+
* takes it, recover to workflow.initial so the next event is not judged by
|
|
338
|
+
* transitions written for this state.
|
|
339
|
+
*
|
|
340
|
+
* One method for thinking and tool (mechanical) states. The recovery used to
|
|
341
|
+
* live only in the thinking copy, so a tool state that threw with no error
|
|
342
|
+
* route stayed put and judged the next event as itself.
|
|
343
|
+
*/
|
|
344
|
+
async _recoverFromTurnError(error, kind) {
|
|
345
|
+
runtimeLog.error('AgentMachine', `${kind === 'mechanical' ? 'mechanical ' : ''}state=${this.state} error: ${error?.message || error}`);
|
|
346
|
+
const errOutcome = {
|
|
347
|
+
kind: 'error',
|
|
348
|
+
source: 'unknown',
|
|
349
|
+
cause: error?.message || String(error),
|
|
350
|
+
retryable: false,
|
|
351
|
+
};
|
|
352
|
+
applyOutcomeToCtx(this.ctx, errOutcome);
|
|
353
|
+
this._running = false;
|
|
354
|
+
this._notify();
|
|
355
|
+
const fallback = this._evaluateTransitions(errOutcome);
|
|
356
|
+
if (fallback && fallback !== this.state) {
|
|
357
|
+
this.state = fallback;
|
|
358
358
|
this._notify();
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
this.state = fallback;
|
|
364
|
-
this._notify();
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
|
|
368
|
-
this._parkAtInitial();
|
|
369
|
-
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
|
|
362
|
+
this._parkAtInitial();
|
|
370
363
|
}
|
|
371
364
|
}
|
|
372
365
|
/**
|
|
@@ -125,6 +125,11 @@ export function validateWorkflow(workflow) {
|
|
|
125
125
|
else if (typeof p !== 'function') {
|
|
126
126
|
errors.push(`mechanical state "${name}" requires produces (OutcomeKind or fn)`);
|
|
127
127
|
}
|
|
128
|
+
// The same soft check thinking states get. A tool that throws
|
|
129
|
+
// with no error route sends the agent back to its initial state.
|
|
130
|
+
if (!hasErrorTransition(mech.transitions ?? [])) {
|
|
131
|
+
warnings.push(`mechanical state "${name}" has no transition for { kind: 'error' } — a failed tool call sends the agent back to "${workflow.initial}". Add an error transition if it should go elsewhere.`);
|
|
132
|
+
}
|
|
128
133
|
}
|
|
129
134
|
if (!Array.isArray(state.transitions)) {
|
|
130
135
|
errors.push(`state "${name}" requires a transitions array`);
|
|
@@ -189,17 +194,7 @@ export function validateWorkflow(workflow) {
|
|
|
189
194
|
if (!hasWaitFallthrough) {
|
|
190
195
|
warnings.push(`thinking state "${name}" has no fallthrough for { kind: 'wait' } — agent may stall when LLM picks wait. Spread thinkingDefaults().transitions or add explicitly.`);
|
|
191
196
|
}
|
|
192
|
-
|
|
193
|
-
if (!t.when)
|
|
194
|
-
return true;
|
|
195
|
-
try {
|
|
196
|
-
return t.when({ kind: 'error', source: 'unknown', cause: 'probe', retryable: false }, PROBE_CTX);
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
if (!hasErrorFallthrough) {
|
|
197
|
+
if (!hasErrorTransition(state.transitions)) {
|
|
203
198
|
warnings.push(`thinking state "${name}" has no fallthrough for { kind: 'error' } — empty/failed turns may silently recurse until the depth guard. Add an error transition or spread thinkingDefaults().transitions.`);
|
|
204
199
|
}
|
|
205
200
|
}
|
|
@@ -209,3 +204,16 @@ export function validateWorkflow(workflow) {
|
|
|
209
204
|
throw new WorkflowValidationError(errors);
|
|
210
205
|
return { errors, warnings };
|
|
211
206
|
}
|
|
207
|
+
/** Does some transition take an error outcome? A guard that throws counts as no. */
|
|
208
|
+
function hasErrorTransition(transitions) {
|
|
209
|
+
return transitions.some((t) => {
|
|
210
|
+
if (!t.when)
|
|
211
|
+
return true;
|
|
212
|
+
try {
|
|
213
|
+
return t.when({ kind: 'error', source: 'unknown', cause: 'probe', retryable: false }, PROBE_CTX);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backend address, read one way everywhere in the SDK.
|
|
3
|
+
*
|
|
4
|
+
* Explicit config wins, then the runner's overrides (`ZIGGS_BACKEND_URL`,
|
|
5
|
+
* `BACKEND_URL`), then `getBackendUrl()` — what `configureApiClient` set, else
|
|
6
|
+
* production. There were five copies with three different orders, and the one
|
|
7
|
+
* in the per-user connect path fell back to `http://localhost:3000`, so an
|
|
8
|
+
* outside developer who configured the client and nothing else had connection
|
|
9
|
+
* requests posted to their own machine.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveBackendUrl(explicit?: string | null): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getBackendUrl } from '@ziggs-ai/api-client';
|
|
2
|
+
/**
|
|
3
|
+
* The backend address, read one way everywhere in the SDK.
|
|
4
|
+
*
|
|
5
|
+
* Explicit config wins, then the runner's overrides (`ZIGGS_BACKEND_URL`,
|
|
6
|
+
* `BACKEND_URL`), then `getBackendUrl()` — what `configureApiClient` set, else
|
|
7
|
+
* production. There were five copies with three different orders, and the one
|
|
8
|
+
* in the per-user connect path fell back to `http://localhost:3000`, so an
|
|
9
|
+
* outside developer who configured the client and nothing else had connection
|
|
10
|
+
* requests posted to their own machine.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveBackendUrl(explicit) {
|
|
13
|
+
return (explicit ||
|
|
14
|
+
process.env.ZIGGS_BACKEND_URL ||
|
|
15
|
+
process.env.BACKEND_URL ||
|
|
16
|
+
getBackendUrl()).replace(/\/$/, '');
|
|
17
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineTool } from './defineTool.js';
|
|
2
2
|
import { laneFromToolContext } from './toolCreds.js';
|
|
3
|
+
import { resolveBackendUrl } from '../shared/backendUrl.js';
|
|
3
4
|
/**
|
|
4
5
|
* the SDK surface adapter for shared capability definitions
|
|
5
6
|
* (api-client `capabilities/`). One capability (schema + handler + guidance)
|
|
@@ -30,9 +31,7 @@ export function toolFromCapability(cap) {
|
|
|
30
31
|
...(agentId ? { agentId } : {}),
|
|
31
32
|
...(laneId ? { laneId } : {}),
|
|
32
33
|
},
|
|
33
|
-
|
|
34
|
-
// shared clients default to HTTP_URL (api-client convention) otherwise.
|
|
35
|
-
baseUrl: process.env.BACKEND_URL || process.env.ZIGGS_BACKEND_URL || undefined,
|
|
34
|
+
baseUrl: resolveBackendUrl(),
|
|
36
35
|
webUrl: process.env.ZIGGS_WEB_URL,
|
|
37
36
|
surface: 'sdk',
|
|
38
37
|
serviceAgreementId: typeof ctx.serviceAgreementId === 'string' ? ctx.serviceAgreementId : undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/agent-sdk",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.2",
|
|
4
4
|
"description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@anthropic-ai/claude-agent-sdk": "^0.3.284",
|
|
27
27
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
-
"@ziggs-ai/api-client": "0.32.
|
|
29
|
+
"@ziggs-ai/api-client": "0.32.1",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
31
|
"dotenv": "^17.2.3",
|
|
32
32
|
"openai": "^4.0.0",
|