@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.
- package/dist/capabilities/agreementVerbs.d.ts +7 -0
- package/dist/capabilities/agreementVerbs.js +84 -1
- package/dist/capabilities/artifacts.js +53 -26
- package/dist/capabilities/chat.d.ts +31 -1
- package/dist/capabilities/chat.js +40 -1
- package/dist/capabilities/context.d.ts +1 -0
- package/dist/capabilities/context.js +46 -5
- package/dist/capabilities/discovery.js +10 -4
- package/dist/capabilities/index.d.ts +4 -4
- package/dist/capabilities/index.js +4 -4
- package/dist/capabilities/marketplace.js +15 -6
- package/dist/capabilities/nextCall.js +62 -4
- package/dist/capabilities/tasks.d.ts +68 -1
- package/dist/capabilities/tasks.js +112 -3
- package/dist/decisionWords.d.ts +51 -0
- package/dist/decisionWords.js +86 -0
- package/dist/engagementGuide.d.ts +148 -0
- package/dist/engagementGuide.js +349 -0
- package/dist/http/AgreementClient.d.ts +2 -1
- package/dist/http/AgreementClient.js +4 -0
- package/dist/http/ChatClient.d.ts +28 -2
- package/dist/http/ChatClient.js +7 -0
- package/dist/http/ContextGrantsClient.d.ts +17 -1
- package/dist/http/ContextGrantsClient.js +26 -2
- package/dist/http/ContextReadClient.d.ts +8 -1
- package/dist/http/TaskClient.d.ts +14 -2
- package/dist/http/TaskClient.js +21 -6
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +11 -0
- package/package.json +3 -3
|
@@ -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 = [
|
|
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
|
|
244
|
+
const faceWasPassed = strippedFaces.length > 0;
|
|
187
245
|
const reason = hold && missing.length === 0
|
|
188
246
|
? why
|
|
189
|
-
: !ready &&
|
|
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
|
|
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
|
-
|
|
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[];
|