@ziggs-ai/api-client 0.37.28 → 0.38.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/capabilities/artifacts.js +9 -9
  3. package/dist/capabilities/connections.js +3 -8
  4. package/dist/capabilities/context.js +15 -4
  5. package/dist/capabilities/grants.js +1 -4
  6. package/dist/capabilities/mcpBriefs.js +1 -1
  7. package/dist/capabilities/payments.js +2 -7
  8. package/dist/http/AgreementClient.d.ts +10 -2
  9. package/dist/http/AgreementClient.js +6 -4
  10. package/dist/http/ArtifactsClient.d.ts +2 -9
  11. package/dist/http/ArtifactsClient.js +3 -11
  12. package/dist/http/AssistantConnectionClient.d.ts +1 -1
  13. package/dist/http/AssistantConnectionClient.js +2 -2
  14. package/dist/http/ConnectionsClient.d.ts +2 -12
  15. package/dist/http/ConnectionsClient.js +4 -15
  16. package/dist/http/ContextGrantsClient.d.ts +1 -0
  17. package/dist/http/ContextGrantsClient.js +1 -1
  18. package/dist/http/ContextOpenClient.d.ts +2 -3
  19. package/dist/http/ContextOpenClient.js +2 -4
  20. package/dist/http/ContextReadClient.d.ts +5 -13
  21. package/dist/http/ContextReadClient.js +4 -19
  22. package/dist/http/ExecutionContextClient.d.ts +3 -0
  23. package/dist/http/ExecutionContextClient.js +17 -0
  24. package/dist/http/GrantsClient.d.ts +2 -10
  25. package/dist/http/GrantsClient.js +3 -12
  26. package/dist/http/InboxClient.d.ts +2 -2
  27. package/dist/http/InboxClient.js +1 -3
  28. package/dist/http/PaymentsClient.d.ts +2 -15
  29. package/dist/http/PaymentsClient.js +4 -18
  30. package/dist/http/TelemetryClient.js +3 -3
  31. package/dist/http/ToolPickClient.js +2 -2
  32. package/dist/http/index.d.ts +1 -1
  33. package/dist/http/index.js +1 -1
  34. package/dist/http/operatorHeaders.d.ts +14 -36
  35. package/dist/http/operatorHeaders.js +26 -50
  36. package/dist/inboxAck.d.ts +14 -46
  37. package/dist/inboxAck.js +12 -102
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.js +3 -0
  40. package/dist/types.d.ts +15 -16
  41. package/package.json +2 -2
@@ -12,22 +12,13 @@ export class GrantsClient {
12
12
  operatorKey;
13
13
  agentId;
14
14
  baseUrl;
15
- laneId;
16
- /**
17
- * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
18
- * X-Ziggs-Lane. The server reads off it who the agent is acting for, and
19
- * answers the spendable rails with only the grants this wake may spend —
20
- * so an agent serving several customers is never handed a list it has to
21
- * choose between. Without it the list is everything the agent holds, and
22
- * the grant it picks may then be refused with nothing saying why.
23
- */
24
- constructor(operatorKey, agentId, baseUrl, laneId) {
15
+ /** Authority is supplied by withWorkExecution / withWorkExecutionId. */
16
+ constructor(operatorKey, agentId, baseUrl) {
25
17
  if (!operatorKey)
26
18
  throw new Error('GrantsClient: operatorKey is required');
27
19
  this.operatorKey = operatorKey;
28
20
  this.agentId = agentId;
29
21
  this.baseUrl = baseUrl || getBackendUrl();
30
- this.laneId = laneId;
31
22
  }
32
23
  async listGrants(query = {}) {
33
24
  const url = new URL(`${this.baseUrl}/grants`);
@@ -51,7 +42,7 @@ export class GrantsClient {
51
42
  if (query.limit != null)
52
43
  url.searchParams.set('limit', String(query.limit));
53
44
  const res = await fetch(url.toString(), {
54
- headers: buildOperatorHeaders(this.operatorKey, this.agentId, undefined, this.laneId),
45
+ headers: buildOperatorHeaders(this.operatorKey, this.agentId, undefined),
55
46
  });
56
47
  const body = await res.text().catch(() => '');
57
48
  if (!res.ok) {
@@ -49,7 +49,7 @@ export declare class InboxClient {
49
49
  * refused.
50
50
  */
51
51
  ack(upTo: string, opts?: {
52
- handledResourceIds?: string[];
52
+ handledDeliveryIds?: string[];
53
53
  }): Promise<InboxAckResult>;
54
54
  /** The person said to leave this proposal pending. Does not approve or reject. */
55
55
  park(agreementId: string): Promise<{
@@ -62,7 +62,7 @@ export declare class InboxClient {
62
62
  * A second host-conflict is a live competitor and is rethrown unchanged.
63
63
  */
64
64
  ackOrTakeHost(upTo: string, opts?: {
65
- handledResourceIds?: string[];
65
+ handledDeliveryIds?: string[];
66
66
  }): Promise<InboxAckResult>;
67
67
  /**
68
68
  * Drop this process's inbox host lease so another client can take
@@ -119,9 +119,7 @@ export class InboxClient {
119
119
  headers: this.headers(),
120
120
  body: JSON.stringify({
121
121
  upTo,
122
- ...(opts.handledResourceIds
123
- ? { handledResourceIds: opts.handledResourceIds }
124
- : {}),
122
+ ...(opts.handledDeliveryIds ? { handledDeliveryIds: opts.handledDeliveryIds } : {}),
125
123
  }),
126
124
  });
127
125
  const body = await res.text().catch(() => '');
@@ -61,21 +61,8 @@ export declare class PaymentsClient {
61
61
  private readonly operatorKey;
62
62
  private readonly agentId?;
63
63
  private readonly baseUrl;
64
- private readonly laneId?;
65
- /**
66
- * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
67
- * X-Ziggs-Lane. It tells the backend which hire this wake is in, and the
68
- * backend reads off it WHO the spend is for. An agent that serves several
69
- * hirers holds all of their grants at once and the holder is the same agent
70
- * in every one, so without a lane, spend authority one customer gave is
71
- * spendable while the agent works for another. ConnectionsClient and
72
- * ArtifactsClient send the same header for the same reason.
73
- *
74
- * Omitting it cannot widen anything — a stated lane can only cause a
75
- * refusal — but it does leave the spend unfenced, so a caller that knows
76
- * its lane should pass it.
77
- */
78
- constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
64
+ /** Authority is supplied by withWorkExecution / withWorkExecutionId. */
65
+ constructor(operatorKey: string, agentId?: string, baseUrl?: string);
79
66
  balance(): Promise<WalletBalance>;
80
67
  resolve({ userId, agentId }?: {
81
68
  userId?: string;
@@ -22,27 +22,13 @@ export class PaymentsClient {
22
22
  operatorKey;
23
23
  agentId;
24
24
  baseUrl;
25
- laneId;
26
- /**
27
- * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
28
- * X-Ziggs-Lane. It tells the backend which hire this wake is in, and the
29
- * backend reads off it WHO the spend is for. An agent that serves several
30
- * hirers holds all of their grants at once and the holder is the same agent
31
- * in every one, so without a lane, spend authority one customer gave is
32
- * spendable while the agent works for another. ConnectionsClient and
33
- * ArtifactsClient send the same header for the same reason.
34
- *
35
- * Omitting it cannot widen anything — a stated lane can only cause a
36
- * refusal — but it does leave the spend unfenced, so a caller that knows
37
- * its lane should pass it.
38
- */
39
- constructor(operatorKey, agentId, baseUrl, laneId) {
25
+ /** Authority is supplied by withWorkExecution / withWorkExecutionId. */
26
+ constructor(operatorKey, agentId, baseUrl) {
40
27
  if (!operatorKey)
41
28
  throw new Error('PaymentsClient: operatorKey is required');
42
29
  this.operatorKey = operatorKey;
43
30
  this.agentId = agentId;
44
31
  this.baseUrl = baseUrl || getBackendUrl();
45
- this.laneId = laneId;
46
32
  }
47
33
  async balance() {
48
34
  const w = (await this._get('/payments/wallet'));
@@ -87,7 +73,7 @@ export class PaymentsClient {
87
73
  * (retired GET /payments/grants), following the cursor to completion.
88
74
  */
89
75
  async listGrants() {
90
- const grants = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl, this.laneId);
76
+ const grants = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
91
77
  return grants.listAllGrants({ scopeKind: 'wallet', health: 'active' });
92
78
  }
93
79
  async faucet({ amount, description = 'Faucet', idempotencyKey, } = {}) {
@@ -100,7 +86,7 @@ export class PaymentsClient {
100
86
  async _request(method, path, body) {
101
87
  const init = {
102
88
  method,
103
- headers: buildOperatorHeaders(this.operatorKey, this.agentId, body !== undefined ? { 'content-type': 'application/json' } : {}, this.laneId),
89
+ headers: buildOperatorHeaders(this.operatorKey, this.agentId, body !== undefined ? { 'content-type': 'application/json' } : {}),
104
90
  };
105
91
  if (body !== undefined)
106
92
  init.body = JSON.stringify(body);
@@ -1,6 +1,6 @@
1
1
  import { runtimeLog } from '../shared/runtimeLog.js';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
- import { buildOperatorHeaders } from './operatorHeaders.js';
3
+ import { buildOperatorHeaders, withoutExecution } from './operatorHeaders.js';
4
4
  export class TelemetryClient {
5
5
  operatorKey;
6
6
  agentId;
@@ -36,9 +36,9 @@ export class TelemetryClient {
36
36
  try {
37
37
  const res = await fetch(url, {
38
38
  method: 'POST',
39
- headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
39
+ headers: withoutExecution(buildOperatorHeaders(this.operatorKey, this.agentId, {
40
40
  'content-type': 'application/json',
41
- }),
41
+ })),
42
42
  body: JSON.stringify({ payload }),
43
43
  });
44
44
  if (!res.ok) {
@@ -1,4 +1,4 @@
1
- import { buildCredsHeaders } from './operatorHeaders.js';
1
+ import { buildCredsHeaders, withoutExecution } from './operatorHeaders.js';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
3
  /** A listing is always the fallback, so waiting long for a pick buys nothing. */
4
4
  const TIMEOUT_MS = 6000;
@@ -15,7 +15,7 @@ export function backendToolPick(creds) {
15
15
  try {
16
16
  const res = await fetch(`${getBackendUrl()}/internal/tool-pick`, {
17
17
  method: 'POST',
18
- headers: buildCredsHeaders(creds),
18
+ headers: withoutExecution(buildCredsHeaders(creds)),
19
19
  body: JSON.stringify({ need, options }),
20
20
  signal: AbortSignal.timeout(TIMEOUT_MS),
21
21
  });
@@ -35,5 +35,5 @@ export { SessionsClient } from './SessionsClient.js';
35
35
  export type { AssistantSessionRow } from './SessionsClient.js';
36
36
  export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds, } from '../inboxAck.js';
37
37
  export type { InboxAckDecision, InboxAckStep, InboxAckEnvelope, PlanInboxAckOptions, } from '../inboxAck.js';
38
- export { WAKE_LANE_HEADER, WAKE_AGREEMENT_HEADER, laneHeader, laneUnderAgreement, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
38
+ export { laneUnderAgreement, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
39
39
  export { backendToolPick, type ToolPickOption, type ToolPickAnswer, type PickTool } from './ToolPickClient.js';
@@ -26,5 +26,5 @@ export { planInboxAck, planPartialInboxAck, inboxAckAllowed, inboxAckHandledIds,
26
26
  // The lane header, built in one place — see operatorHeaders.ts. Exported
27
27
  // because the MCP transports in agent-sdk and ziggs-mcp send it too, and a
28
28
  // copy of a header nobody notices missing goes stale unseen.
29
- export { WAKE_LANE_HEADER, WAKE_AGREEMENT_HEADER, laneHeader, laneUnderAgreement, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
29
+ export { laneUnderAgreement, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
30
30
  export { backendToolPick } from './ToolPickClient.js';
@@ -1,4 +1,7 @@
1
+ import type { WorkExecutionContext } from '@ziggs-ai/contracts';
1
2
  type AssistantHints = {
3
+ executionContext?: WorkExecutionContext;
4
+ executionContextId?: string;
2
5
  orgId?: string;
3
6
  resource?: string;
4
7
  clientSessionId?: string;
@@ -10,49 +13,24 @@ type AssistantHints = {
10
13
  /** Node-only, opt-in request context. Ordinary HTTP clients remain browser-safe. */
11
14
  export declare function withAssistantRequest<T>(hints: AssistantHints, run: () => T): Promise<Awaited<T>>;
12
15
  export declare function assistantRequestHeaders(): Record<string, string>;
13
- /**
14
- * Auth headers for operator-key HTTP clients.
15
- *
16
- * ONE place builds them, because the lane is the header a caller cannot be
17
- * trusted to remember. Omitting `X-Ziggs-Lane` never fails: the server fences
18
- * the wake to the agent's own org instead, so a door that forgets it answers
19
- * short lists and refuses grants the agent's customer gave it — silently, and
20
- * only for the agents that serve someone other than their owner. A header
21
- * whose absence is indistinguishable from a legitimate answer has to be built
22
- * in one place, not copied into each caller.
23
- */
24
- /** The header the backend's wake fence reads (`access/wake-fence.ts`). */
25
- export declare const WAKE_LANE_HEADER = "X-Ziggs-Lane";
26
- /**
27
- * The header naming the agreement a room wake works under. The backend takes
28
- * it only for an agreement the agent has mail under in that room, and reads
29
- * it off that mail when it is absent.
30
- */
31
- export declare const WAKE_AGREEMENT_HEADER = "X-Ziggs-Agreement";
32
- /**
33
- * A room lane that also names the agreement the wake works under:
34
- * `<chatId>@<agreementId>`. Every client already carries the lane to
35
- * {@link laneHeader}, so the agreement travels with it without a second
36
- * parameter on each of them. An `agrn-` lane already is its agreement.
37
- */
16
+ /** A presentation routing key; it carries no authority. */
38
17
  export declare function laneUnderAgreement(laneId: string, agreementId?: string | null): string;
18
+ export declare function buildOperatorHeaders(operatorKey: string, agentId?: string, extra?: Record<string, string>): Record<string, string>;
39
19
  /**
40
- * The lane headers, or nothing. `laneId` is a chat id, `agrn-<agreementId>`
41
- * for a task with no origin chat, or a chat id with the agreement the wake
42
- * works under (`<chatId>@<agreementId>`, see {@link laneUnderAgreement}).
43
- */
44
- export declare function laneHeader(laneId?: string): Record<string, string>;
45
- export declare function buildOperatorHeaders(operatorKey: string, agentId?: string, extra?: Record<string, string>,
46
- /**
47
- * the lane this call belongs to, sent as `X-Ziggs-Lane`. The
48
- * backend narrows the wake's reach to the engagement's orgs; omitting it
49
- * narrows to the agent's own org, so it can never widen reach.
20
+ * The host's own calls (wake gate, telemetry, tool picker) are not work
21
+ * actions: they never carry the selected execution, so they neither need a
22
+ * route that supports one nor record anything into it.
50
23
  */
51
- laneId?: string): Record<string, string>;
24
+ export declare function withoutExecution(headers: Record<string, string>): Record<string, string>;
52
25
  /** What a `Creds`-shaped client sends on a JSON call: auth, agent, lane. */
53
26
  export declare function buildCredsHeaders(creds: {
54
27
  operatorKey: string;
55
28
  agentId: string;
56
29
  laneId?: string;
30
+ executionContextId?: string;
57
31
  }): Record<string, string>;
32
+ /** Async-local scope: parallel agreement positions never share request headers. */
33
+ export declare function withWorkExecution<T>(executionContext: WorkExecutionContext | undefined, run: () => T): Promise<Awaited<T>>;
34
+ export declare function currentWorkExecution(): WorkExecutionContext | undefined;
35
+ export declare function withWorkExecutionId<T>(executionContextId: string | undefined, run: () => T): Promise<Awaited<T>>;
58
36
  export {};
@@ -14,6 +14,7 @@ export async function withAssistantRequest(hints, run) {
14
14
  export function assistantRequestHeaders() {
15
15
  const hints = assistantRequest?.getStore();
16
16
  return {
17
+ ...((hints?.executionContextId ?? hints?.executionContext?.contextId) ? { 'X-Ziggs-Execution': (hints?.executionContextId ?? hints?.executionContext?.contextId) } : {}),
17
18
  ...(hints?.orgId ? { 'X-Workspace-Id': hints.orgId } : {}),
18
19
  ...(hints?.resource ? { 'X-Ziggs-Resource': hints.resource } : {}),
19
20
  ...(hints?.clientSessionId ? { 'X-Ziggs-Session': hints.clientSessionId } : {}),
@@ -23,73 +24,48 @@ export function assistantRequestHeaders() {
23
24
  ...(hints?.sessionName ? { 'X-Ziggs-Session-Name': hints.sessionName } : {}),
24
25
  };
25
26
  }
26
- /**
27
- * Auth headers for operator-key HTTP clients.
28
- *
29
- * ONE place builds them, because the lane is the header a caller cannot be
30
- * trusted to remember. Omitting `X-Ziggs-Lane` never fails: the server fences
31
- * the wake to the agent's own org instead, so a door that forgets it answers
32
- * short lists and refuses grants the agent's customer gave it — silently, and
33
- * only for the agents that serve someone other than their owner. A header
34
- * whose absence is indistinguishable from a legitimate answer has to be built
35
- * in one place, not copied into each caller.
36
- */
37
- /** The header the backend's wake fence reads (`access/wake-fence.ts`). */
38
- export const WAKE_LANE_HEADER = 'X-Ziggs-Lane';
39
- /**
40
- * The header naming the agreement a room wake works under. The backend takes
41
- * it only for an agreement the agent has mail under in that room, and reads
42
- * it off that mail when it is absent.
43
- */
44
- export const WAKE_AGREEMENT_HEADER = 'X-Ziggs-Agreement';
45
- /**
46
- * A room lane that also names the agreement the wake works under:
47
- * `<chatId>@<agreementId>`. Every client already carries the lane to
48
- * {@link laneHeader}, so the agreement travels with it without a second
49
- * parameter on each of them. An `agrn-` lane already is its agreement.
50
- */
27
+ /** A presentation routing key; it carries no authority. */
51
28
  export function laneUnderAgreement(laneId, agreementId) {
52
29
  if (!agreementId || laneId.startsWith('agrn-') || laneId.includes('@'))
53
30
  return laneId;
54
31
  return `${laneId}@${agreementId}`;
55
32
  }
56
- /**
57
- * The lane headers, or nothing. `laneId` is a chat id, `agrn-<agreementId>`
58
- * for a task with no origin chat, or a chat id with the agreement the wake
59
- * works under (`<chatId>@<agreementId>`, see {@link laneUnderAgreement}).
60
- */
61
- export function laneHeader(laneId) {
62
- if (!laneId)
63
- return {};
64
- const at = laneId.indexOf('@');
65
- if (at <= 0)
66
- return { [WAKE_LANE_HEADER]: laneId };
67
- return {
68
- [WAKE_LANE_HEADER]: laneId.slice(0, at),
69
- [WAKE_AGREEMENT_HEADER]: laneId.slice(at + 1),
70
- };
71
- }
72
- export function buildOperatorHeaders(operatorKey, agentId, extra,
73
- /**
74
- * the lane this call belongs to, sent as `X-Ziggs-Lane`. The
75
- * backend narrows the wake's reach to the engagement's orgs; omitting it
76
- * narrows to the agent's own org, so it can never widen reach.
77
- */
78
- laneId) {
33
+ export function buildOperatorHeaders(operatorKey, agentId, extra) {
79
34
  return {
80
35
  Authorization: `Bearer ${operatorKey}`,
81
36
  ...assistantRequestHeaders(),
82
37
  // `X-Agent-Id` is only sent when an agentId is present — agent-scoped keys
83
38
  // identify the agent themselves; fleet keys must pass one.
84
39
  ...(agentId ? { 'X-Agent-Id': agentId } : {}),
85
- ...laneHeader(laneId),
86
40
  ...extra,
87
41
  };
88
42
  }
43
+ /**
44
+ * The host's own calls (wake gate, telemetry, tool picker) are not work
45
+ * actions: they never carry the selected execution, so they neither need a
46
+ * route that supports one nor record anything into it.
47
+ */
48
+ export function withoutExecution(headers) {
49
+ const out = { ...headers };
50
+ delete out['X-Ziggs-Execution'];
51
+ delete out['x-ziggs-execution'];
52
+ return out;
53
+ }
89
54
  /** What a `Creds`-shaped client sends on a JSON call: auth, agent, lane. */
90
55
  export function buildCredsHeaders(creds) {
91
56
  return {
92
57
  'content-type': 'application/json',
93
- ...buildOperatorHeaders(creds.operatorKey, creds.agentId, undefined, creds.laneId),
58
+ ...(creds.executionContextId ? { 'X-Ziggs-Execution': creds.executionContextId } : {}),
59
+ ...buildOperatorHeaders(creds.operatorKey, creds.agentId),
94
60
  };
95
61
  }
62
+ /** Async-local scope: parallel agreement positions never share request headers. */
63
+ export async function withWorkExecution(executionContext, run) {
64
+ return withAssistantRequest({ ...assistantRequest?.getStore(), executionContext, executionContextId: executionContext?.contextId }, run);
65
+ }
66
+ export function currentWorkExecution() {
67
+ return assistantRequest?.getStore()?.executionContext;
68
+ }
69
+ export async function withWorkExecutionId(executionContextId, run) {
70
+ return withAssistantRequest({ ...assistantRequest?.getStore(), executionContextId: executionContextId ?? assistantRequest?.getStore()?.executionContextId }, run);
71
+ }
@@ -1,70 +1,38 @@
1
- /**
2
- * One answer for "what does this envelope's ack list, and may we send it?"
3
- *
4
- * The SDK loop and the MCP inbox tool both call this. The bury-guard is the
5
- * same question on both rails: list every assigned row this reader actually
6
- * handled, and do not ack a window that is incomplete or that failed in its
7
- * hands.
8
- */
1
+ /** Observation and handling are independent. Only explicit responsibility IDs settle work. */
9
2
  export interface InboxAckEnvelope {
10
3
  ackTo?: string | null;
11
4
  deliveries?: ReadonlyArray<{
12
5
  resourceId?: string | null;
6
+ responsibilityId?: string | null;
13
7
  assigneeId?: string | null;
14
8
  outOfReach?: unknown;
15
- /** ISO instant; the axis a mark moves along. */
9
+ executionBlocked?: unknown;
16
10
  ts?: string | null;
17
- /** Mark covering this row and everything before it in this window. */
18
11
  ackTo?: string | null;
19
12
  }>;
20
13
  openRequestsAwaitingMe?: ReadonlyArray<{
21
14
  agreementId?: string | null;
15
+ responsibilityId?: string | null;
22
16
  ts?: string | null;
23
17
  }>;
24
18
  }
25
- export interface InboxAckDecision {
26
- /** True when this envelope may be acknowledged. */
27
- allowed: boolean;
28
- handledResourceIds: string[];
29
- }
30
19
  export interface PlanInboxAckOptions {
31
- /**
32
- * Rows whose `assigneeId` equals this are mine. Empty or omitted: every
33
- * delivery counts (the MCP tool with no configured self id).
34
- */
35
20
  ownAgentId?: string;
36
21
  ownAgentIds?: readonly string[];
37
- /** Failed handovers hold the watermark unless `forceAck`. */
38
22
  failures?: number;
39
23
  forceAck?: boolean;
40
24
  }
41
- /** Resource ids this envelope's ack must list. */
42
- export declare function inboxAckHandledIds(envelope: InboxAckEnvelope, ownAgentId?: string, ownAgentIds?: readonly string[]): string[];
43
- /** Whether an ack is allowed for this envelope, given handover state. */
44
- export declare function inboxAckAllowed(envelope: InboxAckEnvelope, opts?: PlanInboxAckOptions): boolean;
45
- export declare function planInboxAck(envelope: InboxAckEnvelope, opts?: PlanInboxAckOptions): InboxAckDecision;
46
- /** How far a reader may ack right now, mid-pass. */
25
+ export interface InboxAckDecision {
26
+ allowed: boolean;
27
+ handledDeliveryIds: string[];
28
+ }
47
29
  export interface InboxAckStep {
48
- /** A row's own token — never one the client made up. */
49
30
  ackTo: string;
50
- handledResourceIds: string[];
31
+ handledDeliveryIds: string[];
51
32
  }
52
- /**
53
- * The furthest point in this envelope that is safe to ack RIGHT NOW, given what
54
- * the reader has finished so far, or null when that is nowhere.
55
- *
56
- * Acking once at the end of a pass freezes the watermark for the whole pass,
57
- * which is long enough for the stand-in's 90s takeover to elapse while the
58
- * agent is still working. This lets the reader give the mark back in steps.
59
- *
60
- * The stopping point is a global instant, NOT the reader's current unit of
61
- * work. A mark advances every source it names at once, so acking after one
62
- * chat fold would bury an older row from a different chat — the guard would
63
- * refuse it, correctly. So: walk the instants the server minted a token for,
64
- * and stop at the last one where everything at or before it is handled.
65
- * Anything else is either refused or a bug.
66
- *
67
- * Returns null when the server minted no per-row tokens, which is how an older
68
- * backend is handled: the caller keeps its single end-of-pass ack.
69
- */
33
+ export declare function inboxAckHandledIds(envelope: InboxAckEnvelope, ownAgentId?: string, ownAgentIds?: readonly string[]): string[];
34
+ export declare function inboxAckAllowed(envelope: InboxAckEnvelope, opts?: PlanInboxAckOptions): boolean;
35
+ /** Full-pass helper: only use after every listed responsibility succeeded. */
36
+ export declare function planInboxAck(envelope: InboxAckEnvelope, opts?: PlanInboxAckOptions): InboxAckDecision;
37
+ /** Successful jobs can be handled in any order, even when another job failed. */
70
38
  export declare function planPartialInboxAck(envelope: InboxAckEnvelope, handled: ReadonlySet<string>, opts?: PlanInboxAckOptions): InboxAckStep | null;
package/dist/inboxAck.js CHANGED
@@ -1,111 +1,21 @@
1
- /**
2
- * One answer for "what does this envelope's ack list, and may we send it?"
3
- *
4
- * The SDK loop and the MCP inbox tool both call this. The bury-guard is the
5
- * same question on both rails: list every assigned row this reader actually
6
- * handled, and do not ack a window that is incomplete or that failed in its
7
- * hands.
8
- */
9
- function isOutOfReach(note) {
10
- if (!note || typeof note !== 'object')
11
- return false;
12
- const n = note;
13
- return (typeof n.scopeKind === 'string' &&
14
- typeof n.scopeId === 'string' &&
15
- typeof n.remedy === 'string');
16
- }
17
- /** Resource ids this envelope's ack must list. */
18
1
  export function inboxAckHandledIds(envelope, ownAgentId, ownAgentIds) {
19
- const mine = (assigneeId) => ownAgentIds ? ownAgentIds.includes(assigneeId ?? '') : !ownAgentId || assigneeId === ownAgentId;
20
- return [
21
- ...new Set([
22
- ...(envelope.deliveries ?? [])
23
- .filter((d) => mine(d.assigneeId))
24
- .filter((d) => !isOutOfReach(d.outOfReach))
25
- .map((d) => d.resourceId),
26
- ...(envelope.openRequestsAwaitingMe ?? []).map((q) => q.agreementId),
27
- ]),
28
- ].filter((id) => typeof id === 'string' && id.length > 0);
2
+ const mine = (id) => ownAgentIds ? ownAgentIds.includes(id ?? '') : ownAgentId ? id === ownAgentId : !!id;
3
+ return [...new Set([
4
+ ...(envelope.deliveries ?? []).filter(d => mine(d.assigneeId) && !d.outOfReach && !d.executionBlocked).map(d => d.responsibilityId),
5
+ ...(envelope.openRequestsAwaitingMe ?? []).map(r => r.responsibilityId),
6
+ ].filter((id) => !!id))];
29
7
  }
30
- /** Whether an ack is allowed for this envelope, given handover state. */
31
8
  export function inboxAckAllowed(envelope, opts = {}) {
32
- if (!envelope.ackTo)
33
- return false;
34
- const failures = opts.failures ?? 0;
35
- if (failures > 0 && !opts.forceAck)
36
- return false;
37
- return true;
9
+ return !!envelope.ackTo && (!(opts.failures ?? 0) || !!opts.forceAck);
38
10
  }
11
+ /** Full-pass helper: only use after every listed responsibility succeeded. */
39
12
  export function planInboxAck(envelope, opts = {}) {
40
- return {
41
- allowed: inboxAckAllowed(envelope, opts),
42
- handledResourceIds: inboxAckHandledIds(envelope, opts.ownAgentId, opts.ownAgentIds),
43
- };
44
- }
45
- /** Every row whose instant the server's bury-guard will ask about. */
46
- function gatingRows(envelope, ownAgentId, ownAgentIds) {
47
- const mine = (assigneeId) => ownAgentIds ? ownAgentIds.includes(assigneeId ?? '') : !ownAgentId || assigneeId === ownAgentId;
48
- return [
49
- ...(envelope.deliveries ?? [])
50
- .filter((d) => mine(d.assigneeId))
51
- .filter((d) => !isOutOfReach(d.outOfReach))
52
- .map((d) => ({ id: d.resourceId ?? '', ts: d.ts ?? '' })),
53
- // Requests are projected out of `deliveries` for triage, but their rows
54
- // are still in the log under the caller's stamp, so they gate a mark
55
- // exactly as a delivery does.
56
- ...(envelope.openRequestsAwaitingMe ?? []).map((q) => ({
57
- id: q.agreementId ?? '',
58
- ts: q.ts ?? '',
59
- })),
60
- ].filter((r) => r.id.length > 0 && r.ts.length > 0);
13
+ return { allowed: inboxAckAllowed(envelope, opts), handledDeliveryIds: inboxAckHandledIds(envelope, opts.ownAgentId, opts.ownAgentIds) };
61
14
  }
62
- /**
63
- * The furthest point in this envelope that is safe to ack RIGHT NOW, given what
64
- * the reader has finished so far, or null when that is nowhere.
65
- *
66
- * Acking once at the end of a pass freezes the watermark for the whole pass,
67
- * which is long enough for the stand-in's 90s takeover to elapse while the
68
- * agent is still working. This lets the reader give the mark back in steps.
69
- *
70
- * The stopping point is a global instant, NOT the reader's current unit of
71
- * work. A mark advances every source it names at once, so acking after one
72
- * chat fold would bury an older row from a different chat — the guard would
73
- * refuse it, correctly. So: walk the instants the server minted a token for,
74
- * and stop at the last one where everything at or before it is handled.
75
- * Anything else is either refused or a bug.
76
- *
77
- * Returns null when the server minted no per-row tokens, which is how an older
78
- * backend is handled: the caller keeps its single end-of-pass ack.
79
- */
15
+ /** Successful jobs can be handled in any order, even when another job failed. */
80
16
  export function planPartialInboxAck(envelope, handled, opts = {}) {
81
- const failures = opts.failures ?? 0;
82
- if (failures > 0 && !opts.forceAck)
83
- return null;
84
- const tokenByInstant = new Map();
85
- for (const d of envelope.deliveries ?? []) {
86
- if (d.ts && d.ackTo)
87
- tokenByInstant.set(d.ts, d.ackTo);
88
- }
89
- if (tokenByInstant.size === 0)
90
- return null;
91
- const gating = gatingRows(envelope, opts.ownAgentId, opts.ownAgentIds);
92
- // ISO-8601 sorts lexicographically in time order, which is what every other
93
- // ts comparison on this rail already relies on.
94
- const instants = [...tokenByInstant.keys()].sort();
95
- let upTo = null;
96
- for (const instant of instants) {
97
- const settled = gating.every((r) => r.ts > instant || handled.has(r.id));
98
- if (!settled)
99
- break;
100
- upTo = instant;
101
- }
102
- if (!upTo)
17
+ if (!envelope.ackTo)
103
18
  return null;
104
- const reached = upTo;
105
- return {
106
- ackTo: tokenByInstant.get(reached),
107
- handledResourceIds: [
108
- ...new Set(gating.filter((r) => r.ts <= reached).map((r) => r.id)),
109
- ],
110
- };
19
+ const ids = inboxAckHandledIds(envelope, opts.ownAgentId, opts.ownAgentIds).filter(id => handled.has(id));
20
+ return ids.length ? { ackTo: envelope.ackTo, handledDeliveryIds: ids } : null;
111
21
  }
package/dist/index.d.ts CHANGED
@@ -34,3 +34,6 @@ export { AssistantConnectionClient, type AssistantWorkspace, type McpEventStatus
34
34
  export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHeaders.js';
35
35
  export { HANDLE_ALPHABET, findHandles, looksLikeHandle, parseHandle, parseRef, type HandleKind } from './handles.js';
36
36
  export { describeOperationError, type OperationError } from './shared/operationError.js';
37
+ export { withWorkExecution, withWorkExecutionId, currentWorkExecution, withoutExecution } from './http/operatorHeaders.js';
38
+ export { deliveryHandlingId, type WorkExecutionContext, type WorkPositionView } from '@ziggs-ai/contracts';
39
+ export { validateWorkExecution } from './http/ExecutionContextClient.js';
package/dist/index.js CHANGED
@@ -32,3 +32,6 @@ export { withAssistantRequest, assistantRequestHeaders } from './http/operatorHe
32
32
  // Short handles: T…, A…, C…, F… (task, agreement, chat, artifact).
33
33
  export { HANDLE_ALPHABET, findHandles, looksLikeHandle, parseHandle, parseRef } from './handles.js';
34
34
  export { describeOperationError } from './shared/operationError.js';
35
+ export { withWorkExecution, withWorkExecutionId, currentWorkExecution, withoutExecution } from './http/operatorHeaders.js';
36
+ export { deliveryHandlingId } from '@ziggs-ai/contracts';
37
+ export { validateWorkExecution } from './http/ExecutionContextClient.js';