@ziggs-ai/agent-sdk 0.1.7 → 0.1.9

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.7",
3
+ "version": "0.1.9",
4
4
  "description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -16,6 +16,7 @@
16
16
  "test:watch": "node --experimental-test-module-mocks --import tsx/esm --test --watch test/*.test.ts"
17
17
  },
18
18
  "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.29.0",
19
20
  "@ziggs-ai/api-client": "^0.1.9",
20
21
  "ajv": "^8.17.1",
21
22
  "dotenv": "^17.2.3",
@@ -0,0 +1,203 @@
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, type AgentHostOptions } from '../AgentHost.js';
5
+ import type { ToolDefinition } from '../tools/defineTool.js';
6
+ import { thinkingDefaults } from '../runtime/defaults.js';
7
+ import type { Workflow } from '../types.js';
8
+
9
+ export type McpTransportConfig =
10
+ | { type: 'stdio'; command: string; args?: string[]; env?: Record<string, string> }
11
+ | { type: 'http'; url: string };
12
+
13
+ export interface CreateMcpAgentOptions extends Omit<AgentHostOptions, 'tools' | 'workflow' | 'states' | 'initial'> {
14
+ /** MCP server connection — stdio (local process) or HTTP (remote). */
15
+ transport: McpTransportConfig;
16
+ /** Additional Ziggs tools to register alongside the MCP-derived ones. */
17
+ extraTools?: ToolDefinition[];
18
+ /** Override the default single-state thinking workflow. */
19
+ workflow?: Workflow;
20
+ }
21
+
22
+ export interface McpAgentHandle {
23
+ host: AgentHost;
24
+ /** Disconnect the MCP client (call on shutdown alongside host.disconnect()). */
25
+ closeMcp: () => Promise<void>;
26
+ /** Names of MCP tools that were mapped to Ziggs tools on startup. */
27
+ toolNames: string[];
28
+ }
29
+
30
+ function buildTransport(config: McpTransportConfig) {
31
+ if (config.type === 'stdio') {
32
+ return new StdioClientTransport({
33
+ command: config.command,
34
+ args: config.args ?? [],
35
+ env: config.env,
36
+ });
37
+ }
38
+ return new StreamableHTTPClientTransport(new URL(config.url));
39
+ }
40
+
41
+ type McpTool = {
42
+ name: string;
43
+ description?: string;
44
+ inputSchema?: Record<string, unknown>;
45
+ };
46
+
47
+ type McpContent = { type: string; text?: string };
48
+
49
+ function mcpContentToResult(content: McpContent[]): unknown {
50
+ const text = content
51
+ .map(c => (c.type === 'text' ? c.text ?? '' : JSON.stringify(c)))
52
+ .join('\n');
53
+ try {
54
+ return JSON.parse(text);
55
+ } catch {
56
+ return text;
57
+ }
58
+ }
59
+
60
+ function mapMcpTools(client: Client, tools: McpTool[]): ToolDefinition[] {
61
+ return tools.map(tool => ({
62
+ schema: {
63
+ type: 'function' as const,
64
+ function: {
65
+ name: tool.name,
66
+ description: tool.description ?? '',
67
+ parameters: tool.inputSchema ?? { type: 'object', properties: {} },
68
+ },
69
+ },
70
+ handler: async (args: Record<string, unknown>) => {
71
+ const result = await client.callTool({ name: tool.name, arguments: args }) as {
72
+ isError?: boolean;
73
+ content: McpContent[];
74
+ };
75
+ if (result.isError) {
76
+ throw new Error(mcpContentToResult(result.content) as string);
77
+ }
78
+ return mcpContentToResult(result.content);
79
+ },
80
+ isGenericFallback: true,
81
+ }));
82
+ }
83
+
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, mcpTools: McpTool[]): Workflow {
107
+ const dflt = thinkingDefaults({ initial: 'idle' });
108
+ // Bind each MCP tool as an explicit action so runTurn includes them in the
109
+ // LLM API call and the decision schema. Without this, allowedToolNames is
110
+ // empty and the tools array passed to the LLM is also empty.
111
+ const toolActions = Object.fromEntries(
112
+ mcpTools.map(t => [t.name, {
113
+ tool: t.name,
114
+ prompt: { when: t.description || `Use ${t.name}` },
115
+ }]),
116
+ );
117
+ return {
118
+ id: 'mcp-agent',
119
+ description: description || 'MCP-backed agent',
120
+ initial: 'idle',
121
+ states: {
122
+ idle: {
123
+ kind: 'thinking',
124
+ prompt: {
125
+ role: 'You are a helpful agent powered by an MCP server.',
126
+ goal: description || 'Help the user by calling the available tools.',
127
+ 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.',
128
+ },
129
+ actions: {
130
+ ...dflt.actions,
131
+ ...toolActions,
132
+ respond: {
133
+ prompt: {
134
+ instruction: 'Send your final answer to the user.',
135
+ when: 'You have gathered enough information and are ready to reply to the user.',
136
+ format: 'args: { receiverId: "<userId>", message: "<your answer>" }',
137
+ },
138
+ produces: 'message-sent',
139
+ },
140
+ },
141
+ transitions: [
142
+ { to: 'idle', when: (o) => o.kind === 'wait' },
143
+ { to: 'idle', when: (o) => o.kind === 'error' },
144
+ { to: 'done', when: (o) => o.kind === 'message-sent' },
145
+ ],
146
+ },
147
+ done: {
148
+ kind: 'parked',
149
+ transitions: [
150
+ { to: 'idle', when: (o) => o.kind === 'message-received' },
151
+ ],
152
+ },
153
+ },
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Wraps any MCP server as a Ziggs AgentHost.
159
+ *
160
+ * 1. Opens a connection to the MCP server (stdio or HTTP).
161
+ * 2. Calls tools/list and maps each tool to a Ziggs ToolDefinition.
162
+ * 3. Returns an AgentHost pre-loaded with those tools, ready to connectAsync().
163
+ *
164
+ * Usage:
165
+ * const { host, closeMcp } = await createMcpAgent({
166
+ * transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] },
167
+ * agentId: 'my-github-agent',
168
+ * name: 'GitHub Agent',
169
+ * description: 'Wraps the GitHub MCP server',
170
+ * });
171
+ * await host.connectAsync();
172
+ *
173
+ * process.on('SIGTERM', async () => {
174
+ * host.disconnect();
175
+ * await host.drain();
176
+ * await closeMcp();
177
+ * });
178
+ */
179
+ export async function createMcpAgent(options: CreateMcpAgentOptions): Promise<McpAgentHandle> {
180
+ const { transport: transportConfig, extraTools = [], workflow, ...hostOptions } = options;
181
+
182
+ const transport = buildTransport(transportConfig);
183
+ const client = new Client({ name: 'ziggs-mcp-agent', version: '1.0.0' });
184
+
185
+ await client.connect(transport);
186
+
187
+ const { tools: rawTools } = await client.listTools();
188
+ const mcpTools = mapMcpTools(client, rawTools as McpTool[]);
189
+
190
+ const resolvedWorkflow = workflow ?? buildDefaultWorkflow(hostOptions.description ?? '', rawTools as McpTool[]);
191
+
192
+ const host = new AgentHost({
193
+ ...hostOptions,
194
+ workflow: resolvedWorkflow,
195
+ tools: [...mcpTools, ...extraTools],
196
+ });
197
+
198
+ return {
199
+ host,
200
+ closeMcp: () => client.close(),
201
+ toolNames: rawTools.map((t: McpTool) => t.name),
202
+ };
203
+ }
@@ -62,49 +62,85 @@ export class OpenAIAdapter {
62
62
  : undefined;
63
63
 
64
64
  try {
65
- const { usage: _u, ...restOptions } = options as Record<string, unknown>;
66
- // Verbose LLM request/response logging — commented out to reduce log noise.
67
- // Re-enable by uncommenting to see full prompt/output blocks per LLM call.
68
- // const divider = '─'.repeat(80);
69
- // console.log(`\n${divider}`);
70
- // console.log(`📤 LLM REQUEST → ${this.model} (${messages.length} msg${messages.length === 1 ? '' : 's'})`);
71
- // console.log(divider);
72
- // for (const m of messages) {
73
- // const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
74
- // const len = content?.length ?? 0;
75
- // console.log(`\n[${m.role.toUpperCase()}] ${len} chars`);
76
- // console.log(content ?? '');
77
- // }
78
- // console.log(`${divider}\n`);
65
+ const { usage: _u, response_format: _explicitResponseFormat, ...restOptions } = options as Record<string, unknown>;
66
+ const responseFormat = openAITools
67
+ ? undefined
68
+ : ((_explicitResponseFormat as OpenAI.ResponseFormatJSONObject) ?? { type: 'json_object' });
69
+
70
+ // OpenAI 400s response_format:{type:'json_object'} unless the word "json"
71
+ // appears somewhere in the messages. Callers are expected to mention it
72
+ // in their system prompt, but that's an implicit cross-file contract —
73
+ // guarantee it here instead of trusting every call site to remember.
74
+ let finalMessages = messages;
75
+ if (
76
+ responseFormat?.type === 'json_object' &&
77
+ !messages.some(m => {
78
+ const c = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
79
+ return /json/i.test(c);
80
+ })
81
+ ) {
82
+ finalMessages = [...messages, { role: 'system', content: 'Respond with valid JSON.' }];
83
+ }
84
+
85
+ // Verbose LLM request/response logging — enabled for debugging.
86
+ // Comment out again to reduce log noise.
87
+ const divider = '─'.repeat(80);
88
+ console.log(`\n${divider}`);
89
+ console.log(`📤 LLM REQUEST → ${this.model} (${finalMessages.length} msg${finalMessages.length === 1 ? '' : 's'})`);
90
+ console.log(divider);
91
+ for (const m of finalMessages) {
92
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
93
+ const len = content?.length ?? 0;
94
+ console.log(`\n[${m.role.toUpperCase()}] ${len} chars`);
95
+ console.log(content ?? '');
96
+ }
97
+ console.log(`${divider}\n`);
79
98
  const res = await this.client.chat.completions.create({
80
99
  model: this.model,
81
- messages,
100
+ messages: finalMessages,
82
101
  stream: false,
83
- max_completion_tokens: 3000,
84
- response_format: openAITools ? undefined : ((restOptions['response_format'] as OpenAI.ResponseFormatJSONObject) ?? { type: 'json_object' }),
102
+ // 25000 reserves room for hidden reasoning tokens on reasoning models
103
+ // (gpt-5.x/o-series) — they're billed against this cap before any
104
+ // visible content, so a low cap can exhaust the budget and return
105
+ // empty content with finish_reason "length".
106
+ max_completion_tokens: 25000,
107
+ response_format: responseFormat,
85
108
  ...(openAITools && { tools: openAITools }),
86
109
  ...restOptions,
87
110
  } as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming);
88
111
 
89
112
  const msg = res.choices[0].message;
113
+ const finishReason = res.choices[0].finish_reason;
90
114
  const content = msg.content?.trim() ?? null;
91
115
  const toolCalls = msg.tool_calls?.length ? msg.tool_calls : null;
92
- // Verbose LLM response logging — commented out to reduce log noise.
93
- // console.log(`\n${divider}`);
94
- // console.log(`📥 LLM RESPONSE ← ${this.model}`);
95
- // console.log(divider);
96
- // if (content) {
97
- // console.log(`\n[CONTENT] ${content.length} chars`);
98
- // console.log(content);
99
- // }
100
- // if (toolCalls) {
101
- // console.log(`\n[TOOL CALLS] ${toolCalls.length}`);
102
- // for (const tc of toolCalls) {
103
- // console.log(` → ${tc.function.name}(${tc.function.arguments})`);
104
- // }
105
- // }
106
- // if (!content && !toolCalls) console.log('\n(no content, no tool calls)');
107
- // console.log(`${divider}\n`);
116
+ // Verbose LLM response logging — enabled for debugging.
117
+ // Comment out again to reduce log noise.
118
+ console.log(`\n${divider}`);
119
+ console.log(`📥 LLM RESPONSE ← ${this.model} finish=${finishReason}`);
120
+ console.log(divider);
121
+ if (content) {
122
+ console.log(`\n[CONTENT] ${content.length} chars`);
123
+ console.log(content);
124
+ }
125
+ if (toolCalls) {
126
+ console.log(`\n[TOOL CALLS] ${toolCalls.length}`);
127
+ for (const tc of toolCalls) {
128
+ console.log(` → ${tc.function.name}(${tc.function.arguments})`);
129
+ }
130
+ }
131
+ if (!content && !toolCalls) console.log('\n(no content, no tool calls)');
132
+ if (res.usage) {
133
+ const u = res.usage as OpenAI.CompletionUsage & { completion_tokens_details?: { reasoning_tokens?: number } };
134
+ 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}`);
135
+ }
136
+ console.log(`${divider}\n`);
137
+ if (!content && !toolCalls && finishReason === 'length') {
138
+ const u = res.usage as (OpenAI.CompletionUsage & { completion_tokens_details?: { reasoning_tokens?: number } }) | undefined;
139
+ throw new Error(
140
+ `LLM returned no usable output — completion token budget exhausted before any content ` +
141
+ `(finish_reason=length, reasoning_tokens=${u?.completion_tokens_details?.reasoning_tokens ?? 'unknown'})`,
142
+ );
143
+ }
108
144
  return {
109
145
  content,
110
146
  tool_calls: toolCalls,
@@ -131,7 +167,7 @@ export class OpenAIAdapter {
131
167
  model: this.model,
132
168
  messages,
133
169
  stream: true,
134
- max_completion_tokens: 3000,
170
+ max_completion_tokens: 25000,
135
171
  });
136
172
  let text = '';
137
173
  for await (const chunk of stream) {
@@ -116,7 +116,13 @@ export class HistoryFormatter {
116
116
 
117
117
  formatParticipant(participant: AnyObj | null | undefined, agentId: string): string {
118
118
  if (!participant) return 'unknown';
119
- const id = (participant['id'] as string) || 'unknown';
119
+ // Message.sender is stored as {agentId, underAgreementId} (no `id`, no
120
+ // `type`) while Message.receiver is {id, type} — two different shapes on
121
+ // the same document (see backend Message schema). Without this fallback,
122
+ // every sender renders as "unknown (unknown)" and self-sent history rows
123
+ // never match `id === agentId`, so the agent can't recognize its own
124
+ // past messages in <history>.
125
+ const id = (participant['id'] as string) || (participant['agentId'] as string) || 'unknown';
120
126
  if (participant['type'] === 'system') return 'System';
121
127
  if (id === agentId) return 'You';
122
128
  return `${id} (${participant['type'] || 'unknown'})`;
package/src/index.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  */
10
10
 
11
11
  export { defineAgent } from './defineAgent.js';
12
+ export { createMcpAgent } from './adapters/McpAdapter.js';
13
+ export type { CreateMcpAgentOptions, McpAgentHandle, McpTransportConfig } from './adapters/McpAdapter.js';
12
14
  export { AgentHost, createAgent, createAgentPool } from './AgentHost.js';
13
15
  export { Agent } from './agent/Agent.js';
14
16
  export type { TickInput, TickOutput, AgentOptions } from './agent/Agent.js';
@@ -144,11 +144,18 @@ export function applyFleetAgreementPricing(
144
144
  return next;
145
145
  }
146
146
 
147
- /** Default tier price when a fleet agent counters without an explicit `price`. */
148
- export function applyFleetCounterPricing(
149
- payload: ProtocolPricingPayload,
147
+ /**
148
+ * Default tier price when a fleet agent counters without an explicit `price`.
149
+ *
150
+ * Generic over the payload shape so the precise field types of the caller's
151
+ * payload survive the round-trip — this helper only ever sets `price`, so a
152
+ * richer input type (e.g. protocolRunner's `ProtocolPayload`) must come back
153
+ * out unwidened by `ProtocolPricingPayload`'s `[key: string]: unknown` index.
154
+ */
155
+ export function applyFleetCounterPricing<T extends ProtocolPricingPayload>(
156
+ payload: T,
150
157
  actingAgentId: string,
151
- ): ProtocolPricingPayload {
158
+ ): T {
152
159
  if (payload.price != null && payload.price !== undefined) {
153
160
  return payload;
154
161
  }
@@ -157,7 +164,7 @@ export function applyFleetCounterPricing(
157
164
  payload.description ?? payload.agreementDescription,
158
165
  );
159
166
  if (cents == null) return payload;
160
- return { ...payload, price: cents };
167
+ return { ...payload, price: cents } as T;
161
168
  }
162
169
 
163
170
  /** User or agent direct-create / marketplace: default from provider specialist. */
@@ -208,7 +208,9 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj, omitMessage
208
208
  const content = event.text || event.result || event.content;
209
209
  if (!content) return `<event type="${eventType}" from="${from}">(Empty)</event>`;
210
210
  const str = typeof content === 'object' ? JSON.stringify(content) : String(content);
211
- const display = str.length > 1000 ? str.slice(0, 1000) + '...' : str;
211
+ // Matches MAX_MSG_CHARS in runTurn.ts — the incoming instruction shouldn't
212
+ // be truncated tighter than what the agent itself is allowed to send back.
213
+ const display = str.length > 3500 ? str.slice(0, 3500) + '...' : str;
212
214
  return `<event type="${eventType}" from="${from}">\n${display}\n</event>`;
213
215
  }
214
216
 
@@ -424,9 +424,13 @@ function eventsAreTerminal(events: EmittedEvent[]): boolean {
424
424
  // (api-client now returns Agreement directly, not wrapped in { agreement }).
425
425
  const ag = (r.agreement ?? (r.agreementId ? r : null)) as Record<string, unknown> | null | undefined;
426
426
  const task = (r.task || (r.taskId ? r : null)) as Record<string, unknown> | null | undefined;
427
+ // Agreement's own status lives flat as `proposalStatus` (see api-client
428
+ // Agreement type) — not nested under `.proposal.status`. Check both so
429
+ // a successful agreement_propose is recognized as terminal instead of
430
+ // being re-issued by the LLM on the next loop iteration.
427
431
  const agProposal = ag?.proposal as Record<string, unknown> | undefined;
428
432
  const taskProposal = task?.proposal as Record<string, unknown> | undefined;
429
- const proposalStatus = agProposal?.status || taskProposal?.status;
433
+ const proposalStatus = ag?.proposalStatus ?? agProposal?.status ?? taskProposal?.status;
430
434
  if (proposalStatus === 'pending' || proposalStatus === 'approved') return true;
431
435
  const state = task?.state || r.state;
432
436
  if (state === 'active' || state === 'in-progress' ||
@@ -445,10 +449,11 @@ function eventsContainAgreementCreation(events: EmittedEvent[]): boolean {
445
449
  if (r.success === false || r.error) return false;
446
450
  // Support both nested { agreement: {...} } and flat Agreement objects.
447
451
  const ag = (r.agreement ?? (r.agreementId ? r : null)) as Record<string, unknown> | undefined;
452
+ // See eventsAreTerminal above: proposalStatus is flat on Agreement, not nested.
448
453
  const agProposal = ag?.proposal as Record<string, unknown> | undefined;
449
454
  const task = r.task as Record<string, unknown> | undefined;
450
455
  const taskProposal = task?.proposal as Record<string, unknown> | undefined;
451
- const proposalStatus = agProposal?.status || taskProposal?.status;
456
+ const proposalStatus = ag?.proposalStatus ?? agProposal?.status ?? taskProposal?.status;
452
457
  return proposalStatus === 'pending' || proposalStatus === 'approved';
453
458
  });
454
459
  }
@@ -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
  }
@@ -171,7 +171,14 @@ export async function runInboxCatchUp(deps: InboxCatchUpDeps): Promise<InboxCatc
171
171
  const toAck: InboxAck[] = [];
172
172
 
173
173
  if (envelope.truncatedScopes > 0) {
174
- warn(label, `inbox truncatedScopes=${envelope.truncatedScopes} — skipping ack until narrowed`);
174
+ // Informational only — does NOT block acking the scopes actually
175
+ // returned below. truncatedScopes just means the caller's total reach
176
+ // exceeded the server's per-call scope cap (backend/src/inbox/inbox.service.ts,
177
+ // INBOX_MAX_SCOPES); each entry in envelope.scopes is still an
178
+ // independently accurate, safe-to-ack unit of news for that scope. Never
179
+ // acking here meant a busy long-lived agent (reach > cap) would never ack
180
+ // anything and re-deliver its entire backlog on every reconnect forever.
181
+ warn(label, `inbox truncatedScopes=${envelope.truncatedScopes} (reach exceeds per-call cap — examined scopes are still acked normally)`);
175
182
  }
176
183
 
177
184
  for (const scope of envelope.scopes) {
@@ -202,11 +209,7 @@ export async function runInboxCatchUp(deps: InboxCatchUpDeps): Promise<InboxCatc
202
209
  }
203
210
  }
204
211
 
205
- if (
206
- envelope.truncatedScopes === 0 &&
207
- scope.latestAt &&
208
- (scope.newMessages > 0 || scope.newArtifacts > 0)
209
- ) {
212
+ if (scope.latestAt && (scope.newMessages > 0 || scope.newArtifacts > 0)) {
210
213
  toAck.push({
211
214
  kind: scope.scope.kind,
212
215
  id: scope.scope.id,
@@ -3,13 +3,13 @@ import {
3
3
  getAgreementStatus, listAgreements, getMyAgreements, getAgreement,
4
4
  publishOffer as publishOfferApi, createAgreement, revokeAgreement, getAgreementsByChat, linkAgreementToChat,
5
5
  getChatsForAgreement, joinAgreement, linkArtifactToAgreement, getArtifactsForAgreement,
6
- linkUserToAgreement, getUsersForAgreement, createContract,
6
+ linkUserToAgreement, getUsersForAgreement,
7
7
  type Creds, type Agreement,
8
8
  type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput,
9
9
  type PublishOfferPayload,
10
10
  type DelegateAgreementData, type CounterAgreementData,
11
11
  type CreateAgreementBody, type ListAgreementsFilters, type GetMyAgreementsFilters,
12
- type ChatLinkType, type ArtifactLinkType, type UserRole, type CreateContractInput,
12
+ type ChatLinkType, type ArtifactLinkType, type UserRole,
13
13
  } from '@ziggs-ai/api-client';
14
14
 
15
15
  export class AgreementService {
@@ -21,11 +21,6 @@ export class AgreementService {
21
21
  this.creds = { operatorKey, agentId };
22
22
  }
23
23
 
24
- /** @deprecated Prefer {@link proposeDirect} or {@link proposeBroadcast}. */
25
- contract(input: CreateContractInput): Promise<Agreement> {
26
- return createContract(input, this.creds);
27
- }
28
-
29
24
  /** 1:1 proposal. Server defaults engagementKind to `service` when omitted. */
30
25
  proposeDirect(payload: ProposeDirectInput): Promise<Agreement> {
31
26
  return proposeAgreement(payload, this.creds);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createTask, getTask, getActiveTasksForAgent, getActiveTasksForChat, getSubtasks,
3
3
  updateTaskState, cancelTask,
4
- claimLedgerTask, reportTask, setTaskSatisfaction, countTasks,
4
+ reportTask, setTaskSatisfaction, countTasks,
5
5
  type Creds, type Task, type TaskState,
6
6
  } from '@ziggs-ai/api-client';
7
7
  import type { CountTasksFilters } from '@ziggs-ai/api-client';
@@ -50,7 +50,7 @@ export class TaskService {
50
50
  return getSubtasks(parentTaskId, this.creds);
51
51
  }
52
52
 
53
- async spawnUnderAgreement(agreementId: string, taskData: { description: string; parentTaskId?: string; plan?: unknown; planReviewTiming?: string; requireMidWorkPlanAck?: boolean }): Promise<Task> {
53
+ async spawnUnderAgreement(agreementId: string, taskData: { description: string; parentTaskId?: string; plan?: unknown; planReviewTiming?: string; requireMidWorkPlanAck?: boolean; assigneeId?: string }): Promise<Task> {
54
54
  if (!agreementId) throw new Error('TaskService.spawnUnderAgreement: agreementId is required');
55
55
  try {
56
56
  const result = await createTask({ ...taskData, agreementId } as Parameters<typeof createTask>[0], this.creds);
@@ -85,17 +85,6 @@ export class TaskService {
85
85
  }
86
86
  }
87
87
 
88
- async claimLedgerTask(taskId: string): Promise<Task> {
89
- try {
90
- const task = await claimLedgerTask(taskId, this.creds);
91
- runtimeLog.info('TaskService', `[ledger] Claimed task ${taskId}`);
92
- return task as unknown as Task;
93
- } catch (error) {
94
- runtimeLog.warn('TaskService', `[ledger] Claim failed for ${taskId}: ${(error as Error).message}`);
95
- throw error;
96
- }
97
- }
98
-
99
88
  async report(taskId: string, message = ''): Promise<Task> {
100
89
  return reportTask(taskId, message, this.creds);
101
90
  }
@@ -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;
@@ -1,5 +1,5 @@
1
1
  import { defineTool, type ToolDefinition } from '../../../tools/defineTool.js';
2
- import { pullFromLedger } from '@ziggs-ai/api-client';
2
+ import { pullQuests } from '@ziggs-ai/api-client';
3
3
 
4
4
  type Ctx = Record<string, unknown>;
5
5
 
@@ -18,7 +18,7 @@ export const marketplaceViewTool: ToolDefinition = defineTool('marketplace_view'
18
18
  limit: typeof args['limit'] === 'number' ? args['limit'] as number : 20,
19
19
  };
20
20
  if (args['since']) options.since = args['since'] as string;
21
- const quests = await pullFromLedger(options, { operatorKey, agentId });
21
+ const quests = await pullQuests(options, { operatorKey, agentId });
22
22
  return { quests, count: quests.length };
23
23
  },
24
24
  { description: 'Browse open quests on the marketplace — work buyers have broadcast to willing agents. Returns public quests plus any org-scoped quests in your org. To claim a quest, use agreement_respond on the returned agreementId. To publish your own, use agreement_propose({ proposedTo: "everyone" }) for a public quest or proposedTo: "org" to keep it inside your org.' },
@@ -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');