@ziggs-ai/agent-sdk 0.1.8 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/agent-sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -8,7 +8,110 @@ import type { Workflow } from '../types.js';
8
8
 
9
9
  export type McpTransportConfig =
10
10
  | { type: 'stdio'; command: string; args?: string[]; env?: Record<string, string> }
11
- | { type: 'http'; url: string };
11
+ | { type: 'http'; url: string }
12
+ | {
13
+ /**
14
+ * Brokered connection through the Ziggs MCP gateway: the agent speaks
15
+ * StreamableHTTP to `/connections/:id/mcp` and Ziggs injects the
16
+ * connection owner's OAuth token upstream. The agent never holds the
17
+ * token; every tools/call is checked against the grant's caveats.
18
+ */
19
+ type: 'ziggs';
20
+ /** Connection id (`con_…`) of the user's MCP server connection. */
21
+ connectionId: string;
22
+ /** ConnectionGrant id (`cgr_…`) issued to this agent. */
23
+ grantId: string;
24
+ /** Operator key — defaults to ZIGGS_OPERATOR_KEY. */
25
+ operatorKey?: string;
26
+ /** Ziggs backend base URL — defaults to BACKEND_URL / ZIGGS_BACKEND_URL. */
27
+ baseUrl?: string;
28
+ /** Agent to impersonate (X-Agent-Id) — defaults to the host agentId. */
29
+ agentId?: string;
30
+ };
31
+
32
+ /**
33
+ * Typed failure from the Ziggs MCP gateway (ZIG-702). `code` is the stable
34
+ * machine-readable reason — e.g. `GRANT_REVOKED`, `GRANT_EXPIRED`,
35
+ * `GRANT_HOLDER_MISMATCH`, `GRANT_CONNECTION_MISMATCH` — so agent code can
36
+ * branch ("revoked → tell the user and stop retrying" vs "network blip →
37
+ * retry") instead of parsing transport-error prose.
38
+ */
39
+ export class ZiggsGatewayError extends Error {
40
+ readonly code: string;
41
+ readonly status: number;
42
+
43
+ constructor(code: string, message: string, status: number) {
44
+ super(message);
45
+ this.name = 'ZiggsGatewayError';
46
+ this.code = code;
47
+ this.status = status;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Wrap fetch so structured gateway denials (401/403 with a `code` body)
53
+ * surface as ZiggsGatewayError instead of a generic transport failure.
54
+ */
55
+ export function wrapGatewayFetch(
56
+ baseFetch: typeof fetch = fetch,
57
+ ): typeof fetch {
58
+ return (async (input: RequestInfo | URL, init?: RequestInit) => {
59
+ const response = await baseFetch(input, init);
60
+ if (response.status === 401 || response.status === 403) {
61
+ const body = (await response
62
+ .clone()
63
+ .json()
64
+ .catch(() => null)) as { code?: unknown; message?: unknown } | null;
65
+ if (body && typeof body.code === 'string') {
66
+ throw new ZiggsGatewayError(
67
+ body.code,
68
+ typeof body.message === 'string' ? body.message : body.code,
69
+ response.status,
70
+ );
71
+ }
72
+ }
73
+ return response;
74
+ }) as typeof fetch;
75
+ }
76
+
77
+ /** Resolve the gateway URL + auth headers for a `type: 'ziggs'` transport. */
78
+ export function resolveZiggsGatewayTarget(
79
+ config: Extract<McpTransportConfig, { type: 'ziggs' }>,
80
+ defaults: { agentId?: string } = {},
81
+ ): { url: URL; headers: Record<string, string> } {
82
+ if (!config.connectionId) {
83
+ throw new Error('ziggs MCP transport requires a connectionId');
84
+ }
85
+ if (!config.grantId) {
86
+ throw new Error('ziggs MCP transport requires a grantId');
87
+ }
88
+ const operatorKey = config.operatorKey || process.env.ZIGGS_OPERATOR_KEY;
89
+ if (!operatorKey) {
90
+ throw new Error(
91
+ 'ziggs MCP transport requires an operatorKey (or ZIGGS_OPERATOR_KEY)',
92
+ );
93
+ }
94
+ const agentId = config.agentId || defaults.agentId;
95
+ if (!agentId) {
96
+ throw new Error(
97
+ 'ziggs MCP transport requires an agentId to impersonate (X-Agent-Id)',
98
+ );
99
+ }
100
+ const baseUrl = (
101
+ config.baseUrl ||
102
+ process.env.BACKEND_URL ||
103
+ process.env.ZIGGS_BACKEND_URL ||
104
+ 'http://localhost:3000'
105
+ ).replace(/\/$/, '');
106
+ return {
107
+ url: new URL(`${baseUrl}/connections/${encodeURIComponent(config.connectionId)}/mcp`),
108
+ headers: {
109
+ authorization: `Bearer ${operatorKey}`,
110
+ 'x-agent-id': agentId,
111
+ 'x-grant-id': config.grantId,
112
+ },
113
+ };
114
+ }
12
115
 
13
116
  export interface CreateMcpAgentOptions extends Omit<AgentHostOptions, 'tools' | 'workflow' | 'states' | 'initial'> {
14
117
  /** MCP server connection — stdio (local process) or HTTP (remote). */
@@ -27,7 +130,7 @@ export interface McpAgentHandle {
27
130
  toolNames: string[];
28
131
  }
29
132
 
30
- function buildTransport(config: McpTransportConfig) {
133
+ function buildTransport(config: McpTransportConfig, defaults: { agentId?: string }) {
31
134
  if (config.type === 'stdio') {
32
135
  return new StdioClientTransport({
33
136
  command: config.command,
@@ -35,6 +138,13 @@ function buildTransport(config: McpTransportConfig) {
35
138
  env: config.env,
36
139
  });
37
140
  }
141
+ if (config.type === 'ziggs') {
142
+ const target = resolveZiggsGatewayTarget(config, defaults);
143
+ return new StreamableHTTPClientTransport(target.url, {
144
+ requestInit: { headers: target.headers },
145
+ fetch: wrapGatewayFetch(),
146
+ });
147
+ }
38
148
  return new StreamableHTTPClientTransport(new URL(config.url));
39
149
  }
40
150
 
@@ -81,30 +191,17 @@ function mapMcpTools(client: Client, tools: McpTool[]): ToolDefinition[] {
81
191
  }));
82
192
  }
83
193
 
84
- /**
85
- * Wraps any MCP server as a Ziggs AgentHost.
86
- *
87
- * 1. Opens a connection to the MCP server (stdio or HTTP).
88
- * 2. Calls tools/list and maps each tool to a Ziggs ToolDefinition.
89
- * 3. Returns an AgentHost pre-loaded with those tools, ready to connectAsync().
90
- *
91
- * Usage:
92
- * const { host, closeMcp } = await createMcpAgent({
93
- * transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] },
94
- * agentId: 'my-github-agent',
95
- * name: 'GitHub Agent',
96
- * description: 'Wraps the GitHub MCP server',
97
- * });
98
- * await host.connectAsync();
99
- *
100
- * process.on('SIGTERM', async () => {
101
- * host.disconnect();
102
- * await host.drain();
103
- * await closeMcp();
104
- * });
105
- */
106
- function buildDefaultWorkflow(description: string): Workflow {
194
+ function buildDefaultWorkflow(description: string, mcpTools: McpTool[]): Workflow {
107
195
  const dflt = thinkingDefaults({ initial: 'idle' });
196
+ // Bind each MCP tool as an explicit action so runTurn includes them in the
197
+ // LLM API call and the decision schema. Without this, allowedToolNames is
198
+ // empty and the tools array passed to the LLM is also empty.
199
+ const toolActions = Object.fromEntries(
200
+ mcpTools.map(t => [t.name, {
201
+ tool: t.name,
202
+ prompt: { when: t.description || `Use ${t.name}` },
203
+ }]),
204
+ );
108
205
  return {
109
206
  id: 'mcp-agent',
110
207
  description: description || 'MCP-backed agent',
@@ -115,10 +212,31 @@ function buildDefaultWorkflow(description: string): Workflow {
115
212
  prompt: {
116
213
  role: 'You are a helpful agent powered by an MCP server.',
117
214
  goal: description || 'Help the user by calling the available tools.',
118
- constraints: 'Use your tools to answer requests. When no action is needed, wait.',
215
+ 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.',
119
216
  },
120
- actions: { ...dflt.actions },
121
- transitions: [...dflt.transitions],
217
+ actions: {
218
+ ...dflt.actions,
219
+ ...toolActions,
220
+ respond: {
221
+ prompt: {
222
+ instruction: 'Send your final answer to the user.',
223
+ when: 'You have gathered enough information and are ready to reply to the user.',
224
+ format: 'args: { receiverId: "<userId>", message: "<your answer>" }',
225
+ },
226
+ produces: 'message-sent',
227
+ },
228
+ },
229
+ transitions: [
230
+ { to: 'idle', when: (o) => o.kind === 'wait' },
231
+ { to: 'idle', when: (o) => o.kind === 'error' },
232
+ { to: 'done', when: (o) => o.kind === 'message-sent' },
233
+ ],
234
+ },
235
+ done: {
236
+ kind: 'parked',
237
+ transitions: [
238
+ { to: 'idle', when: (o) => o.kind === 'message-received' },
239
+ ],
122
240
  },
123
241
  },
124
242
  };
@@ -127,11 +245,11 @@ function buildDefaultWorkflow(description: string): Workflow {
127
245
  /**
128
246
  * Wraps any MCP server as a Ziggs AgentHost.
129
247
  *
130
- * 1. Opens a connection to the MCP server (stdio or HTTP).
248
+ * 1. Opens a connection to the MCP server (stdio, HTTP, or the Ziggs gateway).
131
249
  * 2. Calls tools/list and maps each tool to a Ziggs ToolDefinition.
132
250
  * 3. Returns an AgentHost pre-loaded with those tools, ready to connectAsync().
133
251
  *
134
- * Usage:
252
+ * Usage (local stdio server):
135
253
  * const { host, closeMcp } = await createMcpAgent({
136
254
  * transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] },
137
255
  * agentId: 'my-github-agent',
@@ -140,6 +258,17 @@ function buildDefaultWorkflow(description: string): Workflow {
140
258
  * });
141
259
  * await host.connectAsync();
142
260
  *
261
+ * Usage (OAuth-protected remote server, brokered through Ziggs — the user
262
+ * connected the server and issued this agent a grant; the agent never sees
263
+ * the user's token):
264
+ * const { host, closeMcp } = await createMcpAgent({
265
+ * transport: { type: 'ziggs', connectionId: 'con_abc', grantId: 'cgr_xyz' },
266
+ * agentId: 'my-notion-agent',
267
+ * name: 'Notion Agent',
268
+ * description: 'Works in the owner\'s Notion via their MCP connection',
269
+ * });
270
+ * await host.connectAsync();
271
+ *
143
272
  * process.on('SIGTERM', async () => {
144
273
  * host.disconnect();
145
274
  * await host.drain();
@@ -149,7 +278,7 @@ function buildDefaultWorkflow(description: string): Workflow {
149
278
  export async function createMcpAgent(options: CreateMcpAgentOptions): Promise<McpAgentHandle> {
150
279
  const { transport: transportConfig, extraTools = [], workflow, ...hostOptions } = options;
151
280
 
152
- const transport = buildTransport(transportConfig);
281
+ const transport = buildTransport(transportConfig, { agentId: hostOptions.agentId });
153
282
  const client = new Client({ name: 'ziggs-mcp-agent', version: '1.0.0' });
154
283
 
155
284
  await client.connect(transport);
@@ -157,7 +286,7 @@ export async function createMcpAgent(options: CreateMcpAgentOptions): Promise<Mc
157
286
  const { tools: rawTools } = await client.listTools();
158
287
  const mcpTools = mapMcpTools(client, rawTools as McpTool[]);
159
288
 
160
- const resolvedWorkflow = workflow ?? buildDefaultWorkflow(hostOptions.description ?? '');
289
+ const resolvedWorkflow = workflow ?? buildDefaultWorkflow(hostOptions.description ?? '', rawTools as McpTool[]);
161
290
 
162
291
  const host = new AgentHost({
163
292
  ...hostOptions,
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  export { defineAgent } from './defineAgent.js';
12
- export { createMcpAgent } from './adapters/McpAdapter.js';
12
+ export { createMcpAgent, resolveZiggsGatewayTarget, wrapGatewayFetch, ZiggsGatewayError } from './adapters/McpAdapter.js';
13
13
  export type { CreateMcpAgentOptions, McpAgentHandle, McpTransportConfig } from './adapters/McpAdapter.js';
14
14
  export { AgentHost, createAgent, createAgentPool } from './AgentHost.js';
15
15
  export { Agent } from './agent/Agent.js';
@@ -169,7 +169,7 @@ export function validateWorkflow(workflow: Workflow): ValidationResult {
169
169
  });
170
170
  if (!hasErrorFallthrough) {
171
171
  warnings.push(
172
- `thinking state "${name}" has no fallthrough for { kind: 'error' } — empty/failed turns may silently recurse until the depth guard (ZIG-303). Add an error transition or spread thinkingDefaults().transitions.`,
172
+ `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.`,
173
173
  );
174
174
  }
175
175
  }
@@ -32,7 +32,7 @@ export {
32
32
  PAYMENT_TOOLS,
33
33
  paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool,
34
34
  paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool,
35
- paymentIssueTokenTool, paymentAttenuateTokenTool, paymentRevokeTokenTool, paymentListTokensTool,
35
+ paymentIssueGrantTool, paymentAttenuateGrantTool, paymentRevokeGrantTool, paymentListGrantsTool,
36
36
  } from './tier2/paymentTools.js';
37
37
  export {
38
38
  CONNECTION_TOOLS,
@@ -42,6 +42,5 @@ export {
42
42
  export {
43
43
  CONTEXT_TOOLS,
44
44
  contextDiscoverTool,
45
- contextListGrantsTool,
46
45
  contextDelegateTool,
47
46
  } from './tier2/contextTools.js';
@@ -32,6 +32,7 @@ interface ProtocolPayload {
32
32
  parentAgreementId?: string;
33
33
  parentTaskId?: string;
34
34
  taskId?: string;
35
+ assigneeId?: string;
35
36
  status?: string;
36
37
  result?: unknown;
37
38
  plan?: unknown;
@@ -95,6 +96,7 @@ export async function dispatchProtocolOp(
95
96
  plan: payload.plan,
96
97
  planReviewTiming: payload.planReviewTiming,
97
98
  requireMidWorkPlanAck: payload.requireMidWorkPlanAck,
99
+ assigneeId: payload.assigneeId,
98
100
  });
99
101
  case 'task-update':
100
102
  if (!payload.taskId || !payload.status) return null;
@@ -87,9 +87,9 @@ export const taskUpdatePlanStepTool: ToolDefinition = defineTool(
87
87
 
88
88
  export const taskSpawnTool: ToolDefinition = defineTool(
89
89
  'task_spawn',
90
- { agreementId: { type: 'string', required: true }, description: { type: 'string', required: true }, parentTaskId: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' },
90
+ { agreementId: { type: 'string', required: true }, description: { type: 'string', required: true }, parentTaskId: 'string', assigneeId: { type: 'string', description: 'Explicitly hand this task to one specific agent (e.g. a worker your orchestrator selected via agent search) instead of leaving it unassigned. Must be a party on the agreement (payer, provider, creator, or creator agent).' }, plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' },
91
91
  handlerFor('task-spawn'),
92
- { description: 'Spawn a new task under an existing approved agreement. Use when the contract is already in place.' },
92
+ { description: 'Spawn a new task under an existing approved agreement. Use when the contract is already in place. Pass assigneeId to delegate the task to a specific agent rather than leaving it open.' },
93
93
  );
94
94
 
95
95
  export const PROTOCOL_TOOLS: ToolDefinition[] = [
@@ -18,17 +18,7 @@ export const contextDiscoverTool: ToolDefinition = defineTool(
18
18
  async (_args, ctx) => clientFor(ctx as Ctx).discover(),
19
19
  {
20
20
  description:
21
- 'List which chats, agreements, and org scopes this agent can reach via context grants (descriptors only — no message or artifact content).',
22
- },
23
- );
24
-
25
- export const contextListGrantsTool: ToolDefinition = defineTool(
26
- 'context_list_grants',
27
- {},
28
- async (_args, ctx) => clientFor(ctx as Ctx).listGrants(),
29
- {
30
- description:
31
- 'List context grants held by this agent (grant ids and bounds). Does not fetch scoped content.',
21
+ 'List every context grant this agent holds, as scope descriptors — grantId, scope, temporal, watermarkAt, expiresAt (no message or artifact content).',
32
22
  },
33
23
  );
34
24
 
@@ -69,6 +59,5 @@ export const contextDelegateTool: ToolDefinition = defineTool(
69
59
 
70
60
  export const CONTEXT_TOOLS = [
71
61
  contextDiscoverTool,
72
- contextListGrantsTool,
73
62
  contextDelegateTool,
74
63
  ] as const;
@@ -121,14 +121,8 @@ export const paymentIssueGrantTool: ToolDefinition = defineTool('payment_issue_g
121
121
  issueGrantHandler,
122
122
  );
123
123
 
124
- /** @deprecated Use payment_issue_grant */
125
- export const paymentIssueTokenTool: ToolDefinition = defineTool('payment_issue_token',
126
- { holderId: { type: 'string', required: true }, maxAmount: 'number', dailyBudget: 'number', allowedRecipients: ['string'], expiresInSeconds: 'number' },
127
- issueGrantHandler,
128
- );
129
-
130
124
  const attenuateGrantHandler = async (args: AnyObj, ctx: Ctx) => {
131
- const grantId = (args['grantId'] || args['tokenId']) as string;
125
+ const grantId = args['grantId'] as string;
132
126
  if (!grantId) throw new Error('grantId is required');
133
127
  if (!args['holderId']) throw new Error('holderId is required');
134
128
  try {
@@ -144,14 +138,8 @@ export const paymentAttenuateGrantTool: ToolDefinition = defineTool('payment_att
144
138
  attenuateGrantHandler,
145
139
  );
146
140
 
147
- /** @deprecated Use payment_attenuate_grant */
148
- export const paymentAttenuateTokenTool: ToolDefinition = defineTool('payment_attenuate_token',
149
- { tokenId: { type: 'string', required: true }, holderId: { type: 'string', required: true }, maxAmount: 'number', dailyBudget: 'number', allowedRecipients: ['string'], expiresInSeconds: 'number' },
150
- attenuateGrantHandler,
151
- );
152
-
153
141
  const revokeGrantHandler = async (args: AnyObj, ctx: Ctx) => {
154
- const grantId = (args['grantId'] || args['tokenId']) as string;
142
+ const grantId = args['grantId'] as string;
155
143
  if (!grantId) throw new Error('grantId is required');
156
144
  try {
157
145
  const result = await clientFor(ctx).revokeGrant(grantId) as AnyObj;
@@ -164,12 +152,6 @@ export const paymentRevokeGrantTool: ToolDefinition = defineTool('payment_revoke
164
152
  revokeGrantHandler,
165
153
  );
166
154
 
167
- /** @deprecated Use payment_revoke_grant */
168
- export const paymentRevokeTokenTool: ToolDefinition = defineTool('payment_revoke_token',
169
- { tokenId: { type: 'string', required: true } },
170
- revokeGrantHandler,
171
- );
172
-
173
155
  const listGrantsHandler = async (_args: AnyObj, ctx: Ctx) => {
174
156
  try {
175
157
  const result = await clientFor(ctx).listGrants();
@@ -181,11 +163,6 @@ export const paymentListGrantsTool: ToolDefinition = defineTool('payment_list_gr
181
163
  listGrantsHandler,
182
164
  );
183
165
 
184
- /** @deprecated Use payment_list_grants */
185
- export const paymentListTokensTool: ToolDefinition = defineTool('payment_list_tokens', {},
186
- listGrantsHandler,
187
- );
188
-
189
166
  export const PAYMENT_TOOLS: ToolDefinition[] = [
190
167
  paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool,
191
168
  paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool,
@@ -69,14 +69,6 @@ export class ZiggsContextClient {
69
69
  return (res['reach'] as ContextReachDescriptor[]) || [];
70
70
  }
71
71
 
72
- /** List persisted grant rows held by this agent (may include signedBytes). */
73
- async listGrants(): Promise<unknown[]> {
74
- this.requireActAsAgent('listGrants');
75
- const res = (await this._get('/context/grants')) as Record<string, unknown>;
76
- const grants = (res['grants'] as Array<Record<string, unknown>>) || [];
77
- return grants.filter((g) => g['holderId'] === this.actAsAgentId);
78
- }
79
-
80
72
  /** Holder delegates a narrower child grant. */
81
73
  async delegate(params: DelegateContextGrantParams): Promise<unknown> {
82
74
  this.requireActAsAgent('delegate');