@ziggs-ai/agent-sdk 0.38.2 → 0.38.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,9 +131,9 @@ configureApiClient({ httpUrl: 'https://api.example.com' });
131
131
 
132
132
  A **runner** is the operator’s process. It starts agents, serves health and
133
133
  coordinates shutdown. It is not another part of the customer’s agent model.
134
- The repository’s private `@ziggs-ai/agent-runner` workspace owns `runAgents` and
135
- `createHealthServer`; these are not SDK exports. An operator deploys the runner
136
- and supplies a supervisor such as ECS to restart the process when it exits.
134
+ `runAgents` and `createHealthServer` are exported from this package. The health
135
+ page answers `/` and `/health`. A process that needs another route, such as
136
+ starting one of our hosted agents, adds it itself.
137
137
 
138
138
  ## Migration
139
139
 
@@ -146,7 +146,7 @@ This is a breaking pre-beta rename; update imports before publishing this releas
146
146
  | `ClaudeSdkAgent`, `ClaudeSdkAgentOptions` | `ClaudeBrain`, `ClaudeBrainOptions` |
147
147
  | `host.agent` | `agent.brain` |
148
148
  | `createMcpAgent(...).host` | `createMcpAgent(...).agent` |
149
- | SDK `runLauncher`, `createHealthServer` | Operator package `runAgents`, `createHealthServer` |
149
+ | SDK `runLauncher`, `createHealthServer` | SDK `runAgents`, `createHealthServer` |
150
150
 
151
151
  `createAgent`, `ziggsBrain`, `claudeBrain` and agent IDs on the wire retain their
152
152
  names. No backend identity or stored data migration is required.
package/dist/Agent.js CHANGED
@@ -314,7 +314,7 @@ export class Agent {
314
314
  operatorKey: this.options.operatorKey,
315
315
  agentId: this.options.agentId,
316
316
  };
317
- const messageSender = async (message, receiverId, sessionId, messageId, conversationAgreementId, underAgreementId, taskId) => {
317
+ const messageSender = async (message, receiverId, sessionId, messageId, conversationAgreementId, underAgreementId, taskId, sources) => {
318
318
  const sentId = messageId ?? `sdk_${randomUUID()}`;
319
319
  const result = await sendChatMessage({
320
320
  chatId: sessionId,
@@ -330,6 +330,7 @@ export class Agent {
330
330
  conversationAgreementId,
331
331
  underAgreementId,
332
332
  taskId,
333
+ ...(sources ? { sources } : {}),
333
334
  }, creds);
334
335
  if (!result.success)
335
336
  throw new Error('sendChatMessage: the server did not accept this message — nothing was delivered and nobody was woken. Retry with the same messageId, which the backend dedups on.');
@@ -5,7 +5,7 @@ import type { ToolManager } from '../tools/ToolManager.js';
5
5
  import { type RawEvent } from '../runtime/buildOutcome.js';
6
6
  import type { TickInput, TickOutput } from './ZiggsBrain.js';
7
7
  import { type AskWakeGate } from './wakeGate.js';
8
- export { isPublishProbeChat, isCapabilityProbeSession } from '../probeChat.js';
8
+ export { isPublishProbeChat } from '../probeChat.js';
9
9
  /**
10
10
  * status notices must not spend a Claude turn answering
11
11
  * nobody. Real work wakes arrive as chat `message`, synthetic `task_result`
@@ -17,7 +17,7 @@ import { filterStandingOfferNotices, isReadableAgreementNoticeLane } from './sta
17
17
  import { formedByFromWake, shouldSkipAgentFormedHireActivation, } from './hireActivationNotice.js';
18
18
  import { isPublishProbeChat } from '../probeChat.js';
19
19
  import { gateableMessages, wakeAgreementId } from './wakeGate.js';
20
- export { isPublishProbeChat, isCapabilityProbeSession } from '../probeChat.js';
20
+ export { isPublishProbeChat } from '../probeChat.js';
21
21
  /**
22
22
  * status notices must not spend a Claude turn answering
23
23
  * nobody. Real work wakes arrive as chat `message`, synthetic `task_result`
@@ -1,3 +1,4 @@
1
+ import { SOURCES_PARAM, sourcesArg } from '@ziggs-ai/api-client';
1
2
  import { isAgreementId, agreementLaneId } from '@ziggs-ai/api-client';
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import { credsFromToolContext } from '../tools/toolCreds.js';
@@ -77,8 +78,10 @@ export const chatSendTool = defineTool('chat_send', {
77
78
  receiverId: { type: 'string', description: 'Name someone only when you mean them. Leave it out to write back in this room: one other participant is the recipient, and with several Jev may select one. Otherwise the message stays in the room with no specific wake. To name a masked party, pass its rpb_ ref as it came. Pass human only for a broadcast to the people in the room; it does not wake assistants or stand-ins. For a reply from a person or their assistant, name that person\'s returned participant id. This does not grant their assistant access or guarantee a reply.' },
78
79
  text: { type: 'string', required: true },
79
80
  idempotencyKey: { type: 'string', description: 'Reuse for retry-safe resend of the same logical message.' },
81
+ sources: SOURCES_PARAM,
80
82
  }, async (args, ctx) => {
81
83
  const c = ctx;
84
+ const sources = sourcesArg(args);
82
85
  const explicitChatId = asChatLane(firstFilled(args.chatId));
83
86
  const to = firstFilled(args.to);
84
87
  const text = String(args.text);
@@ -104,6 +107,7 @@ export const chatSendTool = defineTool('chat_send', {
104
107
  underAgreementId: firstFilled(args.underAgreementId, c.serviceAgreementId),
105
108
  taskId: firstFilled(args.taskId),
106
109
  ...(namedReceiver ? { receiverId: namedReceiver } : {}),
110
+ ...sources,
107
111
  }, creds);
108
112
  // Never presented as a send that happened: the message does not exist,
109
113
  // so whoever was waiting for it is still waiting. Retrying with the same
@@ -141,6 +145,7 @@ export const chatSendTool = defineTool('chat_send', {
141
145
  ...(receiverId ? { receiverId } : {}),
142
146
  text,
143
147
  messageId,
148
+ ...sources,
144
149
  });
145
150
  },
146
151
  // This line used to read "Conversation only — never use for
package/dist/index.d.ts CHANGED
@@ -41,7 +41,7 @@ export { runMechanical } from './runtime/runMechanical.js';
41
41
  export { PromptBuilder } from './runtime/PromptBuilder.js';
42
42
  export { thinkingDefaults, defineThinkingState, defineMechanicalState, DEFAULT_WAIT_PROMPT } from './runtime/defaults.js';
43
43
  export { KICKOFF_STATE, CLAIM_KICKOFF_STATE, injectKickoffDefault, buildKickoffState, buildClaimKickoffState, handlesClaimWake, } from './runtime/kickoff.js';
44
- export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
44
+ export { validateWorkflow, WorkflowValidationError, PROBE_CTX } from './runtime/validateWorkflow.js';
45
45
  export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
46
46
  export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
47
47
  export { createDeliveryWriteKeys } from './runtime/deliveryWriteKeys.js';
@@ -67,7 +67,7 @@ export { HistoryFormatter, historyFormatter, AgreementFormatter, agreementFormat
67
67
  export { ProactiveTrigger } from './platform/proactive/ProactiveTrigger.js';
68
68
  export { PollRegistry, declaredPolls, type PollBinding, type PollContext, type PollKind, type PollResult, } from './platform/polls/PollKind.js';
69
69
  export { PollRunner, type PollRunnerOptions, type WakeTarget } from './platform/polls/PollRunner.js';
70
- export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool, agreementFulfillTool, taskCreateTool, taskSetResultTool, taskUpdatePlanStepTool, dispatchProtocolOp, PAYMENT_TOOLS, paymentBalanceTool, CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, MCP_CONNECTION_TOOLS, mcpToolCallTool, mcpToolsListTool, DISCOVERY_TOOLS, agentSearchTool, agentGetTool, MARKETPLACE_TOOLS, marketplaceViewTool, CONTEXT_TOOLS, contextReadTool, contextRequestTool, accessRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, GRANTS_TOOLS, listGrantsTool, PLAN_TOOLS, taskReplacePlanTool, LINK_TOOLS, linkProposeTool, linkListTool, INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, ARTIFACT_TOOLS, recordArtifactTool, CHAT_TOOLS, openConversationTool, LISTING_TOOLS, } from './platform/tools/index.js';
70
+ export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool, agreementFulfillTool, taskCreateTool, taskSetResultTool, taskUpdatePlanStepTool, dispatchProtocolOp, PAYMENT_TOOLS, paymentBalanceTool, CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, MCP_CONNECTION_TOOLS, mcpToolCallTool, mcpToolsListTool, DISCOVERY_TOOLS, agentSearchTool, agentGetTool, MARKETPLACE_TOOLS, marketplaceViewTool, CONTEXT_TOOLS, contextReadTool, accessRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, GRANTS_TOOLS, listGrantsTool, PLAN_TOOLS, taskReplacePlanTool, LINK_TOOLS, linkProposeTool, linkListTool, INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, ARTIFACT_TOOLS, recordArtifactTool, CHAT_TOOLS, openConversationTool, LISTING_TOOLS, } from './platform/tools/index.js';
71
71
  export { CLAUDE_SDK_TOOLS } from './brain/claudeSdkTools.js';
72
72
  export { hostedModelTools, isHostedDeferred, hostedToolLane } from './brain/hostedToolSurface.js';
73
73
  export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from './tasks/protocolRegistry.js';
@@ -83,3 +83,7 @@ export type { ApiClientConfig } from '@ziggs-ai/api-client';
83
83
  export { SeenMessages } from './platform/SeenMessages.js';
84
84
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
85
85
  export type { RuntimeLogLevel } from './shared/runtimeLog.js';
86
+ export { runAgents } from './launcher/runAgents.js';
87
+ export type { RunnerOptions, SingleAgentRun, FleetRun } from './launcher/runAgents.js';
88
+ export { createHealthServer, inboxHealth } from './launcher/createHealthServer.js';
89
+ export type { AgentLiveness, HealthRequest, HealthRequestHandler } from './launcher/createHealthServer.js';
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ export { runMechanical } from './runtime/runMechanical.js';
42
42
  export { PromptBuilder } from './runtime/PromptBuilder.js';
43
43
  export { thinkingDefaults, defineThinkingState, defineMechanicalState, DEFAULT_WAIT_PROMPT } from './runtime/defaults.js';
44
44
  export { KICKOFF_STATE, CLAIM_KICKOFF_STATE, injectKickoffDefault, buildKickoffState, buildClaimKickoffState, handlesClaimWake, } from './runtime/kickoff.js';
45
- export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
45
+ export { validateWorkflow, WorkflowValidationError, PROBE_CTX } from './runtime/validateWorkflow.js';
46
46
  export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
47
47
  export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
48
48
  export { createDeliveryWriteKeys } from './runtime/deliveryWriteKeys.js';
@@ -79,7 +79,7 @@ export {
79
79
  // Tier 1 — protocol grammar (framework-managed via taskTools).
80
80
  PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool, agreementFulfillTool, taskCreateTool, taskSetResultTool, taskUpdatePlanStepTool, dispatchProtocolOp,
81
81
  // Tier 2 — opt-in capability bundles (spread into `tools:`).
82
- PAYMENT_TOOLS, paymentBalanceTool, CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, MCP_CONNECTION_TOOLS, mcpToolCallTool, mcpToolsListTool, DISCOVERY_TOOLS, agentSearchTool, agentGetTool, MARKETPLACE_TOOLS, marketplaceViewTool, CONTEXT_TOOLS, contextReadTool, contextRequestTool, accessRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, GRANTS_TOOLS, listGrantsTool, PLAN_TOOLS, taskReplacePlanTool, LINK_TOOLS, linkProposeTool, linkListTool, INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, ARTIFACT_TOOLS, recordArtifactTool, CHAT_TOOLS, openConversationTool, LISTING_TOOLS, } from './platform/tools/index.js';
82
+ PAYMENT_TOOLS, paymentBalanceTool, CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, MCP_CONNECTION_TOOLS, mcpToolCallTool, mcpToolsListTool, DISCOVERY_TOOLS, agentSearchTool, agentGetTool, MARKETPLACE_TOOLS, marketplaceViewTool, CONTEXT_TOOLS, contextReadTool, accessRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, GRANTS_TOOLS, listGrantsTool, PLAN_TOOLS, taskReplacePlanTool, LINK_TOOLS, linkProposeTool, linkListTool, INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, ARTIFACT_TOOLS, recordArtifactTool, CHAT_TOOLS, openConversationTool, LISTING_TOOLS, } from './platform/tools/index.js';
83
83
  // The always-on toolset for the hosted Claude cognition (parity guard
84
84
  // reads this as part of the real SDK registry).
85
85
  export { CLAUDE_SDK_TOOLS } from './brain/claudeSdkTools.js';
@@ -96,3 +96,5 @@ export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl,
96
96
  configureApiClient, } from '@ziggs-ai/api-client';
97
97
  export { SeenMessages } from './platform/SeenMessages.js';
98
98
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
99
+ export { runAgents } from './launcher/runAgents.js';
100
+ export { createHealthServer, inboxHealth } from './launcher/createHealthServer.js';
@@ -0,0 +1,50 @@
1
+ import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
2
+ import type { Agent } from '../Agent.js';
3
+ import type { ConnectionPool } from '../platform/ConnectionPool.js';
4
+ /** What one agent reports about its own inbox reading. */
5
+ export type AgentLiveness = ReturnType<Agent['inboxLiveness']>;
6
+ /**
7
+ * The health verdict. A process is alive when its agents are still reading,
8
+ * so the answer is derived from the inbox loop rather than from the fact that
9
+ * this HTTP server replied — a wedged process replies happily while none of
10
+ * its agents read anything.
11
+ *
12
+ * The restart decision reads `lastAttemptAt`, not `lastPassAt`. When the
13
+ * backend is down every pass fails while the loops are perfectly healthy, and
14
+ * restarting the fleet would fix nothing and lose in-flight turns. Read
15
+ * staleness is still reported per agent, for the store and for monitoring.
16
+ */
17
+ export declare function inboxHealth(rows: AgentLiveness[], now?: number, staleMs?: number): {
18
+ ok: boolean;
19
+ reason?: string;
20
+ staleMs: number;
21
+ watching: number;
22
+ oldestAttemptAgeMs: number | null;
23
+ oldestPassAgeMs: number | null;
24
+ agents: (AgentLiveness & {
25
+ passAgeMs: number | null;
26
+ attemptAgeMs: number | null;
27
+ })[];
28
+ };
29
+ export type HealthRequest = {
30
+ req: IncomingMessage;
31
+ res: ServerResponse;
32
+ pool?: ConnectionPool;
33
+ };
34
+ /**
35
+ * A route the embedding process adds. Return true once the response is sent.
36
+ * The health page itself only answers `/` and `/health`.
37
+ */
38
+ export type HealthRequestHandler = (call: HealthRequest) => Promise<boolean>;
39
+ export declare function createHealthServer({ port, label, pool, liveness, onRequest, }?: {
40
+ port?: number;
41
+ label?: string;
42
+ pool?: ConnectionPool;
43
+ /**
44
+ * Every agent this process is responsible for. Omit only when there is
45
+ * nothing to watch: without it `/health` can say no more than "this server
46
+ * replied", which is the answer this route exists to stop giving.
47
+ */
48
+ liveness?: () => AgentLiveness[];
49
+ onRequest?: HealthRequestHandler;
50
+ }): Server;
@@ -0,0 +1,68 @@
1
+ import { createServer } from 'node:http';
2
+ import { runtimeLog } from '../shared/runtimeLog.js';
3
+ /**
4
+ * How long an agent's inbox loop may go without turning before this process is
5
+ * called unhealthy. An idle loop completes a pass every `DEFAULT_WAIT_SECONDS`
6
+ * (110s), so this is three missed cycles plus slack: long enough that a slow
7
+ * server or a long turn never flaps, short enough that a wedged process is
8
+ * replaced within minutes.
9
+ */
10
+ const STALE_MS = Number(process.env.FLEET_INBOX_STALE_MS) || 360_000;
11
+ function age(now, at) {
12
+ return at === null ? null : now - at;
13
+ }
14
+ /**
15
+ * The health verdict. A process is alive when its agents are still reading,
16
+ * so the answer is derived from the inbox loop rather than from the fact that
17
+ * this HTTP server replied — a wedged process replies happily while none of
18
+ * its agents read anything.
19
+ *
20
+ * The restart decision reads `lastAttemptAt`, not `lastPassAt`. When the
21
+ * backend is down every pass fails while the loops are perfectly healthy, and
22
+ * restarting the fleet would fix nothing and lose in-flight turns. Read
23
+ * staleness is still reported per agent, for the store and for monitoring.
24
+ */
25
+ export function inboxHealth(rows, now = Date.now(), staleMs = STALE_MS) {
26
+ const agents = rows.map((r) => ({
27
+ ...r,
28
+ passAgeMs: age(now, r.lastPassAt),
29
+ attemptAgeMs: age(now, r.lastAttemptAt),
30
+ }));
31
+ // Only agents that are supposed to be polling can be stale. A connected
32
+ // agent with the inbox loop off (lab and eval paths) has nothing to report,
33
+ // and an agent that was deliberately stopped is not this process failing.
34
+ const watched = agents.filter((r) => r.pollingInbox && r.connected);
35
+ const turning = watched.filter((r) => r.attemptAgeMs !== null && r.attemptAgeMs <= staleMs);
36
+ const ages = watched.map((r) => r.attemptAgeMs).filter((n) => n !== null);
37
+ const passAges = watched.map((r) => r.passAgeMs).filter((n) => n !== null);
38
+ const ok = watched.length === 0 || turning.length > 0;
39
+ return {
40
+ ok,
41
+ ...(ok ? {} : { reason: `no inbox pass attempted by any of ${watched.length} agent(s) within ${staleMs}ms` }),
42
+ staleMs,
43
+ watching: watched.length,
44
+ oldestAttemptAgeMs: ages.length ? Math.max(...ages) : null,
45
+ oldestPassAgeMs: passAges.length ? Math.max(...passAges) : null,
46
+ agents,
47
+ };
48
+ }
49
+ export function createHealthServer({ port = Number(process.env.PORT) || 8080, label = 'agents', pool, liveness, onRequest, } = {}) {
50
+ const server = createServer(async (req, res) => {
51
+ if (await onRequest?.({ req, res, pool }))
52
+ return;
53
+ const url = req.url ?? '/';
54
+ if (url === '/health' || url === '/') {
55
+ const report = inboxHealth(liveness?.() ?? []);
56
+ if (!report.ok) {
57
+ runtimeLog.warn(label, `health: ${report.reason}`);
58
+ }
59
+ res.writeHead(report.ok ? 200 : 503, { 'Content-Type': 'application/json' });
60
+ res.end(JSON.stringify(report));
61
+ return;
62
+ }
63
+ res.writeHead(404);
64
+ res.end();
65
+ });
66
+ server.listen(port, () => runtimeLog.info(label, `health server on port ${port}`));
67
+ return server;
68
+ }
@@ -0,0 +1,32 @@
1
+ import { type Agent, type AgentOptions } from '../Agent.js';
2
+ import { ConnectionPool } from '../platform/ConnectionPool.js';
3
+ import { type HealthRequestHandler } from './createHealthServer.js';
4
+ import type { Server } from 'node:http';
5
+ /** Process concerns only. Every Agent owns its inbox and external polls. */
6
+ export interface RunnerOptions {
7
+ operatorKey?: string;
8
+ label?: string;
9
+ port?: number | string;
10
+ healthServer?: boolean;
11
+ /** Disable when the embedding process owns its signal handlers. */
12
+ handleSignals?: boolean;
13
+ drainTimeoutMs?: number;
14
+ onShutdown?: () => Promise<void>;
15
+ agentMeta?: Record<string, unknown>[];
16
+ primary?: AgentOptions | ((pool: ConnectionPool) => AgentOptions);
17
+ /** A route this process adds. The health page itself only answers `/` and `/health`. */
18
+ onRequest?: HealthRequestHandler;
19
+ }
20
+ export interface SingleAgentRun {
21
+ agent: Agent;
22
+ healthServer?: Server;
23
+ shutdown(): Promise<void>;
24
+ }
25
+ export interface FleetRun {
26
+ pool: ConnectionPool;
27
+ primary: Agent | null;
28
+ healthServer?: Server;
29
+ shutdown(): Promise<void>;
30
+ }
31
+ export declare function runAgents(config: AgentOptions, options?: RunnerOptions): Promise<SingleAgentRun>;
32
+ export declare function runAgents(configs: AgentOptions[], options?: RunnerOptions): Promise<FleetRun>;
@@ -0,0 +1,119 @@
1
+ import { createAgent } from '../Agent.js';
2
+ import { ConnectionPool } from '../platform/ConnectionPool.js';
3
+ import { boundKeyFleetWarning } from '../shared/operatorKeyClaims.js';
4
+ import { runtimeLog } from '../shared/runtimeLog.js';
5
+ import { createHealthServer } from './createHealthServer.js';
6
+ const DRAIN_TIMEOUT_MS = Number(process.env.FLEET_DRAIN_TIMEOUT_MS) || 90_000;
7
+ function shutdownHandler(stop, server, options) {
8
+ let pending;
9
+ const shutdown = () => {
10
+ if (!pending) {
11
+ process.off('SIGTERM', signal);
12
+ process.off('SIGINT', signal);
13
+ pending = (async () => {
14
+ try {
15
+ await stop();
16
+ await options.onShutdown?.();
17
+ }
18
+ finally {
19
+ if (server)
20
+ await new Promise((resolve) => server.close(() => resolve()));
21
+ }
22
+ })();
23
+ }
24
+ return pending;
25
+ };
26
+ const signal = () => {
27
+ runtimeLog.info(options.label || 'runner', 'Shutting down');
28
+ void shutdown().then(() => process.exit(0), (err) => {
29
+ runtimeLog.error(options.label || 'runner', `Shutdown failed: ${err.message}`);
30
+ process.exit(1);
31
+ });
32
+ };
33
+ if (options.handleSignals !== false) {
34
+ process.on('SIGTERM', signal);
35
+ process.on('SIGINT', signal);
36
+ }
37
+ return shutdown;
38
+ }
39
+ async function healthServer(options, liveness, pool) {
40
+ if (options.healthServer === false)
41
+ return undefined;
42
+ const server = createHealthServer({
43
+ port: Number(options.port ?? process.env.PORT ?? 8080),
44
+ label: options.label || 'runner',
45
+ pool,
46
+ liveness,
47
+ onRequest: options.onRequest,
48
+ });
49
+ await new Promise((resolve, reject) => {
50
+ server.once('error', reject);
51
+ server.once('listening', () => { server.off('error', reject); resolve(); });
52
+ });
53
+ return server;
54
+ }
55
+ export async function runAgents(configOrConfigs, options = {}) {
56
+ const effectiveKey = options.operatorKey ?? process.env.ZIGGS_OPERATOR_KEY;
57
+ const decorate = (config) => effectiveKey && !config.operatorKey ? { ...config, operatorKey: effectiveKey } : config;
58
+ const drainTimeout = options.drainTimeoutMs ?? DRAIN_TIMEOUT_MS;
59
+ if (!Array.isArray(configOrConfigs)) {
60
+ const agent = createAgent(decorate(configOrConfigs));
61
+ let server;
62
+ // One budget for stopping intake and finishing in-flight work: the inbox
63
+ // pass gets up to the whole of it to acknowledge, the queue what is left.
64
+ const stop = async () => {
65
+ const deadline = Date.now() + drainTimeout;
66
+ await agent.disconnect(drainTimeout);
67
+ await agent.drain(Math.max(0, deadline - Date.now()));
68
+ };
69
+ try {
70
+ await agent.connectAsync();
71
+ server = await healthServer(options, () => [agent.inboxLiveness()]);
72
+ }
73
+ catch (err) {
74
+ await stop();
75
+ throw err;
76
+ }
77
+ const shutdown = shutdownHandler(stop, server, options);
78
+ return { agent, healthServer: server, shutdown };
79
+ }
80
+ const pool = new ConnectionPool();
81
+ const configs = configOrConfigs.map(decorate);
82
+ pool.register(configs, options.agentMeta);
83
+ const primaryConfig = typeof options.primary === 'function' ? options.primary(pool) : options.primary;
84
+ let primary = null;
85
+ let server;
86
+ // Same single budget as the one-agent path, shared by the pool and the primary.
87
+ const stop = async () => {
88
+ const deadline = Date.now() + drainTimeout;
89
+ await Promise.all([primary?.disconnect(drainTimeout), pool.shutdown(drainTimeout)]);
90
+ await primary?.drain(Math.max(0, deadline - Date.now()));
91
+ };
92
+ try {
93
+ if (primaryConfig)
94
+ primary = createAgent(decorate(primaryConfig));
95
+ const controlKey = effectiveKey ?? configs.find(c => c.operatorKey)?.operatorKey ?? primary?.options.operatorKey;
96
+ if (controlKey) {
97
+ const warning = boundKeyFleetWarning(controlKey, pool.size + (primary ? 1 : 0));
98
+ if (warning)
99
+ runtimeLog.warn(options.label || 'runner', warning);
100
+ }
101
+ await primary?.connectAsync();
102
+ await pool.connectAll();
103
+ const connected = pool.listActive().length;
104
+ if (configs.length > 0 && connected === 0) {
105
+ throw new Error(`0/${configs.length} agents started; process must restart`);
106
+ }
107
+ if (connected !== pool.size || pool.size !== configs.length) {
108
+ runtimeLog.warn(options.label || 'runner', `Only ${connected}/${configs.length} agents started — hosting the ones that came up`);
109
+ }
110
+ server = await healthServer(options, () => [...(primary ? [primary.inboxLiveness()] : []), ...pool.liveness()], pool);
111
+ runtimeLog.info(options.label || 'runner', `Ready — ${connected}/${pool.size} agents started`);
112
+ }
113
+ catch (err) {
114
+ await stop();
115
+ throw err;
116
+ }
117
+ const shutdown = shutdownHandler(stop, server, options);
118
+ return { pool, primary, healthServer: server, shutdown };
119
+ }
@@ -1,3 +1,4 @@
1
+ import type { WriteSource } from '@ziggs-ai/api-client';
1
2
  /**
2
3
  * Ziggs-flavored EffectHandler.
3
4
  *
@@ -36,7 +37,7 @@ export interface ZiggsEffectDeps {
36
37
  contextReadClient: ContextReadClient;
37
38
  taskService: TaskService;
38
39
  agreementService: AgreementService;
39
- messageSender: (text: string, receiverId: string | undefined, sessionId: string, messageId?: string, conversationAgreementId?: string, underAgreementId?: string, taskId?: string) => Promise<unknown>;
40
+ messageSender: (text: string, receiverId: string | undefined, sessionId: string, messageId?: string, conversationAgreementId?: string, underAgreementId?: string, taskId?: string, sources?: WriteSource[]) => Promise<unknown>;
40
41
  /** Sends a streaming chunk to a client. Called per-token during stream-text effects. */
41
42
  chunkSender?: (sessionId: string, receiverId: string, text: string, messageId: string) => void;
42
43
  toolManager: ToolManager;
@@ -318,7 +318,7 @@ export function createZiggsEffectHandler(deps) {
318
318
  });
319
319
  }
320
320
  case 'send-message': {
321
- const sent = await deps.messageSender(effect.text, effect.receiverId, effect.sessionId, effect.messageId, effect.conversationAgreementId, effect.underAgreementId, effect.taskId);
321
+ const sent = await deps.messageSender(effect.text, effect.receiverId, effect.sessionId, effect.messageId, effect.conversationAgreementId, effect.underAgreementId, effect.taskId, effect.sources);
322
322
  outbox.track(effect.sessionId, { text: effect.text, receiverId: effect.receiverId });
323
323
  // Carry the send's own report back to whoever asked for the send. A
324
324
  // sender that cannot see where its message went cannot tell a delivered
@@ -1,3 +1,4 @@
1
+ import type { WriteSource } from '@ziggs-ai/api-client';
1
2
  import { type Task, type TaskState } from '@ziggs-ai/api-client';
2
3
  import type { CountTasksFilters, CreateTaskData } from '@ziggs-ai/api-client';
3
4
  export declare class TaskService {
@@ -9,7 +10,7 @@ export declare class TaskService {
9
10
  getActiveTasksForChat(chatId: string): Promise<Task[]>;
10
11
  getSubtasks(parentTaskId: string): Promise<Task[]>;
11
12
  spawnUnderAgreement(agreementId: string, taskData: Omit<CreateTaskData, 'agreementId'>): Promise<Task>;
12
- updateState(taskId: string, state: TaskState, result?: unknown, idempotencyKey?: string): Promise<{
13
+ updateState(taskId: string, state: TaskState, result?: unknown, idempotencyKey?: string, sources?: WriteSource[]): Promise<{
13
14
  ok: true;
14
15
  taskId: string;
15
16
  state: string;
@@ -46,7 +47,7 @@ export declare class TaskService {
46
47
  updatePlanStep(taskId: string, stepId: string, status: string, result?: unknown, waitingOn?: {
47
48
  kind: 'user' | 'agent';
48
49
  id: string;
49
- } | null): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
50
+ } | null, sources?: WriteSource[]): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
50
51
  cancel(taskId: string): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
51
52
  report(taskId: string, message?: string): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
52
53
  setSatisfaction(taskId: string, satisfaction: 'positive' | 'negative'): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
@@ -60,11 +60,12 @@ export class TaskService {
60
60
  throw error;
61
61
  }
62
62
  }
63
- async updateState(taskId, state, result, idempotencyKey) {
63
+ async updateState(taskId, state, result, idempotencyKey, sources) {
64
64
  try {
65
65
  const data = {
66
66
  ...(result !== undefined ? { result: safeJsonStringify(result) } : {}),
67
67
  ...(idempotencyKey ? { idempotencyKey } : {}),
68
+ ...(sources ? { sources } : {}),
68
69
  };
69
70
  const updated = await updateTaskState(taskId, state, data, this.creds);
70
71
  runtimeLog.info('TaskService', `Task state: ${taskId} → ${state}`);
@@ -97,9 +98,9 @@ export class TaskService {
97
98
  throw error;
98
99
  }
99
100
  }
100
- async updatePlanStep(taskId, stepId, status, result, waitingOn) {
101
+ async updatePlanStep(taskId, stepId, status, result, waitingOn, sources) {
101
102
  try {
102
- const updated = await updateTaskPlanStep(taskId, stepId, status, result, this.creds, waitingOn);
103
+ const updated = await updateTaskPlanStep(taskId, stepId, status, result, this.creds, waitingOn, sources);
103
104
  runtimeLog.info('TaskService', `Task plan step: ${taskId}/${stepId} → ${status}`);
104
105
  return updated;
105
106
  }
@@ -21,7 +21,7 @@ export { DISCOVERY_TOOLS, agentSearchTool, agentGetTool } from './tier2/discover
21
21
  export { MARKETPLACE_TOOLS, marketplaceViewTool } from './tier2/marketplaceTools.js';
22
22
  export { PAYMENT_TOOLS, paymentBalanceTool } from './tier2/paymentTools.js';
23
23
  export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
24
- export { CONTEXT_TOOLS, contextReadTool, contextRequestTool, accessRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
24
+ export { CONTEXT_TOOLS, contextReadTool, accessRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
25
25
  export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
26
26
  export { TASK_TOOLS, listTasksTool, cancelTaskTool } from './tier2/taskTools.js';
27
27
  export { PLAN_TOOLS, taskReplacePlanTool } from './tier2/planTools.js';
@@ -23,7 +23,7 @@ export { DISCOVERY_TOOLS, agentSearchTool, agentGetTool } from './tier2/discover
23
23
  export { MARKETPLACE_TOOLS, marketplaceViewTool } from './tier2/marketplaceTools.js';
24
24
  export { PAYMENT_TOOLS, paymentBalanceTool } from './tier2/paymentTools.js';
25
25
  export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
26
- export { CONTEXT_TOOLS, contextReadTool, contextRequestTool, accessRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
26
+ export { CONTEXT_TOOLS, contextReadTool, accessRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
27
27
  // one grant lister across every rail (retires context_discover +
28
28
  // payment_list_grants). Always-available on the hosted agent — see CLAUDE_SDK_TOOLS.
29
29
  export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
@@ -45,6 +45,12 @@ function normalizePlanInPlace(payload) {
45
45
  ? 'before_execution'
46
46
  : 'with_proposal';
47
47
  }
48
+ /** The `sources` a protocol call carried, when it carried a list. */
49
+ function sourcesOf(payload) {
50
+ return Array.isArray(payload.sources)
51
+ ? { sources: payload.sources }
52
+ : {};
53
+ }
48
54
  export async function dispatchProtocolOp(services, payload, chatId, _agents = [], actingAgentId = null, writeKeys = null,
49
55
  /** #8 — prefer the cached session snapshot over a fresh HTTP read. */
50
56
  cachedAgreements) {
@@ -93,6 +99,7 @@ cachedAgreements) {
93
99
  lifecycle: payload.lifecycle,
94
100
  maxExecutions: payload.maxExecutions,
95
101
  description: payload.description,
102
+ ...sourcesOf(payload),
96
103
  });
97
104
  case 'agreement-check-proposal':
98
105
  if (!payload.agreementId)
@@ -182,6 +189,7 @@ cachedAgreements) {
182
189
  ...(payload.checkBack && typeof payload.checkBack === 'object'
183
190
  ? { checkBack: payload.checkBack }
184
191
  : {}),
192
+ ...sourcesOf(payload),
185
193
  // Wake-derived idempotency key (delivery law): a crash-replay re-issuing
186
194
  // this spawn resolves to the already-created task server-side instead of
187
195
  // creating a duplicate. Keyed on what is actually written (the resolved
@@ -204,7 +212,7 @@ cachedAgreements) {
204
212
  const state = normalizeState(terminalRaw);
205
213
  // Wake-derived idempotency key: a crash-replay re-issuing this transition
206
214
  // no-ops server-side instead of erroring on the already-terminal task.
207
- return taskService.updateState(payload.taskId, state, payload.result, writeKeys?.taskStateKey(payload.taskId, state));
215
+ return taskService.updateState(payload.taskId, state, payload.result, writeKeys?.taskStateKey(payload.taskId, state), sourcesOf(payload).sources);
208
216
  }
209
217
  case 'task-update-plan-step': {
210
218
  if (!payload.taskId)
@@ -225,7 +233,7 @@ cachedAgreements) {
225
233
  // a stepId that isn't in the plan — the backend 400s on both. That must never
226
234
  // hard-fail the tool → fail the task → wake-loop. Make it best-effort.
227
235
  try {
228
- return await taskService.updatePlanStep(payload.taskId, payload.stepId, payload.status, payload.result, waitingOn);
236
+ return await taskService.updatePlanStep(payload.taskId, payload.stepId, payload.status, payload.result, waitingOn, sourcesOf(payload).sources);
229
237
  }
230
238
  catch {
231
239
  return { ok: false, skipped: 'plan-step-not-updatable', stepId: payload.stepId };
@@ -286,6 +294,7 @@ async function proposeAgreement(services, payload, chatId, writeKeys = null) {
286
294
  billing,
287
295
  ...(payload.conversationAllowance !== undefined ? { conversationAllowance: payload.conversationAllowance } : {}),
288
296
  agreementDescription: payload.agreementDescription,
297
+ ...sourcesOf(payload),
289
298
  parentAgreementId: payload.parentAgreementId,
290
299
  engagementKind,
291
300
  idempotencyKey,
@@ -1,7 +1,7 @@
1
1
  import { defineTool } from '../../../tools/defineTool.js';
2
2
  import { requireProtocolOperation } from '../../../tasks/protocolRegistry.js';
3
3
  import { dispatchProtocolOp } from './protocolRunner.js';
4
- import { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION, agreementClaimCapability, agreementSubcontractCapability, } from '@ziggs-ai/api-client';
4
+ import { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION, agreementClaimCapability, agreementSubcontractCapability, SOURCES_PARAM, } from '@ziggs-ai/api-client';
5
5
  /** keep these enums in lockstep with MCP ziggs_agreement_* / ziggs_task_create. */
6
6
  const LIFECYCLE_ENUM = ['open', 'time-bound', 'count-bound'];
7
7
  const PLAN_REVIEW_TIMING_ENUM = ['with_proposal', 'before_execution'];
@@ -60,21 +60,22 @@ function handlerFor(toolName) {
60
60
  return result;
61
61
  };
62
62
  }
63
- export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service'], description: 'Contract type. Defaults to service. For a bilateral trust link, use link_propose.' }, parentAgreementId: 'string', providerId: { type: 'string', description: AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION }, price: { type: 'number', description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Convert both ways: a human asking for "ϟ2 per task" means 200, not 2. ϟ is the points symbol, not a hundredth.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', conversationAllowance: { type: 'number', description: 'Incoming-message limit shared across covered rooms; omitted on new agreements means unlimited.' }, billing: { type: 'string', enum: ['total', 'per_task'], description: "How price reads: 'total' (default, escrowed and paid at the end) or 'per_task' (a rate settled as each task completes — standing agreements only, and the default for a hire)." }, agreementDescription: 'string' }, handlerFor('agreement_propose'), { description: 'Propose an agreement — direct, broadcast, or link; there are no separate publish tools. Engagement precedence (posted-first): 1) REUSE an active agreement on matching terms, 2) CLAIM the counterparty\'s listing (marketplace_view → agreement_claim; never counter a listing), 3) POST a request broadcast here when nothing listed fits, 4) direct-propose ONLY for bespoke terms, renegotiation, or commissioning a named counterparty with no listing — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay") on direct proposals. Broadcast with proposedTo="everyone" (fully public) or "org" (your org only) and no providerId posts a REQUEST any agent may claim. A person or the deploy publishes a standing listing; this agent does not. For a bilateral trust link, use link_propose: it takes an email or an agent id, or neither for a share link. engagementKind defaults to service. Agreements are STANDING by default (no expiry, unlimited tasks under one agreement); set expiresAt or maxExecutions only when the engagement should end on its own.', isAgreementCreation: true });
63
+ export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service'], description: 'Contract type. Defaults to service. For a bilateral trust link, use link_propose.' }, parentAgreementId: 'string', providerId: { type: 'string', description: AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION }, price: { type: 'number', description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Convert both ways: a human asking for "ϟ2 per task" means 200, not 2. ϟ is the points symbol, not a hundredth.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', conversationAllowance: { type: 'number', description: 'Incoming-message limit shared across covered rooms; omitted on new agreements means unlimited.' }, billing: { type: 'string', enum: ['total', 'per_task'], description: "How price reads: 'total' (default, escrowed and paid at the end) or 'per_task' (a rate settled as each task completes — standing agreements only, and the default for a hire)." }, agreementDescription: 'string', sources: SOURCES_PARAM }, handlerFor('agreement_propose'), { description: 'Propose an agreement — direct, broadcast, or link; there are no separate publish tools. Engagement precedence (posted-first): 1) REUSE an active agreement on matching terms, 2) CLAIM the counterparty\'s listing (marketplace_view → agreement_claim; never counter a listing), 3) POST a request broadcast here when nothing listed fits, 4) direct-propose ONLY for bespoke terms, renegotiation, or commissioning a named counterparty with no listing — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay") on direct proposals. Broadcast with proposedTo="everyone" (fully public) or "org" (your org only) and no providerId posts a REQUEST any agent may claim. A person or the deploy publishes a standing listing; this agent does not. For a bilateral trust link, use link_propose: it takes an email or an agent id, or neither for a share link. engagementKind defaults to service. Agreements are STANDING by default (no expiry, unlimited tasks under one agreement); set expiresAt or maxExecutions only when the engagement should end on its own.', isAgreementCreation: true });
64
64
  export const agreementSubcontractTool = defineTool('agreement_subcontract', agreementSubcontractCapability.params, handlerFor('agreement_subcontract'), { description: agreementSubcontractCapability.descriptions.sdk, isAgreementCreation: true });
65
65
  export const agreementRespondTool = defineTool('agreement_respond', { agreementId: { type: 'string', required: true }, action: { type: 'string', required: true, enum: ['approve', 'reject'] } }, handlerFor('agreement_respond'), { description: 'Approve or reject a pending DIRECT agreement proposal addressed to you. Open broadcasts (requests, standing offers, link invites) have no personal approval slot — claim those with agreement_claim instead, or ignore them to pass. ONE exception: a hand-off that pins YOU as provider carries your pending approval even as an open broadcast — approving consents to serving whoever claims it (the row stays open for claims); rejecting cancels the hand-off. Ignoring it leaves the hand-off unclaimable, so always respond.' });
66
66
  export const agreementClaimTool = defineTool('agreement_claim', agreementClaimCapability.params, handlerFor('agreement_claim'), { description: agreementClaimCapability.descriptions.sdk });
67
67
  export const taskSetResultTool = defineTool('task_set_result', {
68
68
  taskId: { type: 'string', required: true },
69
69
  // same param name as MCP ziggs_task_set_result / PATCH /tasks/:id/state.
70
- state: { type: 'string', required: true, enum: ['completed', 'failed', 'cancelled'] },
70
+ state: { type: 'string', required: true, enum: ['completed', 'failed'] },
71
71
  // Same object contract as MCP ({ summary, status, links, … }).
72
72
  result: {
73
73
  type: 'object',
74
74
  description: 'Outcome payload (summary, status, links, …). This is what the next agent reads when it picks the work up from its own inbox.',
75
75
  },
76
- }, handlerFor('task_set_result'), { description: 'Transition a task to its terminal state — completed, failed, or cancelled — and record the result. Set the result here as an object ({ summary, status, links }): a task the parties opted into is closed by this call, whatever else you also sent them. Pass state (not status). This closes the task and not the engagement: a standing hire stays active for the next task, ending one is agreement_fulfill. The reply reports what actually happened — what was stored, who the completion was queued for (queued, not read), what per-task metering did — so report those and no more; a completed task is not by itself a paid one.' });
77
- export const agreementCounterTool = defineTool('agreement_counter', { agreementId: { type: 'string', required: true }, price: { type: 'number', description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Convert both ways: a human asking for "ϟ2 per task" means 200, not 2. ϟ is the points symbol, not a hundredth.' }, agreementDescription: 'string', expiresAt: 'string', lifecycle: lifecycleParam, maxExecutions: 'integer', conversationAllowance: { type: 'number', description: 'Incoming-message limit shared across covered rooms; omitted on new agreements means unlimited.' }, description: 'string' }, handlerFor('agreement_counter'), { description: 'Counter a pending proposal with revised terms instead of approving or rejecting. Check status first with agreement_get.' });
76
+ sources: SOURCES_PARAM,
77
+ }, handlerFor('task_set_result'), { description: 'Transition a task to its terminal state, completed or failed, and record the result. To cancel a task use task_cancel. Set the result here as an object ({ summary, status, links }): a task the parties opted into is closed by this call, whatever else you also sent them. Pass state (not status). This closes the task and not the engagement: a standing hire stays active for the next task, ending one is agreement_fulfill. The reply reports what actually happened — what was stored, who the completion was queued for (queued, not read), what per-task metering did — so report those and no more; a completed task is not by itself a paid one.' });
78
+ export const agreementCounterTool = defineTool('agreement_counter', { agreementId: { type: 'string', required: true }, price: { type: 'number', description: 'Price in POINTS, as an integer of hundredths — 500 means ϟ5.00. Convert both ways: a human asking for "ϟ2 per task" means 200, not 2. ϟ is the points symbol, not a hundredth.' }, agreementDescription: 'string', expiresAt: 'string', lifecycle: lifecycleParam, maxExecutions: 'integer', conversationAllowance: { type: 'number', description: 'Incoming-message limit shared across covered rooms; omitted on new agreements means unlimited.' }, description: 'string', sources: SOURCES_PARAM }, handlerFor('agreement_counter'), { description: 'Counter a pending proposal with revised terms instead of approving or rejecting. Check status first with agreement_get.' });
78
79
  export const agreementGetTool = defineTool('agreement_get', { agreementId: { type: 'string', required: true } }, handlerFor('agreement_get'), { description: 'Read the current proposal status for an agreement (pending, approved, rejected, countered, expired).' });
79
80
  export const agreementRevokeTool = defineTool('agreement_revoke', { agreementId: { type: 'string', required: true } }, handlerFor('agreement_revoke'), { description: 'Revoke a pending proposal you created so it can no longer be approved. Use when the user has clearly moved on to a different request before responding to the previous proposal card.' });
80
81
  export const agreementFulfillTool = defineTool('agreement_fulfill', { agreementId: { type: 'string', required: true } }, handlerFor('agreement_fulfill'), { description: 'END an agreement you provide — permanently. Fulfilling terminates the whole relationship, not one deliverable: every grant it conferred is revoked and it cannot be reopened. Linked chats remain independent rooms governed by their own grants. Finished WORK is reported via task_set_result (the task closes; the agreement stays standing for the next task). Only fulfill when the engagement\'s full scope is delivered and nothing more is expected — never a standing hire that just finished a task.' });
@@ -92,6 +93,7 @@ export const taskUpdatePlanStepTool = defineTool('task_update_plan_step', {
92
93
  description: 'Step progress. Required with stepId.',
93
94
  },
94
95
  result: 'string',
96
+ sources: SOURCES_PARAM,
95
97
  // Both fields are required, but the required list cannot be declared here:
96
98
  // defineTool reads a sibling `required` as the flag that makes waitingOn
97
99
  // itself mandatory. The backend enforces the pair; the description says so.
@@ -108,7 +110,7 @@ export const taskUpdatePlanStepTool = defineTool('task_update_plan_step', {
108
110
  description: "Who this task now waits on, because you just asked them: { kind: 'user' | 'agent', id } — both fields required. Send it when you ask — with the step you are ticking, or on its own when the ask is all that happened. Your person then sees the task as waiting on them rather than as running, which is the difference between a job that looks stalled and one that is waiting for an answer. It clears itself: any later progress that does not restate it, and any terminal state, drops it.",
109
111
  },
110
112
  }, handlerFor('task_update_plan_step'), { description: "Update the status of a single step within a task's plan, and say who you are waiting on when you have just asked somebody. A task with no plan can still carry the waiting-on on its own." });
111
- export const taskCreateTool = defineTool('task_create', { 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).' }, inputArtifactIds: { type: 'array', items: { type: 'string' }, description: 'Prior-step output artifact handles, for structured input links.' }, plan: { type: 'object' }, planReviewTiming: planReviewTimingParam, requireMidWorkPlanAck: 'boolean', dueAt: dueAtParam, checkBack: checkBackParam }, handlerFor('task_create'), { description: 'Create 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. For work that means chasing an answer out of somebody, set dueAt and checkBack so the holder knows when to stop and how often it may remind.' });
113
+ export const taskCreateTool = defineTool('task_create', { 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).' }, inputArtifactIds: { type: 'array', items: { type: 'string' }, description: 'Prior-step output artifact handles, for structured input links.' }, plan: { type: 'object' }, planReviewTiming: planReviewTimingParam, requireMidWorkPlanAck: 'boolean', dueAt: dueAtParam, checkBack: checkBackParam, sources: SOURCES_PARAM }, handlerFor('task_create'), { description: 'Create 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. For work that means chasing an answer out of somebody, set dueAt and checkBack so the holder knows when to stop and how often it may remind.' });
112
114
  export const PROTOCOL_TOOLS = [
113
115
  agreementProposeTool, agreementSubcontractTool,
114
116
  agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool,
@@ -3,7 +3,6 @@ export declare const contextReadTool: ToolDefinition;
3
3
  export declare const openTool: ToolDefinition;
4
4
  export declare const accessExplainTool: ToolDefinition;
5
5
  export declare const accessRequestTool: ToolDefinition;
6
- export declare const contextRequestTool: ToolDefinition;
7
6
  export declare const contextDelegateTool: ToolDefinition;
8
7
  export declare const contextExpandReachTool: ToolDefinition;
9
8
  export declare const contextDiscoverGrantableTool: ToolDefinition;
@@ -1,4 +1,4 @@
1
- import { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, contextRequestCapability, accessRequestCapability, } from '@ziggs-ai/api-client';
1
+ import { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, accessRequestCapability, } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
3
  // context grant tools ride the same api-client
4
4
  // clients as ziggs-mcp; the whole definition (schema + handler) is now shared
@@ -7,7 +7,6 @@ export const contextReadTool = toolFromCapability(contextReadCapability);
7
7
  export const openTool = toolFromCapability(openCapability);
8
8
  export const accessExplainTool = toolFromCapability(accessExplainCapability);
9
9
  export const accessRequestTool = toolFromCapability(accessRequestCapability);
10
- export const contextRequestTool = toolFromCapability(contextRequestCapability);
11
10
  export const contextDelegateTool = toolFromCapability(contextDelegateCapability);
12
11
  export const contextExpandReachTool = toolFromCapability(contextExpandReachCapability);
13
12
  export const contextDiscoverGrantableTool = toolFromCapability(contextDiscoverGrantableCapability);
@@ -1,4 +1,4 @@
1
- import { replaceTaskPlan } from '@ziggs-ai/api-client';
1
+ import { replaceTaskPlan, SOURCES_PARAM, } from '@ziggs-ai/api-client';
2
2
  import { defineTool } from '../../../tools/defineTool.js';
3
3
  import { credsFromToolContext } from '../../../tools/toolCreds.js';
4
4
  /**
@@ -44,6 +44,7 @@ export const taskReplacePlanTool = defineTool('task_replace_plan', {
44
44
  type: 'object',
45
45
  description: 'What this step produced, as an object — e.g. { "agreementId": "agr_…" } for an approval you requested, or { "artifactId": "art_…" } for a pack you recorded. Stored with the step and read back by task_list.',
46
46
  },
47
+ sources: SOURCES_PARAM,
47
48
  },
48
49
  required: ['description'],
49
50
  },
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * One classifier for store-publish probe rooms.
3
3
  *
4
- * The runner mints `probe_<agentId>_<ts>`. An id prefix must not buy
5
- * capability evidence or a canned brain reply; it only names the room so
6
- * inbox pin and wake order can find it. Capability vs liveness is decided
7
- * by the store sync, not by this prefix.
4
+ * The runner mints `probe_<agentId>_<ts>`. That prefix skips the wake gate
5
+ * and the wake-rate breaker, so a store publish still hears a model reply.
6
+ * It does not write a canned answer, and it is not capability evidence.
7
+ * Capability versus liveness is decided by the store sync.
8
8
  */
9
9
  export declare const PROBE_CHAT_ID_PREFIX = "probe_";
10
10
  export declare function isPublishProbeChat(chatId: string | undefined | null): boolean;
11
- /** @deprecated use {@link isPublishProbeChat} — same classifier, old name. */
12
- export declare const isCapabilityProbeSession: typeof isPublishProbeChat;
package/dist/probeChat.js CHANGED
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * One classifier for store-publish probe rooms.
3
3
  *
4
- * The runner mints `probe_<agentId>_<ts>`. An id prefix must not buy
5
- * capability evidence or a canned brain reply; it only names the room so
6
- * inbox pin and wake order can find it. Capability vs liveness is decided
7
- * by the store sync, not by this prefix.
4
+ * The runner mints `probe_<agentId>_<ts>`. That prefix skips the wake gate
5
+ * and the wake-rate breaker, so a store publish still hears a model reply.
6
+ * It does not write a canned answer, and it is not capability evidence.
7
+ * Capability versus liveness is decided by the store sync.
8
8
  */
9
9
  export const PROBE_CHAT_ID_PREFIX = 'probe_';
10
10
  export function isPublishProbeChat(chatId) {
11
11
  return typeof chatId === 'string' && chatId.startsWith(PROBE_CHAT_ID_PREFIX);
12
12
  }
13
- /** @deprecated use {@link isPublishProbeChat} — same classifier, old name. */
14
- export const isCapabilityProbeSession = isPublishProbeChat;
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { WriteSource } from '@ziggs-ai/api-client';
1
2
  /**
2
3
  * Foundation types for the agent SDK workflow runtime.
3
4
  *
@@ -519,6 +520,7 @@ export type Effect = {
519
520
  conversationAgreementId?: string;
520
521
  underAgreementId?: string;
521
522
  taskId?: string;
523
+ sources?: WriteSource[];
522
524
  } | {
523
525
  kind: 'record-event';
524
526
  sessionId: string;
package/dist/types.js CHANGED
@@ -1,7 +1 @@
1
- /**
2
- * Foundation types for the agent SDK workflow runtime.
3
- *
4
- * One outcome vocabulary, one ctx shape, one transition signature.
5
- * Every state — parked or thinking — re-enters with exactly one Outcome.
6
- */
7
1
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/agent-sdk",
3
- "version": "0.38.2",
3
+ "version": "0.38.4",
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.37.4",
29
+ "@ziggs-ai/api-client": "0.37.6",
30
30
  "@ziggs-ai/contracts": "^0.23.3",
31
31
  "ajv": "^8.17.1",
32
32
  "dotenv": "^17.2.3",