@setluca/mcp 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/README.md +110 -80
- package/dist/annotations.js +2 -2
- package/dist/completions.js +32 -12
- package/dist/config.js +3 -3
- package/dist/errors.js +6 -10
- package/dist/http.js +5 -3
- package/dist/oauth-resolver.js +3 -3
- package/dist/observability.js +1 -1
- package/dist/openapi-zod.js +28 -26
- package/dist/operations/groups/webhooks.js +1 -1
- package/dist/operations/manifest.js +2 -2
- package/dist/operations/registry.js +12 -12
- package/dist/operations.js +2 -2
- package/dist/page-shape.js +26 -11
- package/dist/pagination.js +2 -1
- package/dist/prompts.js +26 -0
- package/dist/provenance.js +2 -2
- package/dist/remote.js +2 -2
- package/dist/resilience.js +22 -12
- package/dist/resources.js +17 -2
- package/dist/scopes.js +154 -0
- package/dist/server.js +53 -86
- package/dist/task-tools/analytics.js +1 -1
- package/dist/task-tools/bookings.js +8 -5
- package/dist/task-tools/calls.js +2 -2
- package/dist/task-tools/inbox.js +63 -17
- package/dist/task-tools/runtime.js +5 -5
- package/dist/task-tools.js +1 -1
- package/dist/tool.js +6 -6
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
package/dist/server.js
CHANGED
|
@@ -8,9 +8,9 @@ import { LucaApi, LucaApiLive } from "./http.js";
|
|
|
8
8
|
import { logAgentSurfaceEvent } from "./observability.js";
|
|
9
9
|
import { LUCA_OPERATIONS, operationGroups, operationManifest, } from "./operations.js";
|
|
10
10
|
import { optionalField } from "./optional-field.js";
|
|
11
|
-
import { LUCA_PROMPTS, } from "./prompts.js";
|
|
11
|
+
import { LUCA_API_PLANNER_PROMPT, LUCA_PROMPTS, } from "./prompts.js";
|
|
12
12
|
import { provenanceFields } from "./provenance.js";
|
|
13
|
-
import { LUCA_RESOURCES } from "./resources.js";
|
|
13
|
+
import { LUCA_OPERATION_MANIFEST_RESOURCE, LUCA_RESOURCES, } from "./resources.js";
|
|
14
14
|
import { toJsonText } from "./serialization.js";
|
|
15
15
|
import { LUCA_TASK_TOOLS } from "./task-tools.js";
|
|
16
16
|
import { fromOperation, fromTaskTool, operationProgram, registerTool, } from "./tool.js";
|
|
@@ -48,9 +48,9 @@ export const LUCA_SERVER_INFO = {
|
|
|
48
48
|
*
|
|
49
49
|
* `private` throughout: the tool surface varies with the `?toolset=` query
|
|
50
50
|
* param, so a shared cache keyed on anything less than the whole request
|
|
51
|
-
* could answer one shape with another. `resources/read` is
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* could answer one shape with another. `resources/read` is left out on purpose.
|
|
52
|
+
* It returns live coach data and keeps the conservative default of no caching
|
|
53
|
+
* at all.
|
|
54
54
|
*
|
|
55
55
|
* These hints reach a client on the remote Streamable HTTP transport only.
|
|
56
56
|
* The SDK installs the modern-revision handlers from its HTTP entry
|
|
@@ -58,8 +58,8 @@ export const LUCA_SERVER_INFO = {
|
|
|
58
58
|
* constructed here and connected straight to a stdio or in-memory transport
|
|
59
59
|
* negotiates the legacy `initialize` handshake instead, and the legacy wire
|
|
60
60
|
* codec stamps no cache fields. Passing the table is still correct on both
|
|
61
|
-
* paths
|
|
62
|
-
*
|
|
61
|
+
* paths, inert on stdio rather than wrong, but only the remote handler can be
|
|
62
|
+
* asserted against, which is where the coverage lives.
|
|
63
63
|
*/
|
|
64
64
|
const STATIC_LIST_TTL_MS = 3_600_000;
|
|
65
65
|
const LUCA_CACHE_HINTS = {
|
|
@@ -79,8 +79,8 @@ export function parseToolset(value) {
|
|
|
79
79
|
}
|
|
80
80
|
const OPERATIONS_BY_ID = new Map(
|
|
81
81
|
// This map is built once at module load, so a mutant inside it never takes
|
|
82
|
-
// effect under a test runner that caches the module
|
|
83
|
-
// construction rather than untested
|
|
82
|
+
// effect under a test runner that caches the module. It is unkillable by
|
|
83
|
+
// construction rather than untested, and the lookups below are covered.
|
|
84
84
|
// Stryker disable next-line ArrowFunction: module-level initializer, never re-evaluated per test
|
|
85
85
|
LUCA_OPERATIONS.map((operation) => [operation.id, operation]));
|
|
86
86
|
function operationByIdOrThrow(id) {
|
|
@@ -88,7 +88,7 @@ function operationByIdOrThrow(id) {
|
|
|
88
88
|
// the same CI-validated manifest, so this throw is unreachable with real data
|
|
89
89
|
// and the message text is never observed.
|
|
90
90
|
return Option.fromNullable(OPERATIONS_BY_ID.get(id)).pipe(Option.getOrThrowWith(
|
|
91
|
-
// Stryker disable next-line all: unreachable defensive throw
|
|
91
|
+
// Stryker disable next-line all: unreachable defensive throw, every composed id exists in the manifest
|
|
92
92
|
() => new Error(`task tool composes unknown operation: ${id}`)));
|
|
93
93
|
}
|
|
94
94
|
function registerLucaResources(server, lucaLayer, completions) {
|
|
@@ -114,15 +114,15 @@ function resourceOperationOrThrow(operationId) {
|
|
|
114
114
|
// CI-validated manifest, so this throw is unreachable with real data and the
|
|
115
115
|
// message text is never observed.
|
|
116
116
|
Option.getOrThrowWith(
|
|
117
|
-
// Stryker disable next-line all: unreachable defensive throw
|
|
117
|
+
// Stryker disable next-line all: unreachable defensive throw, every resource operation id exists in the manifest
|
|
118
118
|
() => new Error(`Unknown resource operation: ${operationId}`)));
|
|
119
119
|
}
|
|
120
120
|
function registerTemplatedResource(server, resource, operation, lucaLayer, metadata, completions) {
|
|
121
121
|
return Option.fromNullable(resource.uriTemplate).pipe(Option.match({
|
|
122
122
|
onNone: () => undefined,
|
|
123
123
|
onSome: (template) => server.registerResource(resource.name,
|
|
124
|
-
// These templates are not enumerable
|
|
125
|
-
// addressable but not listable
|
|
124
|
+
// These templates are not enumerable. A lead or conversation uri is
|
|
125
|
+
// addressable but not listable, which an explicit `list: undefined`
|
|
126
126
|
// states at the call site. Omitting the key entirely reads the same
|
|
127
127
|
// to the SDK, so a mutant that drops it changes nothing observable.
|
|
128
128
|
// Stryker disable next-line ObjectLiteral: an absent `list` and an undefined one are the same to the SDK
|
|
@@ -158,14 +158,16 @@ function readResult(operation, uri, result) {
|
|
|
158
158
|
contents: [resourceContent(uri, resourcePayload(operation, result))],
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* The operation input a resource read runs, recorded as it is built.
|
|
163
|
+
*
|
|
164
|
+
* Only the resource's name is logged. Every variable a template carries is an
|
|
165
|
+
* id the client already typed, and a resource read takes no cursor: each one
|
|
166
|
+
* answers with a whole record or a single page.
|
|
167
|
+
*/
|
|
162
168
|
function resourceReadInput(resource, variables) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
resourceType: resource.name,
|
|
166
|
-
hasCursor: input.cursor !== undefined,
|
|
167
|
-
});
|
|
168
|
-
return input;
|
|
169
|
+
logAgentSurfaceEvent("mcp.resources.read", { resourceType: resource.name });
|
|
170
|
+
return resource.buildInput(variables);
|
|
169
171
|
}
|
|
170
172
|
/**
|
|
171
173
|
* The payload of a successful read, provenance-tagged when it carries text an
|
|
@@ -185,9 +187,9 @@ function resourceContent(uri, payload) {
|
|
|
185
187
|
};
|
|
186
188
|
}
|
|
187
189
|
function registerOperationManifestResource(server) {
|
|
188
|
-
server.registerResource(
|
|
189
|
-
title:
|
|
190
|
-
description:
|
|
190
|
+
server.registerResource(LUCA_OPERATION_MANIFEST_RESOURCE.name, LUCA_OPERATION_MANIFEST_RESOURCE.uri, {
|
|
191
|
+
title: LUCA_OPERATION_MANIFEST_RESOURCE.title,
|
|
192
|
+
description: LUCA_OPERATION_MANIFEST_RESOURCE.description,
|
|
191
193
|
mimeType: "application/json",
|
|
192
194
|
}, async (uri) => ({
|
|
193
195
|
contents: [
|
|
@@ -211,14 +213,11 @@ function registerOperationManifestResource(server) {
|
|
|
211
213
|
* Two details of the SDK shape this. It looks for the completion metadata on
|
|
212
214
|
* the schema *inside* the optional wrapper, not on the wrapper. And
|
|
213
215
|
* `completable` stamps that metadata with a non-configurable property, so a
|
|
214
|
-
* schema can be stamped once and never again
|
|
216
|
+
* schema can be stamped once and never again, while `LUCA_PROMPTS` is a
|
|
215
217
|
* module singleton the Worker re-registers on every request. Cloning the inner
|
|
216
218
|
* schema first gives each build its own object to stamp.
|
|
217
219
|
*/
|
|
218
220
|
function completableArgs(schema, completions) {
|
|
219
|
-
if (!completions) {
|
|
220
|
-
return schema;
|
|
221
|
-
}
|
|
222
221
|
return Object.fromEntries(Object.entries(schema).map(([name, field]) => [
|
|
223
222
|
name,
|
|
224
223
|
isCompletionVariable(name)
|
|
@@ -241,26 +240,33 @@ function promptArgsSchema(prompt, completions) {
|
|
|
241
240
|
return z.object(completableArgs(prompt.argsSchema, completions));
|
|
242
241
|
}
|
|
243
242
|
/**
|
|
244
|
-
* Registers the
|
|
243
|
+
* Registers the planner prompt plus every prompt in `prompts`.
|
|
245
244
|
*
|
|
246
245
|
* The list is a parameter so a test can drive this with its own prompt instead
|
|
247
246
|
* of mutating the exported `LUCA_PROMPTS` array out from under other tests.
|
|
248
247
|
*/
|
|
249
|
-
export function registerPrompts(server, prompts
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
248
|
+
export function registerPrompts(server, prompts, completions) {
|
|
249
|
+
for (const prompt of [LUCA_API_PLANNER_PROMPT, ...prompts]) {
|
|
250
|
+
registerPrompt(server, prompt, completions);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function registerPrompt(server, prompt, completions) {
|
|
254
|
+
const config = {
|
|
255
|
+
title: prompt.title,
|
|
256
|
+
description: prompt.description,
|
|
257
|
+
...optionalField("argsSchema", promptArgsSchema(prompt, completions)),
|
|
258
|
+
};
|
|
259
|
+
server.registerPrompt(prompt.name, config, (args) => {
|
|
260
|
+
// Parse with the prompt's own argsSchema, not a shared guess at one. A
|
|
261
|
+
// schema that does not match what the client was told to send silently
|
|
262
|
+
// drops any argument outside it before `prompt.build` ever sees it.
|
|
263
|
+
const parsedArgs = z.object(prompt.argsSchema).parse(args ?? {});
|
|
261
264
|
logAgentSurfaceEvent("mcp.prompts.get", {
|
|
262
|
-
prompt:
|
|
263
|
-
|
|
265
|
+
prompt: prompt.name,
|
|
266
|
+
// Booleans and the arg name only, never guidance text: `guidance` only
|
|
267
|
+
// exists on one prompt's schema, so this reads it without assuming
|
|
268
|
+
// every prompt shares that field.
|
|
269
|
+
hasGuidance: Boolean(parsedArgs.guidance),
|
|
264
270
|
});
|
|
265
271
|
return {
|
|
266
272
|
messages: [
|
|
@@ -268,58 +274,19 @@ export function registerPrompts(server, prompts = LUCA_PROMPTS, completions) {
|
|
|
268
274
|
role: "user",
|
|
269
275
|
content: {
|
|
270
276
|
type: "text",
|
|
271
|
-
text:
|
|
272
|
-
"Use Luca MCP tools only for public API operations.",
|
|
273
|
-
"Do not ask for database access, internal endpoints, webhook ingress secrets, or browser cookies.",
|
|
274
|
-
"For writes, pass a stable idempotencyKey when retrying the same user intent.",
|
|
275
|
-
"Check luca://operations when you need exact tool names, scopes, and paths.",
|
|
276
|
-
Option.fromNullable(task).pipe(Option.filter((value) => value.length > 0), Option.map((value) => `User task: ${value}`), Option.getOrUndefined),
|
|
277
|
-
]
|
|
278
|
-
.filter(Boolean)
|
|
279
|
-
.join("\n"),
|
|
277
|
+
text: prompt.build(parsedArgs),
|
|
280
278
|
},
|
|
281
279
|
},
|
|
282
280
|
],
|
|
283
281
|
};
|
|
284
282
|
});
|
|
285
|
-
for (const prompt of prompts) {
|
|
286
|
-
const config = {
|
|
287
|
-
title: prompt.title,
|
|
288
|
-
description: prompt.description,
|
|
289
|
-
...optionalField("argsSchema", promptArgsSchema(prompt, completions)),
|
|
290
|
-
};
|
|
291
|
-
server.registerPrompt(prompt.name, config, (args) => {
|
|
292
|
-
// Parse with the prompt's own argsSchema, not a shared guess at one — a
|
|
293
|
-
// schema that does not match what the client was told to send silently
|
|
294
|
-
// drops any argument outside it before `prompt.build` ever sees it.
|
|
295
|
-
const parsedArgs = z.object(prompt.argsSchema).parse(args ?? {});
|
|
296
|
-
logAgentSurfaceEvent("mcp.prompts.get", {
|
|
297
|
-
prompt: prompt.name,
|
|
298
|
-
// Booleans and the arg name only, never guidance text: `guidance` only
|
|
299
|
-
// exists on one prompt's schema, so this reads it without assuming
|
|
300
|
-
// every prompt shares that field.
|
|
301
|
-
hasGuidance: Boolean(parsedArgs.guidance),
|
|
302
|
-
});
|
|
303
|
-
return {
|
|
304
|
-
messages: [
|
|
305
|
-
{
|
|
306
|
-
role: "user",
|
|
307
|
-
content: {
|
|
308
|
-
type: "text",
|
|
309
|
-
text: prompt.build(parsedArgs),
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
],
|
|
313
|
-
};
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
283
|
}
|
|
317
284
|
/**
|
|
318
285
|
* Build the Luca MCP server: registers the operation tools, the task tools, the
|
|
319
286
|
* `luca://…` resources, and the planner prompt against a shared `LucaApi` layer.
|
|
320
287
|
* `options.lucaLayer` swaps the HTTP client (tests inject an in-memory handler);
|
|
321
|
-
* `options.toolset`
|
|
322
|
-
* plus capability discovery, `"full"
|
|
288
|
+
* `options.toolset` picks the tool set. `"tasks"` exposes only the task tools
|
|
289
|
+
* plus capability discovery, and `"full"`, the default, exposes everything.
|
|
323
290
|
*/
|
|
324
291
|
export function createLucaServer(options = {}) {
|
|
325
292
|
const lucaLayer = options.lucaLayer ?? LucaLive;
|
|
@@ -331,8 +298,8 @@ export function createLucaServer(options = {}) {
|
|
|
331
298
|
const server = new McpServer(LUCA_SERVER_INFO, {
|
|
332
299
|
cacheHints: LUCA_CACHE_HINTS,
|
|
333
300
|
});
|
|
334
|
-
// The tasks
|
|
335
|
-
//
|
|
301
|
+
// The tasks set carries one operation, capability discovery, so a client can
|
|
302
|
+
// still learn what the workspace supports without the full tool list.
|
|
336
303
|
const operations = toolset === "tasks"
|
|
337
304
|
? [operationByIdOrThrow("capabilities.get")]
|
|
338
305
|
: LUCA_OPERATIONS;
|
|
@@ -30,7 +30,7 @@ const getAnalytics = {
|
|
|
30
30
|
}));
|
|
31
31
|
// broadcasts.list doesn't depend on the per-campaign analytics loop, so
|
|
32
32
|
// it runs alongside it instead of after it. Effect.all defaults to
|
|
33
|
-
// concurrency 1 (sequential)
|
|
33
|
+
// concurrency 1 (sequential), so without `concurrency: "unbounded"` here
|
|
34
34
|
// the broadcasts call would still wait on the whole loop below.
|
|
35
35
|
const [perCampaign, broadcasts] = yield* Effect.all([
|
|
36
36
|
Effect.forEach(listingIds(campaignPage, campaignLimit), (id) => api
|
|
@@ -11,7 +11,7 @@ const AVAILABILITY_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
|
11
11
|
* `Option.none()` means "we could not tell", which is different from "nothing
|
|
12
12
|
* is open". The availability route answers with `source: "fallback"` or
|
|
13
13
|
* `"provider_error"` when it could not reach the provider, and it defaults to
|
|
14
|
-
* Calendly when the caller names no provider
|
|
14
|
+
* Calendly when the caller names no provider, so a coach on Cal.com asking
|
|
15
15
|
* without one gets a confident-looking answer about the wrong calendar. Only a
|
|
16
16
|
* `live_provider` read is worth blocking a booking over.
|
|
17
17
|
*/
|
|
@@ -40,11 +40,14 @@ const AvailabilityPageSchema = z.object({
|
|
|
40
40
|
source: z.string(),
|
|
41
41
|
slots: z.array(z.object({ startsAt: z.string() })),
|
|
42
42
|
});
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Whether two ISO-8601 instants name the same moment, whatever their offset.
|
|
45
|
+
*
|
|
46
|
+
* An unparseable instant is `NaN`, which is not equal to itself, so a garbled
|
|
47
|
+
* slot on either side answers false without a guard of its own.
|
|
48
|
+
*/
|
|
44
49
|
function sameInstant(left, right) {
|
|
45
|
-
|
|
46
|
-
const b = new Date(right).getTime();
|
|
47
|
-
return !(Number.isNaN(a) || Number.isNaN(b)) && a === b;
|
|
50
|
+
return new Date(left).getTime() === new Date(right).getTime();
|
|
48
51
|
}
|
|
49
52
|
const bookCall = {
|
|
50
53
|
name: "luca_book_call",
|
package/dist/task-tools/calls.js
CHANGED
|
@@ -24,7 +24,7 @@ const postCallQueue = {
|
|
|
24
24
|
/**
|
|
25
25
|
* The id of the call event attached to a booking, if the API has one. A booking
|
|
26
26
|
* whose call has not happened yet answers 404, which is the normal case rather
|
|
27
|
-
* than a failure
|
|
27
|
+
* than a failure. So is any other read error here, because the outcome write
|
|
28
28
|
* below is worth doing on its own.
|
|
29
29
|
*/
|
|
30
30
|
const CallEventId = Schema.Struct({
|
|
@@ -153,7 +153,7 @@ const closeCallLoop = {
|
|
|
153
153
|
const feedback = yield* Option.match(decodeCallEventId(page), {
|
|
154
154
|
onNone: () =>
|
|
155
155
|
// No call event means attendance has nowhere to go. The revenue write
|
|
156
|
-
// below still runs
|
|
156
|
+
// below still runs, and that is the half the coach is usually after.
|
|
157
157
|
Effect.succeed({
|
|
158
158
|
reported: false,
|
|
159
159
|
reason: "no_call_event_for_booking",
|
package/dist/task-tools/inbox.js
CHANGED
|
@@ -1,13 +1,63 @@
|
|
|
1
1
|
import { Effect, Record } from "effect";
|
|
2
2
|
import * as z from "zod/v4";
|
|
3
3
|
import { formatError } from "../errors.js";
|
|
4
|
+
import { logAgentSurfaceEvent } from "../observability.js";
|
|
4
5
|
import { WORKSPACE_INPUT_FIELDS } from "../operations.js";
|
|
5
6
|
import { listingIds, numberInput, taskRequest, } from "./runtime.js";
|
|
6
7
|
const DEFAULT_QUEUE_LIMIT = 20;
|
|
7
8
|
const DEFAULT_EXPLAIN_TOP = 3;
|
|
9
|
+
const TRIAGE_INBOX_TOOL_NAME = "luca_triage_inbox";
|
|
10
|
+
/**
|
|
11
|
+
* One explanation, with a failed lookup carried in the result instead of
|
|
12
|
+
* cancelling the others. A queue the coach can read with two explanations
|
|
13
|
+
* missing beats no queue at all.
|
|
14
|
+
*/
|
|
15
|
+
function explainOne(api, operation, input, id) {
|
|
16
|
+
return api
|
|
17
|
+
.request(taskRequest(operation, input, { pathParams: { id } }))
|
|
18
|
+
.pipe(Effect.map((explanation) => ({
|
|
19
|
+
id,
|
|
20
|
+
explanation,
|
|
21
|
+
failed: false,
|
|
22
|
+
})), Effect.catchAll((error) =>
|
|
23
|
+
// Keep the structured formatter, not a lossy String(error): a
|
|
24
|
+
// LucaHttpError's status/requestId survive for debugging a partial
|
|
25
|
+
// failure inside the composed tool.
|
|
26
|
+
Effect.succeed({
|
|
27
|
+
id,
|
|
28
|
+
explanation: { explainError: formatError(error) },
|
|
29
|
+
failed: true,
|
|
30
|
+
})));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* How the explain fan-out went, as counts.
|
|
34
|
+
*
|
|
35
|
+
* `mcp.tool.called` reports this tool as a success even when every explanation
|
|
36
|
+
* inside it failed, because a failed lookup comes back as an `explainError`
|
|
37
|
+
* field rather than an error result. Without this line, an explain route that
|
|
38
|
+
* is broken for every caller looks identical in logs to one that works.
|
|
39
|
+
*
|
|
40
|
+
* The single-route explain tools need no line of their own: `mcp.tool.called`
|
|
41
|
+
* already carries their name and outcome. This fan-out is the only place where
|
|
42
|
+
* a failure has nowhere else to show up.
|
|
43
|
+
*
|
|
44
|
+
* Counts only, never an id or an explanation. Both are lead-derived.
|
|
45
|
+
*/
|
|
46
|
+
function logExplainFanOut(attempts) {
|
|
47
|
+
if (attempts.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const failed = attempts.filter((attempt) => attempt.failed).length;
|
|
51
|
+
logAgentSurfaceEvent("mcp.explain", {
|
|
52
|
+
toolName: TRIAGE_INBOX_TOOL_NAME,
|
|
53
|
+
requested: attempts.length,
|
|
54
|
+
explained: attempts.length - failed,
|
|
55
|
+
failed,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
8
58
|
/** The review session's entry point: the queue, with the top items explained. */
|
|
9
59
|
const triageInbox = {
|
|
10
|
-
name:
|
|
60
|
+
name: TRIAGE_INBOX_TOOL_NAME,
|
|
11
61
|
title: "Triage the review queue",
|
|
12
62
|
description: "Return the coach's prioritized reply queue: pending drafts with lead context and, for the top items, why Luca drafted what it drafted. Start every review session here, then act on an item with luca_approve_and_send, luca_draft_reply, or luca_reviewQueue_reject. Example: call with { explainTop: 3 } to get the queue plus explanations for the three most urgent drafts.",
|
|
13
63
|
inputSchema: {
|
|
@@ -35,24 +85,20 @@ const triageInbox = {
|
|
|
35
85
|
query: { limit: input.limit ?? DEFAULT_QUEUE_LIMIT },
|
|
36
86
|
}));
|
|
37
87
|
const ids = listingIds(queue, numberInput(input.explainTop, DEFAULT_EXPLAIN_TOP));
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
pathParams: { id },
|
|
41
|
-
}))
|
|
42
|
-
.pipe(Effect.catchAll((error) =>
|
|
43
|
-
// Keep the structured formatter, not a lossy String(error): a
|
|
44
|
-
// LucaHttpError's status/requestId survive for debugging a
|
|
45
|
-
// partial failure inside the composed tool.
|
|
46
|
-
Effect.succeed({ explainError: formatError(error) })), Effect.map((explanation) => [
|
|
47
|
-
id,
|
|
48
|
-
explanation,
|
|
49
|
-
])),
|
|
88
|
+
const explain = operationById("reviewQueue.explain");
|
|
89
|
+
const attempts = yield* Effect.forEach(ids, (id) => explainOne(api, explain, input, id),
|
|
50
90
|
// Effect.forEach defaults to concurrency 1 (sequential). `explainTop`
|
|
51
|
-
// caps this fan-out at 5 independent lookups, so unbounded is fine
|
|
52
|
-
//
|
|
53
|
-
// thundering herd.
|
|
91
|
+
// caps this fan-out at 5 independent lookups, so unbounded is fine.
|
|
92
|
+
// No caller input can grow it into a thundering herd.
|
|
54
93
|
{ concurrency: "unbounded" });
|
|
55
|
-
|
|
94
|
+
logExplainFanOut(attempts);
|
|
95
|
+
return {
|
|
96
|
+
queue,
|
|
97
|
+
explanations: Record.fromEntries(attempts.map((attempt) => [
|
|
98
|
+
attempt.id,
|
|
99
|
+
attempt.explanation,
|
|
100
|
+
])),
|
|
101
|
+
};
|
|
56
102
|
}),
|
|
57
103
|
};
|
|
58
104
|
/** The day's opening read: the last 24h of queue activity, already counted. */
|
|
@@ -18,7 +18,7 @@ function workspaceOverride(input) {
|
|
|
18
18
|
* Assemble the request a task tool sends: the resolved operation plus its
|
|
19
19
|
* path/query/body, with the workspace override and idempotency key folded in
|
|
20
20
|
* the same way for every tool. This is the task-tool analogue of operations.ts
|
|
21
|
-
* `commonInput
|
|
21
|
+
* `commonInput`. One seam owns "how a task request is built", so a tool body
|
|
22
22
|
* reads as the fields it maps, not the plumbing. Pass `body: undefined` to omit
|
|
23
23
|
* the body entirely (an absent optional field, never `{ body: undefined }`).
|
|
24
24
|
*/
|
|
@@ -35,8 +35,8 @@ export function taskRequest(operation, input, parts = {}) {
|
|
|
35
35
|
/**
|
|
36
36
|
* The only part of a listing page a composing tool reads for itself: the ids of
|
|
37
37
|
* the items it then fans out over. Decoding that alone leaves the raw page
|
|
38
|
-
* intact for the caller
|
|
39
|
-
*
|
|
38
|
+
* intact for the caller, returned exactly as the API sent it, while the ids the
|
|
39
|
+
* tool acts on are validated rather than asserted.
|
|
40
40
|
*/
|
|
41
41
|
const ListingIds = Schema.Struct({
|
|
42
42
|
items: Schema.optional(Schema.Array(Schema.Struct({ id: Schema.optional(Schema.String) }))),
|
|
@@ -44,7 +44,7 @@ const ListingIds = Schema.Struct({
|
|
|
44
44
|
const decodeListingIds = Schema.decodeUnknownOption(ListingIds);
|
|
45
45
|
/**
|
|
46
46
|
* Ids of the first `limit` items on a listing page. A page whose fields we
|
|
47
|
-
* cannot read, or whose items carry no id, contributes nothing
|
|
47
|
+
* cannot read, or whose items carry no id, contributes nothing. The tool still
|
|
48
48
|
* returns the page itself rather than failing over a fan-out it cannot do.
|
|
49
49
|
*/
|
|
50
50
|
export function listingIds(page, limit) {
|
|
@@ -66,7 +66,7 @@ export function listingIds(page, limit) {
|
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
68
|
* A request body carrying a single text field, or no body at all when the input
|
|
69
|
-
* is absent or blank
|
|
69
|
+
* is absent or blank. Sending an empty string would mean something different to
|
|
70
70
|
* the API than leaving the field off.
|
|
71
71
|
*/
|
|
72
72
|
export function textBody(key, value) {
|
package/dist/task-tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The task-tool registry: composed, intent-level entry points layered over the
|
|
2
2
|
// generated 1:1 operation tools. It is deliberately NOT `LUCA_OPERATIONS`,
|
|
3
3
|
// whose entries the OpenAPI drift checker matches one-to-one against REST
|
|
4
|
-
// routes
|
|
4
|
+
// routes, and a task tool has no route of its own. Each tool hand-writes its
|
|
5
5
|
// input fields and composes existing operations through `LucaApi.request`, and
|
|
6
6
|
// they are split one file per domain under `task-tools/`. The shape they all
|
|
7
7
|
// share, and the request plumbing they all use, lives in `task-tools/runtime.ts`.
|
package/dist/tool.js
CHANGED
|
@@ -105,10 +105,10 @@ function ended(outcome, result) {
|
|
|
105
105
|
return { outcome, result };
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* A tool call's result, confirmation gate included. Every way the call can end
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
108
|
+
* A tool call's result, confirmation gate included. Every way the call can end
|
|
109
|
+
* leaves as a tool result, so nothing reaches the transport as an unhandled
|
|
110
|
+
* rejection. That covers unreadable arguments, a rejected confirmation, a
|
|
111
|
+
* failed request, and a defect thrown while the request was being built.
|
|
112
112
|
*
|
|
113
113
|
* The log line sits after the defect handler, so a call produces exactly one
|
|
114
114
|
* line whichever way it ended.
|
|
@@ -156,8 +156,8 @@ export function operationProgram(operation, toolInput) {
|
|
|
156
156
|
}
|
|
157
157
|
/**
|
|
158
158
|
* A tool's successful result. A response that can carry external-user text is
|
|
159
|
-
* flagged untrusted on both halves
|
|
160
|
-
* field
|
|
159
|
+
* flagged untrusted on both halves, the structured `provenance.untrusted`
|
|
160
|
+
* field that a lead cannot spoof and a human-facing preamble on the text, so a
|
|
161
161
|
* downstream agent treats embedded text as data, not instructions. What the
|
|
162
162
|
* structured half attaches is `provenanceFields`, shared with the resource
|
|
163
163
|
* reads in `server.ts`.
|