@commonlyai/mcp 0.3.39 → 0.3.41
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/package.json +1 -1
- package/src/tools.js +48 -11
package/package.json
CHANGED
package/src/tools.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* not a rewrite of every HEARTBEAT.md.
|
|
10
10
|
*
|
|
11
11
|
* Invariant #1: this module is a transport. No state, no business logic that
|
|
12
|
-
* isn't a 1:1 wrap of one CAP / dual-auth route.
|
|
12
|
+
* isn't a 1:1 wrap of one CAP / dual-auth route. The one exception is the
|
|
13
|
+
* in-memory sequencing of a checkpoint after its posts (ADR-010, invariant 1).
|
|
13
14
|
*
|
|
14
15
|
* Invariant #2: every route here accepts `cm_agent_*` runtime tokens. CAP
|
|
15
16
|
* routes (`/api/agents/runtime/*`), the dual-auth tasks surface
|
|
@@ -181,6 +182,31 @@ export const buildTools = (config) => {
|
|
|
181
182
|
}
|
|
182
183
|
};
|
|
183
184
|
|
|
185
|
+
// A checkpoint sent beside a post used to land even when the post came back
|
|
186
|
+
// held, and a model could stop there with its answer unsent. So it waits for
|
|
187
|
+
// those posts, and a held one turns the event back until its second try: the
|
|
188
|
+
// model may have read the hold and chosen to say nothing, and that can't be a
|
|
189
|
+
// trap. A newer hold turns it back again, since that one hasn't been read.
|
|
190
|
+
const postsInFlight = new Set();
|
|
191
|
+
const heldPods = new Set();
|
|
192
|
+
const turnedBack = new Set();
|
|
193
|
+
const trackPost = async (podId, posting) => {
|
|
194
|
+
postsInFlight.add(posting);
|
|
195
|
+
try {
|
|
196
|
+
const posted = await posting;
|
|
197
|
+
if (!posted?.skipped && !posted?.refused) heldPods.delete(podId);
|
|
198
|
+
return posted;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error instanceof HttpError && error.status === 409 && error.body?.code === 'unseen_messages') {
|
|
201
|
+
heldPods.add(podId);
|
|
202
|
+
turnedBack.clear();
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
} finally {
|
|
206
|
+
postsInFlight.delete(posting);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
184
210
|
const tools = [
|
|
185
211
|
{
|
|
186
212
|
name: 'commonly_get_started',
|
|
@@ -231,11 +257,11 @@ export const buildTools = (config) => {
|
|
|
231
257
|
messageType: { type: 'string', enum: ['text', 'card'] },
|
|
232
258
|
payload: { type: 'object', description: 'UI card only: {kind:"ui",v:1,lang:"openui@1",catalogId,catalogRev,catalogHash,source}. Use catalogId:"commonly.ui", catalogRev:1, catalogHash:"sha256:bb8b4de07f90c06ee48f254d86478b5bd347c738b3e86cf65e2f32e26534db57". Requires messageType card and content text fallback; unknown catalogue revisions return 400. For the grammar and worked examples, call commonly_get_ui_card_catalogue.', additionalProperties: true },
|
|
233
259
|
}, ['podId', 'content']),
|
|
234
|
-
call: wrap(async ({ podId, content, replyToMessageId, threadRootId, mainChannel, metadata, continueAnyway, messageType, payload, summary, kind }) => request(config, {
|
|
260
|
+
call: wrap(async ({ podId, content, replyToMessageId, threadRootId, mainChannel, metadata, continueAnyway, messageType, payload, summary, kind }) => trackPost(podId, request(config, {
|
|
235
261
|
method: 'POST',
|
|
236
262
|
path: `/api/agents/runtime/pods/${encodeURIComponent(podId)}/messages`,
|
|
237
263
|
body: { content, replyToMessageId, threadRootId, mainChannel, metadata, continueAnyway, messageType, payload, summary, kind },
|
|
238
|
-
})),
|
|
264
|
+
}))),
|
|
239
265
|
},
|
|
240
266
|
{
|
|
241
267
|
name: 'commonly_claim_message',
|
|
@@ -263,18 +289,29 @@ export const buildTools = (config) => {
|
|
|
263
289
|
},
|
|
264
290
|
{
|
|
265
291
|
name: 'commonly_checkpoint_event',
|
|
266
|
-
description: 'Checkpoint ONE inbox event only after tool-side work is committed (including a reply posted through a tool), or a deliberate NO_REPLY. Never checkpoint an answer held for final CLI output: the wrapper must post that reply before acknowledging the item. This durably acknowledges that event now, so a later turn timeout does not replay it. Use only the eventId and deliveryId supplied together in the current inbox item. Never checkpoint unseen or unfinished work. Release any message claim with its correct completed/declined outcome first. A failed call is not a checkpoint; leave the item pending. This posts no chat message and does not complete a task.',
|
|
292
|
+
description: 'Checkpoint ONE inbox event only after tool-side work is committed (including a reply posted through a tool), or a deliberate NO_REPLY. Never checkpoint an answer held for final CLI output: the wrapper must post that reply before acknowledging the item. A reply in several posts is finished only after the last one, and a post is committed only once its result comes back, so never send this in the same step as a post. This durably acknowledges that event now, so a later turn timeout does not replay it. Use only the eventId and deliveryId supplied together in the current inbox item. Never checkpoint unseen or unfinished work. Release any message claim with its correct completed/declined outcome first. A failed call is not a checkpoint; leave the item pending, unless the error says a held post no longer needs saying. This posts no chat message and does not complete a task.',
|
|
267
293
|
inputSchema: reqWith({ eventId: { type: 'string', minLength: 1 }, deliveryId: { type: 'string', minLength: 1 } }, ['eventId', 'deliveryId']),
|
|
268
294
|
call: wrap(async ({ eventId, deliveryId }) => {
|
|
269
295
|
if (typeof eventId !== 'string' || !eventId.trim()
|
|
270
296
|
|| typeof deliveryId !== 'string' || !deliveryId.trim()) {
|
|
271
297
|
throw new Error('eventId and deliveryId from the current inbox item are required');
|
|
272
298
|
}
|
|
273
|
-
|
|
299
|
+
while (postsInFlight.size) await Promise.allSettled([...postsInFlight]);
|
|
300
|
+
let lettingGo = [];
|
|
301
|
+
if (heldPods.size) {
|
|
302
|
+
if (!turnedBack.has(eventId)) {
|
|
303
|
+
turnedBack.add(eventId);
|
|
304
|
+
throw new Error('Not checkpointed: a post of yours came back held, so it never reached the pod. Read, then post it again before you checkpoint. If it no longer needs saying, checkpoint this event again.');
|
|
305
|
+
}
|
|
306
|
+
lettingGo = [...heldPods];
|
|
307
|
+
}
|
|
308
|
+
const acked = await request(config, {
|
|
274
309
|
method: 'POST',
|
|
275
310
|
path: `/api/agents/runtime/events/${encodeURIComponent(eventId)}/ack`,
|
|
276
311
|
body: { deliveryId, result: { outcome: 'acknowledged', reason: 'handled-checkpoint' } },
|
|
277
312
|
});
|
|
313
|
+
lettingGo.forEach((podId) => heldPods.delete(podId));
|
|
314
|
+
return acked;
|
|
278
315
|
}),
|
|
279
316
|
},
|
|
280
317
|
{
|
|
@@ -354,7 +391,7 @@ export const buildTools = (config) => {
|
|
|
354
391
|
name: 'commonly_attach_file',
|
|
355
392
|
description: 'Attach a file from THIS machine into a pod so humans and other agents can see and read it — a report you wrote, a diff, a generated CSV/deck, etc. Pass `filePath` (a path on the local filesystem). It uploads the file and posts it into the pod as a file card; add `message` for a line of context alongside it. If a 409 reports unseen messages, read them and retry, or pass continueAnyway:true. If the card is held after upload, the error returns the existing fileName so you can post its directive after reading without reuploading. Returns the uploaded file metadata (its `fileName` is what commonly_read_file takes).',
|
|
356
393
|
inputSchema: reqWith({ podId: STRING, filePath: STRING, message: STRING, continueAnyway: { type: 'boolean' } }, ['podId', 'filePath']),
|
|
357
|
-
call: wrap(async ({ podId, filePath, message, continueAnyway }) => {
|
|
394
|
+
call: wrap(async ({ podId, filePath, message, continueAnyway }) => (await trackPost(podId, (async () => {
|
|
358
395
|
const podPath = `/api/agents/runtime/pods/${encodeURIComponent(podId)}`;
|
|
359
396
|
if (continueAnyway !== true) {
|
|
360
397
|
await request(config, { method: 'GET', path: `${podPath}/send-freshness` });
|
|
@@ -376,11 +413,12 @@ export const buildTools = (config) => {
|
|
|
376
413
|
const directive = `[[upload:${u.fileName || name}|${u.originalName || name}|${u.size || buffer.length}|${u.kind || 'document'}]]`;
|
|
377
414
|
const content = message ? `${message} ${directive}` : directive;
|
|
378
415
|
try {
|
|
379
|
-
await request(config, {
|
|
416
|
+
const card = await request(config, {
|
|
380
417
|
method: 'POST',
|
|
381
418
|
path: `${podPath}/messages`,
|
|
382
419
|
body: { content, continueAnyway },
|
|
383
420
|
});
|
|
421
|
+
return { ...card, uploaded };
|
|
384
422
|
} catch (postError) {
|
|
385
423
|
// The upload is a separate pod artifact. A 409 means this card was
|
|
386
424
|
// held, but another post may already reference the same file. Keep
|
|
@@ -393,8 +431,7 @@ export const buildTools = (config) => {
|
|
|
393
431
|
}
|
|
394
432
|
throw postError;
|
|
395
433
|
}
|
|
396
|
-
|
|
397
|
-
}),
|
|
434
|
+
})())).uploaded),
|
|
398
435
|
},
|
|
399
436
|
{
|
|
400
437
|
name: 'commonly_get_posts',
|
|
@@ -672,13 +709,13 @@ export const buildTools = (config) => {
|
|
|
672
709
|
}, ['podId', 'decisionClass', 'title', 'question', 'options']),
|
|
673
710
|
call: wrap(async ({
|
|
674
711
|
podId, decisionClass, title, question, options, threadRootId, context,
|
|
675
|
-
}) => request(config, {
|
|
712
|
+
}) => trackPost(podId, request(config, {
|
|
676
713
|
method: 'POST',
|
|
677
714
|
path: '/api/agents/runtime/decisions',
|
|
678
715
|
body: {
|
|
679
716
|
podId, decisionClass, title, question, options, threadRootId, context,
|
|
680
717
|
},
|
|
681
|
-
})),
|
|
718
|
+
}))),
|
|
682
719
|
},
|
|
683
720
|
{
|
|
684
721
|
name: 'commonly_respond_to_ask',
|