@axiom-lattice/core 7.0.3 → 8.0.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/dist/index.d.mts +158 -3
- package/dist/index.d.ts +158 -3
- package/dist/index.js +655 -92
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +652 -92
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -7016,7 +7016,13 @@ If the Worker has task capability, it may truthfully update the same task.
|
|
|
7016
7016
|
Otherwise, the owning caller interprets the returned observation.
|
|
7017
7017
|
|
|
7018
7018
|
An execution result is an observation, not automatic proof of business
|
|
7019
|
-
completion. \`task\` returns its exact \`executionResultId
|
|
7019
|
+
completion. \`task\` returns its exact \`executionResultId\` and the task's
|
|
7020
|
+
\`taskStatus\` (re-read **after** the worker settled). **Check \`taskStatus\`
|
|
7021
|
+
before completing:** if it is already \`completed\` the task is done \u2014 do NOT
|
|
7022
|
+
complete it again (use \`add_activity\` for extra notes); if it is still
|
|
7023
|
+
\`in_progress\` you are the single completer and may \`set_status completed\`;
|
|
7024
|
+
if it is \`failed\` or \`interrupted\`, act on that (retry / resume / new task)
|
|
7025
|
+
instead. After \`task\`
|
|
7020
7026
|
returns, inspect the observation, then pass that \`executionResultId\` to the
|
|
7021
7027
|
meaningful \`manage_task set_status\` (status transition), \`manage_task
|
|
7022
7028
|
update\` (content fields), or \`manage_task add_activity\` that interprets
|
|
@@ -7060,7 +7066,11 @@ handled by the configured HITL lifecycle; \`review_required\` is not a task stat
|
|
|
7060
7066
|
Create the active parent and each started subtask as records: \`manage_task
|
|
7061
7067
|
create\` always persists \`pending\`. **Process the task you were assigned in this
|
|
7062
7068
|
run: set it \`in_progress\` with \`manage_task set_status\` when you start (a
|
|
7063
|
-
dispatched task is claimed automatically by the engine).**
|
|
7069
|
+
dispatched task is claimed automatically by the engine).** \`in_progress\` means a
|
|
7070
|
+
run is executing that task: set it yourself ONLY for the task THIS run executes,
|
|
7071
|
+
and for a task you delegate leave it \`pending\` \u2014 \`task\` dispatch claims it.
|
|
7072
|
+
Complete a task exactly once: if it is already \`completed\`, do NOT complete it
|
|
7073
|
+
again (the terminal state stands; use \`add_activity\` for extra notes). To bring
|
|
7064
7074
|
in a more suitable expert, create a subtask (\`pending\`) and dispatch THAT with
|
|
7065
7075
|
\`task { taskId, agentId }\` (a specialist, or agentId "general-purpose" to run your
|
|
7066
7076
|
own capabilities in an isolated run). **Never re-dispatch the task you are
|
|
@@ -8626,7 +8636,9 @@ criteria are truly met \u2014 never as a workaround.
|
|
|
8626
8636
|
\`manage_task create(title: <goal>, description: <contract + Belief State>, ownerType: "agent")\`
|
|
8627
8637
|
\`create\` always yields a \`pending\` record \u2014 it never starts execution. Record
|
|
8628
8638
|
the returned parent task id, then **process it in this run**, setting it
|
|
8629
|
-
\`in_progress\` with \`manage_task set_status\` when you start.
|
|
8639
|
+
\`in_progress\` with \`manage_task set_status\` when you start. Set \`in_progress\`
|
|
8640
|
+
only for a task THIS run executes; a task you delegate stays \`pending\` and is
|
|
8641
|
+
claimed by \`task\` dispatch. To decompose, create each subtask
|
|
8630
8642
|
with \`manage_task create\` (\`pending\`) and delegate it with \`task { taskId,
|
|
8631
8643
|
agentId }\` to the agent that should execute it (a specialist, or
|
|
8632
8644
|
"general-purpose" for your own capabilities). The executor does not need
|
|
@@ -8659,10 +8671,13 @@ ${TASK_BELIEF_REFERENCE_EXAMPLE}
|
|
|
8659
8671
|
## Status discipline \u2014 MANDATORY
|
|
8660
8672
|
|
|
8661
8673
|
- Execution ownership: process the task you were assigned in this run; set it
|
|
8662
|
-
\`in_progress\` with \`manage_task set_status\` when you start.
|
|
8674
|
+
\`in_progress\` with \`manage_task set_status\` when you start. Set it only for a
|
|
8675
|
+
task THIS run executes; a task you delegate stays \`pending\`. To involve a more
|
|
8663
8676
|
suitable expert, create a subtask and dispatch it with \`task { taskId, agentId }\`
|
|
8664
8677
|
\u2014 never re-dispatch the task you are processing. Report the outcome with
|
|
8665
8678
|
\`set_status completed\` (result + beliefImpact), \`failed\`, or \`interrupted\`.
|
|
8679
|
+
Complete a task exactly once: if it is already \`completed\`, do not complete it
|
|
8680
|
+
again \u2014 use \`add_activity\` for extra notes.
|
|
8666
8681
|
- All status transitions go through \`manage_task set_status\` (with
|
|
8667
8682
|
\`result\` on completed, \`failureReason\` on failed). \`update\` is for
|
|
8668
8683
|
content fields only and rejects a status field.
|
|
@@ -19345,6 +19360,7 @@ async function runDiscoverHandler(executor, manifest, handler, config, tenantId2
|
|
|
19345
19360
|
var PLUGINS_ROOT2 = "/root/.agents/plugins";
|
|
19346
19361
|
var PLUGIN_TYPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
19347
19362
|
var DEFAULT_HASH_TTL_MS = 3e4;
|
|
19363
|
+
var BACKUP_DIR_NAME = ".backup";
|
|
19348
19364
|
function pluginTypeFromPath(dirPath) {
|
|
19349
19365
|
return dirPath.slice(dirPath.lastIndexOf("/") + 1);
|
|
19350
19366
|
}
|
|
@@ -19356,6 +19372,10 @@ function topLevelPluginDirs(entries) {
|
|
|
19356
19372
|
return remainder.length > 0 && !remainder.includes("/");
|
|
19357
19373
|
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
19358
19374
|
}
|
|
19375
|
+
function isUnderBackup(path8, pluginDir) {
|
|
19376
|
+
const backupDir = `${pluginDir}/${BACKUP_DIR_NAME}`;
|
|
19377
|
+
return path8 === backupDir || path8.startsWith(`${backupDir}/`);
|
|
19378
|
+
}
|
|
19359
19379
|
var TenantPluginCatalog = class {
|
|
19360
19380
|
/**
|
|
19361
19381
|
* @param deps - Sandbox resolver, executor, reserved types, and cache/logger seams
|
|
@@ -19391,6 +19411,177 @@ var TenantPluginCatalog = class {
|
|
|
19391
19411
|
const snapshot = await this.ensureLoaded(tenantId2);
|
|
19392
19412
|
return snapshot.manifests[type];
|
|
19393
19413
|
}
|
|
19414
|
+
/**
|
|
19415
|
+
* Validate a single plugin by reading its files from the sandbox volume.
|
|
19416
|
+
*
|
|
19417
|
+
* Dry-run counterpart to {@link reload}: it lists the plugin directory, reads
|
|
19418
|
+
* `plugin.json`, and runs the same manifest validation the catalog uses when
|
|
19419
|
+
* loading, with handler existence checked against the files actually present
|
|
19420
|
+
* on disk. It never touches the cache and never publishes anything, so a
|
|
19421
|
+
* caller can validate an on-disk plugin without making it live.
|
|
19422
|
+
*
|
|
19423
|
+
* @param tenantId - Tenant that owns the plugin
|
|
19424
|
+
* @param type - Plugin type (directory name); must match the kebab-case type pattern
|
|
19425
|
+
* @returns The structured `diagnostics` and whether the manifest passed fatal
|
|
19426
|
+
* validation (`valid`). A non-kebab-case `type`, an unreadable directory, or
|
|
19427
|
+
* a missing/invalid `plugin.json` yields a diagnostic with `valid: false`.
|
|
19428
|
+
*/
|
|
19429
|
+
async validatePluginFromDisk(tenantId2, type) {
|
|
19430
|
+
if (typeof type !== "string" || !PLUGIN_TYPE_RE.test(type)) {
|
|
19431
|
+
return {
|
|
19432
|
+
diagnostics: [
|
|
19433
|
+
{
|
|
19434
|
+
level: "error",
|
|
19435
|
+
code: "TYPE_INVALID",
|
|
19436
|
+
pluginType: type,
|
|
19437
|
+
message: `type "${type}" must match ${PLUGIN_TYPE_RE}`
|
|
19438
|
+
}
|
|
19439
|
+
],
|
|
19440
|
+
valid: false
|
|
19441
|
+
};
|
|
19442
|
+
}
|
|
19443
|
+
const pluginDir = `${PLUGINS_ROOT2}/${type}`;
|
|
19444
|
+
const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
|
|
19445
|
+
let listing;
|
|
19446
|
+
try {
|
|
19447
|
+
listing = await sandbox.file.listPath(pluginDir, { recursive: true });
|
|
19448
|
+
} catch (err) {
|
|
19449
|
+
return {
|
|
19450
|
+
diagnostics: [
|
|
19451
|
+
{
|
|
19452
|
+
level: "error",
|
|
19453
|
+
code: "PLUGINS_LIST_FAILED",
|
|
19454
|
+
pluginType: type,
|
|
19455
|
+
message: err instanceof Error ? err.message : String(err)
|
|
19456
|
+
}
|
|
19457
|
+
],
|
|
19458
|
+
valid: false
|
|
19459
|
+
};
|
|
19460
|
+
}
|
|
19461
|
+
let raw;
|
|
19462
|
+
let manifestJson;
|
|
19463
|
+
try {
|
|
19464
|
+
const { content } = await sandbox.file.readFile(`${pluginDir}/plugin.json`);
|
|
19465
|
+
raw = JSON.parse(content);
|
|
19466
|
+
manifestJson = JSON.stringify(raw);
|
|
19467
|
+
} catch (err) {
|
|
19468
|
+
return {
|
|
19469
|
+
diagnostics: [
|
|
19470
|
+
{
|
|
19471
|
+
level: "error",
|
|
19472
|
+
code: "MANIFEST_READ",
|
|
19473
|
+
pluginType: type,
|
|
19474
|
+
message: err instanceof Error ? err.message : String(err)
|
|
19475
|
+
}
|
|
19476
|
+
],
|
|
19477
|
+
valid: false
|
|
19478
|
+
};
|
|
19479
|
+
}
|
|
19480
|
+
const files = listing.files ?? [];
|
|
19481
|
+
const handlerExists = (rel) => files.some((f) => !f.is_dir && f.path === `${pluginDir}/${rel}`);
|
|
19482
|
+
const knownTypes = new Set(
|
|
19483
|
+
Object.keys(this.cache.get(tenantId2)?.value.manifests ?? {}).filter((t) => t !== type)
|
|
19484
|
+
);
|
|
19485
|
+
const result = validateSandboxPluginManifest({
|
|
19486
|
+
raw,
|
|
19487
|
+
dirName: type,
|
|
19488
|
+
reservedTypes: this.deps.reservedTypes(),
|
|
19489
|
+
knownTypes,
|
|
19490
|
+
handlerExists,
|
|
19491
|
+
rawSizeBytes: Buffer.byteLength(manifestJson, "utf8")
|
|
19492
|
+
});
|
|
19493
|
+
return { diagnostics: result.diagnostics, valid: Boolean(result.manifest) };
|
|
19494
|
+
}
|
|
19495
|
+
/**
|
|
19496
|
+
* Snapshot a plugin's current files into its in-directory backup.
|
|
19497
|
+
*
|
|
19498
|
+
* Keeps a single backup (`<pluginDir>/.backup/`): the previous backup is
|
|
19499
|
+
* replaced. Files already under `.backup/` are excluded, so a backup never
|
|
19500
|
+
* nests. Manual action — it is not triggered by {@link writePlugin}.
|
|
19501
|
+
*
|
|
19502
|
+
* @param tenantId - Tenant that owns the plugin
|
|
19503
|
+
* @param type - Plugin type (directory name)
|
|
19504
|
+
* @returns The number of files snapshotted (`0` when the plugin has no files)
|
|
19505
|
+
*/
|
|
19506
|
+
async backupPlugin(tenantId2, type) {
|
|
19507
|
+
this.assertSafeType(type);
|
|
19508
|
+
const pluginDir = `${PLUGINS_ROOT2}/${type}`;
|
|
19509
|
+
const backupDir = `${pluginDir}/${BACKUP_DIR_NAME}`;
|
|
19510
|
+
const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
|
|
19511
|
+
const entries = (await sandbox.file.listPath(pluginDir, { recursive: true })).files ?? [];
|
|
19512
|
+
const files = entries.filter((entry) => !entry.is_dir && !isUnderBackup(entry.path, pluginDir));
|
|
19513
|
+
if (files.length === 0) {
|
|
19514
|
+
return { files: 0 };
|
|
19515
|
+
}
|
|
19516
|
+
if (entries.some((entry) => isUnderBackup(entry.path, pluginDir))) {
|
|
19517
|
+
await sandbox.file.deletePath(backupDir);
|
|
19518
|
+
}
|
|
19519
|
+
let count = 0;
|
|
19520
|
+
for (const entry of files) {
|
|
19521
|
+
const rel = entry.path.slice(pluginDir.length + 1);
|
|
19522
|
+
const { content } = await sandbox.file.readFile(entry.path);
|
|
19523
|
+
await this.writeRelativeFile(sandbox, backupDir, rel, content);
|
|
19524
|
+
count += 1;
|
|
19525
|
+
}
|
|
19526
|
+
return { files: count };
|
|
19527
|
+
}
|
|
19528
|
+
/**
|
|
19529
|
+
* Restore a plugin's files from its in-directory backup.
|
|
19530
|
+
*
|
|
19531
|
+
* The plugin directory (excluding `.backup/`) is made to match the backup:
|
|
19532
|
+
* files present in the backup are (re)written and files absent from it are
|
|
19533
|
+
* removed. Callers should {@link reload} afterward so the restored shape is
|
|
19534
|
+
* picked up.
|
|
19535
|
+
*
|
|
19536
|
+
* @param tenantId - Tenant that owns the plugin
|
|
19537
|
+
* @param type - Plugin type (directory name)
|
|
19538
|
+
* @returns The number of files restored
|
|
19539
|
+
* @throws Error when the plugin has no backup
|
|
19540
|
+
*/
|
|
19541
|
+
async restorePlugin(tenantId2, type) {
|
|
19542
|
+
this.assertSafeType(type);
|
|
19543
|
+
const pluginDir = `${PLUGINS_ROOT2}/${type}`;
|
|
19544
|
+
const backupDir = `${pluginDir}/${BACKUP_DIR_NAME}`;
|
|
19545
|
+
const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
|
|
19546
|
+
const backupFiles = await (async () => {
|
|
19547
|
+
try {
|
|
19548
|
+
return ((await sandbox.file.listPath(backupDir, { recursive: true })).files ?? []).filter((entry) => !entry.is_dir);
|
|
19549
|
+
} catch {
|
|
19550
|
+
return [];
|
|
19551
|
+
}
|
|
19552
|
+
})();
|
|
19553
|
+
if (backupFiles.length === 0) {
|
|
19554
|
+
throw new Error(`no backup found for plugin "${type}"`);
|
|
19555
|
+
}
|
|
19556
|
+
const backupRels = new Set(backupFiles.map((entry) => entry.path.slice(backupDir.length + 1)));
|
|
19557
|
+
const currentFiles = ((await sandbox.file.listPath(pluginDir, { recursive: true })).files ?? []).filter((entry) => !entry.is_dir && !isUnderBackup(entry.path, pluginDir));
|
|
19558
|
+
for (const entry of currentFiles) {
|
|
19559
|
+
const rel = entry.path.slice(pluginDir.length + 1);
|
|
19560
|
+
if (!backupRels.has(rel)) await sandbox.file.deletePath(entry.path);
|
|
19561
|
+
}
|
|
19562
|
+
let count = 0;
|
|
19563
|
+
for (const entry of backupFiles) {
|
|
19564
|
+
const rel = entry.path.slice(backupDir.length + 1);
|
|
19565
|
+
const { content } = await sandbox.file.readFile(entry.path);
|
|
19566
|
+
await this.writeRelativeFile(sandbox, pluginDir, rel, content);
|
|
19567
|
+
count += 1;
|
|
19568
|
+
}
|
|
19569
|
+
return { files: count };
|
|
19570
|
+
}
|
|
19571
|
+
/**
|
|
19572
|
+
* Write a file under a base directory, creating its parent directory first.
|
|
19573
|
+
*
|
|
19574
|
+
* @param sandbox - Sandbox whose file service receives the write
|
|
19575
|
+
* @param baseDir - Absolute target directory (a plugin or backup directory)
|
|
19576
|
+
* @param rel - Safe path relative to `baseDir`
|
|
19577
|
+
* @param content - File contents
|
|
19578
|
+
* @returns Nothing once the file is written
|
|
19579
|
+
*/
|
|
19580
|
+
async writeRelativeFile(sandbox, baseDir, rel, content) {
|
|
19581
|
+
const slash = rel.lastIndexOf("/");
|
|
19582
|
+
if (slash !== -1) await sandbox.file.createDirectory(`${baseDir}/${rel.slice(0, slash)}`);
|
|
19583
|
+
await sandbox.file.writeFile(`${baseDir}/${rel}`, content);
|
|
19584
|
+
}
|
|
19394
19585
|
/**
|
|
19395
19586
|
* Drop the cached snapshot for a tenant.
|
|
19396
19587
|
*
|
|
@@ -19467,6 +19658,30 @@ var TenantPluginCatalog = class {
|
|
|
19467
19658
|
const { content } = await sandbox.file.readFile(`${PLUGINS_ROOT2}/${type}/${relPath}`);
|
|
19468
19659
|
return content;
|
|
19469
19660
|
}
|
|
19661
|
+
/**
|
|
19662
|
+
* List the relative file paths currently present under a plugin directory.
|
|
19663
|
+
*
|
|
19664
|
+
* Used to decide whether a plugin already exists before scaffolding, and to
|
|
19665
|
+
* surface a plugin's files without reading their contents.
|
|
19666
|
+
*
|
|
19667
|
+
* @param tenantId - Tenant that owns the plugin
|
|
19668
|
+
* @param type - Plugin type (directory name); must match the kebab-case type pattern
|
|
19669
|
+
* @returns Relative paths (e.g. `plugin.json`, `handlers/x.js`); empty when the
|
|
19670
|
+
* directory is missing or the listing fails
|
|
19671
|
+
*/
|
|
19672
|
+
async listPluginFiles(tenantId2, type) {
|
|
19673
|
+
this.assertSafeType(type);
|
|
19674
|
+
const sandbox = await this.deps.resolveSandbox({ tenantId: tenantId2 });
|
|
19675
|
+
const pluginDir = `${PLUGINS_ROOT2}/${type}`;
|
|
19676
|
+
let listing;
|
|
19677
|
+
try {
|
|
19678
|
+
listing = await sandbox.file.listPath(pluginDir, { recursive: true });
|
|
19679
|
+
} catch {
|
|
19680
|
+
return [];
|
|
19681
|
+
}
|
|
19682
|
+
const prefix = `${pluginDir}/`;
|
|
19683
|
+
return listing.files.filter((entry) => !entry.is_dir).map((entry) => entry.path.startsWith(prefix) ? entry.path.slice(prefix.length) : entry.path);
|
|
19684
|
+
}
|
|
19470
19685
|
/**
|
|
19471
19686
|
* Write a plugin manifest and handler files into the tenant volume.
|
|
19472
19687
|
*
|
|
@@ -19491,11 +19706,7 @@ var TenantPluginCatalog = class {
|
|
|
19491
19706
|
await sandbox.file.createDirectory(dir);
|
|
19492
19707
|
await sandbox.file.writeFile(`${dir}/plugin.json`, JSON.stringify(manifest, null, 2));
|
|
19493
19708
|
for (const [rel, content] of Object.entries(handlers)) {
|
|
19494
|
-
|
|
19495
|
-
if (slash !== -1) {
|
|
19496
|
-
await sandbox.file.createDirectory(`${dir}/${rel.slice(0, slash)}`);
|
|
19497
|
-
}
|
|
19498
|
-
await sandbox.file.writeFile(`${dir}/${rel}`, content);
|
|
19709
|
+
await this.writeRelativeFile(sandbox, dir, rel, content);
|
|
19499
19710
|
}
|
|
19500
19711
|
}
|
|
19501
19712
|
/**
|
|
@@ -19600,7 +19811,7 @@ var TenantPluginCatalog = class {
|
|
|
19600
19811
|
const dirFiles = entries.filter(
|
|
19601
19812
|
(entry) => entry.path === dir.path || entry.path.startsWith(`${dir.path}/`)
|
|
19602
19813
|
);
|
|
19603
|
-
const relPaths = dirFiles.filter((entry) => !entry.is_dir && entry.path !== `${dir.path}/plugin.json`).map((entry) => entry.path.slice(dir.path.length + 1)).sort();
|
|
19814
|
+
const relPaths = dirFiles.filter((entry) => !entry.is_dir && entry.path !== `${dir.path}/plugin.json` && !isUnderBackup(entry.path, dir.path)).map((entry) => entry.path.slice(dir.path.length + 1)).sort();
|
|
19604
19815
|
const manifestPath = `${dir.path}/plugin.json`;
|
|
19605
19816
|
let raw;
|
|
19606
19817
|
let manifestJson;
|
|
@@ -19631,7 +19842,7 @@ var TenantPluginCatalog = class {
|
|
|
19631
19842
|
const manifests = {};
|
|
19632
19843
|
for (const candidate of candidates) {
|
|
19633
19844
|
try {
|
|
19634
|
-
const handlerExists = (rel) => candidate.files.some((f) => f.path === `${candidate.dirPath}/${rel}`);
|
|
19845
|
+
const handlerExists = (rel) => candidate.files.some((f) => !f.is_dir && f.path === `${candidate.dirPath}/${rel}`);
|
|
19635
19846
|
const result = validateSandboxPluginManifest({
|
|
19636
19847
|
raw: candidate.raw,
|
|
19637
19848
|
dirName: candidate.type,
|
|
@@ -31102,6 +31313,12 @@ var _AgentLatticeManager = class _AgentLatticeManager extends BaseLatticeManager
|
|
|
31102
31313
|
* {@link buildingKeys}.
|
|
31103
31314
|
*/
|
|
31104
31315
|
this.buildContext = new AsyncLocalStorage();
|
|
31316
|
+
/**
|
|
31317
|
+
* Store-backed assistant ids previously reconciled per tenant. Used to
|
|
31318
|
+
* unregister assistants that were deleted on another replica without
|
|
31319
|
+
* touching built-in / plugin agents (which are re-ensured on every reload).
|
|
31320
|
+
*/
|
|
31321
|
+
this.storedAssistantKeys = /* @__PURE__ */ new Map();
|
|
31105
31322
|
this.subscribeToAssistantEvents();
|
|
31106
31323
|
}
|
|
31107
31324
|
/**
|
|
@@ -31330,11 +31547,29 @@ var _AgentLatticeManager = class _AgentLatticeManager extends BaseLatticeManager
|
|
|
31330
31547
|
try {
|
|
31331
31548
|
const storeLattice = getStoreLattice("default", "assistant");
|
|
31332
31549
|
const assistants = await storeLattice.store.getAllAssistants(tenantId2);
|
|
31550
|
+
const currentIds = /* @__PURE__ */ new Set();
|
|
31333
31551
|
for (const assistant of assistants) {
|
|
31334
|
-
|
|
31552
|
+
currentIds.add(assistant.id);
|
|
31335
31553
|
const config = assistantToConfig(assistant);
|
|
31336
|
-
this.
|
|
31554
|
+
const existing = this.getAgentConfigWithTenant(tenantId2, assistant.id);
|
|
31555
|
+
if (existing === void 0) {
|
|
31556
|
+
this.registerLatticeWithTenant(tenantId2, config);
|
|
31557
|
+
continue;
|
|
31558
|
+
}
|
|
31559
|
+
if (JSON.stringify(existing) !== JSON.stringify(config)) {
|
|
31560
|
+
this.removeWithTenant(tenantId2, assistant.id);
|
|
31561
|
+
this.registerLatticeWithTenant(tenantId2, config);
|
|
31562
|
+
}
|
|
31563
|
+
}
|
|
31564
|
+
const previousIds = this.storedAssistantKeys.get(tenantId2);
|
|
31565
|
+
if (previousIds) {
|
|
31566
|
+
for (const id of previousIds) {
|
|
31567
|
+
if (!currentIds.has(id)) {
|
|
31568
|
+
this.removeWithTenant(tenantId2, id);
|
|
31569
|
+
}
|
|
31570
|
+
}
|
|
31337
31571
|
}
|
|
31572
|
+
this.storedAssistantKeys.set(tenantId2, currentIds);
|
|
31338
31573
|
} catch (error) {
|
|
31339
31574
|
console.error(`Failed to initialize stored assistants for tenant ${tenantId2}:`, error);
|
|
31340
31575
|
}
|
|
@@ -38375,7 +38610,11 @@ var TaskLifecycleService = class {
|
|
|
38375
38610
|
expectedStatuses: ["completed", "cancelled"],
|
|
38376
38611
|
updates: { status: "in_progress", failureReason: null, result: null },
|
|
38377
38612
|
summary: "Task reopened",
|
|
38378
|
-
detail: { type: "reopen" }
|
|
38613
|
+
detail: { type: "reopen" },
|
|
38614
|
+
// Preserve an existing thread by default (the project runtime reuses the
|
|
38615
|
+
// canonical Task Thread); a dispatch that allocated a fresh run thread
|
|
38616
|
+
// opts in to replacing it.
|
|
38617
|
+
replaceThread: input.replaceThread ?? false
|
|
38379
38618
|
});
|
|
38380
38619
|
}
|
|
38381
38620
|
/**
|
|
@@ -40236,6 +40475,39 @@ function createTaskRunBridge(deps) {
|
|
|
40236
40475
|
transitioned: true
|
|
40237
40476
|
};
|
|
40238
40477
|
}
|
|
40478
|
+
if (task.status === "completed" || task.status === "cancelled") {
|
|
40479
|
+
if (task.ownerType !== "agent") {
|
|
40480
|
+
return {
|
|
40481
|
+
success: false,
|
|
40482
|
+
code: "AGENT_TASK_REQUIRED",
|
|
40483
|
+
error: "Lifecycle transitions only support agent tasks."
|
|
40484
|
+
};
|
|
40485
|
+
}
|
|
40486
|
+
const reopened = await deps.lifecycle.reopenTask({
|
|
40487
|
+
tenantId: tenantId2,
|
|
40488
|
+
taskId: task.id,
|
|
40489
|
+
actor,
|
|
40490
|
+
...threadId !== void 0 ? { threadId } : {},
|
|
40491
|
+
// The caller allocated a fresh run thread for this dispatch.
|
|
40492
|
+
replaceThread: true
|
|
40493
|
+
});
|
|
40494
|
+
if (!reopened.success) {
|
|
40495
|
+
return {
|
|
40496
|
+
success: false,
|
|
40497
|
+
code: reopened.code,
|
|
40498
|
+
error: reopened.error,
|
|
40499
|
+
...reopened.hint !== void 0 ? { hint: reopened.hint } : {},
|
|
40500
|
+
...reopened.retryable !== void 0 ? { retryable: reopened.retryable } : {}
|
|
40501
|
+
};
|
|
40502
|
+
}
|
|
40503
|
+
return {
|
|
40504
|
+
success: true,
|
|
40505
|
+
task: reopened.task,
|
|
40506
|
+
warnings: reopened.warnings ?? [],
|
|
40507
|
+
mutated: true,
|
|
40508
|
+
transitioned: true
|
|
40509
|
+
};
|
|
40510
|
+
}
|
|
40239
40511
|
return { success: true, task, warnings, mutated: false, transitioned: false };
|
|
40240
40512
|
},
|
|
40241
40513
|
async settleRun(input) {
|
|
@@ -40408,6 +40680,45 @@ async function tryAcquireTaskRun(input) {
|
|
|
40408
40680
|
};
|
|
40409
40681
|
}
|
|
40410
40682
|
|
|
40683
|
+
// src/services/taskLiveness.ts
|
|
40684
|
+
function registeredLeaseStore() {
|
|
40685
|
+
try {
|
|
40686
|
+
return storeLatticeManager.getStoreLattice("default", "lease").store;
|
|
40687
|
+
} catch {
|
|
40688
|
+
return void 0;
|
|
40689
|
+
}
|
|
40690
|
+
}
|
|
40691
|
+
async function isTaskRunLeaseHeld(task) {
|
|
40692
|
+
const store = registeredLeaseStore();
|
|
40693
|
+
if (!store) return false;
|
|
40694
|
+
try {
|
|
40695
|
+
if (await store.get(taskRunLeaseKey(task.id))) return true;
|
|
40696
|
+
const threadId = typeof task.context?.thread_id === "string" ? task.context.thread_id : void 0;
|
|
40697
|
+
if (threadId && task.ownerId && await store.get(threadRunLeaseKey(task.tenantId, task.ownerId, threadId))) {
|
|
40698
|
+
return true;
|
|
40699
|
+
}
|
|
40700
|
+
return false;
|
|
40701
|
+
} catch {
|
|
40702
|
+
return true;
|
|
40703
|
+
}
|
|
40704
|
+
}
|
|
40705
|
+
function createTaskRunLiveness() {
|
|
40706
|
+
return async (task) => taskRunRegistry.isLive(task.id) || isTaskRunLeaseHeld(task);
|
|
40707
|
+
}
|
|
40708
|
+
function isMultiReplicaDeclared() {
|
|
40709
|
+
const id = process.env.AXIOM_REPLICA_ID;
|
|
40710
|
+
return typeof id === "string" && id.trim() !== "";
|
|
40711
|
+
}
|
|
40712
|
+
function isSharedLeaseStore() {
|
|
40713
|
+
const store = registeredLeaseStore();
|
|
40714
|
+
const name = store?.constructor?.name;
|
|
40715
|
+
return typeof name === "string" && !name.startsWith("InMemory");
|
|
40716
|
+
}
|
|
40717
|
+
function canConvergeOrphan() {
|
|
40718
|
+
if (!isMultiReplicaDeclared()) return true;
|
|
40719
|
+
return isSharedLeaseStore();
|
|
40720
|
+
}
|
|
40721
|
+
|
|
40411
40722
|
// src/services/ExecutionAuthorityRegistry.ts
|
|
40412
40723
|
var ManagedTaskStartError = class extends Error {
|
|
40413
40724
|
/**
|
|
@@ -40491,7 +40802,10 @@ async function prepareTaskDispatch(deps, input) {
|
|
|
40491
40802
|
}
|
|
40492
40803
|
};
|
|
40493
40804
|
}
|
|
40494
|
-
|
|
40805
|
+
const managedAuthority = executionAuthorityRegistry.resolve(task);
|
|
40806
|
+
const terminal = task.status === "completed" || task.status === "cancelled";
|
|
40807
|
+
const reopenableTerminal = terminal && task.ownerType === "agent" && !managedAuthority;
|
|
40808
|
+
if (terminal && !reopenableTerminal) {
|
|
40495
40809
|
return {
|
|
40496
40810
|
kind: "failure",
|
|
40497
40811
|
failure: {
|
|
@@ -40515,19 +40829,18 @@ async function prepareTaskDispatch(deps, input) {
|
|
|
40515
40829
|
}
|
|
40516
40830
|
};
|
|
40517
40831
|
}
|
|
40518
|
-
if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed") {
|
|
40832
|
+
if (task.status !== "pending" && task.status !== "interrupted" && task.status !== "failed" && !reopenableTerminal) {
|
|
40519
40833
|
return {
|
|
40520
40834
|
kind: "failure",
|
|
40521
40835
|
failure: {
|
|
40522
40836
|
success: false,
|
|
40523
40837
|
code: "TASK_NOT_DISPATCHABLE",
|
|
40524
|
-
error: "Only a `pending`, `interrupted`,
|
|
40525
|
-
hint: `This task is '${task.status}'. A \`pending\` task starts, an \`interrupted\` task resumes,
|
|
40838
|
+
error: "Only a `pending`, `interrupted`, `failed`, or reopenable terminal task can be dispatched.",
|
|
40839
|
+
hint: `This task is '${task.status}'. A \`pending\` task starts, an \`interrupted\` task resumes, a \`failed\` task retries, and an agent-owned \`completed\`/\`cancelled\` task reopens. A task already \`in_progress\` is held by a live run.`,
|
|
40526
40840
|
taskId: task.id
|
|
40527
40841
|
}
|
|
40528
40842
|
};
|
|
40529
40843
|
}
|
|
40530
|
-
const managedAuthority = executionAuthorityRegistry.resolve(task);
|
|
40531
40844
|
if (managedAuthority) {
|
|
40532
40845
|
if (managedAuthority.onManagedStart) {
|
|
40533
40846
|
try {
|
|
@@ -40697,7 +41010,8 @@ function persistedExecutionObservation(item, attempted) {
|
|
|
40697
41010
|
}
|
|
40698
41011
|
function buildWorkerMessage(task, parent, dependencies) {
|
|
40699
41012
|
const sections2 = [
|
|
40700
|
-
|
|
41013
|
+
`You have been assigned persistent task \`${task.id}\` to execute in THIS run. Do NOT dispatch this task \u2014 \`task\` is only for delegating a NEW child subtask; this task is already yours.`,
|
|
41014
|
+
"When the acceptance criteria are truly met, record the outcome yourself with `manage_task set_status completed` (result + beliefImpact), or `failed`/`interrupted`. If you do not have `manage_task`, return your result as an observation for the caller to record.",
|
|
40701
41015
|
`Persisted Task ID: \`${task.id}\``,
|
|
40702
41016
|
`# Task: ${task.title}`,
|
|
40703
41017
|
task.description?.trim() || "No task description was provided."
|
|
@@ -40729,9 +41043,17 @@ function createTaskExecutionService(deps) {
|
|
|
40729
41043
|
taskStore: deps.taskStore,
|
|
40730
41044
|
workItemStore: deps.workItemStore
|
|
40731
41045
|
});
|
|
41046
|
+
const isTaskHeldByLiveRun = deps.isTaskHeldByLiveRun ?? createTaskRunLiveness();
|
|
40732
41047
|
const run = async (plan) => {
|
|
40733
41048
|
const { tenantId: tenantId2, agentId, actor, parent, completedDependencies } = plan;
|
|
40734
41049
|
let task = plan.task;
|
|
41050
|
+
const readTaskStatus = async () => {
|
|
41051
|
+
try {
|
|
41052
|
+
return (await deps.taskStore.getById(tenantId2, task.id))?.status;
|
|
41053
|
+
} catch {
|
|
41054
|
+
return void 0;
|
|
41055
|
+
}
|
|
41056
|
+
};
|
|
40735
41057
|
const storedThreadId = (task.status === "interrupted" || task.status === "failed") && typeof task.context?.thread_id === "string" && task.context.thread_id.trim() !== "" ? task.context.thread_id : void 0;
|
|
40736
41058
|
const storedRunId = typeof task.context?.run_id === "string" && task.context.run_id.trim() !== "" ? task.context.run_id : void 0;
|
|
40737
41059
|
let reuseCurrentRun = false;
|
|
@@ -40790,7 +41112,7 @@ function createTaskExecutionService(deps) {
|
|
|
40790
41112
|
task = transfer.task;
|
|
40791
41113
|
}
|
|
40792
41114
|
const warnings = [];
|
|
40793
|
-
if (task.status === "pending" || task.status === "interrupted" || task.status === "failed") {
|
|
41115
|
+
if (task.status === "pending" || task.status === "interrupted" || task.status === "failed" || task.status === "completed" || task.status === "cancelled") {
|
|
40794
41116
|
const begun = await bridge.beginRun({
|
|
40795
41117
|
tenantId: tenantId2,
|
|
40796
41118
|
task,
|
|
@@ -40917,6 +41239,7 @@ function createTaskExecutionService(deps) {
|
|
|
40917
41239
|
...warnings.length > 0 ? { warnings } : {}
|
|
40918
41240
|
};
|
|
40919
41241
|
}
|
|
41242
|
+
const settledStatus = await readTaskStatus();
|
|
40920
41243
|
return {
|
|
40921
41244
|
success: false,
|
|
40922
41245
|
code: failureCode,
|
|
@@ -40925,6 +41248,7 @@ function createTaskExecutionService(deps) {
|
|
|
40925
41248
|
agentId,
|
|
40926
41249
|
threadId,
|
|
40927
41250
|
executionResultId: eventKey,
|
|
41251
|
+
...settledStatus !== void 0 ? { taskStatus: settledStatus } : {},
|
|
40928
41252
|
...warnings.length > 0 ? { warnings } : {}
|
|
40929
41253
|
};
|
|
40930
41254
|
} catch (persistenceError) {
|
|
@@ -40973,6 +41297,7 @@ function createTaskExecutionService(deps) {
|
|
|
40973
41297
|
...warnings.length > 0 ? { warnings } : {}
|
|
40974
41298
|
};
|
|
40975
41299
|
}
|
|
41300
|
+
const taskStatus = await readTaskStatus();
|
|
40976
41301
|
return {
|
|
40977
41302
|
success: true,
|
|
40978
41303
|
taskId: task.id,
|
|
@@ -40980,6 +41305,7 @@ function createTaskExecutionService(deps) {
|
|
|
40980
41305
|
threadId,
|
|
40981
41306
|
observation: validated.observation,
|
|
40982
41307
|
executionResultId: eventKey,
|
|
41308
|
+
...taskStatus !== void 0 ? { taskStatus } : {},
|
|
40983
41309
|
...warnings.length > 0 ? { warnings } : {}
|
|
40984
41310
|
};
|
|
40985
41311
|
} catch (error) {
|
|
@@ -41000,6 +41326,23 @@ function createTaskExecutionService(deps) {
|
|
|
41000
41326
|
};
|
|
41001
41327
|
return {
|
|
41002
41328
|
start: async (input) => {
|
|
41329
|
+
let current;
|
|
41330
|
+
try {
|
|
41331
|
+
current = await deps.taskStore.getById(input.tenantId, input.taskId);
|
|
41332
|
+
} catch {
|
|
41333
|
+
current = null;
|
|
41334
|
+
}
|
|
41335
|
+
if (current && current.status === "in_progress" && current.ownerType === "agent" && canConvergeOrphan() && !executionAuthorityRegistry.resolve(current) && !await isTaskHeldByLiveRun(current)) {
|
|
41336
|
+
try {
|
|
41337
|
+
await lifecycle.failTask({
|
|
41338
|
+
tenantId: input.tenantId,
|
|
41339
|
+
taskId: current.id,
|
|
41340
|
+
actor: input.actor,
|
|
41341
|
+
failureReason: "Run is no longer executing this task (no_executor)"
|
|
41342
|
+
});
|
|
41343
|
+
} catch {
|
|
41344
|
+
}
|
|
41345
|
+
}
|
|
41003
41346
|
const prepared = await prepareTaskDispatch(deps, input);
|
|
41004
41347
|
if (prepared.kind === "managed") return prepared.result;
|
|
41005
41348
|
if (prepared.kind === "failure") return prepared.failure;
|
|
@@ -43144,9 +43487,85 @@ Use \`imagine_html\`. Wrap the entire thing in a single raised card. All content
|
|
|
43144
43487
|
</div>
|
|
43145
43488
|
</div>
|
|
43146
43489
|
\`\`\``;
|
|
43490
|
+
var ANNOTATABLE_MOCKUP = `## Annotatable drafts (reviewable mockups)
|
|
43491
|
+
|
|
43492
|
+
When the output is a **UI design** \u2014 a draft the user critiques element-by-element \u2014 make it **annotatable** so feedback comes back to the conversation and you can revise. **Right-click** (\`contextmenu\`) any marked element to leave a note; a plain left-click stays free for the mock's own interactions. (A structural or functional overview is different: make it **click-to-explore interactive** to confirm, not annotatable.)
|
|
43493
|
+
|
|
43494
|
+
- Show a small **always-visible hint** (in the user's language) so the draft is discoverable, e.g. \`\u53F3\u952E\u4EFB\u610F\u5143\u7D20\u6279\u6CE8\` / \`Right-click an element to comment\`.
|
|
43495
|
+
- Mark each element worth commenting on with \`class="anno"\`; highlight hover with an **\`outline\`** (never \`border\` \u2014 outline does not reflow the layout). Give it a **negative \`outline-offset\`** (e.g. \`-2px\`) so the frame sits *inside* the element: a positive offset can fall outside the container and get clipped, hiding the cue.
|
|
43496
|
+
- Wrap the whole draft in a \`position: relative\` container and put the comment box in an overlay inside it with \`position: absolute; inset: 0\` \u2014 **never** \`position: fixed\` (it collapses the iframe to min-height).
|
|
43497
|
+
- Handle \`contextmenu\` with event delegation; \`preventDefault()\` + \`stopPropagation()\` (this replaces the native browser menu, which is fine on a draft).
|
|
43498
|
+
- The dialog shows the element name and its **current value**, a note field, and **Cancel / Send** buttons (Send is primary). Cancel, clicking the scrim, and \`Escape\` all close without sending. Mark the element being commented (add \`.marked\`, a solid outline) so it is obvious which one is active.
|
|
43499
|
+
- Send the note back with \`sendPrompt()\` as a clear instruction \u2014 the element, its current value, and the requested change \u2014 so the agent knows exactly what to change. The agent then restates the feedback and re-shows a revised draft.
|
|
43500
|
+
|
|
43501
|
+
\`\`\`html
|
|
43502
|
+
<style>
|
|
43503
|
+
.anno{position:relative}
|
|
43504
|
+
.anno:hover{outline:1px dashed var(--color-border-info);outline-offset:-2px;border-radius:var(--border-radius-md)}
|
|
43505
|
+
.anno.marked{outline:1.5px solid var(--color-text-info);outline-offset:-2px}
|
|
43506
|
+
.overlay{position:absolute;inset:0;background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;border-radius:var(--border-radius-lg);z-index:10}
|
|
43507
|
+
.modal{background:var(--color-background-primary);border:0.5px solid var(--color-border-secondary);border-radius:var(--border-radius-lg);padding:16px;width:88%;max-width:420px}
|
|
43508
|
+
.modal .mt{font-size:14px;font-weight:500;margin-bottom:2px}
|
|
43509
|
+
.modal .ms{font-size:11px;color:var(--color-text-tertiary);margin-bottom:10px}
|
|
43510
|
+
.modal textarea{width:100%;box-sizing:border-box;height:auto;min-height:72px;padding:8px 10px;resize:vertical}
|
|
43511
|
+
.modal .mrow{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
|
|
43512
|
+
.primary{background:var(--color-text-info);color:var(--color-background-primary);border-color:var(--color-text-info)}
|
|
43513
|
+
.primary:hover{background:var(--color-text-info);opacity:.9}
|
|
43514
|
+
</style>
|
|
43515
|
+
<div class="panel" style="position:relative">
|
|
43516
|
+
<div style="font-size:12px;color:var(--color-text-tertiary);margin-bottom:8px">Right-click an element to comment</div>
|
|
43517
|
+
<div class="anno" data-el="Title" data-val="Q4 revenue">Q4 revenue</div>
|
|
43518
|
+
<div class="anno" data-el="Primary action" data-val="Export">Export</div>
|
|
43519
|
+
</div>
|
|
43520
|
+
<script>
|
|
43521
|
+
var panel = document.querySelector('.panel');
|
|
43522
|
+
var current = null;
|
|
43523
|
+
function closeBox(){
|
|
43524
|
+
if (current) current.classList.remove('marked');
|
|
43525
|
+
var ov = panel.querySelector('.overlay');
|
|
43526
|
+
if (ov) ov.remove();
|
|
43527
|
+
current = null;
|
|
43528
|
+
}
|
|
43529
|
+
function openBox(el){
|
|
43530
|
+
closeBox();
|
|
43531
|
+
current = el;
|
|
43532
|
+
el.classList.add('marked');
|
|
43533
|
+
var ov = document.createElement('div');
|
|
43534
|
+
ov.className = 'overlay';
|
|
43535
|
+
ov.innerHTML = '<div class="modal">' +
|
|
43536
|
+
'<div class="mt">Note: ' + el.dataset.el + '</div>' +
|
|
43537
|
+
(el.dataset.val ? '<div class="ms">Current: ' + el.dataset.val + '</div>' : '') +
|
|
43538
|
+
'<textarea id="cmt" placeholder="What should change? e.g. bolder bar / expand by default / add a year filter"></textarea>' +
|
|
43539
|
+
'<div class="mrow"><button id="cancel">Cancel</button><button class="primary" id="send">Send note \u2197</button></div>' +
|
|
43540
|
+
'</div>';
|
|
43541
|
+
panel.appendChild(ov);
|
|
43542
|
+
ov.querySelector('#cmt').focus();
|
|
43543
|
+
ov.querySelector('#send').onclick = function(){
|
|
43544
|
+
var text = ov.querySelector('#cmt').value.trim();
|
|
43545
|
+
if (text && typeof sendPrompt === 'function') {
|
|
43546
|
+
sendPrompt('[design note] ' + el.dataset.el + (el.dataset.val ? ' (current: ' + el.dataset.val + ')' : '') + ' \u2014 change: ' + text);
|
|
43547
|
+
}
|
|
43548
|
+
closeBox();
|
|
43549
|
+
};
|
|
43550
|
+
ov.querySelector('#cancel').onclick = closeBox;
|
|
43551
|
+
ov.addEventListener('click', function(e){ if (e.target === ov) closeBox(); });
|
|
43552
|
+
}
|
|
43553
|
+
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeBox(); });
|
|
43554
|
+
panel.addEventListener('contextmenu', function(e){
|
|
43555
|
+
var el = e.target.closest('.anno');
|
|
43556
|
+
if (!el) return;
|
|
43557
|
+
e.preventDefault(); e.stopPropagation();
|
|
43558
|
+
openBox(el);
|
|
43559
|
+
});
|
|
43560
|
+
</script>
|
|
43561
|
+
\`\`\`
|
|
43562
|
+
|
|
43563
|
+
- The host already pre-styles bare \`<button>\` and \`<textarea>\` (token borders, radius, hover/focus) \u2014 use them as-is and add only what is missing: a \`.primary\` variant for the send button and a \`width\`/\`min-height\` override on the \`textarea\`. Do NOT re-declare the base button/field styles. Add a \`\u2197\` to the send button (it triggers \`sendPrompt\`).
|
|
43564
|
+
|
|
43565
|
+
Keep it lightweight: annotate only meaningful elements, reuse one overlay, no libraries, and put \`<script>\` last so the draft streams in top-down.`;
|
|
43147
43566
|
var MODULE_SECTIONS = {
|
|
43148
43567
|
art: [SVG_SETUP, ART_AND_ILLUSTRATION],
|
|
43149
|
-
mockup: [UI_COMPONENTS, COLOR_PALETTE],
|
|
43568
|
+
mockup: [UI_COMPONENTS, COLOR_PALETTE, ANNOTATABLE_MOCKUP],
|
|
43150
43569
|
interactive: [UI_COMPONENTS, COLOR_PALETTE],
|
|
43151
43570
|
chart: [UI_COMPONENTS, COLOR_PALETTE, CHARTS_CHART_JS],
|
|
43152
43571
|
diagram: [COLOR_PALETTE, SVG_SETUP, DIAGRAM_TYPES]
|
|
@@ -43653,7 +44072,7 @@ function renderTaskBinding(taskId) {
|
|
|
43653
44072
|
|
|
43654
44073
|
## Bound Task
|
|
43655
44074
|
|
|
43656
|
-
You are executing task \`${taskId}
|
|
44075
|
+
You are executing task \`${taskId}\` in THIS run \u2014 it is already assigned to you. Read its contract with \`manage_task get\`, update it as you work, and complete it when done. Do NOT dispatch this task with \`task\`; \`task\` is only for delegating a NEW child subtask.
|
|
43657
44076
|
`;
|
|
43658
44077
|
}
|
|
43659
44078
|
function createBoundTaskLifecycle() {
|
|
@@ -44172,7 +44591,7 @@ var manageTaskSchema = z70.object({
|
|
|
44172
44591
|
title: z70.string().optional().describe("Task title (required for create)"),
|
|
44173
44592
|
description: z70.string().optional().describe("Task description in Markdown"),
|
|
44174
44593
|
priority: z70.enum(["low", "medium", "high"]).optional().describe("Priority level"),
|
|
44175
|
-
status: z70.enum(["pending", "in_progress", "review", "failed", "interrupted", "completed", "cancelled"]).optional().describe("Task status. For status-only changes, prefer set_status action. Set 'in_progress'
|
|
44594
|
+
status: z70.enum(["pending", "in_progress", "review", "failed", "interrupted", "completed", "cancelled"]).optional().describe("Task status. For status-only changes, prefer set_status action. Set 'in_progress' only for a task THIS run executes (a delegated task is claimed by `task` dispatch); 'completed'/'failed' require the task to be 'in_progress'. Agent summary-only interrupted updates default to missing_input"),
|
|
44176
44595
|
dueDate: z70.string().optional().describe("Due date (ISO 8601 format)"),
|
|
44177
44596
|
metadata: z70.record(z70.unknown()).optional().describe("Structured metadata (e.g. projectId, module)"),
|
|
44178
44597
|
metadataFilter: z70.record(
|
|
@@ -45763,8 +46182,8 @@ ${TASK_CONVERGENCE_GUIDANCE}
|
|
|
45763
46182
|
- update: Update a task's content fields (title/description/priority/dueDate/metadata/parentId/sourceId/context/ownerId/result/failureReason/requireReview/dependencies/files)
|
|
45764
46183
|
Status changes can also be done here, but use set_status for a simpler dedicated status update.
|
|
45765
46184
|
- set_status: Change a task's status (id and status required). Use this when you only need to change the status.
|
|
45766
|
-
Start
|
|
45767
|
-
To mark complete: set_status with status='completed', result='...' (the task must be in_progress)
|
|
46185
|
+
Start a task THIS run executes: set_status with status='in_progress' (the engine binds it to this run's thread). Do NOT set in_progress on a task you will delegate \u2014 leave it pending; \`task\` dispatch claims it.
|
|
46186
|
+
To mark complete: set_status with status='completed', result='...' (the task must be in_progress). Complete a task exactly once: if it is already completed, do not complete it again \u2014 use add_activity for extra notes.
|
|
45768
46187
|
To mark failed: set_status with status='failed', failureReason='...'
|
|
45769
46188
|
To pause for input: set_status with status='interrupted', summary='what is needed'
|
|
45770
46189
|
Valid status values: pending, in_progress, review, failed, interrupted, completed, cancelled.
|
|
@@ -45781,16 +46200,16 @@ Report existing keys directly; set 'isNew: true' to add a key the owner does not
|
|
|
45781
46200
|
track yet. Belief is evidence metadata: it never blocks a completion, and an
|
|
45782
46201
|
unmatched or malformed entry is recorded without changing the owner's table.
|
|
45783
46202
|
Create always persists a pending record \u2014 a supplied status is coerced and no
|
|
45784
|
-
execution is dispatched.
|
|
45785
|
-
|
|
45786
|
-
|
|
46203
|
+
execution is dispatched. Set \`in_progress\` yourself ONLY for a task THIS run
|
|
46204
|
+
executes; a task you delegate stays \`pending\` and is claimed automatically by
|
|
46205
|
+
\`task\` dispatch. Use \`task { taskId, agentId }\` to delegate a subtask
|
|
45787
46206
|
to an expert \u2014 never re-dispatch the task you are processing.`,
|
|
45788
46207
|
schema: manageTaskSchema
|
|
45789
46208
|
}
|
|
45790
46209
|
),
|
|
45791
46210
|
tool64(handleStartTask, {
|
|
45792
46211
|
name: "task",
|
|
45793
|
-
description: `Start a persisted TaskItem with any existing Agent in a fresh isolated thread. Use this to delegate work \u2014 dispatch a subtask you created to the agent that should execute it; never re-dispatch the task you are currently processing (give the expert a child task instead). Pass agentId "general-purpose" for the general-purpose worker \u2014 ${DEFAULT_GENERAL_PURPOSE_DESCRIPTION} It has access to all the caller's tools, model, and middleware, so use it for general-purpose subtasks, to isolate context and token usage, and whenever no specialized agent fits. The target does NOT need task-management tools: it runs and returns an observation
|
|
46212
|
+
description: `Start a persisted TaskItem with any existing Agent in a fresh isolated thread. Use this to delegate work \u2014 dispatch a subtask you created to the agent that should execute it; never re-dispatch the task you are currently processing (give the expert a child task instead). Pass agentId "general-purpose" for the general-purpose worker \u2014 ${DEFAULT_GENERAL_PURPOSE_DESCRIPTION} It has access to all the caller's tools, model, and middleware, so use it for general-purpose subtasks, to isolate context and token usage, and whenever no specialized agent fits. The target does NOT need task-management tools: it runs and returns an observation, an executionResultId, and the task's taskStatus (re-read after the worker settled). Check taskStatus before completing: if it is already 'completed' the worker finished it \u2014 do NOT complete again (use manage_task add_activity); if it is still 'in_progress' you are the single completer and pass the executionResultId to manage_task set_status completed; if it is 'failed' or 'interrupted', act on that. The caller reconciles via manage_task, which interprets the result and records the outcome. A project task instead returns a managed result (success:true, managed:true) because the project-lifecycle queue owns execution.`,
|
|
45794
46213
|
schema: startTaskSchema
|
|
45795
46214
|
})
|
|
45796
46215
|
]
|
|
@@ -47493,7 +47912,7 @@ A plugin is the SET of classes it needs, e.g. \`connection + tools + skills\` or
|
|
|
47493
47912
|
| Tool shows an interactive panel | \`sandbox-plugins-ui\` |
|
|
47494
47913
|
| Tool orchestrates platform tools/agents | \`sandbox-plugins-consume-open\` |
|
|
47495
47914
|
| Tool callable by external MCP clients | \`sandbox-plugins-expose\` |
|
|
47496
|
-
| Run as a long-lived resident service | \`sandbox-plugins-deploy\` |
|
|
47915
|
+
| Run as a long-lived resident service (only on the human's explicit request) | \`sandbox-plugins-deploy\` |
|
|
47497
47916
|
| Validate / test / debug | \`sandbox-plugins-testing\` |
|
|
47498
47917
|
|
|
47499
47918
|
## Agents (not a supported authoring path)
|
|
@@ -47535,8 +47954,8 @@ chart, table, viewer\u2026). It can also call the plugin's OWN tools back from t
|
|
|
47535
47954
|
- It is NOT the tool's JSON \`schema\` (that is the input contract for the model).
|
|
47536
47955
|
- It is NOT the tool \`description\`.
|
|
47537
47956
|
- It is NOT required: a text/JSON-returning tool needs no UI.
|
|
47538
|
-
- Saying "add a UI" means: add a \`ui\` field to the tool in \`plugin.json\`
|
|
47539
|
-
\`ui/<tool>.html
|
|
47957
|
+
- Saying "add a UI" means: add a \`ui\` field to the tool in \`plugin.json\` and write
|
|
47958
|
+
\`ui/<tool>.html\` (with \`write_file\`). Details in \`sandbox-plugins-ui\`.
|
|
47540
47959
|
|
|
47541
47960
|
## Decide first \u2014 clarify these with the user before writing files
|
|
47542
47961
|
Ask for anything not already stated; do not guess silently:
|
|
@@ -47551,7 +47970,7 @@ Ask for anything not already stated; do not guess silently:
|
|
|
47551
47970
|
multi-select? connection test needed? \u2192 read \`sandbox-plugins-connection\`.
|
|
47552
47971
|
- **UI**: should the tool show an interactive panel instead of only text? what
|
|
47553
47972
|
does it show, how do users interact, external origins? \u2192 read
|
|
47554
|
-
|
|
47973
|
+
\`sandbox-plugins-ui\` and add \`ui\` + \`ui/<tool>.html\` (step 6 below). If the
|
|
47555
47974
|
user says no UI, skip it \u2014 text-only is fine.
|
|
47556
47975
|
- **Skills**: procedure or knowledge? who consumes it? trigger phrase? tied to a
|
|
47557
47976
|
specific tool?
|
|
@@ -47584,13 +48003,34 @@ Ask for anything not already stated; do not guess silently:
|
|
|
47584
48003
|
\`\`\`
|
|
47585
48004
|
|
|
47586
48005
|
## Flow
|
|
47587
|
-
1.
|
|
47588
|
-
|
|
47589
|
-
|
|
47590
|
-
|
|
47591
|
-
|
|
48006
|
+
1. **Design first, and iterate \u2014 then get explicit approval before you build (non-trivial plugins).**
|
|
48007
|
+
Before writing any file, produce the design and review it with the human. It has two parts,
|
|
48008
|
+
with different treatments:
|
|
48009
|
+
- **Functional design** (always): an **interactive** component map of what you intend to
|
|
48010
|
+
build \u2014 the tools, their inputs/outputs, whether a connection is needed, whether a UI
|
|
48011
|
+
is needed, and how the agent calls them. Make it click-to-explore (e.g. click a tool to
|
|
48012
|
+
reveal its handler / IO / connection); it is a map to **confirm**, not to annotate.
|
|
48013
|
+
- **UI design** (only when the UI class applies): an **annotatable** mock of the tool's UI \u2014
|
|
48014
|
+
the human right-clicks elements to leave notes (see \`sandbox-plugins-ui\` \u2192
|
|
48015
|
+
"Reviewable drafts").
|
|
48016
|
+
Show both drafts with the \`widget\` middleware, then run the review loop:
|
|
48017
|
+
- Ask the human to review (say how to comment). On each round of comments, **restate the
|
|
48018
|
+
feedback** (element + requested change), then **regenerate the draft with the feedback and
|
|
48019
|
+
present it again** \u2014 comments are "revise and re-show", never "start building".
|
|
48020
|
+
- **Do NOT scaffold or write any file during the loop.** Implement only after the human
|
|
48021
|
+
**explicitly approves**, and ask for that go-ahead yourself before you start.
|
|
48022
|
+
Skip the drafts only for a trivial single-tool plugin whose shape is already agreed \u2014 but
|
|
48023
|
+
still confirm the goal.
|
|
48024
|
+
2. Scaffold with \`manage_plugin { action: "create", type, name }\` \u2014 creates
|
|
48025
|
+
\`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It carries no content.
|
|
48026
|
+
3. Read \`sandbox-plugins-manifest\` and write \`plugin.json\` with \`write_file\`.
|
|
48027
|
+
4. Read \`sandbox-plugins-handlers\` and write each handler with \`write_file\`.
|
|
48028
|
+
5. If the plugin needs credentials: read \`sandbox-plugins-connection\`.
|
|
48029
|
+
6. **Only if a tool needs a UI** (the UI class from \`sandbox-plugins-patterns\`): read \`sandbox-plugins-ui\`, add \`ui\` to that tool in \`plugin.json\`, and write \`ui/<tool>.html\` with \`write_file\`.
|
|
48030
|
+
7. If a tool must be callable by external MCP clients: read \`sandbox-plugins-expose\`
|
|
47592
48031
|
and add \`expose: true\` (plus \`readOnly\`/\`destructive\` accuracy hints).
|
|
47593
|
-
|
|
48032
|
+
8. Read \`sandbox-plugins-testing\` and iterate: edit files with \`edit_file\`, then
|
|
48033
|
+
\`validate\` \u2192 \`reload\` \u2192 \`test\`.
|
|
47594
48034
|
|
|
47595
48035
|
Templates: \`templates/plugin.json\`, \`templates/handler.js\`, \`templates/test.js\`, \`templates/discover.js\`, \`templates/ui.html\`.
|
|
47596
48036
|
`,
|
|
@@ -47652,6 +48092,20 @@ description: Add an interactive HTML UI to a tenant plugin tool (rendered as a s
|
|
|
47652
48092
|
A tool can declare an HTML file that Axiom renders as a sandboxed MCP App when
|
|
47653
48093
|
the tool runs. The app can call the plugin's OWN tools back.
|
|
47654
48094
|
|
|
48095
|
+
## Reviewable drafts (before you build the real UI)
|
|
48096
|
+
When you show a **UI mock with \`show_widget\`** for the user to react to (the design
|
|
48097
|
+
step, before writing \`ui/<tool>.html\`), make it **annotatable**: the user right-clicks
|
|
48098
|
+
any element to leave a comment that comes back to you as a message. Turn it on by
|
|
48099
|
+
default for review drafts and include a small visible hint (e.g. "\u53F3\u952E\u4EFB\u610F\u5143\u7D20\u6279\u6CE8");
|
|
48100
|
+
the \`widget\` mockup guidelines have the pattern. Omit it only when the draft is meant
|
|
48101
|
+
to be purely experienced (a live demo with no review) \u2014 then leave out the hint and
|
|
48102
|
+
the handlers. This is only for the **mock**: the real \`ui/<tool>.html\` is the plugin's
|
|
48103
|
+
product and is never annotatable.
|
|
48104
|
+
A round of annotations is **NOT approval**: restate the comments you received, regenerate
|
|
48105
|
+
the mock from them, and show it again, iterating until the human is satisfied. Build
|
|
48106
|
+
\`ui/<tool>.html\` only after the human **explicitly approves** \u2014 and ask for that go-ahead
|
|
48107
|
+
yourself.
|
|
48108
|
+
|
|
47655
48109
|
## Declare it
|
|
47656
48110
|
\`\`\`
|
|
47657
48111
|
/root/.agents/plugins/<type>/
|
|
@@ -47684,11 +48138,10 @@ the tool runs. The app can call the plugin's OWN tools back.
|
|
|
47684
48138
|
- Omit \`csp\` for the restrictive default (\`default-src 'none'\`). WebAssembly is allowed by default.
|
|
47685
48139
|
|
|
47686
48140
|
## How to write the HTML file
|
|
47687
|
-
|
|
47688
|
-
|
|
47689
|
-
|
|
47690
|
-
|
|
47691
|
-
- Or write the file directly in the sandbox under \`ui/\` and call \`manage_plugin reload\`.
|
|
48141
|
+
Write the HTML to \`/root/.agents/plugins/<type>/ui/<tool>.html\` with \`write_file\`,
|
|
48142
|
+
then call \`manage_plugin { action: "reload", type }\` so the host serves the new HTML.
|
|
48143
|
+
The file must be declared by a tool's \`ui.resource\` in \`plugin.json\`, otherwise it is
|
|
48144
|
+
treated as missing (\`TOOL_UI\`).
|
|
47692
48145
|
|
|
47693
48146
|
## How it behaves at runtime
|
|
47694
48147
|
- The tool result carries a hidden \`mcp_app\` marker; Axiom renders \`ui/<tool>.html\` inline. \`displayMode\` is recorded but NOT yet honored \u2014 fullscreen happens only when the app itself calls \`ui/request-display-mode\`.
|
|
@@ -47784,7 +48237,7 @@ grants apply to the UI too). See \`sandbox-plugins-expose\`.
|
|
|
47784
48237
|
## Gotchas
|
|
47785
48238
|
- \`ui.resource\` must be a safe relative path ending in \`.html\` that EXISTS; otherwise the UI is dropped (diagnostic \`TOOL_UI\`) but the tool still loads.
|
|
47786
48239
|
- \`displayMode\`, \`permissions\`, and \`prefersBorder\` are accepted but NOT yet applied.
|
|
47787
|
-
- After editing the HTML, call \`manage_plugin reload
|
|
48240
|
+
- After editing the HTML, call \`manage_plugin { action: "reload", type }\`; the host caches the resource, so a reload is what makes it re-read the new file.
|
|
47788
48241
|
|
|
47789
48242
|
Start from \`templates/ui.html\`.
|
|
47790
48243
|
`,
|
|
@@ -48048,21 +48501,23 @@ description: Validate, test, and reload a tenant sandbox plugin using the manage
|
|
|
48048
48501
|
|
|
48049
48502
|
Use the \`manage_plugin\` tool. The loop:
|
|
48050
48503
|
|
|
48051
|
-
1.
|
|
48052
|
-
|
|
48504
|
+
1. Write/edit the plugin files with the filesystem tools (\`write_file\`/\`edit_file\`) under
|
|
48505
|
+
\`/root/.agents/plugins/<type>/\`; \`create\` only scaffolds the directory + stub.
|
|
48506
|
+
2. \`action: "validate"\` with \`{ type }\` \u2014 reads the plugin back **from the sandbox** and returns diagnostics (no writes). The files must already be on disk; a \`type\` with no files reports a read error.
|
|
48053
48507
|
3. If the plugin has \`connection.fields\` and a tool reads credentials: a **human**
|
|
48054
48508
|
must configure a connection of this plugin type. You cannot create credentials.
|
|
48055
48509
|
Call \`list_connections { type }\` first \u2014 if none exist, use \`ask_user_to_clarify\`
|
|
48056
48510
|
to state explicitly that only the human can configure the connector, ask them to
|
|
48057
48511
|
add it (Console \u2192 Connections) and to give you the connection key. Do not claim
|
|
48058
48512
|
the tool works until a real \`connectionKey\` test passes.
|
|
48059
|
-
4. \`action: "
|
|
48513
|
+
4. \`action: "reload"\` \u2014 refresh the compiled catalog after writing/editing files (the catalog caches; an edited file is invisible to \`test\` until you reload).
|
|
48514
|
+
5. \`action: "test"\` to exercise a handler without an LLM:
|
|
48060
48515
|
- Tool: \`{ type, target: { kind: "tool", name, input, connectionKey? } }\`
|
|
48061
48516
|
- Connection: \`{ type, target: { kind: "connection-test" } }\` or \`{ kind: "connection-discover" }\`
|
|
48062
48517
|
Returns \`{ ok, result, exitCode, durationMs, stdout, stderr }\`.
|
|
48063
48518
|
For credentialed tools, pass the real \`connectionKey\`. Without one, the handler
|
|
48064
48519
|
receives an empty connection \u2014 that only checks plumbing, NOT functional success.
|
|
48065
|
-
|
|
48520
|
+
6. \`action: "backup"\` / \`"restore"\` \u2014 manual snapshot and rollback. \`backup\` copies the plugin into \`.backup/\` **inside its own directory** (keeps only the latest), and \`restore\` makes the plugin match that snapshot (adds back missing files, removes files added since). Use it before a risky rewrite.
|
|
48066
48521
|
|
|
48067
48522
|
## Secrets
|
|
48068
48523
|
- \`list_connections\` returns identifiers only (\`key\`/\`name\`/\`description\`), never config or secrets.
|
|
@@ -48077,10 +48532,10 @@ Use the \`manage_plugin\` tool. The loop:
|
|
|
48077
48532
|
| HANDLER_MISSING | handler path wrong or file not written |
|
|
48078
48533
|
| TOOL_SCHEMA | unsupported JSON Schema keyword |
|
|
48079
48534
|
| non-zero exit | handler threw; read stdout/stderr from \`test\` |
|
|
48080
|
-
| old behaviour after edit |
|
|
48535
|
+
| old behaviour after edit | the catalog caches by content hash/TTL \u2014 call \`reload\` |
|
|
48081
48536
|
| empty connection in handler | no \`connectionKey\` passed, or no connection configured for this type |
|
|
48082
48537
|
|
|
48083
|
-
Fall back to \`
|
|
48538
|
+
Fall back to \`shell_exec\` to run \`node handlers/x.js\` manually with
|
|
48084
48539
|
\`PLUGIN_INPUT\` and a synthetic \`PLUGIN_CONNECTION_FILE\` to reproduce logic bugs.
|
|
48085
48540
|
A synthetic fixture only verifies plumbing \u2014 a real upstream call requires a real
|
|
48086
48541
|
\`connectionKey\` configured by a human.
|
|
@@ -48091,7 +48546,7 @@ A synthetic fixture only verifies plumbing \u2014 a real upstream call requires
|
|
|
48091
48546
|
version: "1.0.0",
|
|
48092
48547
|
content: `---
|
|
48093
48548
|
name: sandbox-plugins-deploy
|
|
48094
|
-
description: Deploy a validated tenant plugin as a resident service (alwaysOn vs onDemand), check its status, and undeploy it. Trigger:
|
|
48549
|
+
description: Deploy a validated tenant plugin as a resident service (alwaysOn vs onDemand), check its status, and undeploy it. Trigger: the human explicitly asks to run a plugin as a long-lived resident service (or to inspect/remove one) \u2014 deployment is human-gated, never automatic.
|
|
48095
48550
|
---
|
|
48096
48551
|
|
|
48097
48552
|
# Deploying a Plugin as a Resident Service
|
|
@@ -48101,10 +48556,13 @@ Optionally, you can **deploy** the plugin as a **resident service**: one
|
|
|
48101
48556
|
long-lived sandbox per plugin that serves every call. This is faster for
|
|
48102
48557
|
frequent or heavy calls, but costs a running sandbox.
|
|
48103
48558
|
|
|
48104
|
-
## Precondition: tests must pass first
|
|
48105
|
-
|
|
48106
|
-
|
|
48107
|
-
|
|
48559
|
+
## Precondition: human request, then tests must pass first
|
|
48560
|
+
Deployment is **opt-in and human-gated**: NEVER deploy on your own after tests
|
|
48561
|
+
pass. Wait until the human explicitly asks for it or approves it. A passing test
|
|
48562
|
+
is necessary but NOT sufficient to deploy.
|
|
48563
|
+
Then confirm the plugin is validated AND its tests pass:
|
|
48564
|
+
1. Files written with the filesystem tools and \`reload\` run.
|
|
48565
|
+
2. \`manage_plugin validate\` \u2014 no diagnostics.
|
|
48108
48566
|
3. \`manage_plugin test\` \u2014 every tool/connection test returns \`ok: true\`
|
|
48109
48567
|
(a real \`connectionKey\` test for credentialed tools).
|
|
48110
48568
|
Never deploy a plugin you have not tested: a resident service wraps the same
|
|
@@ -48172,6 +48630,30 @@ load "sandbox-plugins-expose" and follow it (\`expose: true\` + key grants).
|
|
|
48172
48630
|
If a tool must call the platform's own tools/agents from its handler, load
|
|
48173
48631
|
"sandbox-plugins-consume-open".
|
|
48174
48632
|
|
|
48633
|
+
Design first, and iterate on it before you build \u2014 the design is a conversation, not
|
|
48634
|
+
a one-shot. Use the \`widget\` middleware to present it (a decision surface, not a showcase):
|
|
48635
|
+
- **Functional design** (always): an **interactive** component map of the shape you
|
|
48636
|
+
intend to build \u2014 which tools, their inputs/outputs, whether a connection or a UI
|
|
48637
|
+
is needed, how the agent calls them. Make it click-to-explore (and prefer it over
|
|
48638
|
+
an SVG flowchart); it is a map to **confirm**, NOT an annotatable draft.
|
|
48639
|
+
- **UI design** (UI class only): an **annotatable** mock of the tool's UI \u2014 the human
|
|
48640
|
+
right-clicks elements to leave notes.
|
|
48641
|
+
Then run a review loop:
|
|
48642
|
+
- Ask the human to review (tell them how to comment). When they send comments, **restate the
|
|
48643
|
+
feedback** (which element, what change) so they can confirm you understood, then **build a
|
|
48644
|
+
NEW draft that incorporates the feedback and present it again**; keep iterating until
|
|
48645
|
+
they are satisfied. Comments mean "revise and re-show", NOT "start building".
|
|
48646
|
+
- Do NOT scaffold or write any file during the loop. Implement only once the human
|
|
48647
|
+
**explicitly approves** the design \u2014 and still ASK for that go-ahead yourself; never
|
|
48648
|
+
start building on your own initiative.
|
|
48649
|
+
Also surface genuinely different options (scope, layout, which tools) visually and let
|
|
48650
|
+
the human choose, instead of describing them in prose.
|
|
48651
|
+
|
|
48652
|
+
How to render a good widget is the widget middleware's own concern \u2014 follow it
|
|
48653
|
+
there; keep this prompt to plugin-design intent. These widgets are design aids
|
|
48654
|
+
only; the plugin's actual UI is still the MCP App HTML you author and test with
|
|
48655
|
+
\`manage_plugin\`.
|
|
48656
|
+
|
|
48175
48657
|
Plan with persistent Tasks: MANDATORY until you load "task-tracking" with the
|
|
48176
48658
|
\`skill\` tool, call neither \`manage_task\` nor any other task operation. Use
|
|
48177
48659
|
persistent Tasks as the development planning surface and create one Task per
|
|
@@ -48179,7 +48661,16 @@ independently verifiable outcome (for example "manifest written", "handlers
|
|
|
48179
48661
|
implemented", "UI added", "validated/tested via manage_plugin"). As you work,
|
|
48180
48662
|
update each Task's status to reflect reality. Never use \`write_todos\`.
|
|
48181
48663
|
|
|
48182
|
-
|
|
48664
|
+
After the human confirms the design, author the plugin files directly with the filesystem tools, then verify with \`manage_plugin\`.
|
|
48665
|
+
1. Scaffold once: \`manage_plugin { action: "create", type: "<type>", name }\` \u2014 it creates
|
|
48666
|
+
\`/root/.agents/plugins/<type>/\` with a stub \`plugin.json\`. It never carries content.
|
|
48667
|
+
2. Write and edit every file yourself with \`write_file\` / \`edit_file\` (read back with
|
|
48668
|
+
\`read_file\`) under \`/root/.agents/plugins/<type>/\`: \`plugin.json\`, \`handlers/*.js\`, and
|
|
48669
|
+
(UI class) \`ui/*.html\`. Change one file at a time; edit in place instead of rewriting.
|
|
48670
|
+
3. Verify in order: \`manage_plugin { action: "validate", type }\` (reads the files back from
|
|
48671
|
+
the sandbox) \u2192 \`manage_plugin { action: "reload", type }\` (the catalog caches, so a
|
|
48672
|
+
freshly written file is invisible to \`test\` until you reload) \u2192 \`manage_plugin
|
|
48673
|
+
{ action: "test", type, target }\`.
|
|
48183
48674
|
If a tool needs credentials, a human must configure the connection \u2014 use
|
|
48184
48675
|
\`list_connections\` to check, and \`ask_user_to_clarify\` to ask the human to add
|
|
48185
48676
|
the connector and give you its key. Only after a real \`connectionKey\` test
|
|
@@ -48188,14 +48679,15 @@ unverified (missing credentials), not as passing. Use \`shell_exec\` (the tool
|
|
|
48188
48679
|
provided by the \`code_eval\` middleware) to run \`node handlers/x.js\` for manual
|
|
48189
48680
|
checks when reproducing a failure.
|
|
48190
48681
|
|
|
48191
|
-
|
|
48192
|
-
|
|
48682
|
+
Deployment is human-gated: do NOT deploy on your own after the tests pass.
|
|
48683
|
+
Only deploy when the human explicitly asks for it or approves it, then load
|
|
48684
|
+
"sandbox-plugins-deploy" and follow it. Choose \`onDemand\`
|
|
48193
48685
|
(default) or \`alwaysOn\` for hot paths, call \`manage_plugin deploy\` with the
|
|
48194
48686
|
chosen \`mode\`, then confirm with \`manage_plugin status\` (\`running\`). Only claim
|
|
48195
48687
|
the plugin is deployed and serving once \`status\` reports \`running\`; if the
|
|
48196
48688
|
environment reports no deployment service, say so and treat deployment as
|
|
48197
|
-
unavailable \u2014 do NOT claim success.
|
|
48198
|
-
touching the plugin files.`;
|
|
48689
|
+
unavailable \u2014 do NOT claim success. Report the plugin as ready and let the human
|
|
48690
|
+
decide; \`undeploy\` removes the service without touching the plugin files.`;
|
|
48199
48691
|
|
|
48200
48692
|
// src/middlewares/sandboxPluginsUi.ts
|
|
48201
48693
|
var SANDBOX_PLUGINS_UI_RESOURCE = "ui://sandbox-plugins/app";
|
|
@@ -48371,6 +48863,7 @@ var SANDBOX_PLUGINS_APP_HTML = `<!doctype html>
|
|
|
48371
48863
|
`;
|
|
48372
48864
|
|
|
48373
48865
|
// src/middlewares/sandboxPluginsMiddleware.ts
|
|
48866
|
+
var PLUGINS_ROOT3 = "/root/.agents/plugins";
|
|
48374
48867
|
var SANDBOX_PLUGINS_UI_REF = {
|
|
48375
48868
|
kind: "plugin",
|
|
48376
48869
|
pluginType: "sandbox-plugins",
|
|
@@ -48427,18 +48920,19 @@ var managePluginSchema = z73.object({
|
|
|
48427
48920
|
"get",
|
|
48428
48921
|
"validate",
|
|
48429
48922
|
"create",
|
|
48430
|
-
"update",
|
|
48431
48923
|
"delete",
|
|
48432
48924
|
"reload",
|
|
48433
48925
|
"test",
|
|
48926
|
+
"backup",
|
|
48927
|
+
"restore",
|
|
48434
48928
|
"deploy",
|
|
48435
48929
|
"undeploy",
|
|
48436
48930
|
"status",
|
|
48437
48931
|
"deployments"
|
|
48438
48932
|
]),
|
|
48439
48933
|
type: z73.string().optional(),
|
|
48440
|
-
|
|
48441
|
-
|
|
48934
|
+
name: z73.string().optional(),
|
|
48935
|
+
description: z73.string().optional(),
|
|
48442
48936
|
mode: z73.enum(["onDemand", "alwaysOn"]).optional(),
|
|
48443
48937
|
target: z73.object({
|
|
48444
48938
|
kind: z73.enum(["tool", "connection-test", "connection-discover"]),
|
|
@@ -48452,12 +48946,12 @@ var sandboxPluginsPlugin = {
|
|
|
48452
48946
|
type: "sandbox-plugins",
|
|
48453
48947
|
category: "execution",
|
|
48454
48948
|
name: "Sandbox Plugins",
|
|
48455
|
-
description: "Author and manage tenant plugins stored in the sandbox: list/validate/create/
|
|
48949
|
+
description: "Author and manage tenant plugins stored in the sandbox: list/validate/create/delete/reload/test/backup/restore, and deploy/undeploy/status/deployments to run a plugin as a resident service. Tools are agent-callable JS handlers and may declare an optional interactive HTML UI (rendered as a sandboxed MCP App). Pairs with the Skill plugin for authoring guidance.",
|
|
48456
48950
|
recommends: ["skill"],
|
|
48457
48951
|
tools: [
|
|
48458
48952
|
{
|
|
48459
48953
|
name: "manage_plugin",
|
|
48460
|
-
description: "Author and manage tenant sandbox plugins (tools, optional per-tool HTML UI, connections, Open/MCP exposure, and resident deployment). Actions: list/get/validate/create/
|
|
48954
|
+
description: "Author and manage tenant sandbox plugins (tools, optional per-tool HTML UI, connections, Open/MCP exposure, and resident deployment). Actions: list/get/validate/create/delete/reload/test/backup/restore, deploy/undeploy/status, and `deployments` to list every resident deployment for the tenant.",
|
|
48461
48955
|
ui: { resource: SANDBOX_PLUGINS_UI_RESOURCE, displayMode: "inline" }
|
|
48462
48956
|
}
|
|
48463
48957
|
],
|
|
@@ -48484,7 +48978,7 @@ var sandboxPluginsPlugin = {
|
|
|
48484
48978
|
id: "sandbox-plugins",
|
|
48485
48979
|
type: "sandbox-plugins",
|
|
48486
48980
|
name: "Sandbox Plugins",
|
|
48487
|
-
description: "Author and manage tenant plugins (list/validate/create/
|
|
48981
|
+
description: "Author and manage tenant plugins (list/validate/create/delete/reload/test/backup/restore/deploy/undeploy/status)",
|
|
48488
48982
|
enabled: true,
|
|
48489
48983
|
config: {}
|
|
48490
48984
|
},
|
|
@@ -48504,6 +48998,14 @@ var sandboxPluginsPlugin = {
|
|
|
48504
48998
|
enabled: true,
|
|
48505
48999
|
config: {}
|
|
48506
49000
|
},
|
|
49001
|
+
{
|
|
49002
|
+
id: "widget",
|
|
49003
|
+
type: "widget",
|
|
49004
|
+
name: "Widget",
|
|
49005
|
+
description: "Align on the plugin design before building it: show the shape you intend to build (tools, IO, connections, UI) and a UI mock for the human to correct scope before files exist.",
|
|
49006
|
+
enabled: true,
|
|
49007
|
+
config: {}
|
|
49008
|
+
},
|
|
48507
49009
|
{
|
|
48508
49010
|
id: "ask_user_to_clarify",
|
|
48509
49011
|
type: "ask_user_to_clarify",
|
|
@@ -48544,6 +49046,7 @@ var sandboxPluginsPlugin = {
|
|
|
48544
49046
|
return JSON.stringify({
|
|
48545
49047
|
plugins: loaded.plugins.map((p) => ({
|
|
48546
49048
|
type: p.meta.type,
|
|
49049
|
+
path: `${PLUGINS_ROOT3}/${p.meta.type}`,
|
|
48547
49050
|
name: p.meta.name,
|
|
48548
49051
|
tools: p.meta.tools?.map((t) => t.name) ?? [],
|
|
48549
49052
|
source: p.meta.source
|
|
@@ -48554,32 +49057,87 @@ var sandboxPluginsPlugin = {
|
|
|
48554
49057
|
}
|
|
48555
49058
|
case "get": {
|
|
48556
49059
|
if (!input.type) return "type is required";
|
|
48557
|
-
|
|
49060
|
+
const content = await catalog2.readPluginFile(tenantId2, input.type, "plugin.json");
|
|
49061
|
+
let manifest = content;
|
|
49062
|
+
try {
|
|
49063
|
+
manifest = JSON.parse(content);
|
|
49064
|
+
} catch {
|
|
49065
|
+
}
|
|
49066
|
+
return JSON.stringify({
|
|
49067
|
+
type: input.type,
|
|
49068
|
+
path: `${PLUGINS_ROOT3}/${input.type}`,
|
|
49069
|
+
manifest
|
|
49070
|
+
});
|
|
48558
49071
|
}
|
|
48559
49072
|
case "reload": {
|
|
48560
49073
|
const loaded = await catalog2.reload(tenantId2);
|
|
48561
49074
|
void publishPluginCatalogChanged(tenantId2);
|
|
48562
49075
|
return JSON.stringify({ reloaded: true, plugins: loaded.plugins.length, diagnostics: loaded.diagnostics });
|
|
48563
49076
|
}
|
|
48564
|
-
case "
|
|
48565
|
-
|
|
48566
|
-
|
|
48567
|
-
|
|
49077
|
+
case "backup": {
|
|
49078
|
+
if (!input.type) return "type is required";
|
|
49079
|
+
try {
|
|
49080
|
+
const result = await catalog2.backupPlugin(tenantId2, input.type);
|
|
49081
|
+
if (result.files === 0) {
|
|
49082
|
+
return JSON.stringify({ backedUp: false, error: "nothing to back up (plugin has no files)" });
|
|
49083
|
+
}
|
|
49084
|
+
return JSON.stringify({ backedUp: true, files: result.files });
|
|
49085
|
+
} catch (err) {
|
|
49086
|
+
return JSON.stringify({ backedUp: false, error: errorText(err) });
|
|
49087
|
+
}
|
|
49088
|
+
}
|
|
49089
|
+
case "restore": {
|
|
49090
|
+
if (!input.type) return "type is required";
|
|
49091
|
+
let restored;
|
|
49092
|
+
let failure3;
|
|
49093
|
+
try {
|
|
49094
|
+
restored = await catalog2.restorePlugin(tenantId2, input.type);
|
|
49095
|
+
} catch (err) {
|
|
49096
|
+
failure3 = errorText(err);
|
|
49097
|
+
}
|
|
49098
|
+
try {
|
|
49099
|
+
await catalog2.reload(tenantId2);
|
|
49100
|
+
void publishPluginCatalogChanged(tenantId2);
|
|
49101
|
+
} catch {
|
|
49102
|
+
}
|
|
49103
|
+
if (failure3) return JSON.stringify({ restored: false, error: failure3 });
|
|
49104
|
+
return JSON.stringify({ restored: true, files: restored?.files ?? 0 });
|
|
49105
|
+
}
|
|
49106
|
+
case "create": {
|
|
49107
|
+
if (!input.type) return "type is required";
|
|
48568
49108
|
const known = new Set(
|
|
48569
49109
|
(catalog2.getCached(tenantId2)?.plugins ?? []).map((p) => p.meta.type).filter((t) => t !== input.type)
|
|
48570
49110
|
);
|
|
49111
|
+
const stub = {
|
|
49112
|
+
schemaVersion: 1,
|
|
49113
|
+
type: input.type,
|
|
49114
|
+
name: input.name ?? input.type,
|
|
49115
|
+
description: input.description ?? "",
|
|
49116
|
+
version: "0.1.0",
|
|
49117
|
+
tools: []
|
|
49118
|
+
};
|
|
48571
49119
|
const check = validateSandboxPluginManifest({
|
|
48572
|
-
raw:
|
|
49120
|
+
raw: stub,
|
|
48573
49121
|
dirName: input.type,
|
|
48574
49122
|
reservedTypes: new Set(PluginRegistry.list()),
|
|
48575
49123
|
knownTypes: known,
|
|
48576
|
-
handlerExists: (
|
|
49124
|
+
handlerExists: () => true
|
|
48577
49125
|
});
|
|
48578
|
-
if (!check.manifest)
|
|
48579
|
-
|
|
49126
|
+
if (!check.manifest) {
|
|
49127
|
+
return JSON.stringify({ created: false, diagnostics: check.diagnostics });
|
|
49128
|
+
}
|
|
49129
|
+
const files = await catalog2.listPluginFiles(tenantId2, input.type);
|
|
49130
|
+
if (files.includes("plugin.json")) {
|
|
49131
|
+
return JSON.stringify({ created: false, error: "plugin already exists" });
|
|
49132
|
+
}
|
|
49133
|
+
await catalog2.writePlugin(tenantId2, input.type, check.manifest, {});
|
|
48580
49134
|
const loaded = await catalog2.reload(tenantId2);
|
|
48581
49135
|
void publishPluginCatalogChanged(tenantId2);
|
|
48582
|
-
return JSON.stringify({
|
|
49136
|
+
return JSON.stringify({
|
|
49137
|
+
created: true,
|
|
49138
|
+
path: `${PLUGINS_ROOT3}/${input.type}`,
|
|
49139
|
+
diagnostics: loaded.diagnostics
|
|
49140
|
+
});
|
|
48583
49141
|
}
|
|
48584
49142
|
case "delete": {
|
|
48585
49143
|
if (!input.type) return "type is required";
|
|
@@ -48590,18 +49148,8 @@ var sandboxPluginsPlugin = {
|
|
|
48590
49148
|
}
|
|
48591
49149
|
case "validate": {
|
|
48592
49150
|
if (!input.type) return "type is required";
|
|
48593
|
-
const
|
|
48594
|
-
|
|
48595
|
-
(catalog2.getCached(tenantId2)?.plugins ?? []).map((p) => p.meta.type).filter((t) => t !== input.type)
|
|
48596
|
-
);
|
|
48597
|
-
const result = validateSandboxPluginManifest({
|
|
48598
|
-
raw: input.manifest ?? {},
|
|
48599
|
-
dirName: input.type,
|
|
48600
|
-
reservedTypes: new Set(PluginRegistry.list()),
|
|
48601
|
-
knownTypes: known,
|
|
48602
|
-
handlerExists: (rel) => rel in handlers
|
|
48603
|
-
});
|
|
48604
|
-
return JSON.stringify({ diagnostics: result.diagnostics, valid: Boolean(result.manifest) });
|
|
49151
|
+
const result = await catalog2.validatePluginFromDisk(tenantId2, input.type);
|
|
49152
|
+
return JSON.stringify({ diagnostics: result.diagnostics, valid: result.valid });
|
|
48605
49153
|
}
|
|
48606
49154
|
case "test": {
|
|
48607
49155
|
if (!input.type || !input.target) return "type and target are required";
|
|
@@ -48675,7 +49223,16 @@ var sandboxPluginsPlugin = {
|
|
|
48675
49223
|
assistantId: typeof runCfg.assistant_id === "string" ? runCfg.assistant_id : void 0
|
|
48676
49224
|
}
|
|
48677
49225
|
});
|
|
48678
|
-
|
|
49226
|
+
const text = handlerResultText(redactSecretValues(result, secrets));
|
|
49227
|
+
const ui = toolDef.ui;
|
|
49228
|
+
if (!ui?.resource) return text;
|
|
49229
|
+
const uiRef = {
|
|
49230
|
+
kind: "plugin",
|
|
49231
|
+
pluginType: input.type,
|
|
49232
|
+
resource: ui.resource,
|
|
49233
|
+
displayMode: ui.displayMode ?? "inline"
|
|
49234
|
+
};
|
|
49235
|
+
return appendUiFence(text, uiRef);
|
|
48679
49236
|
} catch (err) {
|
|
48680
49237
|
return JSON.stringify(redactSecretValues({ ok: false, error: errorText(err) }, secrets));
|
|
48681
49238
|
}
|
|
@@ -48747,7 +49304,7 @@ var sandboxPluginsPlugin = {
|
|
|
48747
49304
|
},
|
|
48748
49305
|
{
|
|
48749
49306
|
name: "manage_plugin",
|
|
48750
|
-
description: "Author and manage tenant sandbox plugins (list/get/validate/create/
|
|
49307
|
+
description: "Author and manage tenant sandbox plugins (list/get/validate/create/delete/reload/test/backup/restore) and deploy/undeploy/status a plugin as a resident service.",
|
|
48751
49308
|
schema: managePluginSchema
|
|
48752
49309
|
}
|
|
48753
49310
|
)
|
|
@@ -49923,6 +50480,7 @@ export {
|
|
|
49923
50480
|
buildTableName,
|
|
49924
50481
|
buildTaskMutationFallback,
|
|
49925
50482
|
buildTaskNormalizationFallback,
|
|
50483
|
+
canConvergeOrphan,
|
|
49926
50484
|
checkEmptyContent,
|
|
49927
50485
|
clearEncryptionKeyCache,
|
|
49928
50486
|
clearEvalRunService,
|
|
@@ -49972,6 +50530,7 @@ export {
|
|
|
49972
50530
|
createTaskLifecycleService,
|
|
49973
50531
|
createTaskMiddleware,
|
|
49974
50532
|
createTaskRunBridge,
|
|
50533
|
+
createTaskRunLiveness,
|
|
49975
50534
|
createTeamMiddleware,
|
|
49976
50535
|
createTeammateTools,
|
|
49977
50536
|
createTrustedAgentMessageDispatcher,
|
|
@@ -50069,6 +50628,7 @@ export {
|
|
|
50069
50628
|
isMcpConnectionError,
|
|
50070
50629
|
isPluginSkillAuthorizedInSandbox,
|
|
50071
50630
|
isPluginSkillManagedInSandbox,
|
|
50631
|
+
isTaskRunLeaseHeld,
|
|
50072
50632
|
isUsingDefaultKey,
|
|
50073
50633
|
isValidCronExpression,
|
|
50074
50634
|
isValidSandboxName,
|