@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.
Files changed (37) hide show
  1. package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
  2. package/dist/auto-ui/beam/photon-management.js +28 -1
  3. package/dist/auto-ui/beam/photon-management.js.map +1 -1
  4. package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
  5. package/dist/auto-ui/beam/routes/api-marketplace.js +10 -5
  6. package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
  7. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  8. package/dist/auto-ui/streamable-http-transport.js +259 -88
  9. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  10. package/dist/auto-ui/types.d.ts +2 -0
  11. package/dist/auto-ui/types.d.ts.map +1 -1
  12. package/dist/auto-ui/types.js +5 -0
  13. package/dist/auto-ui/types.js.map +1 -1
  14. package/dist/beam.bundle.js +226 -29
  15. package/dist/beam.bundle.js.map +2 -2
  16. package/dist/loader.d.ts.map +1 -1
  17. package/dist/loader.js +9 -8
  18. package/dist/loader.js.map +1 -1
  19. package/dist/photon-cli-runner.d.ts.map +1 -1
  20. package/dist/photon-cli-runner.js +11 -0
  21. package/dist/photon-cli-runner.js.map +1 -1
  22. package/dist/server.d.ts.map +1 -1
  23. package/dist/server.js +155 -1
  24. package/dist/server.js.map +1 -1
  25. package/dist/tasks/executor.d.ts +47 -0
  26. package/dist/tasks/executor.d.ts.map +1 -0
  27. package/dist/tasks/executor.js +180 -0
  28. package/dist/tasks/executor.js.map +1 -0
  29. package/dist/tasks/store.d.ts +13 -6
  30. package/dist/tasks/store.d.ts.map +1 -1
  31. package/dist/tasks/store.js +50 -9
  32. package/dist/tasks/store.js.map +1 -1
  33. package/dist/tasks/types.d.ts +23 -2
  34. package/dist/tasks/types.d.ts.map +1 -1
  35. package/dist/tasks/types.js +23 -3
  36. package/dist/tasks/types.js.map +1 -1
  37. 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
- // Run execution in background don't await
1834
- const taskId = task.id;
1835
- void (async () => {
1836
- try {
1837
- let result;
1838
- if (ctx.loader) {
1839
- result = await ctx.loader.executeTool(mcp, methodName, params || {}, {
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
- finally {
1892
- unregisterController(taskId);
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: { id: task.id, state: task.state, createdAt: task.createdAt } },
1911
+ result: { task: toWireFormat(task) },
1899
1912
  };
1900
1913
  },
1901
1914
  'tasks/get': async (req, _session, _ctx) => {
1902
- const { id } = req.params;
1903
- if (!id) {
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: id' },
1920
+ error: { code: -32602, message: 'Missing required param: taskId' },
1908
1921
  };
1909
1922
  }
1910
- const task = getTask(id);
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: ${id}` },
1928
+ error: { code: -32602, message: `Task not found: ${taskId}` },
1916
1929
  };
1917
1930
  }
1918
- return { jsonrpc: '2.0', id: req.id, result: { task } };
1931
+ return { jsonrpc: '2.0', id: req.id, result: toWireFormat(task) };
1919
1932
  },
1920
1933
  'tasks/list': async (req, _session, _ctx) => {
1921
- const { photon: photonFilter } = (req.params || {});
1922
- const tasks = listTasks(photonFilter);
1923
- return { jsonrpc: '2.0', id: req.id, result: { tasks } };
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 { id } = req.params;
1927
- if (!id) {
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: id' },
1956
+ error: { code: -32602, message: 'Missing required param: taskId' },
1932
1957
  };
1933
1958
  }
1934
- const task = getTask(id);
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: ${id}` },
1964
+ error: { code: -32602, message: `Task not found: ${taskId}` },
1940
1965
  };
1941
1966
  }
1942
- if (task.state !== 'working' && task.state !== 'input_required') {
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(id);
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
  */