@ateam-ai/mcp 0.4.65 → 0.4.67
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/api.js +40 -1
- package/src/tools.js +202 -39
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -173,7 +173,7 @@ export function isExplicitlyAuthenticated(sessionId) {
|
|
|
173
173
|
* Record activity on a session — called on every tool call.
|
|
174
174
|
* Keeps the session alive and updates context for smarter UX.
|
|
175
175
|
*/
|
|
176
|
-
export function touchSession(sessionId, { toolName, solutionId, skillId } = {}) {
|
|
176
|
+
export function touchSession(sessionId, { toolName, solutionId, skillId, actorId } = {}) {
|
|
177
177
|
const session = sessions.get(sessionId);
|
|
178
178
|
if (!session) return;
|
|
179
179
|
|
|
@@ -183,6 +183,39 @@ export function touchSession(sessionId, { toolName, solutionId, skillId } = {})
|
|
|
183
183
|
if (toolName) session.context.lastToolName = toolName;
|
|
184
184
|
if (solutionId) session.context.activeSolutionId = solutionId;
|
|
185
185
|
if (skillId) session.context.lastSkillId = skillId;
|
|
186
|
+
// THE ACTOR IS SESSION STATE, NOT A PER-CALL ARGUMENT.
|
|
187
|
+
//
|
|
188
|
+
// A job belongs to an ACTOR, and Core enforces that on every per-job read.
|
|
189
|
+
// The tenant API key is roleless — it identifies a tenant, i.e. NOBODY — so
|
|
190
|
+
// without an actor a caller is refused reads of jobs it kicked off itself and
|
|
191
|
+
// just listed. Threading actor_id through each tool made five of six job-facing
|
|
192
|
+
// tools forget it (get_chain, chain_status, test_status, test_abort,
|
|
193
|
+
// get_metrics) — and get_chain's own description PROMISED actor scoping its
|
|
194
|
+
// schema could not express. Remembering it here means no tool can forget.
|
|
195
|
+
//
|
|
196
|
+
// Learned from whatever the caller last supplied, and from ateam_conversation's
|
|
197
|
+
// reply, which is where an actor id comes from in the first place. Safe to keep:
|
|
198
|
+
// the Builder applies realActorId() before forwarding, so a generated
|
|
199
|
+
// test_<ts>_<rand> thread key is dropped rather than sent to Core (which 401s on
|
|
200
|
+
// an actor it cannot find). An explicit actor_id on a call still wins. (2026-08-22.)
|
|
201
|
+
// ONLY A REAL ACTOR. ateam_conversation mints a throwaway THREAD key
|
|
202
|
+
// (test_<ts>_<rand>) for anonymous use and returns it as actor_id — the docs
|
|
203
|
+
// tell callers to pass it back for multi-turn. It is not an actor Core can
|
|
204
|
+
// resolve, and Core 401s the WHOLE REQUEST on an actor it cannot find.
|
|
205
|
+
//
|
|
206
|
+
// I shipped this without the filter and broke ateam_chain_status — the tool
|
|
207
|
+
// every agent polls in a loop — for a chain the same session had just
|
|
208
|
+
// started: 403 "Access denied" became 401 "Authentication required". The
|
|
209
|
+
// commit claimed it was safe because "the Builder applies realActorId()
|
|
210
|
+
// first", which is true only of the two routes I had touched, not of the
|
|
211
|
+
// status/chain pipes. Asserting a safety property that holds locally and
|
|
212
|
+
// assuming it holds everywhere is how the header reached Core unfiltered.
|
|
213
|
+
//
|
|
214
|
+
// So the rule lives at the SOURCE too, matching the Builder's
|
|
215
|
+
// GENERATED_THREAD_ACTOR_RE exactly. (2026-08-22.)
|
|
216
|
+
if (actorId && !/^test_\d+_[a-z0-9]+$/i.test(String(actorId))) {
|
|
217
|
+
session.context.actorId = String(actorId);
|
|
218
|
+
}
|
|
186
219
|
}
|
|
187
220
|
|
|
188
221
|
/**
|
|
@@ -420,6 +453,7 @@ function headers(sessionId) {
|
|
|
420
453
|
const h = { "Content-Type": "application/json" };
|
|
421
454
|
h["x-adas-token"] = session.masterKey;
|
|
422
455
|
h["X-ADAS-TENANT"] = session.tenant;
|
|
456
|
+
if (session.context?.actorId) h["X-ADAS-ACTOR-ID"] = session.context.actorId;
|
|
423
457
|
return h;
|
|
424
458
|
}
|
|
425
459
|
|
|
@@ -428,6 +462,11 @@ function headers(sessionId) {
|
|
|
428
462
|
const h = { "Content-Type": "application/json" };
|
|
429
463
|
if (tenant) h["X-ADAS-TENANT"] = tenant;
|
|
430
464
|
if (apiKey) h["X-API-KEY"] = apiKey;
|
|
465
|
+
// The acting actor rides with the tenant on EVERY call — see touchSession.
|
|
466
|
+
// The tenant says WHICH account; the actor says WHO, and per-job reads need
|
|
467
|
+
// both. Adds no authority: Core resolves the actor INSIDE the authenticated
|
|
468
|
+
// tenant and 401s if it is not there.
|
|
469
|
+
if (session?.context?.actorId) h["X-ADAS-ACTOR-ID"] = session.context.actorId;
|
|
431
470
|
return h;
|
|
432
471
|
}
|
|
433
472
|
|
package/src/tools.js
CHANGED
|
@@ -1454,11 +1454,21 @@ export const tools = [
|
|
|
1454
1454
|
monitoring: { safe: true, cost: "cheap", latency_ms_p95: 500, output: "bounded", poll_interval_s: 2,
|
|
1455
1455
|
note: "safe:false when include_chain:true (that fetches the full tree)." },
|
|
1456
1456
|
description:
|
|
1457
|
-
"Poll the progress of an async
|
|
1457
|
+
"Poll the progress of an async test. Pass chain_id for the WHOLE run (recommended — the root job finishing does NOT mean the run finished; a handoff may still be going). Pass job_id to poll one job alone: iteration count, tool call steps, status, and result when done.\n\n" +
|
|
1458
1458
|
"Set include_chain:true to ALSO include the full chain tree (every job in the chain, rooted at this job_id, with parent/child linkage). Use when this job dispatched askAnySkill subcalls and you want a single snapshot of the whole multi-skill state instead of polling each child job_id separately.",
|
|
1459
1459
|
inputSchema: {
|
|
1460
1460
|
type: "object",
|
|
1461
1461
|
properties: {
|
|
1462
|
+
chain_id: {
|
|
1463
|
+
type: "string",
|
|
1464
|
+
description:
|
|
1465
|
+
"THE EXECUTION'S IDENTITY — what ateam_conversation returns and what you actually hold. A chain is the whole run: root job + every handoff + every askAnySkill subcall. Prefer this.",
|
|
1466
|
+
},
|
|
1467
|
+
actor_id: {
|
|
1468
|
+
type: "string",
|
|
1469
|
+
description:
|
|
1470
|
+
"Optional. WHO is asking. A job belongs to an actor and Core enforces that on per-job reads, so a tenant key alone is refused. Usually unnecessary — the session remembers the actor from ateam_conversation/ateam_test_skill. Pass it to inspect a job run by a DIFFERENT actor (e.g. a real user's).",
|
|
1471
|
+
},
|
|
1462
1472
|
solution_id: {
|
|
1463
1473
|
type: "string",
|
|
1464
1474
|
description: "The solution ID",
|
|
@@ -1469,7 +1479,7 @@ export const tools = [
|
|
|
1469
1479
|
},
|
|
1470
1480
|
job_id: {
|
|
1471
1481
|
type: "string",
|
|
1472
|
-
description: "
|
|
1482
|
+
description: "ONE job inside the chain, when you want that job alone. Omit and pass chain_id for the whole run — a root job can be 'completed' while a handoff is still running.",
|
|
1473
1483
|
},
|
|
1474
1484
|
include_chain: {
|
|
1475
1485
|
type: "boolean",
|
|
@@ -1477,7 +1487,7 @@ export const tools = [
|
|
|
1477
1487
|
"If true, includes response.chain — the full chain tree rooted at this job_id (chainJobs[] with parentJobId/relation/depth, executionSteps[] with tool-nesting). Costs one extra Core call. Default false (back-compat).",
|
|
1478
1488
|
},
|
|
1479
1489
|
},
|
|
1480
|
-
required: ["solution_id"
|
|
1490
|
+
required: ["solution_id"],
|
|
1481
1491
|
},
|
|
1482
1492
|
},
|
|
1483
1493
|
{
|
|
@@ -1488,7 +1498,7 @@ export const tools = [
|
|
|
1488
1498
|
// degrades. Use once at the end; poll ateam_chain_status instead.
|
|
1489
1499
|
monitoring: { safe: false, cost: "heavy", output: "grows_with_run", use_instead: "ateam_chain_status" },
|
|
1490
1500
|
description:
|
|
1491
|
-
"Inspect the full chain tree
|
|
1501
|
+
"Inspect the full chain tree — the whole run rooted at chain_id, walking down through every handoff and askAnySkill subcall.\n\n" +
|
|
1492
1502
|
"Use when a chain has already run and you want to analyze the structure: which skill called which, how deep the call tree went, which tool inside which job invoked which sub-tool. The two main shapes:\n" +
|
|
1493
1503
|
" • response.chain.chainJobs[] — one entry per job in the chain. Fields: jobId, skill, status, iteration, depth (0 = root, +1 per askAnySkill subcall hop), relation ('root' | 'subcall' | 'handoff'), parentJobId, parentSkill, goal.\n" +
|
|
1494
1504
|
" • response.chain.executionSteps[] — every tool call across all chain jobs, tagged with _skill, _jobId, _depth (= job depth), _relation, _parentSkill, _parentJobId, _toolDepth (tool-in-tool nesting via opId/parentOpId).\n\n" +
|
|
@@ -1497,16 +1507,26 @@ export const tools = [
|
|
|
1497
1507
|
inputSchema: {
|
|
1498
1508
|
type: "object",
|
|
1499
1509
|
properties: {
|
|
1510
|
+
chain_id: {
|
|
1511
|
+
type: "string",
|
|
1512
|
+
description:
|
|
1513
|
+
"THE EXECUTION'S IDENTITY — what ateam_conversation returns and what you actually hold. A chain is the whole run: root job + every handoff + every askAnySkill subcall. Prefer this.",
|
|
1514
|
+
},
|
|
1515
|
+
actor_id: {
|
|
1516
|
+
type: "string",
|
|
1517
|
+
description:
|
|
1518
|
+
"Optional. WHO is asking. A job belongs to an actor and Core enforces that on per-job reads, so a tenant key alone is refused. Usually unnecessary — the session remembers the actor from ateam_conversation/ateam_test_skill. Pass it to inspect a job run by a DIFFERENT actor (e.g. a real user's).",
|
|
1519
|
+
},
|
|
1500
1520
|
job_id: {
|
|
1501
1521
|
type: "string",
|
|
1502
|
-
description: "
|
|
1522
|
+
description: "Alias for chain_id. Any job inside the chain works — Core walks up to the root — but you rarely hold one; prefer chain_id.",
|
|
1503
1523
|
},
|
|
1504
1524
|
skill_slug: {
|
|
1505
1525
|
type: "string",
|
|
1506
1526
|
description: "Optional. The skill slug for the job — speeds up the lookup when the job isn't in memory and must be loaded from storage. Omit if you don't have it; lookup still works but does an extra round-trip.",
|
|
1507
1527
|
},
|
|
1508
1528
|
},
|
|
1509
|
-
required: [
|
|
1529
|
+
required: [],
|
|
1510
1530
|
},
|
|
1511
1531
|
},
|
|
1512
1532
|
{
|
|
@@ -1530,10 +1550,19 @@ export const tools = [
|
|
|
1530
1550
|
inputSchema: {
|
|
1531
1551
|
type: "object",
|
|
1532
1552
|
properties: {
|
|
1553
|
+
actor_id: {
|
|
1554
|
+
type: "string",
|
|
1555
|
+
description:
|
|
1556
|
+
"Optional. WHO is asking. A job belongs to an actor and Core enforces that on per-job reads, so a tenant key alone is refused. Usually unnecessary — the session remembers the actor from ateam_conversation/ateam_test_skill. Pass it to inspect a job run by a DIFFERENT actor (e.g. a real user's).",
|
|
1557
|
+
},
|
|
1533
1558
|
chain_id: {
|
|
1534
1559
|
type: "string",
|
|
1535
1560
|
description: "The chain id returned by ateam_conversation (the conversation's identity). Any job id in the chain also works — Core resolves the chain aggregate.",
|
|
1536
1561
|
},
|
|
1562
|
+
job_id: {
|
|
1563
|
+
type: "string",
|
|
1564
|
+
description: "Alias for chain_id — any job in the chain resolves to the chain aggregate. The handler has always accepted it; without this declaration MCP stripped it before the handler could see it.",
|
|
1565
|
+
},
|
|
1537
1566
|
},
|
|
1538
1567
|
required: ["chain_id"],
|
|
1539
1568
|
},
|
|
@@ -1581,10 +1610,20 @@ export const tools = [
|
|
|
1581
1610
|
name: "ateam_test_abort",
|
|
1582
1611
|
core: true,
|
|
1583
1612
|
description:
|
|
1584
|
-
"Abort a running
|
|
1613
|
+
"Abort a running test. Pass chain_id to abort the WHOLE run — every job in the chain — and get back which ones stopped. Aborting by job_id stops that job only, leaving handoffs running. Stops at the next iteration boundary. (Advanced.)",
|
|
1585
1614
|
inputSchema: {
|
|
1586
1615
|
type: "object",
|
|
1587
1616
|
properties: {
|
|
1617
|
+
chain_id: {
|
|
1618
|
+
type: "string",
|
|
1619
|
+
description:
|
|
1620
|
+
"THE EXECUTION'S IDENTITY — what ateam_conversation returns and what you actually hold. A chain is the whole run: root job + every handoff + every askAnySkill subcall. Prefer this.",
|
|
1621
|
+
},
|
|
1622
|
+
actor_id: {
|
|
1623
|
+
type: "string",
|
|
1624
|
+
description:
|
|
1625
|
+
"Optional. WHO is asking. A job belongs to an actor and Core enforces that on per-job reads, so a tenant key alone is refused. Usually unnecessary — the session remembers the actor from ateam_conversation/ateam_test_skill. Pass it to inspect a job run by a DIFFERENT actor (e.g. a real user's).",
|
|
1626
|
+
},
|
|
1588
1627
|
solution_id: {
|
|
1589
1628
|
type: "string",
|
|
1590
1629
|
description: "The solution ID",
|
|
@@ -1595,10 +1634,10 @@ export const tools = [
|
|
|
1595
1634
|
},
|
|
1596
1635
|
job_id: {
|
|
1597
1636
|
type: "string",
|
|
1598
|
-
description: "
|
|
1637
|
+
description: "Abort ONE job only. Prefer chain_id: aborting the root leaves handoffs running while reporting the test aborted.",
|
|
1599
1638
|
},
|
|
1600
1639
|
},
|
|
1601
|
-
required: ["solution_id"
|
|
1640
|
+
required: ["solution_id"],
|
|
1602
1641
|
},
|
|
1603
1642
|
},
|
|
1604
1643
|
{
|
|
@@ -1665,6 +1704,11 @@ export const tools = [
|
|
|
1665
1704
|
inputSchema: {
|
|
1666
1705
|
type: "object",
|
|
1667
1706
|
properties: {
|
|
1707
|
+
actor_id: {
|
|
1708
|
+
type: "string",
|
|
1709
|
+
description:
|
|
1710
|
+
"Optional. WHO is asking. A job belongs to an actor and Core enforces that on per-job reads, so a tenant key alone is refused. Usually unnecessary — the session remembers the actor from ateam_conversation/ateam_test_skill. Pass it to inspect a job run by a DIFFERENT actor (e.g. a real user's).",
|
|
1711
|
+
},
|
|
1668
1712
|
solution_id: {
|
|
1669
1713
|
type: "string",
|
|
1670
1714
|
description: "The solution ID",
|
|
@@ -1673,6 +1717,11 @@ export const tools = [
|
|
|
1673
1717
|
type: "string",
|
|
1674
1718
|
description: "Optional: deep analysis for a specific job",
|
|
1675
1719
|
},
|
|
1720
|
+
chain_id: {
|
|
1721
|
+
type: "string",
|
|
1722
|
+
description:
|
|
1723
|
+
"Optional: deep analysis for the job behind a CHAIN id — what ateam_conversation returns and what you actually hold. Resolved to the job for you.",
|
|
1724
|
+
},
|
|
1676
1725
|
skill_id: {
|
|
1677
1726
|
type: "string",
|
|
1678
1727
|
description: "Optional: recent metrics for a specific skill",
|
|
@@ -2832,6 +2881,7 @@ module.exports.default = plugin;
|
|
|
2832
2881
|
return files;
|
|
2833
2882
|
}
|
|
2834
2883
|
|
|
2884
|
+
|
|
2835
2885
|
const handlers = {
|
|
2836
2886
|
ateam_bootstrap: async () => ({
|
|
2837
2887
|
runtime: {
|
|
@@ -4332,33 +4382,43 @@ const handlers = {
|
|
|
4332
4382
|
|
|
4333
4383
|
// ─── Developer Tools ────────────────────────────────────────────
|
|
4334
4384
|
|
|
4335
|
-
ateam_get_execution_logs: async ({ solution_id, skill_id, job_id, chain_id,
|
|
4336
|
-
//
|
|
4337
|
-
//
|
|
4338
|
-
//
|
|
4339
|
-
//
|
|
4340
|
-
//
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4385
|
+
ateam_get_execution_logs: async ({ solution_id, skill_id, job_id, chain_id, limit }, sid) => {
|
|
4386
|
+
// A CHAIN IS NOT A JOB — DO NOT RESOLVE ONE DOWN TO THE OTHER.
|
|
4387
|
+
//
|
|
4388
|
+
// Callers hold a chain id (ateam_conversation returns chain_id;
|
|
4389
|
+
// ateam_chain_status takes chain_id) while this tool spoke only job_id, the
|
|
4390
|
+
// inner id nobody ever sees. The tempting fix — look the chain up in the
|
|
4391
|
+
// list and pass its root job on — is WRONG in the direction the system moved:
|
|
4392
|
+
// a chain is root job + every handoff + every askAnySkill subcall, so it
|
|
4393
|
+
// would return ONE job's trace under the name of the whole chain. A partial
|
|
4394
|
+
// trace that calls itself complete is worse than a refusal: it makes the
|
|
4395
|
+
// handoff you are hunting look like it never happened.
|
|
4396
|
+
//
|
|
4397
|
+
// So a chain id goes to the CHAIN endpoint, which returns every job and every
|
|
4398
|
+
// step across the whole tree — the actual "what ran". (2026-08-22.)
|
|
4399
|
+
if (!job_id && chain_id) {
|
|
4400
|
+
const chain = await get(`/deploy/jobs/${encodeURIComponent(chain_id)}/chain`, sid);
|
|
4401
|
+
const jobs = chain?.chain?.chainJobs || [];
|
|
4402
|
+
const steps = chain?.chain?.executionSteps || [];
|
|
4403
|
+
return {
|
|
4404
|
+
ok: true,
|
|
4405
|
+
scope: "chain",
|
|
4406
|
+
chain_id,
|
|
4407
|
+
solution_id,
|
|
4408
|
+
job_count: jobs.length,
|
|
4409
|
+
step_count: steps.length,
|
|
4410
|
+
jobs,
|
|
4411
|
+
steps,
|
|
4412
|
+
_note: `FULL CHAIN: ${jobs.length} job(s) — root + handoffs + subcalls — and ${steps.length} tool call(s) across all of them. Each step carries _skill/_jobId/_depth/_relation so you can see WHICH skill made it. For one job alone, pass job_id.`,
|
|
4413
|
+
};
|
|
4353
4414
|
}
|
|
4354
4415
|
|
|
4355
4416
|
const qs = new URLSearchParams();
|
|
4356
4417
|
if (skill_id) qs.set("skill_id", skill_id);
|
|
4357
|
-
if (
|
|
4358
|
-
//
|
|
4359
|
-
//
|
|
4360
|
-
//
|
|
4361
|
-
if (actor_id) qs.set("actor_id", actor_id);
|
|
4418
|
+
if (job_id) qs.set("job_id", job_id);
|
|
4419
|
+
// actor_id is NOT set here. It rides X-ADAS-ACTOR-ID for every tool, from
|
|
4420
|
+
// the session (api.js headers/touchSession) — this tool having its own
|
|
4421
|
+
// private path was how the other five ended up with none at all.
|
|
4362
4422
|
if (limit) qs.set("limit", String(limit));
|
|
4363
4423
|
const qsStr = qs.toString() ? `?${qs}` : "";
|
|
4364
4424
|
return get(`/deploy/solutions/${solution_id}/logs${qsStr}`, sid);
|
|
@@ -4628,7 +4688,23 @@ const handlers = {
|
|
|
4628
4688
|
return post(`/deploy/voice-test`, body, sid, { timeoutMs: timeoutTotal });
|
|
4629
4689
|
},
|
|
4630
4690
|
|
|
4631
|
-
ateam_test_status: async ({ solution_id, skill_id, job_id, include_chain }, sid) => {
|
|
4691
|
+
ateam_test_status: async ({ solution_id, skill_id, job_id, chain_id, include_chain }, sid) => {
|
|
4692
|
+
// Given a CHAIN id, answer about the chain. The per-skill test endpoint below
|
|
4693
|
+
// is per-job and needs a skill — neither of which a caller holding a chain id
|
|
4694
|
+
// has. Routing a chain id there would report the root job's status as if it
|
|
4695
|
+
// were the run's, and a root can be "completed" while a handoff is still
|
|
4696
|
+
// going. Whole-chain status is what "is it done?" actually means.
|
|
4697
|
+
if (chain_id && !job_id) {
|
|
4698
|
+
const data = await get(`/deploy/jobs/${encodeURIComponent(chain_id)}/status`, sid);
|
|
4699
|
+
return { ok: true, scope: "chain", chain_id, ...data };
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4702
|
+
if (!job_id) {
|
|
4703
|
+
throw new Error("Pass chain_id (the whole run — recommended) or job_id. ateam_conversation returns chain_id.");
|
|
4704
|
+
}
|
|
4705
|
+
if (!skill_id) {
|
|
4706
|
+
throw new Error(`job_id "${job_id}" needs skill_id too — the per-job endpoint is scoped by skill. Pass chain_id instead to poll the whole run without knowing which skill ran it.`);
|
|
4707
|
+
}
|
|
4632
4708
|
// Existing single-job snapshot via Builder (unchanged shape for back-compat).
|
|
4633
4709
|
const single = await get(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid);
|
|
4634
4710
|
if (!include_chain) return single;
|
|
@@ -4646,8 +4722,14 @@ const handlers = {
|
|
|
4646
4722
|
return { ...single, chain };
|
|
4647
4723
|
},
|
|
4648
4724
|
|
|
4649
|
-
ateam_get_chain: async ({ job_id, skill_slug }, sid) => {
|
|
4650
|
-
|
|
4725
|
+
ateam_get_chain: async ({ chain_id, job_id, skill_slug }, sid) => {
|
|
4726
|
+
// CHAIN IS THE UNIT. Core resolves either id to the same chain (it walks up
|
|
4727
|
+
// to the root), so the tool takes the id the caller actually holds — the
|
|
4728
|
+
// chain id from ateam_conversation — and treats job_id as an alias for the
|
|
4729
|
+
// rarer case of holding an inner id. Same `chain_id || job_id` shape as
|
|
4730
|
+
// ateam_chain_status, so the two poll/inspect tools take the same argument.
|
|
4731
|
+
const id = chain_id || job_id;
|
|
4732
|
+
if (!id) throw new Error("chain_id required (job_id accepted as an alias)");
|
|
4651
4733
|
const creds = getCredentials(sid);
|
|
4652
4734
|
const apiKey = creds?.apiKey;
|
|
4653
4735
|
if (!apiKey) throw new Error("No api_key in session — call ateam_auth(api_key) first.");
|
|
@@ -4656,7 +4738,7 @@ const handlers = {
|
|
|
4656
4738
|
const qs = new URLSearchParams();
|
|
4657
4739
|
if (skill_slug) qs.set("skillSlug", skill_slug);
|
|
4658
4740
|
const suffix = qs.toString() ? `?${qs}` : "";
|
|
4659
|
-
return await get(`/deploy/jobs/${encodeURIComponent(
|
|
4741
|
+
return await get(`/deploy/jobs/${encodeURIComponent(id)}/chain${suffix}`, sid);
|
|
4660
4742
|
},
|
|
4661
4743
|
|
|
4662
4744
|
// SLIM chain status — the chip-quick poll. Hits Core /api/job/:id/status
|
|
@@ -4784,8 +4866,45 @@ const handlers = {
|
|
|
4784
4866
|
return { ok: true, generated_at: new Date().toISOString(), counts, widgets: filtered };
|
|
4785
4867
|
},
|
|
4786
4868
|
|
|
4787
|
-
ateam_test_abort: async ({ solution_id, skill_id, job_id }, sid) =>
|
|
4788
|
-
|
|
4869
|
+
ateam_test_abort: async ({ solution_id, skill_id, job_id, chain_id }, sid) => {
|
|
4870
|
+
// ABORTING THE ROOT DOES NOT ABORT THE RUN. A chain is root + handoffs +
|
|
4871
|
+
// subcalls, each its own job; killing the root leaves the handoff running,
|
|
4872
|
+
// still burning tokens, still writing — while the caller has been told the
|
|
4873
|
+
// test was aborted. So a chain id aborts every job in the chain and REPORTS
|
|
4874
|
+
// each one, rather than quietly doing a fraction of what it claims.
|
|
4875
|
+
if (chain_id && !job_id) {
|
|
4876
|
+
const chain = await get(`/deploy/jobs/${encodeURIComponent(chain_id)}/chain`, sid);
|
|
4877
|
+
const jobs = chain?.chain?.chainJobs || [];
|
|
4878
|
+
if (!jobs.length) {
|
|
4879
|
+
return { ok: false, scope: "chain", chain_id, error: `No jobs found for chain "${chain_id}".`,
|
|
4880
|
+
hint: "The chain may belong to another solution or another actor. ateam_get_execution_logs(chain_id) shows what is visible to you." };
|
|
4881
|
+
}
|
|
4882
|
+
const aborted = [];
|
|
4883
|
+
for (const j of jobs) {
|
|
4884
|
+
const slug = j.skill || skill_id;
|
|
4885
|
+
try {
|
|
4886
|
+
await del(`/deploy/solutions/${solution_id}/skills/${encodeURIComponent(slug)}/test/${encodeURIComponent(j.jobId)}`, sid);
|
|
4887
|
+
aborted.push({ job_id: j.jobId, skill: slug, relation: j.relation, aborted: true });
|
|
4888
|
+
} catch (err) {
|
|
4889
|
+
// A job that was ALREADY finished cannot be aborted — that is not a
|
|
4890
|
+
// failure of the abort, but it must still be visible.
|
|
4891
|
+
aborted.push({ job_id: j.jobId, skill: slug, relation: j.relation, aborted: false, error: err.message });
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
return {
|
|
4895
|
+
ok: aborted.some((a) => a.aborted),
|
|
4896
|
+
scope: "chain",
|
|
4897
|
+
chain_id,
|
|
4898
|
+
job_count: jobs.length,
|
|
4899
|
+
aborted_count: aborted.filter((a) => a.aborted).length,
|
|
4900
|
+
jobs: aborted,
|
|
4901
|
+
};
|
|
4902
|
+
}
|
|
4903
|
+
if (!job_id || !skill_id) {
|
|
4904
|
+
throw new Error("Pass chain_id to abort the whole run, or job_id + skill_id to abort one job.");
|
|
4905
|
+
}
|
|
4906
|
+
return del(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid);
|
|
4907
|
+
},
|
|
4789
4908
|
|
|
4790
4909
|
ateam_get_connector_source: async ({ solution_id, connector_id, path }, sid) => {
|
|
4791
4910
|
const data = await get(`/deploy/solutions/${solution_id}/connectors/${connector_id}/source`, sid);
|
|
@@ -4873,7 +4992,38 @@ const handlers = {
|
|
|
4873
4992
|
};
|
|
4874
4993
|
},
|
|
4875
4994
|
|
|
4876
|
-
ateam_get_metrics: async ({ solution_id, job_id, skill_id }, sid) => {
|
|
4995
|
+
ateam_get_metrics: async ({ solution_id, job_id, chain_id, skill_id }, sid) => {
|
|
4996
|
+
// Same rule as ateam_get_execution_logs: a chain is not a job. Core's
|
|
4997
|
+
// insight is per-job, so a chain is measured by measuring EVERY job in it —
|
|
4998
|
+
// never by silently reporting the root and calling that the chain.
|
|
4999
|
+
if (!job_id && chain_id) {
|
|
5000
|
+
const chain = await get(`/deploy/jobs/${encodeURIComponent(chain_id)}/chain`, sid);
|
|
5001
|
+
const jobs = chain?.chain?.chainJobs || [];
|
|
5002
|
+
const CAP = 10;
|
|
5003
|
+
const measured = jobs.slice(0, CAP);
|
|
5004
|
+
const per_job = [];
|
|
5005
|
+
for (const j of measured) {
|
|
5006
|
+
try {
|
|
5007
|
+
const m = await get(`/deploy/solutions/${solution_id}/metrics?job_id=${encodeURIComponent(j.jobId)}`, sid);
|
|
5008
|
+
per_job.push({ job_id: j.jobId, skill: j.skill, relation: j.relation, depth: j.depth, metrics: m });
|
|
5009
|
+
} catch (err) {
|
|
5010
|
+
// One unreadable job must not hide the rest — say which failed and why.
|
|
5011
|
+
per_job.push({ job_id: j.jobId, skill: j.skill, relation: j.relation, depth: j.depth, error: err.message });
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
return {
|
|
5015
|
+
ok: true,
|
|
5016
|
+
scope: "chain",
|
|
5017
|
+
chain_id,
|
|
5018
|
+
solution_id,
|
|
5019
|
+
job_count: jobs.length,
|
|
5020
|
+
measured: per_job.length,
|
|
5021
|
+
// NO SILENT CAPS: if the chain is bigger than we measured, say so here
|
|
5022
|
+
// rather than let the caller read a partial roll-up as the whole chain.
|
|
5023
|
+
truncated: jobs.length > CAP ? `chain has ${jobs.length} jobs; measured the first ${CAP}` : null,
|
|
5024
|
+
per_job,
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
4877
5027
|
const qs = new URLSearchParams();
|
|
4878
5028
|
if (job_id) qs.set("job_id", job_id);
|
|
4879
5029
|
if (skill_id) qs.set("skill_id", skill_id);
|
|
@@ -5576,6 +5726,10 @@ export async function handleToolCall(name, args, sessionId) {
|
|
|
5576
5726
|
toolName: name,
|
|
5577
5727
|
solutionId: args?.solution_id,
|
|
5578
5728
|
skillId: args?.skill_id,
|
|
5729
|
+
// Remember WHO is acting, so every later per-job read carries it. See the
|
|
5730
|
+
// long note in api.js touchSession: threading actor_id per tool left five of
|
|
5731
|
+
// six job-facing tools unable to express it at all.
|
|
5732
|
+
actorId: args?.actor_id,
|
|
5579
5733
|
});
|
|
5580
5734
|
|
|
5581
5735
|
// Check auth for tenant-aware operations — requires explicit ateam_auth call.
|
|
@@ -5628,6 +5782,15 @@ export async function handleToolCall(name, args, sessionId) {
|
|
|
5628
5782
|
try {
|
|
5629
5783
|
const result = await handler(args, sessionId);
|
|
5630
5784
|
|
|
5785
|
+
// An actor id is BORN here: ateam_conversation/ateam_test_skill mint one and
|
|
5786
|
+
// return it, and the docs tell callers to pass it back for multi-turn. Learn
|
|
5787
|
+
// it on the way out so the follow-up ateam_get_execution_logs /
|
|
5788
|
+
// ateam_get_metrics on that very job is not refused for not knowing who ran
|
|
5789
|
+
// it — the single most common dead end when debugging a run.
|
|
5790
|
+
if (result && typeof result === "object" && result.actor_id) {
|
|
5791
|
+
touchSession(sessionId, { actorId: result.actor_id });
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5631
5794
|
// Stamp WHERE this landed (tenant + app URL) on mutating-tool results, so
|
|
5632
5795
|
// any client — desktop, mobile, cloud agent — can tell the user where to
|
|
5633
5796
|
// see the change. Non-fatal + only for object results that don't already
|