@ziggs-ai/agent-sdk 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-07-30
4
+
5
+ ### Changed (breaking)
6
+
7
+ - `task-update` / `task_set_result` accept `state` only — legacy `status` alias removed (ZIG-1057).
8
+ - Removed export `syncAggregateTaskViews`; read pending/collected/delegated via `allPendingSubtasks` / `allCollectedResults` / `allDelegatedAgreementIds`. Ctx aggregate mirrors stay empty.
9
+
3
10
  ## 0.4.0 — 2026-07-23
4
11
 
5
12
  ### Added
@@ -1,7 +1,22 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
2
  import type { Workflow } from '../types.js';
3
3
  import type { ToolManager } from '../tools/ToolManager.js';
4
+ import { type RawEvent } from '../runtime/buildOutcome.js';
4
5
  import type { TickInput, TickOutput } from './Agent.js';
6
+ /**
7
+ * ZIG-1035 / ZIG-1052: status notices must not spend a Claude turn answering
8
+ * nobody. Real work wakes arrive as chat `message`, synthetic `task_result`
9
+ * from `wakeTaskAssignment`, or `agreement_lifecycle` (hire / proposal
10
+ * resolution — the FSM kickoffs on the same event; Claude must act too).
11
+ *
12
+ * Intentionally not mirrored as an FSM "notice skip": the FSM already no-ops
13
+ * enter-only outcomes in AgentMachine; `agreement_lifecycle` never produces
14
+ * `enter`, so hire wakes always step. Claude's filter is narrower — pure
15
+ * `notification` only.
16
+ *
17
+ * Empty / missing batch → notice-only (do not burn a turn on a malformed wake).
18
+ */
19
+ export declare function isNoticeOnlyWake(event: RawEvent | undefined): boolean;
5
20
  export type CognitionKind = 'fsm' | 'claude-sdk';
6
21
  export interface ClaudeSdkAgentOptions {
7
22
  workflow: Workflow;
@@ -9,19 +9,23 @@ import { claudeQueryOptions, buildClaudeEventPrompt, summarizeContext, extractAs
9
9
  import { assertHostedAgentZiggsTools } from './hostedAgentConnection.js';
10
10
  import { runtimeLog } from '../shared/runtimeLog.js';
11
11
  /**
12
- * ZIG-1035: lifecycle / status notices must not spend a Claude turn answering
13
- * nobody. Real work wakes arrive as chat `message` or as synthetic `task_result`
14
- * from `wakeTaskAssignment` (task channel). Terminal task_result notices still
15
- * run so orchestrators can consume completions.
12
+ * ZIG-1035 / ZIG-1052: status notices must not spend a Claude turn answering
13
+ * nobody. Real work wakes arrive as chat `message`, synthetic `task_result`
14
+ * from `wakeTaskAssignment`, or `agreement_lifecycle` (hire / proposal
15
+ * resolution — the FSM kickoffs on the same event; Claude must act too).
16
+ *
17
+ * Intentionally not mirrored as an FSM "notice skip": the FSM already no-ops
18
+ * enter-only outcomes in AgentMachine; `agreement_lifecycle` never produces
19
+ * `enter`, so hire wakes always step. Claude's filter is narrower — pure
20
+ * `notification` only.
21
+ *
22
+ * Empty / missing batch → notice-only (do not burn a turn on a malformed wake).
16
23
  */
17
- function isNoticeOnlyWake(event) {
24
+ export function isNoticeOnlyWake(event) {
18
25
  const events = getBatchEvents(event);
19
26
  if (events.length === 0)
20
- return false;
21
- return events.every((ev) => {
22
- const t = ev?.type;
23
- return t === 'notification' || t === 'agreement_lifecycle';
24
- });
27
+ return true;
28
+ return events.every((ev) => ev?.type === 'notification');
25
29
  }
26
30
  const DEFAULT_STATE = 'ready';
27
31
  /**
@@ -72,17 +72,27 @@ export function summarizeContext(snap) {
72
72
  function renderEventLines(event, lines) {
73
73
  if (event?.type)
74
74
  lines.push(`Event type: ${event.type}`);
75
- // ZIG-1035: lifecycle notices are events, not counterparty speech.
75
+ // ZIG-1035 / ZIG-1052: notifications are not counterparty speech.
76
+ // agreement_lifecycle is a work signal (hire approval) — label it as such.
76
77
  if (event?.text) {
77
- const notice = event.type === 'notification' ||
78
- event.type === 'agreement_lifecycle' ||
79
- event.entryType === 'notification';
80
- lines.push(`${notice ? 'Notification' : 'Message'}: ${event.text}`);
78
+ const label = event.type === 'agreement_lifecycle'
79
+ ? 'Agreement lifecycle'
80
+ : event.type === 'notification' || event.entryType === 'notification'
81
+ ? 'Notification'
82
+ : 'Message';
83
+ lines.push(`${label}: ${event.text}`);
81
84
  }
82
85
  if (event?.senderId)
83
86
  lines.push(`Sender: ${event.senderId}`);
84
87
  if (event?.operation)
85
88
  lines.push(`Operation: ${event.operation}`);
89
+ if (event?.type === 'agreement_lifecycle' &&
90
+ (event.operation === 'proposal_approved' || event.operation === 'proposal_approved_execute')) {
91
+ lines.push('Work signal: proposal approved. If you proposed this agreement, spawn the first task (task_create) under it now. If you are the hired provider, begin fulfilling per the agreement — do not ignore this wake.');
92
+ }
93
+ if (typeof event?.agreementId === 'string' && event.agreementId) {
94
+ lines.push(`Agreement: ${event.agreementId}`);
95
+ }
86
96
  // ZIG-832: surface the task/agreement this event carries. Without it the brain
87
97
  // can't tell WHICH task to update/complete (workers otherwise stall asking for
88
98
  // their own taskId) and can't see whether the referenced step already ran or
package/dist/index.d.ts CHANGED
@@ -34,7 +34,7 @@ export { KICKOFF_STATE, injectKickoffDefault, buildKickoffState } from './runtim
34
34
  export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
35
35
  export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
36
36
  export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
37
- export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, syncAggregateTaskViews, } from './runtime/taskBookkeeping.js';
37
+ export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
38
38
  export { createWakeWriteKeys } from './runtime/wakeWriteKeys.js';
39
39
  export type { Outcome, OutcomeKind, Ctx, Identity, HistoryEntry, ToolResultEntry, PendingSubtask, CollectedSubtaskResult, TaskBookkeeping, Workflow, State, ParkedState, ThinkingState, MechanicalState, Action, Transition, PromptDef, ActionPrompt, Produces, ProducesFn, AgreementRef, TaskRef, ProposalRef, AgentConfig, DefineAgentInput, ServicesConfig, Effect, EffectKind, EffectResult, EffectResultMap, EffectHandler, LlmMessage, LlmToolCall, LlmToolSchema, LlmResponse, ContextSnapshot, ReadContextOpts, ToolCallResult, RecordedEntry, WakeWriteKeys, } from './types.js';
40
40
  export { default } from './AgentHost.js';
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ export { KICKOFF_STATE, injectKickoffDefault, buildKickoffState } from './runtim
28
28
  export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
29
29
  export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
30
30
  export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
31
- export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, syncAggregateTaskViews, } from './runtime/taskBookkeeping.js';
31
+ export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
32
32
  export { createWakeWriteKeys } from './runtime/wakeWriteKeys.js';
33
33
  export { default } from './AgentHost.js';
34
34
  // ── Supporting modules ──────────────────────────────────────────────────────
@@ -64,7 +64,14 @@ interface WireMetadata {
64
64
  service?: {
65
65
  task?: WireTask;
66
66
  operation?: string;
67
- agreement?: unknown;
67
+ agreement?: {
68
+ agreementId?: string;
69
+ status?: string;
70
+ proposal?: {
71
+ status?: string;
72
+ };
73
+ parties?: WireParties;
74
+ };
68
75
  agreementId?: string;
69
76
  };
70
77
  [key: string]: unknown;
@@ -19,12 +19,41 @@ function isTaskRelated(metadata, taskData) {
19
19
  || contentType === 'task' || contentType === 'task_update'
20
20
  || Boolean(taskData.state || taskData.proposal || taskData.parentTaskId || taskData.executorId || taskData.agentId || taskData.payerId || taskData.createdBy));
21
21
  }
22
+ function wireOperation(metadata) {
23
+ const op = metadata.service?.operation ?? metadata.operation;
24
+ return typeof op === 'string' ? op : undefined;
25
+ }
26
+ function wireAgreementId(metadata) {
27
+ const raw = metadata.service?.agreementId ??
28
+ metadata.service?.agreement?.agreementId ??
29
+ metadata.agreementId;
30
+ return typeof raw === 'string' ? raw : '';
31
+ }
32
+ /**
33
+ * Self-hire / fully-consented create activates before the chat fan-out lands.
34
+ * The wire still says `proposal_created`, but the embedded agreement is already
35
+ * `active` / proposal `approved`. That is the hire work signal (same as
36
+ * `proposal_approved_execute`) — Claude must not park it as a notice (ZIG-1052).
37
+ */
38
+ function isAlreadyActivatedProposalCreated(metadata) {
39
+ if (wireOperation(metadata) !== 'proposal_created')
40
+ return false;
41
+ const agr = metadata.service?.agreement;
42
+ if (!agr || typeof agr !== 'object')
43
+ return false;
44
+ const status = agr.status;
45
+ const proposalStatus = agr.proposal?.status;
46
+ return status === 'active' || proposalStatus === 'approved';
47
+ }
22
48
  function isAgreementLifecycleNotification(metadata) {
23
49
  const entryType = metadata.entryType || 'message';
24
- const operation = metadata.service?.operation ?? metadata.operation;
25
- return (entryType === 'notification' &&
26
- typeof operation === 'string' &&
27
- PROPOSAL_RESOLVED_OPERATIONS.has(operation));
50
+ if (entryType !== 'notification')
51
+ return false;
52
+ const operation = wireOperation(metadata);
53
+ if (typeof operation === 'string' && PROPOSAL_RESOLVED_OPERATIONS.has(operation)) {
54
+ return true;
55
+ }
56
+ return isAlreadyActivatedProposalCreated(metadata);
28
57
  }
29
58
  function isTerminalTaskState(state) {
30
59
  return state === 'completed' || state === 'failed' || state === 'cancelled';
@@ -57,9 +86,10 @@ export function normalizeIncomingEvent({ text, metadata = {} }) {
57
86
  ...(entryType ? { entryType } : {}),
58
87
  };
59
88
  if (isAgreementLifecycleNotification(metadata)) {
60
- const operation = metadata.service?.operation ?? metadata.operation;
61
- const rawAgreementId = metadata.service?.agreementId ?? metadata.agreementId;
62
- const agreementId = typeof rawAgreementId === 'string' ? rawAgreementId : '';
89
+ const operation = isAlreadyActivatedProposalCreated(metadata)
90
+ ? 'proposal_approved_execute'
91
+ : wireOperation(metadata);
92
+ const agreementId = wireAgreementId(metadata);
63
93
  return {
64
94
  chatId,
65
95
  event: {
@@ -1,4 +1,4 @@
1
- import { migrateLegacyTaskFields, syncAggregateTaskViews } from './taskBookkeeping.js';
1
+ import { migrateLegacyTaskFields } from './taskBookkeeping.js';
2
2
  /**
3
3
  * The one place a per-lane `Ctx` is constructed.
4
4
  *
@@ -18,16 +18,15 @@ import { migrateLegacyTaskFields, syncAggregateTaskViews } from './taskBookkeepi
18
18
  export function bootstrapCtx(identity, existing) {
19
19
  if (existing) {
20
20
  const legacy = existing;
21
- const ctx = {
21
+ return {
22
22
  ...existing,
23
23
  identity,
24
24
  tasks: migrateLegacyTaskFields(legacy),
25
+ // ZIG-1057: deprecated mirrors stay empty; use all*() readers.
25
26
  pendingSubtasks: {},
26
27
  collectedResults: [],
27
28
  delegatedAgreementIds: [],
28
29
  };
29
- syncAggregateTaskViews(ctx);
30
- return ctx;
31
30
  }
32
31
  return {
33
32
  identity,
@@ -1,6 +1,6 @@
1
1
  import { isProtocolToolName, mapProtocolToolToOperation } from '../tasks/protocolRegistry.js';
2
2
  import { getBatchEvents, isTaskResultRelevantToAgent } from '../context/batch.js';
3
- import { bookForDelegation, ensureTaskBook, findBookHoldingPending, removeTaskBook, syncAggregateTaskViews, } from './taskBookkeeping.js';
3
+ import { bookForDelegation, ensureTaskBook, findBookHoldingPending, removeTaskBook, } from './taskBookkeeping.js';
4
4
  // ── outcomeFromEvent: incoming wire event → Outcome ──────────────────────────
5
5
  /**
6
6
  * Converts a raw incoming event into the single Outcome that re-enters the
@@ -480,5 +480,4 @@ export function applyOutcomeToCtx(ctx, outcome) {
480
480
  default:
481
481
  break;
482
482
  }
483
- syncAggregateTaskViews(ctx);
484
483
  }
@@ -26,10 +26,11 @@ export declare function hasDelegatedAgreement(ctx: Ctx, agreementId: string): bo
26
26
  * focused task. Leaves sibling entries (and the coordinator lane bucket) alone.
27
27
  */
28
28
  export declare function removeTaskBook(ctx: Ctx, taskId: string): void;
29
- /** Refresh deprecated aggregate views from the per-task map. */
30
- export declare function syncAggregateTaskViews(ctx: Ctx): void;
31
29
  /**
32
30
  * Migrate a pre-ZIG-977 persisted ctx (lane-level pendingSubtasks /
33
31
  * collectedResults / delegatedAgreementIds) into the tasks map.
32
+ * ZIG-1057: the deprecated lane aggregates are no longer auto-synced —
33
+ * readers use allPendingSubtasks / allCollectedResults / allDelegatedAgreementIds.
34
+ * This migrator stays so a session persisted before the tasks map still boots.
34
35
  */
35
36
  export declare function migrateLegacyTaskFields(existing: Record<string, unknown>): Record<string, TaskBookkeeping>;
@@ -106,15 +106,12 @@ export function removeTaskBook(ctx, taskId) {
106
106
  ctx.activeAgreementId = nextId ? (ctx.tasks[nextId]?.agreementId ?? null) : null;
107
107
  }
108
108
  }
109
- /** Refresh deprecated aggregate views from the per-task map. */
110
- export function syncAggregateTaskViews(ctx) {
111
- ctx.pendingSubtasks = allPendingSubtasks(ctx);
112
- ctx.collectedResults = allCollectedResults(ctx);
113
- ctx.delegatedAgreementIds = allDelegatedAgreementIds(ctx);
114
- }
115
109
  /**
116
110
  * Migrate a pre-ZIG-977 persisted ctx (lane-level pendingSubtasks /
117
111
  * collectedResults / delegatedAgreementIds) into the tasks map.
112
+ * ZIG-1057: the deprecated lane aggregates are no longer auto-synced —
113
+ * readers use allPendingSubtasks / allCollectedResults / allDelegatedAgreementIds.
114
+ * This migrator stays so a session persisted before the tasks map still boots.
118
115
  */
119
116
  export function migrateLegacyTaskFields(existing) {
120
117
  if (existing.tasks && typeof existing.tasks === 'object') {
@@ -1,4 +1,4 @@
1
- import { type Agreement, type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput, type PublishOfferPayload, type DelegateAgreementData, type CounterAgreementData, type CreateAgreementBody, type ListAgreementsFilters, type GetMyAgreementsFilters, type ChatLinkType, type ArtifactLinkType, type UserRole } from '@ziggs-ai/api-client';
1
+ import { type Agreement, type ClaimedKind, type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput, type PublishOfferPayload, type DelegateAgreementData, type CounterAgreementData, type CreateAgreementBody, type ListAgreementsFilters, type GetMyAgreementsFilters, type ChatLinkType, type ArtifactLinkType, type UserRole } from '@ziggs-ai/api-client';
2
2
  export declare class AgreementService {
3
3
  private creds;
4
4
  constructor(operatorKey: string, agentId: string);
@@ -20,12 +20,14 @@ export declare class AgreementService {
20
20
  delegate(payload: DelegateAgreementData): Promise<Agreement>;
21
21
  respond(agreementId: string, action: 'approve' | 'reject'): Promise<Agreement>;
22
22
  /**
23
- * ZIG-1021 — claim any open broadcast (quest, standing offer, link invite).
24
- * Routing (offer vs agreement claim endpoint) is decided in the shared flow.
23
+ * ZIG-1021 — claim any open broadcast (quest, standing offer, hand-off,
24
+ * link invite). Routing (offer vs agreement claim endpoint) is decided in
25
+ * the shared flow; `kind` is the api-client ClaimedKind so this signature
26
+ * can't drift from the flow again (a 'hand-off' addition broke the build).
25
27
  */
26
28
  claim(agreementId: string): Promise<{
27
29
  agreement: Agreement;
28
- kind: 'link' | 'offer' | 'quest';
30
+ kind: ClaimedKind;
29
31
  }>;
30
32
  /**
31
33
  * ZIG-1022 — a link proposal is an agreement (engagementKind 'link') to one
@@ -38,8 +38,10 @@ export class AgreementService {
38
38
  return respondToAgreement(agreementId, action, this.creds);
39
39
  }
40
40
  /**
41
- * ZIG-1021 — claim any open broadcast (quest, standing offer, link invite).
42
- * Routing (offer vs agreement claim endpoint) is decided in the shared flow.
41
+ * ZIG-1021 — claim any open broadcast (quest, standing offer, hand-off,
42
+ * link invite). Routing (offer vs agreement claim endpoint) is decided in
43
+ * the shared flow; `kind` is the api-client ClaimedKind so this signature
44
+ * can't drift from the flow again (a 'hand-off' addition broke the build).
43
45
  */
44
46
  claim(agreementId) {
45
47
  return claimOpenAgreement(agreementId, this.creds);
@@ -183,11 +183,9 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
183
183
  });
184
184
  }
185
185
  case 'task-update': {
186
- // ZIG-957: canonical tool param is `state` (MCP + REST). Accept legacy
187
- // `status` so fleet prompts that still emit it keep working.
188
- const terminalRaw = (typeof payload.state === 'string' && payload.state) ||
189
- (typeof payload.status === 'string' && payload.status) ||
190
- '';
186
+ // ZIG-1057: `state` only — legacy `status` alias removed. Fleet prompts
187
+ // must emit state (coffee / fruit / ledger / agent-definer).
188
+ const terminalRaw = typeof payload.state === 'string' && payload.state ? payload.state : '';
191
189
  if (!payload.taskId || !terminalRaw)
192
190
  return null;
193
191
  const state = normalizeState(terminalRaw);
@@ -34,8 +34,8 @@ function handlerFor(operation) {
34
34
  }
35
35
  export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service', 'link'], description: "Contract type. Defaults to service. 'link' proposes a bilateral trust link (no work, no money)." }, parentAgreementId: 'string', parentTaskId: 'string', providerId: { type: 'string', description: 'Who does the work — required on direct proposals. Pass your own id when YOU are offering to do the work, or the proposedTo id when you are commissioning the RECIPIENT (any other third party needs a matching published offer). On a broadcast: omit for a quest (the claimer works, you pay), or pass your own id to publish a standing offer (you work, the claimer pays). The payer is always derived as the non-providing side.' }, price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00. Quote it to a human in the same units the agreement charges.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', 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', plan: { type: 'object' }, planReviewTiming: planReviewTimingParam, requireMidWorkPlanAck: 'boolean' }, handlerFor('agreement-propose'), { description: 'Propose an agreement — direct, broadcast, or link; there are no separate publish tools. 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): no providerId publishes a QUEST any agent may claim; providerId = your own id publishes a STANDING OFFER (claiming is agreement_claim; browsing is marketplace_view). engagementKind "link" with proposedTo = an agent id proposes bilateral trust. 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 });
36
36
  export const agreementSubcontractTool = defineTool('agreement_subcontract', { description: { type: 'string', required: true }, executorId: { type: 'string', required: true }, parentAgreementId: { type: 'string', required: true }, parentTaskId: 'string', payerId: 'string', price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00. Quote it to a human in the same units the agreement charges.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', agreementDescription: 'string', plan: { type: 'object' }, planReviewTiming: planReviewTimingParam, requireMidWorkPlanAck: 'boolean' }, handlerFor('agreement-subcontract'), { description: 'Delegate work to another agent under an existing parent agreement. Requires parentAgreementId.', isAgreementCreation: true });
37
- 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 (quests, standing offers, link invites) have no personal approval slot — claim those with agreement_claim instead, or ignore them to pass.' });
38
- export const agreementClaimTool = defineTool('agreement_claim', { agreementId: { type: 'string', required: true, description: 'The open agreement to claim (quest / offer / link invite id)' } }, handlerFor('agreement-claim'), { description: 'Claim an open broadcast agreement by id — a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. Find quests/offers with marketplace_view; direct proposals are approved with agreement_respond instead, not claimed.' });
37
+ 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 (quests, 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.' });
38
+ export const agreementClaimTool = defineTool('agreement_claim', { agreementId: { type: 'string', required: true, description: 'The open agreement to claim (quest / offer / link invite id)' } }, handlerFor('agreement-claim'), { description: 'Claim an open broadcast agreement by id — a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with marketplace_view; direct proposals are approved with agreement_respond instead, not claimed.' });
39
39
  export const taskSetResultTool = defineTool('task_set_result', {
40
40
  taskId: { type: 'string', required: true },
41
41
  // ZIG-957: same param name as MCP ziggs_task_set_result / PATCH /tasks/:id/state.
@@ -50,7 +50,21 @@ export const agreementCounterTool = defineTool('agreement_counter', { agreementI
50
50
  export const agreementGetTool = defineTool('agreement_get', { agreementId: { type: 'string', required: true } }, handlerFor('agreement-check-proposal'), { description: 'Read the current proposal status for an agreement (pending, approved, rejected, countered, expired).' });
51
51
  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.' });
52
52
  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, its shared space is torn down, and it cannot be reopened. 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.' });
53
- export const taskUpdatePlanStepTool = defineTool('task_update_plan_step', { taskId: { type: 'string', required: true }, stepId: { type: 'string', required: true }, status: { type: 'string', required: true }, result: 'string' }, handlerFor('task-update-plan-step'), { description: "Update the status of a single step within a task's plan." });
53
+ // `status` names its four values here because nothing else does: the backend is
54
+ // the only place the vocabulary was written down, and it now rejects anything
55
+ // outside it. Every reader (the chat step badge, the SDK's own plan formatter)
56
+ // already treats an unrecognised status as unknown, so a guessed one was never
57
+ // going to render — this makes the model's choice a closed set instead.
58
+ export const taskUpdatePlanStepTool = defineTool('task_update_plan_step', {
59
+ taskId: { type: 'string', required: true },
60
+ stepId: { type: 'string', required: true },
61
+ status: {
62
+ type: 'string',
63
+ required: true,
64
+ enum: ['pending', 'in_progress', 'completed', 'skipped'],
65
+ },
66
+ result: 'string',
67
+ }, handlerFor('task-update-plan-step'), { description: "Update the status of a single step within a task's plan." });
54
68
  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: 'ZIG-734: prior-step output artifact handles for structured input links.' }, plan: { type: 'object' }, planReviewTiming: planReviewTimingParam, requireMidWorkPlanAck: 'boolean' }, handlerFor('task-spawn'), { 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.' });
55
69
  export const PROTOCOL_TOOLS = [
56
70
  agreementProposeTool, agreementSubcontractTool,
@@ -13,8 +13,10 @@ export interface RecordReportInput {
13
13
  taskId: string;
14
14
  report: TaskReport;
15
15
  /**
16
- * Primary artifact scope — required when `deliverable` is provided.
17
- * Pass exactly one of chatId or agreementId.
16
+ * Optional container for the deliverable artifact (ZIG-1037 — it used to be
17
+ * required). The task binding is what makes the deliverable retrievable and
18
+ * readable by the task's parties; a chat or agreement only widens where it also
19
+ * shows up. Pass at most one; when both arrive, agreementId wins.
18
20
  */
19
21
  chatId?: string;
20
22
  agreementId?: string;
@@ -10,21 +10,37 @@ import { ArtifactsClient, updateTaskState, } from '@ziggs-ai/api-client';
10
10
  */
11
11
  export async function recordReport(input) {
12
12
  const { creds, taskId, report, chatId, agreementId, deliverable } = input;
13
- if (deliverable && !chatId && !agreementId) {
14
- throw new Error('recordReport: chatId or agreementId is required when deliverable is provided');
13
+ // ZIG-1037: no container needed. The deliverable is bound to its task, which
14
+ // is what makes it retrievable as `via=task:<taskId>` and readable by the
15
+ // task's parties — a chat or agreement was never what made that work. Both
16
+ // containers at once IS still a mistake (only one takes effect), so resolve it
17
+ // here the way artifact_record does rather than letting the client throw
18
+ // at the last step of a finished task.
19
+ const scope = agreementId ? { agreementId } : chatId ? { chatId } : {};
20
+ // Deliverable FIRST, then close the task.
21
+ //
22
+ // The old order completed the task and then wrote the artifact with the
23
+ // soft-failing `write`, so a wire error left a task marked completed whose
24
+ // deliverable had silently vanished. Recording first means a failure here
25
+ // leaves the task open — the honest state, and one a retry can finish. The
26
+ // idempotency key is derived from the task, so that retry reuses the artifact
27
+ // row instead of recording the deliverable twice.
28
+ let artifactId;
29
+ if (deliverable) {
30
+ const artifactsClient = new ArtifactsClient(creds.operatorKey, creds.agentId);
31
+ ({ artifactId } = await artifactsClient.writeStrict({
32
+ text: deliverable,
33
+ content_type: 'result',
34
+ taskId,
35
+ ...scope,
36
+ visibility: 'chat',
37
+ // Deterministic, not random: one deliverable per task, so a replay of this
38
+ // whole function reproduces the key and dedups server-side.
39
+ idempotencyKey: `task-result:${taskId}`,
40
+ }));
15
41
  }
16
42
  await updateTaskState(taskId, 'completed', { result: { summary: report.summary, status: report.status ?? 'ok', links: report.links ?? [] } }, creds);
17
- if (!deliverable) {
18
- return { taskId };
19
- }
20
- const artifactsClient = new ArtifactsClient(creds.operatorKey, creds.agentId);
21
- await artifactsClient.write({
22
- text: deliverable,
23
- content_type: 'result',
24
- taskId,
25
- chatId,
26
- agreementId,
27
- visibility: 'chat',
28
- });
29
- return { taskId };
43
+ // The artifactId was being discarded even though RecordReportResult declares
44
+ // it — the caller had no way to reference the deliverable it just recorded.
45
+ return artifactId ? { taskId, artifactId } : { taskId };
30
46
  }
package/dist/types.d.ts CHANGED
@@ -188,13 +188,13 @@ export interface Ctx {
188
188
  activeAgreementId: string | null;
189
189
  pendingProposalAgreementId: string | null;
190
190
  /**
191
- * Aggregate views over `tasks` (all engagements in the lane). Kept in sync by
192
- * applyOutcomeToCtx / bootstrapCtx so existing workflow `when` guards that
193
- * read these fields (e.g. motifs awaitingAll) keep working after ZIG-977.
194
- * Prefer reading `tasks[taskId]` when you need per-task isolation.
191
+ * @deprecated ZIG-1057 — unused mirrors; always `{}` / `[]` after bootstrap.
192
+ * Read via `allPendingSubtasks` / `allCollectedResults` / `allDelegatedAgreementIds`.
195
193
  */
196
194
  pendingSubtasks: Record<string, PendingSubtask>;
195
+ /** @deprecated ZIG-1057 — see `pendingSubtasks`. */
197
196
  collectedResults: CollectedSubtaskResult[];
197
+ /** @deprecated ZIG-1057 — see `pendingSubtasks`. */
198
198
  delegatedAgreementIds: string[];
199
199
  toolResults: ToolResultEntry[];
200
200
  lastOutcome: Outcome;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/agent-sdk",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
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.204",
27
27
  "@anthropic-ai/sdk": "^0.110.0",
28
28
  "@modelcontextprotocol/sdk": "^1.29.0",
29
- "@ziggs-ai/api-client": "^0.6.3",
29
+ "@ziggs-ai/api-client": "^0.8.0",
30
30
  "ajv": "^8.17.1",
31
31
  "dotenv": "^17.2.3",
32
32
  "openai": "^4.0.0",