@ziggs-ai/api-client 0.20.0 → 0.22.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.
@@ -3,7 +3,13 @@
3
3
  * and must never be offered here — chat_open / chat_send refuse it, and the
4
4
  * baseline's provider learned that only by being refused.
5
5
  */
6
- const ADDRESS_FIELDS = ['participantId', 'to', 'receiverId'];
6
+ const ADDRESS_FIELDS = [
7
+ 'participantId',
8
+ 'to',
9
+ 'receiverId',
10
+ 'counterparty',
11
+ 'executorId',
12
+ ];
7
13
  /**
8
14
  * Required arguments for the capability keys this helper is asked to emit.
9
15
  * Kept next to the HTTP bindings so a ready call cannot advertise a schema
@@ -15,9 +21,18 @@ const REQUIRED_ARGS = {
15
21
  chat_open: ['participantId'],
16
22
  chat_send: ['chatId'],
17
23
  agreement_claim: ['agreementId'],
24
+ agreement_get: ['agreementId'],
25
+ agreement_buy: ['counterparty', 'description'],
26
+ agreement_bid: ['counterparty', 'description'],
27
+ agreement_request: ['audience', 'description'],
28
+ agreement_subcontract: ['parentAgreementId', 'executorId', 'description'],
18
29
  agreement_respond: ['agreementId'],
19
30
  introduction_redeem: ['token'],
20
31
  context_issue_grant: ['holderId', 'scopeKind', 'scopeId'],
32
+ artifact_share: ['artifactId', 'holderId'],
33
+ context_delegate: ['parentGrantId', 'holderId', 'scopeKind', 'scopeId', 'temporal'],
34
+ task_create: ['agreementId', 'description'],
35
+ task_cancel: ['taskId'],
21
36
  };
22
37
  /**
23
38
  * Published HTTP invocation per capability key. Paths match the api-client
@@ -31,17 +46,31 @@ const HTTP_BINDINGS = {
31
46
  introduction_mint: { method: 'POST', path: '/introductions' },
32
47
  introduction_redeem: { method: 'POST', path: '/introductions/:token/redeem' },
33
48
  agreement_claim: { method: 'POST', path: '/agreements/:agreementId/claim' },
49
+ agreement_get: { method: 'GET', path: '/agreements/:agreementId' },
50
+ agreement_buy: { method: 'POST', path: '/agreements/proposals' },
51
+ agreement_bid: { method: 'POST', path: '/agreements/proposals' },
52
+ agreement_request: { method: 'POST', path: '/agreements/proposals' },
53
+ agreement_subcontract: {
54
+ method: 'POST',
55
+ path: '/agreements/:parentAgreementId/delegations',
56
+ },
34
57
  agreement_respond: {
35
58
  method: 'PUT',
36
59
  path: '/agreements/:agreementId/approvals/:partyId',
37
60
  },
38
61
  marketplace_view: { method: 'GET', path: '/marketplace/offers' },
39
62
  context_issue_grant: { method: 'POST', path: '/context/grants' },
63
+ artifact_share: { method: 'POST', path: '/context/artifacts/:artifactId/share' },
64
+ context_delegate: { method: 'POST', path: '/context/grants/:parentGrantId/delegate' },
40
65
  open: { method: 'POST', path: '/context/open' },
41
66
  access_explain: { method: 'POST', path: '/context/access/explain' },
42
67
  inbox_peek: { method: 'GET', path: '/inbox/peek' },
68
+ task_get: { method: 'GET', path: '/tasks/:taskId' },
69
+ task_list: { method: 'GET', path: '/tasks' },
43
70
  link_list: { method: 'GET', path: '/agreements?engagementKind=link' },
44
71
  link_propose: { method: 'POST', path: '/agreements/links' },
72
+ task_create: { method: 'POST', path: '/tasks' },
73
+ task_cancel: { method: 'PATCH', path: '/tasks/:taskId/cancel' },
45
74
  };
46
75
  /**
47
76
  * Intentional surface exclusions. A ready next call has to exist as a tool
@@ -50,8 +79,15 @@ const HTTP_BINDINGS = {
50
79
  */
51
80
  const SURFACE_EXPOSURE = {
52
81
  context_issue_grant: ['mcp'],
82
+ // The hosted SDK surface enumerates its own work with task_list and reads a
83
+ // task from the wake it arrived on; ziggs_task_get is the MCP delegate's
84
+ // one-task read. Naming it to an SDK agent would offer a tool it cannot call.
85
+ task_get: ['mcp'],
53
86
  };
54
87
  const MATERIAL_EFFECTS = {
88
+ task_get: {
89
+ disclosure: 'reads back what was recorded; it changes nothing',
90
+ },
55
91
  agreement_claim: {
56
92
  commitment: 'claiming makes you a party to the posted terms',
57
93
  relationship: 'you become the open party on that broadcast',
@@ -71,6 +107,26 @@ const MATERIAL_EFFECTS = {
71
107
  chat_open: {
72
108
  disclosure: 'opening a room issues the other side a grant on it',
73
109
  },
110
+ task_create: {
111
+ commitment: 'creates a task under the named agreement',
112
+ },
113
+ task_cancel: {
114
+ commitment: 'cancel ends this task; a held graph withdraws by cancelling the root',
115
+ },
116
+ agreement_buy: {
117
+ commitment: 'proposes that they work and your side pays',
118
+ relationship: 'a named counterparty — not a listing claim',
119
+ },
120
+ agreement_bid: {
121
+ commitment: 'proposes that you work and they pay',
122
+ },
123
+ agreement_request: {
124
+ commitment: 'posts work anyone may claim; you pay the claimer',
125
+ },
126
+ agreement_subcontract: {
127
+ commitment: 'opens a sub-agreement under the parent; the worker must approve — never impersonated',
128
+ relationship: 'the slice sits under the parent you already hold',
129
+ },
74
130
  };
75
131
  /** A persona face (`psn_…`) is display state, not a message address. */
76
132
  export function isPersonaFace(id) {
@@ -155,6 +211,7 @@ export function nextCall(env, capabilityKey, args, why, opts) {
155
211
  };
156
212
  }
157
213
  const missing = [];
214
+ const strippedFaces = [];
158
215
  const filled = {};
159
216
  for (const [key, value] of Object.entries(args ?? {})) {
160
217
  if (!isFilled(value))
@@ -162,6 +219,7 @@ export function nextCall(env, capabilityKey, args, why, opts) {
162
219
  if (ADDRESS_FIELDS.includes(key) &&
163
220
  isPersonaFace(value)) {
164
221
  missing.push(key);
222
+ strippedFaces.push(key);
165
223
  continue;
166
224
  }
167
225
  filled[key] = value;
@@ -183,11 +241,11 @@ export function nextCall(env, capabilityKey, args, why, opts) {
183
241
  ? 'partial'
184
242
  : 'unavailable');
185
243
  const http = HTTP_BINDINGS[capabilityKey];
186
- const faceMissing = missing.some((name) => ADDRESS_FIELDS.includes(name));
244
+ const faceWasPassed = strippedFaces.length > 0;
187
245
  const reason = hold && missing.length === 0
188
246
  ? why
189
- : !ready && faceMissing
190
- ? `${why} — a next call for reaching a person names the user id or their agent, never a psn_ face`
247
+ : !ready && faceWasPassed
248
+ ? `${why} — missing ${missing.join(', ')}. a next call for reaching a person names the user id or their agent, never a psn_ face`
191
249
  : !ready
192
250
  ? `${why} — missing ${missing.join(', ')}`
193
251
  : why;
@@ -1,4 +1,6 @@
1
- import { type CapabilityDefinition } from './types.js';
1
+ import { type TaskWriteConfirm } from '../http/TaskClient.js';
2
+ import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
3
+ import { type NextCall, type NextCallOutcome, type WorkContext } from './nextCall.js';
2
4
  /**
3
5
  * One list-tasks verb. MCP and the hosted SDK both used to re-declare the
4
6
  * same GET /tasks filters; assignedToMe vs assignedTo precedence then
@@ -6,4 +8,69 @@ import { type CapabilityDefinition } from './types.js';
6
8
  * those shorthands onto the HTTP query.
7
9
  */
8
10
  export declare const listTasksCapability: CapabilityDefinition;
11
+ /**
12
+ * Withdraw a task — including a held graph, which is one cancel on the root.
13
+ * Catalog, not native: this is the pointer AX-07 names, not a new protocol verb.
14
+ */
15
+ export declare const cancelTaskCapability: CapabilityDefinition;
16
+ /**
17
+ * What a terminal transition actually did, as the server reported it.
18
+ *
19
+ * Mirrors the backend's `effects` block on the task write confirmation. Every
20
+ * field is optional because an older server does not send it, and a fact that
21
+ * did not arrive must not be narrated: silence beats a guess about money.
22
+ */
23
+ export interface TaskOutcomeEffects {
24
+ resultRecorded?: boolean;
25
+ /** The transition was a de-duplicated replay; no effect ran a second time. */
26
+ replayed?: boolean;
27
+ /** Who a completion wake was QUEUED for — never who read it. */
28
+ notified?: string[];
29
+ settlement?: {
30
+ metered: boolean;
31
+ settled: boolean;
32
+ reason?: string;
33
+ amount?: number;
34
+ transactionId?: string;
35
+ };
36
+ agreement?: {
37
+ agreementId: string;
38
+ billing?: string | null;
39
+ status?: string | null;
40
+ fulfilledNow: boolean;
41
+ };
42
+ }
43
+ export interface TaskOutcomePresentation {
44
+ ok: true;
45
+ taskId: string;
46
+ state: string;
47
+ outcome: NextCallOutcome;
48
+ /** Where the outcome now lives, and who can read it. */
49
+ recorded: string;
50
+ /** What was queued for whom — stated as queued, never as received. */
51
+ handoff?: string;
52
+ /** Delivery to the person, which a task result is not. */
53
+ delivery?: string;
54
+ /** What metering did on this completion. Absent when the server said nothing. */
55
+ metering?: string;
56
+ /** What the agreement is now. Absent when the server said nothing. */
57
+ agreement?: string;
58
+ next: NextCall[];
59
+ workContext?: WorkContext;
60
+ }
61
+ /**
62
+ * Present a terminal transition as the four separate facts it produces.
63
+ *
64
+ * `{ ok: true, state: 'completed' }` was the whole answer, and four
65
+ * different claims were read out of it: the work is delivered, the next agent
66
+ * has it, the money moved, the engagement is done. Only the first is in the
67
+ * word. The rest are effects the server ran (or declined to run) beside it, and
68
+ * each one gets its own clause here — including the ones that say nothing
69
+ * happened, because "no settlement ran" is exactly the fact an agent otherwise
70
+ * fills in with an assumption.
71
+ *
72
+ * The lookup route is named rather than described: a completion whose response
73
+ * was lost is recovered by reading the task back, not by completing it twice.
74
+ */
75
+ export declare function presentTaskOutcome(confirm: TaskWriteConfirm, env: CapabilityEnv): TaskOutcomePresentation;
9
76
  export declare const TASK_CAPABILITIES: CapabilityDefinition[];
@@ -1,6 +1,7 @@
1
- import { listTasks } from '../http/TaskClient.js';
2
- import { fullCreds } from './types.js';
1
+ import { cancelTask, listTasks } from '../http/TaskClient.js';
2
+ import { fullCreds, rethrowWithContext, } from './types.js';
3
3
  import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
4
+ import { nextCall, workContextFromEnv, } from './nextCall.js';
4
5
  /**
5
6
  * One list-tasks verb. MCP and the hosted SDK both used to re-declare the
6
7
  * same GET /tasks filters; assignedToMe vs assignedTo precedence then
@@ -63,4 +64,112 @@ export const listTasksCapability = {
63
64
  : listed;
64
65
  },
65
66
  };
66
- export const TASK_CAPABILITIES = [listTasksCapability];
67
+ /**
68
+ * Withdraw a task — including a held graph, which is one cancel on the root.
69
+ * Catalog, not native: this is the pointer AX-07 names, not a new protocol verb.
70
+ */
71
+ export const cancelTaskCapability = {
72
+ key: 'task_cancel',
73
+ names: { sdk: 'task_cancel', mcp: 'ziggs_task_cancel' },
74
+ title: 'Cancel a task',
75
+ descriptions: {
76
+ sdk: 'Cancel one task (PATCH /tasks/:id/cancel). A held graph withdraws by cancelling its root — that is one act, not a blocked state. Cancel ends this task; it does not amend a live hire.',
77
+ mcp: 'Cancel one task (PATCH /tasks/:id/cancel). A held graph withdraws by cancelling its root — that is one act, not a blocked state. Cancel ends this task; it does not amend a live hire.',
78
+ },
79
+ annotation: 'write',
80
+ params: {
81
+ taskId: {
82
+ type: 'string',
83
+ required: true,
84
+ description: 'The task to cancel. For a held graph, pass the root.',
85
+ },
86
+ },
87
+ needsAgentId: true,
88
+ handler: async (args, env) => {
89
+ try {
90
+ return await cancelTask(args['taskId'], fullCreds(env));
91
+ }
92
+ catch (e) {
93
+ rethrowWithContext(e, 'task_cancel');
94
+ }
95
+ },
96
+ };
97
+ /** Why a metered settlement moved nothing, in words rather than a code. */
98
+ const SETTLEMENT_REASONS = {
99
+ not_metered: 'this agreement does not bill per completed task',
100
+ unpriced: 'the agreement carries no price',
101
+ parties_unresolved: 'the paying or providing side is not a resolvable party',
102
+ self_hire: 'payer and provider are the same side — paying yourself is a no-op',
103
+ same_wallet: 'both sides resolve to one wallet',
104
+ wallet_missing: 'a wallet is missing',
105
+ transfer_failed: 'the transfer failed and is left for reconciliation',
106
+ agreement_missing: 'the agreement could not be read',
107
+ };
108
+ /**
109
+ * Present a terminal transition as the four separate facts it produces.
110
+ *
111
+ * `{ ok: true, state: 'completed' }` was the whole answer, and four
112
+ * different claims were read out of it: the work is delivered, the next agent
113
+ * has it, the money moved, the engagement is done. Only the first is in the
114
+ * word. The rest are effects the server ran (or declined to run) beside it, and
115
+ * each one gets its own clause here — including the ones that say nothing
116
+ * happened, because "no settlement ran" is exactly the fact an agent otherwise
117
+ * fills in with an assumption.
118
+ *
119
+ * The lookup route is named rather than described: a completion whose response
120
+ * was lost is recovered by reading the task back, not by completing it twice.
121
+ */
122
+ export function presentTaskOutcome(confirm, env) {
123
+ const effects = confirm.effects;
124
+ const workContext = workContextFromEnv(env);
125
+ const taskId = confirm.taskId;
126
+ const state = String(confirm.state);
127
+ const completed = state === 'completed';
128
+ const stored = effects?.resultRecorded ?? confirm.resultRecorded ?? false;
129
+ // MCP delegates read one task by id; the hosted surface lists its own work.
130
+ const lookupKey = env.surface === 'mcp' ? 'task_get' : 'task_list';
131
+ const lookup = nextCall(env, lookupKey, lookupKey === 'task_get' ? { taskId } : { assignedToMe: true }, 'read the recorded outcome back — the route for a completion whose response you never saw, and the one that never completes the task twice');
132
+ const recorded = effects?.replayed
133
+ ? `Task ${taskId} was already ${state}: this call matched a transition already applied, so nothing was recorded, queued, metered or fulfilled a second time.`
134
+ : stored
135
+ ? `Task ${taskId} is ${state} and its result is stored on the task. That is where the next agent reads it — it stays readable whether or not any notification lands.`
136
+ : `Task ${taskId} is ${state} with no result payload stored. Nothing is waiting there for the next reader.`;
137
+ const notified = effects?.notified;
138
+ const handoff = notified === undefined
139
+ ? undefined
140
+ : notified.length > 0
141
+ ? `Completion queued for ${notified.join(', ')}. Queued is not read: the wake says it was sent, never that anyone has it.`
142
+ : 'No completion wake was queued — nobody on this agreement was addressable, so the result waits to be read rather than delivered.';
143
+ const delivery = completed
144
+ ? 'A task result is how the next AGENT collects the work. If a person set this goal, send it to them as well, in the conversation the work rides on — that delivery is a chat message, not a second record.'
145
+ : undefined;
146
+ const settlement = effects?.settlement;
147
+ const metering = !settlement
148
+ ? undefined
149
+ : settlement.settled
150
+ ? `Metered per completed task: this completion settled ${settlement.amount ?? 'the agreed rate'}${settlement.transactionId ? ` (tx ${settlement.transactionId})` : ''}. One execution, once — a retry of this same transition does not charge again.`
151
+ : `No payment moved on this completion — ${SETTLEMENT_REASONS[settlement.reason ?? ''] ?? settlement.reason ?? 'settlement did not run'}. Do not report this task as paid.`;
152
+ const agreementFacts = effects?.agreement;
153
+ const agreement = !agreementFacts
154
+ ? undefined
155
+ : agreementFacts.fulfilledNow
156
+ ? `Agreement ${agreementFacts.agreementId} reached its execution quota and is now fulfilled — the engagement is over, and its grants went with it.`
157
+ : `Agreement ${agreementFacts.agreementId} is still ${agreementFacts.status ?? 'in force'}${agreementFacts.billing === 'per_task' ? ', charging per completed task' : agreementFacts.billing === 'total' ? ', priced as one total for the engagement' : ''}. Finishing a task does not end a hire; ending one is its own deliberate call.`;
158
+ return {
159
+ ok: true,
160
+ taskId,
161
+ state,
162
+ outcome: 'ok',
163
+ recorded,
164
+ ...(handoff ? { handoff } : {}),
165
+ ...(delivery ? { delivery } : {}),
166
+ ...(metering ? { metering } : {}),
167
+ ...(agreement ? { agreement } : {}),
168
+ next: [lookup],
169
+ ...(workContext ? { workContext } : {}),
170
+ };
171
+ }
172
+ export const TASK_CAPABILITIES = [
173
+ listTasksCapability,
174
+ cancelTaskCapability,
175
+ ];
@@ -0,0 +1,51 @@
1
+ /**
2
+ * One decision, one sentence, on every rail.
3
+ *
4
+ * When somebody answers a request an agent made (an access request on an
5
+ * artifact, a connection request, its own proposal), the agent hears about it
6
+ * as an agreement row whose `reason` names the decision. Rendered generically
7
+ * ("open the agreement <id>") that row says nothing about what was decided,
8
+ * and the agent learned it was approved or refused only if it happened to list
9
+ * its agreements. The hosted brain already had words for these reasons; the
10
+ * MCP rail did not. Both now call this function, so they say the same thing.
11
+ *
12
+ * The sentence is server state only. It never interpolates anything a
13
+ * counterparty wrote: no titles, no messages, no descriptions. Ids, an actor id
14
+ * and an instant are all it carries.
15
+ */
16
+ export type DecisionOutcome = 'approved' | 'rejected' | 'fulfilled' | 'held';
17
+ export interface DecisionWordsInput {
18
+ /** The delivery row's `reason`. Anything that is not a decision maps to null. */
19
+ reason: string | null | undefined;
20
+ /** Who answered (the row's `actorId`). Absent on some emits. */
21
+ actorId?: string | null;
22
+ /** When it was answered (the row's `ts`), as an ISO instant. */
23
+ ts?: string | null;
24
+ /** The request agreement. */
25
+ agreementId?: string | null;
26
+ /**
27
+ * What the request was about, when the caller knows it. Named only when
28
+ * `kind` is `artifact`; today's delivery row does not carry it, so the
29
+ * sentence sends the reader to the agreement to find the artifact instead.
30
+ */
31
+ scope?: {
32
+ kind: string;
33
+ id: string;
34
+ } | null;
35
+ /**
36
+ * Which surface's tool names to use. MCP registers `ziggs_open` and
37
+ * `ziggs_context_read`; the hosted SDK registers `open` and `context_read`.
38
+ */
39
+ surface?: 'mcp' | 'sdk';
40
+ }
41
+ export interface DecisionWords {
42
+ outcome: DecisionOutcome;
43
+ /** Plain, model-facing, server state only. */
44
+ sentence: string;
45
+ }
46
+ /**
47
+ * Map a delivery row's decision reason to words. Returns null when the reason
48
+ * is not a decision (an ordinary doorbell, a store claim, a withheld contact),
49
+ * so a caller can use it as the test for "does this row need explaining".
50
+ */
51
+ export declare function decisionWords(input: DecisionWordsInput): DecisionWords | null;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * One decision, one sentence, on every rail.
3
+ *
4
+ * When somebody answers a request an agent made (an access request on an
5
+ * artifact, a connection request, its own proposal), the agent hears about it
6
+ * as an agreement row whose `reason` names the decision. Rendered generically
7
+ * ("open the agreement <id>") that row says nothing about what was decided,
8
+ * and the agent learned it was approved or refused only if it happened to list
9
+ * its agreements. The hosted brain already had words for these reasons; the
10
+ * MCP rail did not. Both now call this function, so they say the same thing.
11
+ *
12
+ * The sentence is server state only. It never interpolates anything a
13
+ * counterparty wrote: no titles, no messages, no descriptions. Ids, an actor id
14
+ * and an instant are all it carries.
15
+ */
16
+ const REFUSED_REASONS = new Set([
17
+ 'approval:rejected',
18
+ 'context_request_rejected',
19
+ 'connection_request_rejected',
20
+ ]);
21
+ function requestNoun(reason) {
22
+ if (reason.startsWith('context_request_'))
23
+ return 'access request';
24
+ if (reason.startsWith('connection_request_'))
25
+ return 'connection request';
26
+ return 'request';
27
+ }
28
+ /**
29
+ * Map a delivery row's decision reason to words. Returns null when the reason
30
+ * is not a decision (an ordinary doorbell, a store claim, a withheld contact),
31
+ * so a caller can use it as the test for "does this row need explaining".
32
+ */
33
+ export function decisionWords(input) {
34
+ const reason = typeof input.reason === 'string' ? input.reason : '';
35
+ if (!reason)
36
+ return null;
37
+ const tool = (key) => input.surface === 'sdk' ? key : `ziggs_${key}`;
38
+ const on = input.agreementId
39
+ ? `agreement ${input.agreementId}`
40
+ : 'this agreement';
41
+ const by = input.actorId ? ` by ${input.actorId}` : '';
42
+ const at = input.ts ? ` at ${input.ts}` : '';
43
+ const artifactId = input.scope?.kind === 'artifact' && input.scope.id ? input.scope.id : null;
44
+ if (REFUSED_REASONS.has(reason)) {
45
+ return {
46
+ outcome: 'rejected',
47
+ sentence: `Your ${requestNoun(reason)} on ${on} was refused${by}${at}. ` +
48
+ 'Report the unresolved dependency where you asked, and ask what to change.',
49
+ };
50
+ }
51
+ if (reason === 'context_request_fulfilled') {
52
+ const open = artifactId
53
+ ? `Open the artifact ${artifactId} with ${tool('context_read')} or ${tool('open')}; `
54
+ : `Open the agreement with ${tool('open')} to find the artifact, then open the artifact with ${tool('context_read')} or ${tool('open')}; `;
55
+ return {
56
+ outcome: 'fulfilled',
57
+ sentence: `Your access request on ${on} was approved${by}${at}. ` +
58
+ open +
59
+ 'the grant is bounded, check its expiry on the agreement.',
60
+ };
61
+ }
62
+ if (reason === 'connection_request_fulfilled') {
63
+ return {
64
+ outcome: 'fulfilled',
65
+ sentence: `Your connection request on ${on} was fulfilled${by}${at}. ` +
66
+ `The connection is ready to use; open the agreement with ${tool('open')} for its connection and grant ids and the bound on the grant.`,
67
+ };
68
+ }
69
+ if (reason === 'approval:approved') {
70
+ return {
71
+ outcome: 'approved',
72
+ sentence: `An approval on ${on} was given${by}${at}. ` +
73
+ 'If it answers a request you made, carry out the next step you promised where you asked; ' +
74
+ `open the agreement with ${tool('open')} to see which slot was answered. ` +
75
+ 'Approval of your own request is not a new hire and needs no introduction.',
76
+ };
77
+ }
78
+ if (reason === 'formation_held') {
79
+ return {
80
+ outcome: 'held',
81
+ sentence: `${on.charAt(0).toUpperCase()}${on.slice(1)} is held${at}: a consent it needs has not been given yet, so nothing is granted. ` +
82
+ `Open the agreement with ${tool('open')} to see whose decision it waits on.`,
83
+ };
84
+ }
85
+ return null;
86
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * What to do with a piece of engagement, from records the caller already has.
3
+ *
4
+ * This is a receipt and a next-call pointer. It does not invent a worker
5
+ * protocol, a wake store, or a blocked task state. Waiting-on-person is a
6
+ * field on the receipt so a later human view can show waiting instead of
7
+ * active. A held graph is not work until deps release; failed or cancelled
8
+ * deps are not inputs. Withdraw is cancel on the root. A live hire does not
9
+ * take a second hire or an in-place amend.
10
+ */
11
+ import type { InboxDeliveryRef, InboxEnvelope, InboxTaskRef, Task } from './types.js';
12
+ import { type NextCall } from './capabilities/nextCall.js';
13
+ import type { CapabilityEnv } from './capabilities/types.js';
14
+ import type { SessionContinuation } from './sessionOrientation.js';
15
+ export type EngagementGuideKind = 'work_order' | 'duplicate' | 'held' | 'waiting' | 'contact' | 'stranger_brief' | 'hire_terms' | 'reminder' | 'reuse' | 'claim' | 'request' | 'direct' | 'subcontract';
16
+ export type EngagementOutcome = 'ok' | 'pending' | 'noop' | 'unavailable';
17
+ export type WaitingOn = {
18
+ kind: 'user' | 'agent';
19
+ id: string;
20
+ };
21
+ export interface GraphHonesty {
22
+ ready: boolean;
23
+ heldByDeps: boolean;
24
+ failedDepsAreNotInputs: true;
25
+ failedDepCount: number;
26
+ }
27
+ export interface HireTermsHonesty {
28
+ secondHire: 'refused';
29
+ amendLive: 'unavailable';
30
+ }
31
+ export interface EngagementGuide {
32
+ kind: EngagementGuideKind;
33
+ outcome: EngagementOutcome;
34
+ summary: string;
35
+ /** Room the brief arrived in — never a substitute for agreementId. */
36
+ chatId?: string | null;
37
+ agreementId?: string | null;
38
+ taskId?: string | null;
39
+ waitingOn?: WaitingOn | null;
40
+ graph?: GraphHonesty;
41
+ hireTerms?: HireTermsHonesty;
42
+ next?: NextCall;
43
+ }
44
+ export interface EngagementTaskHint {
45
+ taskId: string;
46
+ agreementId?: string | null;
47
+ rootTaskId?: string | null;
48
+ heldByDeps?: boolean;
49
+ waitsOn?: string[];
50
+ /** Known terminal states for named deps, when the caller has them. */
51
+ depStates?: Record<string, string>;
52
+ dueAt?: string | null;
53
+ checkBack?: {
54
+ everyMinutes: number;
55
+ maxReminders: number;
56
+ } | null;
57
+ lastAct?: 'question';
58
+ waitingOn?: WaitingOn | null;
59
+ }
60
+ export interface ContactPathInput {
61
+ partyKind: 'person' | 'agent';
62
+ partyId: string;
63
+ /** Active Ziggs link (or same-org membership) to that person. */
64
+ linked: boolean;
65
+ /** The agent has a take-it-or-leave-it listing. */
66
+ listed?: boolean;
67
+ sameOrg?: boolean;
68
+ }
69
+ /**
70
+ * How to form (or refuse to form) an engagement from facts the caller
71
+ * already has. The ladder is reuse → claim → request → direct. A parent
72
+ * subcontract is its own rail. A listing is never countered.
73
+ */
74
+ export interface FormationInput {
75
+ /** Active agreement that already covers this work on matching terms. */
76
+ matchingActive?: {
77
+ agreementId: string;
78
+ } | null;
79
+ /** Live hire with this counterparty, whether or not terms still match. */
80
+ liveHire?: {
81
+ agreementId: string;
82
+ } | null;
83
+ /** Caller wants different terms than the live hire. */
84
+ wantsChangedTerms?: boolean;
85
+ /**
86
+ * A posted listing to claim. `true` means listings exist and the caller
87
+ * still has to pick the id. An object with an id is that listing.
88
+ */
89
+ listing?: {
90
+ agreementId?: string;
91
+ } | true | null;
92
+ /** Active parent under which this slice would be a subcontract. */
93
+ parentAgreementId?: string | null;
94
+ /** Named counterparty for a direct proposal. */
95
+ counterparty?: string | null;
96
+ /** `doors.acceptsProposals` — most published agents are claim-only. */
97
+ acceptsProposals?: boolean;
98
+ /** They work (buy) vs you work (bid). */
99
+ direction?: 'buy' | 'bid';
100
+ }
101
+ export interface InboxEngagementInput {
102
+ env: CapabilityEnv;
103
+ agentId: string;
104
+ inbox: Pick<InboxEnvelope, 'deliveries' | 'tasksAwaitingMe'>;
105
+ tasks?: EngagementTaskHint[];
106
+ continuation?: Pick<SessionContinuation, 'canScheduleWake' | 'kind'>;
107
+ }
108
+ /** Same event twice, or a hire-room brief that already has an open task. */
109
+ export declare function isEchoOrDuplicate(inbox: Pick<InboxEnvelope, 'deliveries' | 'tasksAwaitingMe'>, agentId: string): boolean;
110
+ /**
111
+ * Assigned message under a live hire. Missing chat is not a skip — a fresh
112
+ * hire without a room still runs after access to that agreement is proven.
113
+ * An agrn- notification lane is not treated as a chat row.
114
+ */
115
+ export declare function isHireWorkOrder(delivery: InboxDeliveryRef, agentId: string, tasksAwaitingMe?: readonly InboxTaskRef[]): boolean;
116
+ export declare function waitingOnFromTask(task: EngagementTaskHint): WaitingOn | null;
117
+ export declare function graphReleaseHonesty(task: EngagementTaskHint): GraphHonesty;
118
+ export declare function liveHireChangedTerms(env: CapabilityEnv): EngagementGuide;
119
+ /** Map a hire-formation 409 onto the refuse-second-hire receipt. */
120
+ export declare function liveHireConflictFromStatus(env: CapabilityEnv, status: number): EngagementGuide | null;
121
+ export declare function withdrawHeldGraph(env: CapabilityEnv, rootTaskId: string): EngagementGuide;
122
+ export declare function reminderHonesty(task: EngagementTaskHint, continuation?: Pick<SessionContinuation, 'canScheduleWake' | 'kind'>): EngagementGuide;
123
+ /**
124
+ * Formation receipt. Reuse a matching active agreement. Claim a listing
125
+ * and never counter it. Request when nothing listed fits. Direct only when
126
+ * they accept proposals. Subcontract under a parent you already hold.
127
+ * A second formation on a live hire is refused.
128
+ */
129
+ export declare function formationGuide(env: CapabilityEnv, input: FormationInput): EngagementGuide;
130
+ /** Browse result: claim when rows exist, request when the board is empty. */
131
+ export declare function marketplaceFormation(env: CapabilityEnv, input: {
132
+ listingAgreementIds: readonly string[];
133
+ }): EngagementGuide;
134
+ /**
135
+ * Contact path. Same-org people are reachable under the scoped identity.
136
+ * An outside person needs an active link. A link is not a roster. An agent
137
+ * with a listing is claimed, not chatted into a hire.
138
+ */
139
+ export declare function contactPath(env: CapabilityEnv, input: ContactPathInput): EngagementGuide;
140
+ export declare function hireWorkOrderGuide(env: CapabilityEnv, delivery: InboxDeliveryRef): EngagementGuide;
141
+ export declare function strangerBriefGuide(env: CapabilityEnv, chatId: string | null): EngagementGuide;
142
+ export declare function duplicateGuide(): EngagementGuide;
143
+ /**
144
+ * First engagement that matters on this envelope. Empty mail with no held
145
+ * tasks returns null so an empty inbox result stays a pass-through.
146
+ */
147
+ export declare function inboxEngagement(input: InboxEngagementInput): EngagementGuide | null;
148
+ export declare function hintsFromTasks(tasks: Task[] | undefined): EngagementTaskHint[];