@portel/photon 1.17.6 → 1.18.0
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/auto-ui/beam/photon-management.d.ts.map +1 -1
- package/dist/auto-ui/beam/photon-management.js +28 -1
- package/dist/auto-ui/beam/photon-management.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js +10 -5
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +259 -88
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +2 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js +5 -0
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +226 -29
- package/dist/beam.bundle.js.map +2 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +9 -8
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +11 -0
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +155 -1
- package/dist/server.js.map +1 -1
- package/dist/tasks/executor.d.ts +47 -0
- package/dist/tasks/executor.d.ts.map +1 -0
- package/dist/tasks/executor.js +180 -0
- package/dist/tasks/executor.js.map +1 -0
- package/dist/tasks/store.d.ts +13 -6
- package/dist/tasks/store.d.ts.map +1 -1
- package/dist/tasks/store.js +50 -9
- package/dist/tasks/store.js.map +1 -1
- package/dist/tasks/types.d.ts +23 -2
- package/dist/tasks/types.d.ts.map +1 -1
- package/dist/tasks/types.js +23 -3
- package/dist/tasks/types.js.map +1 -1
- package/package.json +5 -4
|
@@ -28,7 +28,9 @@ import { buildToolMetadataExtensions } from './types.js';
|
|
|
28
28
|
import { generateServerCard } from '../server-card.js';
|
|
29
29
|
import { audit } from '../shared/audit.js';
|
|
30
30
|
import { writePhotonEditorDeclaration } from '../photon-editor-declarations.js';
|
|
31
|
-
import { createTask, getTask, updateTask, listTasks, registerController, unregisterController, getController, } from '../tasks/store.js';
|
|
31
|
+
import { createTask, getTask, updateTask, listTasks, registerController, unregisterController, getController, taskEvents, } from '../tasks/store.js';
|
|
32
|
+
import { toWireFormat, relatedTaskMeta, TERMINAL_STATES } from '../tasks/types.js';
|
|
33
|
+
import { runTaskExecution, resolveTaskInput, waitForTerminalOrInput } from '../tasks/executor.js';
|
|
32
34
|
import { generateAgentCard } from '../a2a/card-generator.js';
|
|
33
35
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
34
36
|
// JWT HELPERS
|
|
@@ -346,7 +348,13 @@ const handlers = {
|
|
|
346
348
|
tools: { listChanged: true },
|
|
347
349
|
prompts: { listChanged: true },
|
|
348
350
|
resources: { listChanged: true },
|
|
349
|
-
tasks: {
|
|
351
|
+
tasks: {
|
|
352
|
+
list: {},
|
|
353
|
+
cancel: {},
|
|
354
|
+
requests: {
|
|
355
|
+
tools: { call: {} },
|
|
356
|
+
},
|
|
357
|
+
},
|
|
350
358
|
experimental: {
|
|
351
359
|
'ag-ui': {
|
|
352
360
|
version: '0.1.0',
|
|
@@ -777,7 +785,7 @@ const handlers = {
|
|
|
777
785
|
});
|
|
778
786
|
tools.push({
|
|
779
787
|
name: 'beam/remove',
|
|
780
|
-
description: 'Remove a photon from the workspace',
|
|
788
|
+
description: 'Remove a photon from the workspace (moves to trash)',
|
|
781
789
|
inputSchema: {
|
|
782
790
|
type: 'object',
|
|
783
791
|
properties: {
|
|
@@ -788,6 +796,7 @@ const handlers = {
|
|
|
788
796
|
},
|
|
789
797
|
required: ['photon'],
|
|
790
798
|
},
|
|
799
|
+
annotations: { destructiveHint: true },
|
|
791
800
|
});
|
|
792
801
|
tools.push({
|
|
793
802
|
name: 'beam/photon-help',
|
|
@@ -1318,6 +1327,54 @@ const handlers = {
|
|
|
1318
1327
|
},
|
|
1319
1328
|
};
|
|
1320
1329
|
}
|
|
1330
|
+
// ── Task mode: when params.task is present, run async and return immediately ──
|
|
1331
|
+
const taskRequest = req.params?.task;
|
|
1332
|
+
if (taskRequest) {
|
|
1333
|
+
const ttl = typeof taskRequest.ttl === 'number' ? taskRequest.ttl : undefined;
|
|
1334
|
+
const task = createTask(photonName, methodName, args, ttl);
|
|
1335
|
+
const controller = new AbortController();
|
|
1336
|
+
registerController(task.id, controller);
|
|
1337
|
+
// Build execution function that the executor will run
|
|
1338
|
+
const executeFn = async (inputProvider, outputHandler) => {
|
|
1339
|
+
if (ctx.loader) {
|
|
1340
|
+
return ctx.loader.executeTool(mcp, methodName, args || {}, {
|
|
1341
|
+
outputHandler,
|
|
1342
|
+
inputProvider,
|
|
1343
|
+
caller: ctx.caller,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
// Fallback: direct method call
|
|
1347
|
+
const target = isStatic ? mcp.classConstructor : mcp.instance;
|
|
1348
|
+
return target[methodName](args || {});
|
|
1349
|
+
};
|
|
1350
|
+
// Broadcast progress/status from task execution
|
|
1351
|
+
const taskOutputHandler = (yieldValue) => {
|
|
1352
|
+
if (!ctx.broadcast)
|
|
1353
|
+
return;
|
|
1354
|
+
if (yieldValue?.emit === 'progress' || yieldValue?.emit === 'status') {
|
|
1355
|
+
ctx.broadcast({
|
|
1356
|
+
jsonrpc: '2.0',
|
|
1357
|
+
method: 'notifications/progress',
|
|
1358
|
+
params: {
|
|
1359
|
+
progressToken: `task_${task.id}`,
|
|
1360
|
+
progress: yieldValue?.emit === 'progress' ? (yieldValue.value ?? 0) : 0,
|
|
1361
|
+
total: 100,
|
|
1362
|
+
message: yieldValue.message || '',
|
|
1363
|
+
},
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
runTaskExecution(task.id, executeFn, {
|
|
1368
|
+
signal: controller.signal,
|
|
1369
|
+
caller: ctx.caller,
|
|
1370
|
+
outputHandler: taskOutputHandler,
|
|
1371
|
+
});
|
|
1372
|
+
return {
|
|
1373
|
+
jsonrpc: '2.0',
|
|
1374
|
+
id: req.id,
|
|
1375
|
+
result: { task: toWireFormat(task) },
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1321
1378
|
try {
|
|
1322
1379
|
// Create outputHandler to capture emits for real-time UI updates
|
|
1323
1380
|
const outputHandler = (yieldValue) => {
|
|
@@ -1811,7 +1868,7 @@ const handlers = {
|
|
|
1811
1868
|
// MCP Tasks (2025-11-25 spec)
|
|
1812
1869
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1813
1870
|
'tasks/create': async (req, session, ctx) => {
|
|
1814
|
-
const { photon: photonName, method: methodName, params, } = req.params;
|
|
1871
|
+
const { photon: photonName, method: methodName, params, ttl: requestedTtl, } = req.params;
|
|
1815
1872
|
if (!photonName || !methodName) {
|
|
1816
1873
|
return {
|
|
1817
1874
|
jsonrpc: '2.0',
|
|
@@ -1820,139 +1877,234 @@ const handlers = {
|
|
|
1820
1877
|
};
|
|
1821
1878
|
}
|
|
1822
1879
|
const mcp = ctx.photonMCPs.get(photonName);
|
|
1823
|
-
if (!mcp) {
|
|
1880
|
+
if (!mcp?.instance) {
|
|
1824
1881
|
return {
|
|
1825
1882
|
jsonrpc: '2.0',
|
|
1826
1883
|
id: req.id,
|
|
1827
1884
|
error: { code: -32602, message: `Photon not found: ${photonName}` },
|
|
1828
1885
|
};
|
|
1829
1886
|
}
|
|
1830
|
-
const task = createTask(photonName, methodName, params);
|
|
1887
|
+
const task = createTask(photonName, methodName, params, requestedTtl);
|
|
1831
1888
|
const controller = new AbortController();
|
|
1832
1889
|
registerController(task.id, controller);
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
caller: ctx.caller,
|
|
1841
|
-
signal: controller.signal,
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
else {
|
|
1845
|
-
const method = mcp.instance?.[methodName];
|
|
1846
|
-
if (typeof method === 'function') {
|
|
1847
|
-
result = await method.call(mcp.instance, params || {});
|
|
1848
|
-
}
|
|
1849
|
-
else {
|
|
1850
|
-
throw new Error(`Method ${methodName} not found on ${photonName}`);
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
// Handle async generators
|
|
1854
|
-
if (result && typeof result[Symbol.asyncIterator] === 'function') {
|
|
1855
|
-
const chunks = [];
|
|
1856
|
-
const iterator = result[Symbol.asyncIterator]();
|
|
1857
|
-
while (true) {
|
|
1858
|
-
if (controller.signal.aborted) {
|
|
1859
|
-
updateTask(taskId, { state: 'cancelled' });
|
|
1860
|
-
unregisterController(taskId);
|
|
1861
|
-
return;
|
|
1862
|
-
}
|
|
1863
|
-
const { value, done } = await iterator.next();
|
|
1864
|
-
if (done) {
|
|
1865
|
-
result = value !== undefined ? value : chunks;
|
|
1866
|
-
break;
|
|
1867
|
-
}
|
|
1868
|
-
if (value?.emit === 'progress' && typeof value.percent === 'number') {
|
|
1869
|
-
updateTask(taskId, {
|
|
1870
|
-
progress: { percent: value.percent, message: value.message },
|
|
1871
|
-
});
|
|
1872
|
-
}
|
|
1873
|
-
else if (value?.ask) {
|
|
1874
|
-
updateTask(taskId, { state: 'input_required' });
|
|
1875
|
-
}
|
|
1876
|
-
else {
|
|
1877
|
-
chunks.push(value);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
if (!controller.signal.aborted) {
|
|
1882
|
-
updateTask(taskId, { state: 'completed', result });
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
catch (err) {
|
|
1886
|
-
if (!controller.signal.aborted) {
|
|
1887
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1888
|
-
updateTask(taskId, { state: 'failed', error: message });
|
|
1889
|
-
}
|
|
1890
|
+
const executeFn = async (inputProvider, outputHandler) => {
|
|
1891
|
+
if (ctx.loader) {
|
|
1892
|
+
return ctx.loader.executeTool(mcp, methodName, params || {}, {
|
|
1893
|
+
outputHandler,
|
|
1894
|
+
inputProvider,
|
|
1895
|
+
caller: ctx.caller,
|
|
1896
|
+
});
|
|
1890
1897
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1898
|
+
const method = mcp.instance?.[methodName];
|
|
1899
|
+
if (typeof method !== 'function') {
|
|
1900
|
+
throw new Error(`Method ${methodName} not found on ${photonName}`);
|
|
1893
1901
|
}
|
|
1894
|
-
|
|
1902
|
+
return method.call(mcp.instance, params || {});
|
|
1903
|
+
};
|
|
1904
|
+
runTaskExecution(task.id, executeFn, {
|
|
1905
|
+
signal: controller.signal,
|
|
1906
|
+
caller: ctx.caller,
|
|
1907
|
+
});
|
|
1895
1908
|
return {
|
|
1896
1909
|
jsonrpc: '2.0',
|
|
1897
1910
|
id: req.id,
|
|
1898
|
-
result: { task:
|
|
1911
|
+
result: { task: toWireFormat(task) },
|
|
1899
1912
|
};
|
|
1900
1913
|
},
|
|
1901
1914
|
'tasks/get': async (req, _session, _ctx) => {
|
|
1902
|
-
const {
|
|
1903
|
-
if (!
|
|
1915
|
+
const { taskId } = req.params;
|
|
1916
|
+
if (!taskId) {
|
|
1904
1917
|
return {
|
|
1905
1918
|
jsonrpc: '2.0',
|
|
1906
1919
|
id: req.id,
|
|
1907
|
-
error: { code: -32602, message: 'Missing required param:
|
|
1920
|
+
error: { code: -32602, message: 'Missing required param: taskId' },
|
|
1908
1921
|
};
|
|
1909
1922
|
}
|
|
1910
|
-
const task = getTask(
|
|
1923
|
+
const task = getTask(taskId);
|
|
1911
1924
|
if (!task) {
|
|
1912
1925
|
return {
|
|
1913
1926
|
jsonrpc: '2.0',
|
|
1914
1927
|
id: req.id,
|
|
1915
|
-
error: { code: -32602, message: `Task not found: ${
|
|
1928
|
+
error: { code: -32602, message: `Task not found: ${taskId}` },
|
|
1916
1929
|
};
|
|
1917
1930
|
}
|
|
1918
|
-
return { jsonrpc: '2.0', id: req.id, result:
|
|
1931
|
+
return { jsonrpc: '2.0', id: req.id, result: toWireFormat(task) };
|
|
1919
1932
|
},
|
|
1920
1933
|
'tasks/list': async (req, _session, _ctx) => {
|
|
1921
|
-
const {
|
|
1922
|
-
const
|
|
1923
|
-
|
|
1934
|
+
const { cursor } = (req.params || {});
|
|
1935
|
+
const allTasks = listTasks();
|
|
1936
|
+
// Simple pagination: cursor is the offset index
|
|
1937
|
+
const offset = cursor ? parseInt(cursor, 10) || 0 : 0;
|
|
1938
|
+
const pageSize = 50;
|
|
1939
|
+
const page = allTasks.slice(offset, offset + pageSize);
|
|
1940
|
+
const nextCursor = offset + pageSize < allTasks.length ? String(offset + pageSize) : undefined;
|
|
1941
|
+
return {
|
|
1942
|
+
jsonrpc: '2.0',
|
|
1943
|
+
id: req.id,
|
|
1944
|
+
result: {
|
|
1945
|
+
tasks: page.map(toWireFormat),
|
|
1946
|
+
...(nextCursor && { nextCursor }),
|
|
1947
|
+
},
|
|
1948
|
+
};
|
|
1924
1949
|
},
|
|
1925
1950
|
'tasks/cancel': async (req, _session, _ctx) => {
|
|
1926
|
-
const {
|
|
1927
|
-
if (!
|
|
1951
|
+
const { taskId } = req.params;
|
|
1952
|
+
if (!taskId) {
|
|
1928
1953
|
return {
|
|
1929
1954
|
jsonrpc: '2.0',
|
|
1930
1955
|
id: req.id,
|
|
1931
|
-
error: { code: -32602, message: 'Missing required param:
|
|
1956
|
+
error: { code: -32602, message: 'Missing required param: taskId' },
|
|
1932
1957
|
};
|
|
1933
1958
|
}
|
|
1934
|
-
const task = getTask(
|
|
1959
|
+
const task = getTask(taskId);
|
|
1935
1960
|
if (!task) {
|
|
1936
1961
|
return {
|
|
1937
1962
|
jsonrpc: '2.0',
|
|
1938
1963
|
id: req.id,
|
|
1939
|
-
error: { code: -32602, message: `Task not found: ${
|
|
1964
|
+
error: { code: -32602, message: `Task not found: ${taskId}` },
|
|
1940
1965
|
};
|
|
1941
1966
|
}
|
|
1942
|
-
if (
|
|
1967
|
+
if (TERMINAL_STATES.includes(task.state)) {
|
|
1943
1968
|
return {
|
|
1944
1969
|
jsonrpc: '2.0',
|
|
1945
1970
|
id: req.id,
|
|
1946
|
-
error: { code: -32602, message: `Cannot cancel task in state: ${task.state}` },
|
|
1971
|
+
error: { code: -32602, message: `Cannot cancel task in terminal state: ${task.state}` },
|
|
1947
1972
|
};
|
|
1948
1973
|
}
|
|
1949
|
-
const controller = getController(
|
|
1950
|
-
if (controller)
|
|
1974
|
+
const controller = getController(taskId);
|
|
1975
|
+
if (controller)
|
|
1951
1976
|
controller.abort();
|
|
1977
|
+
const updated = updateTask(taskId, {
|
|
1978
|
+
state: 'cancelled',
|
|
1979
|
+
statusMessage: 'The task was cancelled by request.',
|
|
1980
|
+
});
|
|
1981
|
+
unregisterController(taskId);
|
|
1982
|
+
return { jsonrpc: '2.0', id: req.id, result: toWireFormat(updated) };
|
|
1983
|
+
},
|
|
1984
|
+
'tasks/result': async (req, session, ctx) => {
|
|
1985
|
+
const { taskId } = req.params;
|
|
1986
|
+
if (!taskId) {
|
|
1987
|
+
return {
|
|
1988
|
+
jsonrpc: '2.0',
|
|
1989
|
+
id: req.id,
|
|
1990
|
+
error: { code: -32602, message: 'Missing required param: taskId' },
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
const task = getTask(taskId);
|
|
1994
|
+
if (!task) {
|
|
1995
|
+
return {
|
|
1996
|
+
jsonrpc: '2.0',
|
|
1997
|
+
id: req.id,
|
|
1998
|
+
error: { code: -32602, message: `Task not found: ${taskId}` },
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
// Helper to format terminal task result as CallToolResult
|
|
2002
|
+
const formatResult = (t) => {
|
|
2003
|
+
if (t.state === 'failed') {
|
|
2004
|
+
return {
|
|
2005
|
+
jsonrpc: '2.0',
|
|
2006
|
+
id: req.id,
|
|
2007
|
+
result: {
|
|
2008
|
+
content: [{ type: 'text', text: t.error || 'Task failed' }],
|
|
2009
|
+
isError: true,
|
|
2010
|
+
_meta: relatedTaskMeta(taskId),
|
|
2011
|
+
},
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
if (t.state === 'cancelled') {
|
|
2015
|
+
return {
|
|
2016
|
+
jsonrpc: '2.0',
|
|
2017
|
+
id: req.id,
|
|
2018
|
+
result: {
|
|
2019
|
+
content: [{ type: 'text', text: 'Task was cancelled.' }],
|
|
2020
|
+
isError: false,
|
|
2021
|
+
_meta: relatedTaskMeta(taskId),
|
|
2022
|
+
},
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
// Completed — result is already a CallToolResult or raw value
|
|
2026
|
+
if (t.result && typeof t.result === 'object' && 'content' in t.result) {
|
|
2027
|
+
// Already CallToolResult format
|
|
2028
|
+
return {
|
|
2029
|
+
jsonrpc: '2.0',
|
|
2030
|
+
id: req.id,
|
|
2031
|
+
result: {
|
|
2032
|
+
...t.result,
|
|
2033
|
+
_meta: relatedTaskMeta(taskId),
|
|
2034
|
+
},
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
// Raw result — wrap in CallToolResult
|
|
2038
|
+
const text = typeof t.result === 'string' ? t.result : JSON.stringify(t.result ?? null);
|
|
2039
|
+
return {
|
|
2040
|
+
jsonrpc: '2.0',
|
|
2041
|
+
id: req.id,
|
|
2042
|
+
result: {
|
|
2043
|
+
content: [{ type: 'text', text }],
|
|
2044
|
+
isError: false,
|
|
2045
|
+
_meta: relatedTaskMeta(taskId),
|
|
2046
|
+
},
|
|
2047
|
+
};
|
|
2048
|
+
};
|
|
2049
|
+
// Already terminal — return immediately
|
|
2050
|
+
if (TERMINAL_STATES.includes(task.state)) {
|
|
2051
|
+
return formatResult(task);
|
|
2052
|
+
}
|
|
2053
|
+
// If input_required right now, handle elicitation before waiting
|
|
2054
|
+
if (task.state === 'input_required' && task.input) {
|
|
2055
|
+
const elicitResult = await requestBeamElicitation(task.input);
|
|
2056
|
+
if (elicitResult.action === 'accept') {
|
|
2057
|
+
resolveTaskInput(taskId, elicitResult.content);
|
|
2058
|
+
}
|
|
2059
|
+
else {
|
|
2060
|
+
resolveTaskInput(taskId, null);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
// Block until terminal state, handling input_required along the way
|
|
2064
|
+
// Use a timeout based on TTL to avoid infinite blocking
|
|
2065
|
+
const timeoutMs = Math.min(task.ttl, 300000); // Max 5 min block per call
|
|
2066
|
+
const abortController = new AbortController();
|
|
2067
|
+
const timeout = setTimeout(() => abortController.abort(), timeoutMs);
|
|
2068
|
+
try {
|
|
2069
|
+
while (true) {
|
|
2070
|
+
const current = await waitForTerminalOrInput(taskId, abortController.signal);
|
|
2071
|
+
if (TERMINAL_STATES.includes(current.state)) {
|
|
2072
|
+
return formatResult(current);
|
|
2073
|
+
}
|
|
2074
|
+
if (current.state === 'input_required' && current.input) {
|
|
2075
|
+
// Send elicitation to the client
|
|
2076
|
+
const elicitResult = await requestBeamElicitation(current.input);
|
|
2077
|
+
if (elicitResult.action === 'accept') {
|
|
2078
|
+
resolveTaskInput(taskId, elicitResult.content);
|
|
2079
|
+
}
|
|
2080
|
+
else {
|
|
2081
|
+
resolveTaskInput(taskId, null);
|
|
2082
|
+
}
|
|
2083
|
+
// Continue loop — wait for next state change
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
catch {
|
|
2088
|
+
// Timeout or abort — return current state info
|
|
2089
|
+
const current = getTask(taskId);
|
|
2090
|
+
if (current && TERMINAL_STATES.includes(current.state)) {
|
|
2091
|
+
return formatResult(current);
|
|
2092
|
+
}
|
|
2093
|
+
return {
|
|
2094
|
+
jsonrpc: '2.0',
|
|
2095
|
+
id: req.id,
|
|
2096
|
+
result: {
|
|
2097
|
+
content: [
|
|
2098
|
+
{ type: 'text', text: `Task ${taskId} is still running. Poll tasks/get for status.` },
|
|
2099
|
+
],
|
|
2100
|
+
isError: false,
|
|
2101
|
+
_meta: relatedTaskMeta(taskId),
|
|
2102
|
+
},
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
finally {
|
|
2106
|
+
clearTimeout(timeout);
|
|
1952
2107
|
}
|
|
1953
|
-
const updated = updateTask(id, { state: 'cancelled' });
|
|
1954
|
-
unregisterController(id);
|
|
1955
|
-
return { jsonrpc: '2.0', id: req.id, result: { task: updated } };
|
|
1956
2108
|
},
|
|
1957
2109
|
};
|
|
1958
2110
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
@@ -2204,6 +2356,21 @@ async function handleBeamRemove(req, ctx, args) {
|
|
|
2204
2356
|
},
|
|
2205
2357
|
};
|
|
2206
2358
|
}
|
|
2359
|
+
// Require explicit confirmation before removing
|
|
2360
|
+
const elicitResult = await requestBeamElicitation({
|
|
2361
|
+
ask: 'confirm',
|
|
2362
|
+
message: `Remove "${photonName}"? The photon and its assets will be moved to trash.`,
|
|
2363
|
+
});
|
|
2364
|
+
if (elicitResult.action !== 'accept' || elicitResult.content === false) {
|
|
2365
|
+
return {
|
|
2366
|
+
jsonrpc: '2.0',
|
|
2367
|
+
id: req.id,
|
|
2368
|
+
result: {
|
|
2369
|
+
content: [{ type: 'text', text: `Remove cancelled` }],
|
|
2370
|
+
isError: false,
|
|
2371
|
+
},
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2207
2374
|
try {
|
|
2208
2375
|
const result = await ctx.removePhoton(photonName);
|
|
2209
2376
|
if (result.success) {
|
|
@@ -3148,6 +3315,10 @@ export function broadcastNotification(method, params, beamOnly = false) {
|
|
|
3148
3315
|
export function broadcastToBeam(method, params) {
|
|
3149
3316
|
broadcastNotification(method, params, true);
|
|
3150
3317
|
}
|
|
3318
|
+
// ── Task status change notifications (MCP 2025-11-25) ──
|
|
3319
|
+
taskEvents.on('stateChange', (_taskId, _newState, task) => {
|
|
3320
|
+
broadcastNotification('notifications/tasks/status', toWireFormat(task));
|
|
3321
|
+
});
|
|
3151
3322
|
/**
|
|
3152
3323
|
* Get count of active sessions (for debugging)
|
|
3153
3324
|
*/
|