@ateam-ai/mcp 0.4.64 → 0.4.66
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 +23 -1
- package/src/tools.js +211 -17
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,22 @@ 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
|
+
if (actorId) session.context.actorId = String(actorId);
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
/**
|
|
@@ -420,6 +436,7 @@ function headers(sessionId) {
|
|
|
420
436
|
const h = { "Content-Type": "application/json" };
|
|
421
437
|
h["x-adas-token"] = session.masterKey;
|
|
422
438
|
h["X-ADAS-TENANT"] = session.tenant;
|
|
439
|
+
if (session.context?.actorId) h["X-ADAS-ACTOR-ID"] = session.context.actorId;
|
|
423
440
|
return h;
|
|
424
441
|
}
|
|
425
442
|
|
|
@@ -428,6 +445,11 @@ function headers(sessionId) {
|
|
|
428
445
|
const h = { "Content-Type": "application/json" };
|
|
429
446
|
if (tenant) h["X-ADAS-TENANT"] = tenant;
|
|
430
447
|
if (apiKey) h["X-API-KEY"] = apiKey;
|
|
448
|
+
// The acting actor rides with the tenant on EVERY call — see touchSession.
|
|
449
|
+
// The tenant says WHICH account; the actor says WHO, and per-job reads need
|
|
450
|
+
// both. Adds no authority: Core resolves the actor INSIDE the authenticated
|
|
451
|
+
// tenant and 401s if it is not there.
|
|
452
|
+
if (session?.context?.actorId) h["X-ADAS-ACTOR-ID"] = session.context.actorId;
|
|
431
453
|
return h;
|
|
432
454
|
}
|
|
433
455
|
|
package/src/tools.js
CHANGED
|
@@ -1430,6 +1430,14 @@ export const tools = [
|
|
|
1430
1430
|
type: "string",
|
|
1431
1431
|
description: "Optional: get detailed trace for a specific job ID",
|
|
1432
1432
|
},
|
|
1433
|
+
chain_id: {
|
|
1434
|
+
type: "string",
|
|
1435
|
+
description: "The CHAIN id — what ateam_conversation returns and ateam_chain_status takes. Prefer this: it is the id you actually hold. Resolved to the underlying job for you.",
|
|
1436
|
+
},
|
|
1437
|
+
actor_id: {
|
|
1438
|
+
type: "string",
|
|
1439
|
+
description: "The actor whose job this is. REQUIRED for per-job detail: a job belongs to an actor and Core refuses the detail endpoint without one (the list form does not check). Use the same actor_id you passed to ateam_conversation.",
|
|
1440
|
+
},
|
|
1433
1441
|
limit: {
|
|
1434
1442
|
type: "number",
|
|
1435
1443
|
description: "Max jobs to return (default: 10, max: 50)",
|
|
@@ -1446,11 +1454,21 @@ export const tools = [
|
|
|
1446
1454
|
monitoring: { safe: true, cost: "cheap", latency_ms_p95: 500, output: "bounded", poll_interval_s: 2,
|
|
1447
1455
|
note: "safe:false when include_chain:true (that fetches the full tree)." },
|
|
1448
1456
|
description:
|
|
1449
|
-
"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" +
|
|
1450
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.",
|
|
1451
1459
|
inputSchema: {
|
|
1452
1460
|
type: "object",
|
|
1453
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
|
+
},
|
|
1454
1472
|
solution_id: {
|
|
1455
1473
|
type: "string",
|
|
1456
1474
|
description: "The solution ID",
|
|
@@ -1461,7 +1479,7 @@ export const tools = [
|
|
|
1461
1479
|
},
|
|
1462
1480
|
job_id: {
|
|
1463
1481
|
type: "string",
|
|
1464
|
-
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.",
|
|
1465
1483
|
},
|
|
1466
1484
|
include_chain: {
|
|
1467
1485
|
type: "boolean",
|
|
@@ -1469,7 +1487,7 @@ export const tools = [
|
|
|
1469
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).",
|
|
1470
1488
|
},
|
|
1471
1489
|
},
|
|
1472
|
-
required: ["solution_id"
|
|
1490
|
+
required: ["solution_id"],
|
|
1473
1491
|
},
|
|
1474
1492
|
},
|
|
1475
1493
|
{
|
|
@@ -1480,7 +1498,7 @@ export const tools = [
|
|
|
1480
1498
|
// degrades. Use once at the end; poll ateam_chain_status instead.
|
|
1481
1499
|
monitoring: { safe: false, cost: "heavy", output: "grows_with_run", use_instead: "ateam_chain_status" },
|
|
1482
1500
|
description:
|
|
1483
|
-
"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" +
|
|
1484
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" +
|
|
1485
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" +
|
|
1486
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" +
|
|
@@ -1489,16 +1507,26 @@ export const tools = [
|
|
|
1489
1507
|
inputSchema: {
|
|
1490
1508
|
type: "object",
|
|
1491
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
|
+
},
|
|
1492
1520
|
job_id: {
|
|
1493
1521
|
type: "string",
|
|
1494
|
-
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.",
|
|
1495
1523
|
},
|
|
1496
1524
|
skill_slug: {
|
|
1497
1525
|
type: "string",
|
|
1498
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.",
|
|
1499
1527
|
},
|
|
1500
1528
|
},
|
|
1501
|
-
required: [
|
|
1529
|
+
required: [],
|
|
1502
1530
|
},
|
|
1503
1531
|
},
|
|
1504
1532
|
{
|
|
@@ -1522,10 +1550,19 @@ export const tools = [
|
|
|
1522
1550
|
inputSchema: {
|
|
1523
1551
|
type: "object",
|
|
1524
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
|
+
},
|
|
1525
1558
|
chain_id: {
|
|
1526
1559
|
type: "string",
|
|
1527
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.",
|
|
1528
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
|
+
},
|
|
1529
1566
|
},
|
|
1530
1567
|
required: ["chain_id"],
|
|
1531
1568
|
},
|
|
@@ -1573,10 +1610,20 @@ export const tools = [
|
|
|
1573
1610
|
name: "ateam_test_abort",
|
|
1574
1611
|
core: true,
|
|
1575
1612
|
description:
|
|
1576
|
-
"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.)",
|
|
1577
1614
|
inputSchema: {
|
|
1578
1615
|
type: "object",
|
|
1579
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
|
+
},
|
|
1580
1627
|
solution_id: {
|
|
1581
1628
|
type: "string",
|
|
1582
1629
|
description: "The solution ID",
|
|
@@ -1587,10 +1634,10 @@ export const tools = [
|
|
|
1587
1634
|
},
|
|
1588
1635
|
job_id: {
|
|
1589
1636
|
type: "string",
|
|
1590
|
-
description: "
|
|
1637
|
+
description: "Abort ONE job only. Prefer chain_id: aborting the root leaves handoffs running while reporting the test aborted.",
|
|
1591
1638
|
},
|
|
1592
1639
|
},
|
|
1593
|
-
required: ["solution_id"
|
|
1640
|
+
required: ["solution_id"],
|
|
1594
1641
|
},
|
|
1595
1642
|
},
|
|
1596
1643
|
{
|
|
@@ -1657,6 +1704,11 @@ export const tools = [
|
|
|
1657
1704
|
inputSchema: {
|
|
1658
1705
|
type: "object",
|
|
1659
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
|
+
},
|
|
1660
1712
|
solution_id: {
|
|
1661
1713
|
type: "string",
|
|
1662
1714
|
description: "The solution ID",
|
|
@@ -1665,6 +1717,11 @@ export const tools = [
|
|
|
1665
1717
|
type: "string",
|
|
1666
1718
|
description: "Optional: deep analysis for a specific job",
|
|
1667
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
|
+
},
|
|
1668
1725
|
skill_id: {
|
|
1669
1726
|
type: "string",
|
|
1670
1727
|
description: "Optional: recent metrics for a specific skill",
|
|
@@ -2824,6 +2881,7 @@ module.exports.default = plugin;
|
|
|
2824
2881
|
return files;
|
|
2825
2882
|
}
|
|
2826
2883
|
|
|
2884
|
+
|
|
2827
2885
|
const handlers = {
|
|
2828
2886
|
ateam_bootstrap: async () => ({
|
|
2829
2887
|
runtime: {
|
|
@@ -4324,10 +4382,43 @@ const handlers = {
|
|
|
4324
4382
|
|
|
4325
4383
|
// ─── Developer Tools ────────────────────────────────────────────
|
|
4326
4384
|
|
|
4327
|
-
ateam_get_execution_logs: async ({ solution_id, skill_id, job_id, limit }, sid) => {
|
|
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
|
+
};
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4328
4416
|
const qs = new URLSearchParams();
|
|
4329
4417
|
if (skill_id) qs.set("skill_id", skill_id);
|
|
4330
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.
|
|
4331
4422
|
if (limit) qs.set("limit", String(limit));
|
|
4332
4423
|
const qsStr = qs.toString() ? `?${qs}` : "";
|
|
4333
4424
|
return get(`/deploy/solutions/${solution_id}/logs${qsStr}`, sid);
|
|
@@ -4597,7 +4688,23 @@ const handlers = {
|
|
|
4597
4688
|
return post(`/deploy/voice-test`, body, sid, { timeoutMs: timeoutTotal });
|
|
4598
4689
|
},
|
|
4599
4690
|
|
|
4600
|
-
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
|
+
}
|
|
4601
4708
|
// Existing single-job snapshot via Builder (unchanged shape for back-compat).
|
|
4602
4709
|
const single = await get(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid);
|
|
4603
4710
|
if (!include_chain) return single;
|
|
@@ -4615,8 +4722,14 @@ const handlers = {
|
|
|
4615
4722
|
return { ...single, chain };
|
|
4616
4723
|
},
|
|
4617
4724
|
|
|
4618
|
-
ateam_get_chain: async ({ job_id, skill_slug }, sid) => {
|
|
4619
|
-
|
|
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)");
|
|
4620
4733
|
const creds = getCredentials(sid);
|
|
4621
4734
|
const apiKey = creds?.apiKey;
|
|
4622
4735
|
if (!apiKey) throw new Error("No api_key in session — call ateam_auth(api_key) first.");
|
|
@@ -4625,7 +4738,7 @@ const handlers = {
|
|
|
4625
4738
|
const qs = new URLSearchParams();
|
|
4626
4739
|
if (skill_slug) qs.set("skillSlug", skill_slug);
|
|
4627
4740
|
const suffix = qs.toString() ? `?${qs}` : "";
|
|
4628
|
-
return await get(`/deploy/jobs/${encodeURIComponent(
|
|
4741
|
+
return await get(`/deploy/jobs/${encodeURIComponent(id)}/chain${suffix}`, sid);
|
|
4629
4742
|
},
|
|
4630
4743
|
|
|
4631
4744
|
// SLIM chain status — the chip-quick poll. Hits Core /api/job/:id/status
|
|
@@ -4753,8 +4866,45 @@ const handlers = {
|
|
|
4753
4866
|
return { ok: true, generated_at: new Date().toISOString(), counts, widgets: filtered };
|
|
4754
4867
|
},
|
|
4755
4868
|
|
|
4756
|
-
ateam_test_abort: async ({ solution_id, skill_id, job_id }, sid) =>
|
|
4757
|
-
|
|
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
|
+
},
|
|
4758
4908
|
|
|
4759
4909
|
ateam_get_connector_source: async ({ solution_id, connector_id, path }, sid) => {
|
|
4760
4910
|
const data = await get(`/deploy/solutions/${solution_id}/connectors/${connector_id}/source`, sid);
|
|
@@ -4842,7 +4992,38 @@ const handlers = {
|
|
|
4842
4992
|
};
|
|
4843
4993
|
},
|
|
4844
4994
|
|
|
4845
|
-
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
|
+
}
|
|
4846
5027
|
const qs = new URLSearchParams();
|
|
4847
5028
|
if (job_id) qs.set("job_id", job_id);
|
|
4848
5029
|
if (skill_id) qs.set("skill_id", skill_id);
|
|
@@ -5545,6 +5726,10 @@ export async function handleToolCall(name, args, sessionId) {
|
|
|
5545
5726
|
toolName: name,
|
|
5546
5727
|
solutionId: args?.solution_id,
|
|
5547
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,
|
|
5548
5733
|
});
|
|
5549
5734
|
|
|
5550
5735
|
// Check auth for tenant-aware operations — requires explicit ateam_auth call.
|
|
@@ -5597,6 +5782,15 @@ export async function handleToolCall(name, args, sessionId) {
|
|
|
5597
5782
|
try {
|
|
5598
5783
|
const result = await handler(args, sessionId);
|
|
5599
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
|
+
|
|
5600
5794
|
// Stamp WHERE this landed (tenant + app URL) on mutating-tool results, so
|
|
5601
5795
|
// any client — desktop, mobile, cloud agent — can tell the user where to
|
|
5602
5796
|
// see the change. Non-fatal + only for object results that don't already
|