experimental-ash 0.18.2 → 0.19.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/CHANGELOG.md +14 -0
- package/dist/docs/internals/context.md +7 -0
- package/dist/docs/public/channels/README.md +5 -0
- package/dist/docs/public/channels/slack.md +58 -4
- package/dist/docs/public/hooks.md +4 -2
- package/dist/docs/public/sandbox.md +71 -49
- package/dist/docs/public/typescript-api.md +6 -1
- package/dist/src/channel/adapter.js +12 -2
- package/dist/src/channel/routes.d.ts +9 -1
- package/dist/src/channel/send.js +3 -3
- package/dist/src/channel/types.d.ts +3 -1
- package/dist/src/chunks/{dev-authored-source-watcher-j7YWh2Gx.js → dev-authored-source-watcher-L3_pagDa.js} +1 -1
- package/dist/src/chunks/{host-DkTSR6YJ.js → host-e2GUqnTr.js} +3 -3
- package/dist/src/chunks/{paths-Dwv0Eash.js → paths-BBleOGpa.js} +25 -25
- package/dist/src/chunks/{prewarm-CQYfka30.js → prewarm-DEymC5M-.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/compiled/.vendor-stamp.json +1 -1
- package/dist/src/compiled/@vercel/sandbox/index.d.ts +6 -1
- package/dist/src/context/hook-lifecycle.js +5 -1
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/sandbox/bindings/local.d.ts +14 -1
- package/dist/src/execution/sandbox/bindings/local.js +5 -1
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +6 -0
- package/dist/src/execution/sandbox/bindings/vercel.js +12 -1
- package/dist/src/execution/sandbox/lazy-backend.d.ts +15 -0
- package/dist/src/execution/sandbox/lazy-backend.js +33 -0
- package/dist/src/execution/workflow-entry.d.ts +2 -4
- package/dist/src/execution/workflow-entry.js +1 -1
- package/dist/src/harness/messages.js +15 -0
- package/dist/src/harness/types.d.ts +6 -7
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/authored-definition/sandbox.d.ts +8 -2
- package/dist/src/internal/authored-definition/sandbox.js +10 -2
- package/dist/src/internal/workflow-bundle/vercel-workflow-output.js +0 -2
- package/dist/src/public/channels/slack/api.d.ts +2 -27
- package/dist/src/public/channels/slack/api.js +6 -82
- package/dist/src/public/channels/slack/defaults.js +2 -2
- package/dist/src/public/channels/slack/hitl.js +6 -3
- package/dist/src/public/channels/slack/inbound.js +1 -1
- package/dist/src/public/channels/slack/index.d.ts +3 -0
- package/dist/src/public/channels/slack/index.js +2 -0
- package/dist/src/public/channels/slack/limits.d.ts +19 -0
- package/dist/src/public/channels/slack/limits.js +23 -0
- package/dist/src/public/channels/slack/mrkdwn.d.ts +38 -0
- package/dist/src/public/channels/slack/mrkdwn.js +89 -0
- package/dist/src/public/channels/slack/slackChannel.d.ts +12 -4
- package/dist/src/public/channels/slack/slackChannel.js +5 -8
- package/dist/src/public/channels/slack/thread.d.ts +26 -0
- package/dist/src/public/channels/slack/thread.js +45 -0
- package/dist/src/public/definitions/defineChannel.d.ts +1 -1
- package/dist/src/public/sandbox/backends/default.d.ts +16 -1
- package/dist/src/public/sandbox/backends/default.js +7 -19
- package/dist/src/public/sandbox/backends/local.d.ts +7 -4
- package/dist/src/public/sandbox/backends/local.js +7 -5
- package/dist/src/public/sandbox/backends/vercel.d.ts +9 -3
- package/dist/src/public/sandbox/backends/vercel.js +9 -3
- package/dist/src/public/sandbox/index.d.ts +2 -1
- package/dist/src/public/sandbox/local-sandbox.d.ts +7 -0
- package/dist/src/public/sandbox/local-sandbox.js +1 -0
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +13 -1
- package/dist/src/runtime/resolve-sandbox.js +5 -1
- package/dist/src/runtime/types.d.ts +10 -1
- package/dist/src/shared/sandbox-definition.d.ts +16 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{E as e,S as t,T as n,_ as r,a as i,c as a,d as o,g as s,h as c,l,m as u,p as ee,s as te,u as d,x as f,y as p}from"./paths-
|
|
1
|
+
import{E as e,S as t,T as n,_ as r,a as i,c as a,d as o,g as s,h as c,l,m as u,p as ee,s as te,u as d,x as f,y as p}from"./paths-BBleOGpa.js";import{t as m}from"./authored-module-loader-XcFLnl49.js";import{t as h}from"./errors-DsO9xmQL.js";import{i as g,t as _}from"./package-DmsQgn4v.js";import{join as v,posix as y}from"node:path";import{mkdir as ne,readFile as re,readdir as ie,realpath as b,writeFile as x}from"node:fs/promises";import{createHash as S}from"node:crypto";import{existsSync as C}from"node:fs";function w(e){return e.dev?{appRoot:e.appRoot,dev:e.dev,moduleMapLoaderPath:g(`src/internal/authored-module-map-loader.ts`)}:{appRoot:e.appRoot,dev:e.dev}}const T=`#ash-channel/`;function E(e){let t=e.compileResult.manifest.channels,n=new Set,r=[],i=new Set,o=a();for(let e of t){if(e.kind===`disabled`){if(!o.has(e.name))throw Error(`agent/channels/${e.name}.ts exports disableRoute() but "${e.name}" is not a framework channel. Rename the file to one of: ${[...o].sort().join(`, `)}.`);i.add(e.name);continue}n.add(e.name),r.push({method:e.method,route:e.urlPath})}let s=l().filter(e=>!n.has(e.name)&&!i.has(e.name)).map(e=>({method:e.method,route:e.urlPath})),c=new Set,u=[];for(let e of[...s,...r]){let t=k(e);c.has(t)||(c.add(t),u.push(e))}return u}function D(e,t){for(let n of t.registrations)A(e,{artifactsConfig:t.artifactsConfig,method:n.method,route:n.route})}function O(e,t){return N(t.previous,t.next)?!1:(j(e),D(e,{artifactsConfig:t.artifactsConfig,registrations:t.next}),e.routing.sync(),!0)}function k(e){return`${e.method.toUpperCase()} ${e.route}`}function A(e,t){let r=k(t),i=`${T}${r}`,a=n(g(`src/internal/nitro/routes/channel-dispatch.ts`));e.options.handlers.push({handler:i,method:t.method,route:t.route}),e.options.virtual[i]=[`import { dispatchChannelRequest } from ${a};`,`const config = ${JSON.stringify(t.artifactsConfig)};`,`export default (event) => dispatchChannelRequest(event, ${JSON.stringify(r)}, config);`].join(`
|
|
2
2
|
`)}function j(e){for(let t=e.options.handlers.length-1;t>=0;--t){let n=e.options.handlers[t];n!==void 0&&M(n)&&e.options.handlers.splice(t,1)}for(let t of Object.keys(e.options.virtual))t.startsWith(T)&&delete e.options.virtual[t]}function M(e){return e.handler.startsWith(T)}function N(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1){let r=e[n],i=t[n];if(r===void 0||i===void 0||r.method!==i.method||r.route!==i.route)return!1}return!0}const P=`ash.schedule.`;var F=class extends Error{scheduleId;sourceId;taskName;constructor(e,t={}){super(e),this.name=`ScheduleRegistrationError`,t.scheduleId!==void 0&&(this.scheduleId=t.scheduleId),t.sourceId!==void 0&&(this.sourceId=t.sourceId),t.taskName!==void 0&&(this.taskName=t.taskName)}};function I(e){let t=e.map(e=>({cron:e.cron,description:`Run Ash schedule "${e.name}" from "${e.logicalPath}".`,logicalPath:e.logicalPath,scheduleId:e.name,sourceId:e.sourceId,taskName:R(e.sourceId)})).sort((e,t)=>e.sourceId.localeCompare(t.sourceId));return L(t),t}function L(e){let t=new Map;for(let n of e){let e=t.get(n.scheduleId);if(e===void 0){t.set(n.scheduleId,n);continue}throw new F(`Duplicate authored schedule id "${n.scheduleId}" found in "${e.logicalPath}" and "${n.logicalPath}".`,{scheduleId:n.scheduleId,sourceId:n.sourceId,taskName:n.taskName})}}function R(e){return`${P}${Buffer.from(e,`utf8`).toString(`base64url`)}`}const z=`#ash-schedule-task/`;function B(e,t){if(t.registrations.length!==0){e.options.experimental.tasks=!0;for(let n of t.registrations)U(e,{artifactsConfig:t.artifactsConfig,dispatchModulePath:t.dispatchModulePath,registration:n})}}function V(e,t){let n=!G(t.previous,t.next);return H(e),B(e,{artifactsConfig:t.artifactsConfig,dispatchModulePath:t.dispatchModulePath,registrations:t.next}),n}function H(e){for(let t of Object.keys(e.options.tasks))t.startsWith(`ash.schedule.`)&&delete e.options.tasks[t];for(let t of Object.keys(e.options.virtual))t.startsWith(z)&&delete e.options.virtual[t];for(let[t,n]of Object.entries(e.options.scheduledTasks)){let r=W(n).filter(e=>!e.startsWith(P));if(r.length===0){delete e.options.scheduledTasks[t];continue}if(r.length===1){let[n]=r;n!==void 0&&(e.options.scheduledTasks[t]=n);continue}e.options.scheduledTasks[t]=r}}function U(e,t){let r=`${z}${t.registration.taskName}`,i=n(t.dispatchModulePath);e.options.tasks[t.registration.taskName]={description:t.registration.description,handler:r},e.options.virtual[r]=[`import { dispatchScheduleTask } from ${i};`,`const config = ${JSON.stringify(t.artifactsConfig)};`,`export default {`,` meta: { description: ${JSON.stringify(t.registration.description)} },`,` async run(event) {`,` return { result: await dispatchScheduleTask(event.name, config) };`,` },`,`};`].join(`
|
|
3
3
|
`),ae(e,t.registration.cron,t.registration.taskName)}function ae(e,t,n){let r=e.options.scheduledTasks[t];if(r===void 0){e.options.scheduledTasks[t]=n;return}if(typeof r==`string`){e.options.scheduledTasks[t]=[r,n];return}r.includes(n)||r.push(n)}function W(e){return typeof e==`string`?[e]:[...e]}function G(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1){let r=e[n],i=t[n];if(r===void 0||i===void 0||r.cron!==i.cron||r.description!==i.description||r.logicalPath!==i.logicalPath||r.scheduleId!==i.scheduleId||r.sourceId!==i.sourceId||r.taskName!==i.taskName)return!1}return!0}async function K(e){return[...e.manifest.schedules].map(e=>{let t={cron:e.cron,hasRun:e.hasRun,logicalPath:e.logicalPath,name:e.name,sourceId:e.sourceId,sourceKind:e.sourceKind};return e.markdown===void 0?t:{...t,markdown:e.markdown}})}async function q(e){return await K({manifest:await o({compiledArtifactsSource:e.compiledArtifactsSource})})}async function J(e){let t=v(e.outDir,`compiled-artifacts-bootstrap.mjs`),n=v(e.outDir,`compiled-artifacts-instrumentation.mjs`),r=se(e.compileResult.manifest.agentRoot);await ne(e.outDir,{recursive:!0}),await x(t,await le({compileResult:e.compileResult,installModulePath:g(`src/runtime/loaders/bundled-artifacts.ts`),moduleMapPath:t,metadata:e.compileResult.metadata})),r!==void 0&&await x(n,ue({agentName:e.compileResult.manifest.config.name,instrumentationPath:r,registerConfigPath:g(`src/harness/instrumentation-config.ts`)}));let i={bootstrapPath:t};return r!==void 0&&(i.instrumentationPluginPath=n,i.instrumentationSourcePath=r),i}const oe=[`.ts`,`.mts`,`.js`,`.mjs`];function se(e){for(let t of oe){let n=v(e,`instrumentation${t}`);if(C(n))return n}}function ce(e){return e.replace(/^export const moduleMap = /m,`const moduleMap = `).replace(/\nexport default moduleMap;\n?$/,`
|
|
4
4
|
`)}async function le(e){let r=ce(t({importSpecifierStyle:`absolute`,manifest:e.compileResult.manifest,moduleMapPath:e.moduleMapPath})).trim();return[`// Generated by Ash. Do not edit by hand.`,`import { installBundledCompiledArtifacts } from ${n(e.installModulePath)};`,``,r,``,`const metadata = ${JSON.stringify(e.metadata,null,2)};`,``,`const manifest = ${JSON.stringify(e.compileResult.manifest,null,2)};`,``,`export function installCompiledArtifactsBootstrap() {`,` installBundledCompiledArtifacts({`,` manifest,`,` metadata,`,` moduleMap,`,` });`,`}`,``,`installCompiledArtifactsBootstrap();`,``,`// Default export satisfies the Nitro plugin contract so this file`,`// can be used directly as a Nitro plugin without a separate wrapper.`,`export default function installCompiledArtifactsPlugin() {`,` // Already installed on import above.`,`}`,``,`export async function __ashInstallCompiledArtifactsStep() {`,` "use step";`,` return null;`,`}`,``].join(`
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{D as e,b as t,t as n,v as r,y as i}from"../../chunks/paths-
|
|
1
|
+
import{D as e,b as t,t as n,v as r,y as i}from"../../chunks/paths-BBleOGpa.js";import{d as a,f as o,h as s}from"../../chunks/types-MZUhN0Zy.js";import{createCliTheme as c,renderCliBanner as l,renderCliSection as u}from"../ui/output.js";async function d(e){let t=await f(e);return{application:n(t?.project.appRoot??e),compiledState:t,messaging:{createSessionRoutePath:o,continueSessionRoutePattern:a,streamRoutePattern:s}}}async function f(n){try{return await i({startPath:n})}catch(n){if(n instanceof r)return n.result;if(n instanceof e||n instanceof t)return null;throw n}}function p(e,t){return`${e} ${t}${e===1?``:`s`}`}function m(e,t){return`${`${e} error${e===1?``:`s`}`}, ${`${t} warning${t===1?``:`s`}`}`}function h(e){switch(e){case`ready`:return`success`;case`failed`:return`danger`;default:return`warning`}}async function g(e,t){let n=await d(t),r=n.compiledState,i=n.application,a=c(),o=[{label:`App Root`,value:i.appRoot}],s=[{label:`Workflow Build`,value:i.workflowBuildDir},{label:`Output`,value:i.outputDir}],f=[];r===null?o.push({label:`Compile`,tone:`warning`,value:`unavailable`}):(o.push({label:`Agent Root`,value:r.project.agentRoot},{label:`Layout`,value:r.project.layout},{label:`Compile`,tone:h(r.metadata.status),value:r.metadata.status},{label:`Diagnostics`,tone:r.metadata.discovery.summary.errors>0?`danger`:r.metadata.discovery.summary.warnings>0?`warning`:`success`,value:m(r.metadata.discovery.summary.errors,r.metadata.discovery.summary.warnings)},{label:`Instructions`,value:r.manifest.instructions?.logicalPath??`none`},{label:`Skills`,value:p(r.manifest.skills.length,`skill`)}),s.unshift({label:`Compiled Manifest`,value:r.paths.compiledManifestPath},{label:`Discovery Manifest`,value:r.paths.discoveryManifestPath},{label:`Diagnostics`,value:r.paths.diagnosticsPath},{label:`Module Map`,value:r.paths.moduleMapPath},{label:`Metadata`,value:r.paths.compileMetadataPath}),f.push(r.manifest.instructions===void 0?{label:`Instructions`,value:`No instructions prompt discovered.`}:{label:`Instructions`,value:r.manifest.instructions.logicalPath})),e.log([l(a,{subtitle:`Resolved application paths and the active message contract.`,title:`Ash Info`}),``,u(a,{rows:o,title:`Application`}),``,u(a,{rows:s,title:`Artifacts`}),...r===null?[]:[``,u(a,{rows:f,title:`Instructions`})],``,u(a,{rows:[{label:`Workflow ID`,value:i.workflowId},{label:`Source Dir`,value:i.workflowSourceDir},{label:`Create`,tone:`info`,value:`POST ${n.messaging.createSessionRoutePath}`},{label:`Continue`,tone:`info`,value:`POST ${n.messaging.continueSessionRoutePattern}`},{label:`Stream`,tone:`info`,value:`GET ${n.messaging.streamRoutePattern}`}],title:`Messaging`})].join(`
|
|
2
2
|
`))}export{g as printApplicationInfo};
|
package/dist/src/cli/run.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-
|
|
1
|
+
import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-e2GUqnTr.js`).then(e=>e.t)).buildHost}async function l(){return(await import(`./commands/info.js`)).printApplicationInfo}async function u(){return(await import(`./dev/repl.js`)).runDevelopmentRepl}async function d(){return(await import(`../evals/cli/eval.js`)).runEvalCommand}async function f(){return(await import(`../chunks/host-e2GUqnTr.js`).then(e=>e.t)).startHost}function p(e=process.cwd()){return s(e)}function m(e){return`Ash (v${e})`}function h(e){return e.name()===`info`||e.name()===`dev`}async function g(e){await new Promise((t,n)=>{let r=!1,i=()=>{process.off(`SIGINT`,a),process.off(`SIGTERM`,a)},a=()=>{r||(r=!0,i(),e.close().then(t,n))};process.once(`SIGINT`,a),process.once(`SIGTERM`,a)})}function _(e){if(!/^-?\d+$/.test(e))throw new r(`Expected a numeric port, received "${e}".`);let t=Number(e);if(!Number.isInteger(t))throw new r(`Expected a numeric port, received "${e}".`);if(t<0||t>65535)throw new r(`Expected a port between 0 and 65535, received "${e}".`);return t}function v(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}function y(e){let t=e[1];return e[0]!==`dev`||e.length!==2||t===void 0||t.startsWith(`-`)?[...e]:[`dev`,`--url`,t]}function b(e){if(e.url){if(e.host!==void 0)throw new r(`The --host option cannot be used with --url.`);if(e.port!==void 0)throw new r(`The --port option cannot be used with --url.`);if(e.repl===!1)throw new r(`The --no-repl option cannot be used with --url.`);return e.url}}function x(r,a){let s=p(),y=e().version,x=new i,S=t();return x.name(`ash`).description(`Build and run an Ash application.`).version(y).showHelpAfterError().exitOverride().hook(`preAction`,(e,t)=>{h(t)&&r.log(m(y))}).configureOutput({writeErr:e=>{r.error(e.trimEnd())},writeOut:e=>{r.log(e.trimEnd())}}),x.command(`build`).description(`Build the current Ash application.`).action(async()=>{let{loadDevelopmentEnvironmentFiles:e}=await import(`./dev/environment.js`);e(s);let t=await(a.buildHost??await c())(s);r.log(n(S,{message:`built output at ${t}`,tag:`build`,tone:`success`}))}),x.command(`dev`).description(`Start the Ash development server or connect the REPL to an existing URL.`).option(`--host <host>`,`Host interface to bind`).option(`--no-repl`,`Start the server without the interactive REPL`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,_).option(`--schedules`,`Run scheduled tasks during development (off by default)`).option(`-u, --url <url>`,`Connect the REPL to an existing server URL`,o).addHelpText(`after`,`
|
|
2
2
|
You can also pass a bare URL as the only argument, for example: ash dev https://example.com
|
|
3
3
|
`).action(async e=>{let t=b(e),{loadDevelopmentEnvironmentFiles:i}=await import(`./dev/environment.js`);if(i(s),t){if(r.log(n(S,{message:`REPL connecting to ${t}`,tag:`dev`,tone:`info`})),!v()){r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`}));return}r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:t});return}let o=await(a.startHost??await f())(s,{host:e.host,port:e.port,schedules:e.schedules===!0}),c=!1,l=async()=>{c||(c=!0,await o.close())};try{if(r.log(n(S,{message:`server listening at ${o.url}`,tag:`dev`,tone:`success`})),e.repl===!1)return await g({close:l});if(!v())return r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`})),await g({close:l});r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:o.url})}finally{await l()}}),x.command(`info`).description(`Print resolved application information.`).action(async()=>{await(a.printApplicationInfo??await l())(r,s)}),x.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await(a.runEvalCommand??await d())(e,r)}),x}async function S(e=process.argv.slice(2),t=console,n={}){let r=x(t,n),i=e.length===0?[`info`]:y(e);try{await r.parseAsync(i,{from:`user`})}catch(e){if(e instanceof a){if(e.exitCode===0)return;throw Error(e.message)}throw e}}export{S as runCli};
|
|
@@ -38,11 +38,16 @@ export interface SandboxUpdateParams {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export interface SandboxCreateOptions {
|
|
41
|
+
env?: Record<string, string> | undefined;
|
|
41
42
|
name?: string | undefined;
|
|
42
43
|
networkPolicy?: NetworkPolicy | undefined;
|
|
43
44
|
persistent?: boolean | undefined;
|
|
44
|
-
|
|
45
|
+
ports?: number[] | undefined;
|
|
46
|
+
resources?: { vcpus?: number | undefined } | undefined;
|
|
47
|
+
runtime?: string | undefined;
|
|
45
48
|
signal?: AbortSignal | undefined;
|
|
49
|
+
snapshotExpiration?: number | undefined;
|
|
50
|
+
source?: unknown;
|
|
46
51
|
tags?: Record<string, string> | undefined;
|
|
47
52
|
timeout?: number | undefined;
|
|
48
53
|
}
|
|
@@ -100,9 +100,13 @@ export async function dispatchHookLifecycle(input) {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
const channelContext = input.input.modelContext ?? [];
|
|
104
|
+
const mergedModelContext = channelContext.length === 0 ? modelContext : [...channelContext, ...modelContext];
|
|
103
105
|
return {
|
|
104
106
|
kind: "proceed",
|
|
105
|
-
input:
|
|
107
|
+
input: mergedModelContext.length > 0
|
|
108
|
+
? { ...input.input, modelContext: mergedModelContext }
|
|
109
|
+
: input.input,
|
|
106
110
|
nextSession: input.session,
|
|
107
111
|
};
|
|
108
112
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"../../chunks/paths-
|
|
1
|
+
import{n as e}from"../../chunks/paths-BBleOGpa.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-CKsU8Li3.js";import{n as a}from"../../chunks/host-e2GUqnTr.js";import{discoverAndImportSuites as o,discoverSuiteFiles as s,importSuiteFile as c}from"../runner/discover.js";import{executeSuite as l}from"../runner/execute-suite.js";import{ConsoleReporter as u}from"../runner/reporters/console.js";var d=n();function f(e,t){e.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await p(e,t)})}async function p(n,r){let i=e();if(t(i),n.listSuites){await y(i,r);return}let s=n.suite,c=await o(i,s);if(c.length===0){s&&s.length>0?r.error(`No suites found matching: ${s.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,d;n.url?d={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),d={kind:`local`,url:u.url});let f=m(d);try{let e=[];for(let t of c){let r=_(t,n),a=v(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await l({suite:r,target:d,reporters:a,appRoot:i,client:f});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function m(e){if(e.kind===`local`)return new i({host:e.url});let t={},n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&(t[r]=n),new i({auth:h(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function h(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:g}}async function g(){try{let e=(await(0,d.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function _(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function v(e,t){let n=t.json?[]:[new u];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}async function y(e,t){let n=await s(e);if(n.length===0){t.log(`No eval suites found.`);return}t.log(`Found ${n.length} eval suite file(s):\n`);for(let r of n){let n=await c(e,r);t.log(` ${n.id}${n.description?` - ${n.description}`:``}`)}}export{f as registerEvalCommand,p as runEvalCommand};
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
|
|
2
|
+
import type { LocalSandboxCreateOptions } from "#public/sandbox/local-sandbox.js";
|
|
3
|
+
/**
|
|
4
|
+
* Construction input for {@link createLocalSandboxBackend}. Internal —
|
|
5
|
+
* the public surface is the `localBackend()` factory under
|
|
6
|
+
* `experimental-ash/sandbox`.
|
|
7
|
+
*/
|
|
8
|
+
export interface CreateLocalSandboxBackendInput {
|
|
9
|
+
readonly createOptions?: LocalSandboxCreateOptions;
|
|
10
|
+
}
|
|
2
11
|
/**
|
|
3
12
|
* Creates the local just-bash-backed sandbox backend.
|
|
4
13
|
*
|
|
5
14
|
* The cache directory is derived from the runtime context's `appRoot`
|
|
6
15
|
* on every `create` call so the backend stays stateless and matches
|
|
7
16
|
* the framework's per-call dispatch contract.
|
|
17
|
+
*
|
|
18
|
+
* Accepts `createOptions` for parity with other backends; the local
|
|
19
|
+
* backend currently has no consumer-controllable create options so the
|
|
20
|
+
* value is reserved for future widening of {@link LocalSandboxCreateOptions}.
|
|
8
21
|
*/
|
|
9
|
-
export declare function createLocalSandboxBackend(): SandboxBackend;
|
|
22
|
+
export declare function createLocalSandboxBackend(_input?: CreateLocalSandboxBackendInput): SandboxBackend;
|
|
@@ -13,8 +13,12 @@ const LOCAL_SANDBOX_SNAPSHOT_VERSION = 1;
|
|
|
13
13
|
* The cache directory is derived from the runtime context's `appRoot`
|
|
14
14
|
* on every `create` call so the backend stays stateless and matches
|
|
15
15
|
* the framework's per-call dispatch contract.
|
|
16
|
+
*
|
|
17
|
+
* Accepts `createOptions` for parity with other backends; the local
|
|
18
|
+
* backend currently has no consumer-controllable create options so the
|
|
19
|
+
* value is reserved for future widening of {@link LocalSandboxCreateOptions}.
|
|
16
20
|
*/
|
|
17
|
-
export function createLocalSandboxBackend() {
|
|
21
|
+
export function createLocalSandboxBackend(_input = {}) {
|
|
18
22
|
return {
|
|
19
23
|
name: "local",
|
|
20
24
|
async prewarm(prewarmInput) {
|
|
@@ -13,10 +13,16 @@ export type VercelSandboxCreateOptions = Omit<NonNullable<Parameters<typeof SdkS
|
|
|
13
13
|
* `experimental-ash/sandbox`.
|
|
14
14
|
*/
|
|
15
15
|
export interface CreateVercelSandboxBackendInput {
|
|
16
|
+
readonly createOptions?: VercelSandboxCreateOptions;
|
|
16
17
|
readonly loadSandboxModule?: () => Promise<VercelSandboxModule>;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
20
|
* Creates the Vercel-backed sandbox backend.
|
|
21
|
+
*
|
|
22
|
+
* Any author-supplied `createOptions` are forwarded to the Vercel SDK
|
|
23
|
+
* `Sandbox.create(...)` for every fresh sandbox the framework creates
|
|
24
|
+
* (template at prewarm time, session at first-time session-create). On
|
|
25
|
+
* resume (`Sandbox.get`) no create happens, so they are not re-applied.
|
|
20
26
|
*/
|
|
21
27
|
export declare function createVercelSandboxBackend(input?: CreateVercelSandboxBackendInput): SandboxBackend<VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions>;
|
|
22
28
|
export {};
|
|
@@ -4,11 +4,17 @@ import { buildSandboxSession } from "#execution/sandbox/session.js";
|
|
|
4
4
|
import { streamToBuffer } from "#execution/sandbox/stream-utils.js";
|
|
5
5
|
/**
|
|
6
6
|
* Creates the Vercel-backed sandbox backend.
|
|
7
|
+
*
|
|
8
|
+
* Any author-supplied `createOptions` are forwarded to the Vercel SDK
|
|
9
|
+
* `Sandbox.create(...)` for every fresh sandbox the framework creates
|
|
10
|
+
* (template at prewarm time, session at first-time session-create). On
|
|
11
|
+
* resume (`Sandbox.get`) no create happens, so they are not re-applied.
|
|
7
12
|
*/
|
|
8
13
|
export function createVercelSandboxBackend(input = {}) {
|
|
9
14
|
const loadSandboxModule = input.loadSandboxModule ?? (async () => await import("#compiled/@vercel/sandbox/index.js"));
|
|
10
15
|
const createOptions = {
|
|
11
16
|
timeout: DEFAULT_SANDBOX_TIMEOUT_MS,
|
|
17
|
+
...input.createOptions,
|
|
12
18
|
};
|
|
13
19
|
const prewarmedTemplates = new Map();
|
|
14
20
|
return {
|
|
@@ -150,8 +156,13 @@ async function ensureSession(input) {
|
|
|
150
156
|
await ensureVercelSandboxTags(existing, input.tags);
|
|
151
157
|
return existing;
|
|
152
158
|
}
|
|
159
|
+
// The Vercel SDK rejects `runtime` when `source` is a snapshot — the
|
|
160
|
+
// runtime is already baked into the snapshot's filesystem. Strip it
|
|
161
|
+
// from the consumer-supplied create options for the session path
|
|
162
|
+
// only; template prewarm still honors `runtime`.
|
|
163
|
+
const { runtime: _runtime, ...sessionCreateOptions } = input.createOptions;
|
|
153
164
|
const createParams = {
|
|
154
|
-
...
|
|
165
|
+
...sessionCreateOptions,
|
|
155
166
|
name: sandboxName,
|
|
156
167
|
persistent: true,
|
|
157
168
|
source: { snapshotId: input.snapshotId, type: "snapshot" },
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
|
|
2
|
+
/**
|
|
3
|
+
* Wraps a backend-producing function in a `SandboxBackend` proxy that
|
|
4
|
+
* invokes the function exactly once, on first access to any of `.name`,
|
|
5
|
+
* `.create`, or `.prewarm`. Subsequent accesses return the same cached
|
|
6
|
+
* underlying backend.
|
|
7
|
+
*
|
|
8
|
+
* Used by `defaultBackend()` for env-conditional selection, and by the
|
|
9
|
+
* authored-definition normalizer when an author passes a callback to
|
|
10
|
+
* `SandboxDefinition.backend` (e.g. `backend: () => vercelBackend({...})`)
|
|
11
|
+
* so the factory runs at first use rather than at module load — while
|
|
12
|
+
* still preserving backend-internal state (such as the Vercel backend's
|
|
13
|
+
* prewarmed-templates map) across every framework call.
|
|
14
|
+
*/
|
|
15
|
+
export declare function lazyBackend<BO, SO>(factory: () => SandboxBackend<BO, SO>): SandboxBackend<BO, SO>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a backend-producing function in a `SandboxBackend` proxy that
|
|
3
|
+
* invokes the function exactly once, on first access to any of `.name`,
|
|
4
|
+
* `.create`, or `.prewarm`. Subsequent accesses return the same cached
|
|
5
|
+
* underlying backend.
|
|
6
|
+
*
|
|
7
|
+
* Used by `defaultBackend()` for env-conditional selection, and by the
|
|
8
|
+
* authored-definition normalizer when an author passes a callback to
|
|
9
|
+
* `SandboxDefinition.backend` (e.g. `backend: () => vercelBackend({...})`)
|
|
10
|
+
* so the factory runs at first use rather than at module load — while
|
|
11
|
+
* still preserving backend-internal state (such as the Vercel backend's
|
|
12
|
+
* prewarmed-templates map) across every framework call.
|
|
13
|
+
*/
|
|
14
|
+
export function lazyBackend(factory) {
|
|
15
|
+
let resolved;
|
|
16
|
+
function resolve() {
|
|
17
|
+
if (resolved === undefined) {
|
|
18
|
+
resolved = factory();
|
|
19
|
+
}
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
get name() {
|
|
24
|
+
return resolve().name;
|
|
25
|
+
},
|
|
26
|
+
create(input) {
|
|
27
|
+
return resolve().create(input);
|
|
28
|
+
},
|
|
29
|
+
async prewarm(input) {
|
|
30
|
+
await resolve().prewarm(input);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RunInput } from "#channel/types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Serializable input for the workflow entrypoint.
|
|
4
4
|
*
|
|
@@ -9,9 +9,7 @@ import type { UserContent } from "ai";
|
|
|
9
9
|
* the serialized record.
|
|
10
10
|
*/
|
|
11
11
|
export interface WorkflowEntryInput {
|
|
12
|
-
readonly input:
|
|
13
|
-
readonly message: string | UserContent;
|
|
14
|
-
};
|
|
12
|
+
readonly input: RunInput["input"];
|
|
15
13
|
readonly serializedContext: Record<string, unknown>;
|
|
16
14
|
}
|
|
17
15
|
/**
|
|
@@ -41,7 +41,7 @@ export async function workflowEntry(input) {
|
|
|
41
41
|
capabilities,
|
|
42
42
|
initialInput: {
|
|
43
43
|
kind: "deliver",
|
|
44
|
-
payloads: [{ message: input.input.message }],
|
|
44
|
+
payloads: [{ message: input.input.message, modelContext: input.input.modelContext }],
|
|
45
45
|
},
|
|
46
46
|
mode,
|
|
47
47
|
serializedContext: input.serializedContext,
|
|
@@ -14,6 +14,10 @@ export function coalesceTurnInputs(a, b) {
|
|
|
14
14
|
a: a.message,
|
|
15
15
|
b: b.message,
|
|
16
16
|
});
|
|
17
|
+
const modelContext = coalesceModelContext({
|
|
18
|
+
a: a.modelContext,
|
|
19
|
+
b: b.modelContext,
|
|
20
|
+
});
|
|
17
21
|
const result = {};
|
|
18
22
|
if (inputResponses !== undefined) {
|
|
19
23
|
result.inputResponses = inputResponses;
|
|
@@ -21,6 +25,9 @@ export function coalesceTurnInputs(a, b) {
|
|
|
21
25
|
if (message !== undefined) {
|
|
22
26
|
result.message = message;
|
|
23
27
|
}
|
|
28
|
+
if (modelContext !== undefined) {
|
|
29
|
+
result.modelContext = modelContext;
|
|
30
|
+
}
|
|
24
31
|
return result;
|
|
25
32
|
}
|
|
26
33
|
/**
|
|
@@ -73,6 +80,14 @@ function coalesceInputResponses(input) {
|
|
|
73
80
|
}
|
|
74
81
|
return [...a, ...b];
|
|
75
82
|
}
|
|
83
|
+
function coalesceModelContext(input) {
|
|
84
|
+
const a = input.a ?? [];
|
|
85
|
+
const b = input.b ?? [];
|
|
86
|
+
if (a.length === 0 && b.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return [...a, ...b];
|
|
90
|
+
}
|
|
76
91
|
/**
|
|
77
92
|
* Merges two optional turn messages into one.
|
|
78
93
|
*
|
|
@@ -79,13 +79,12 @@ export interface StepInput {
|
|
|
79
79
|
readonly inputResponses?: readonly InputResponse[];
|
|
80
80
|
readonly message?: string | UserContent;
|
|
81
81
|
/**
|
|
82
|
-
* Ephemeral messages
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* channels.
|
|
82
|
+
* Ephemeral messages appended to the next model call and never
|
|
83
|
+
* persisted to durable session history. Populated by channels via
|
|
84
|
+
* `SendPayload.modelContext` and by lifecycle hooks via the
|
|
85
|
+
* `modelContext` field on a `LifecycleHookResult`. Channel-provided
|
|
86
|
+
* messages appear first, then session-hook messages, then turn-hook
|
|
87
|
+
* messages.
|
|
89
88
|
*/
|
|
90
89
|
readonly modelContext?: readonly ModelMessage[];
|
|
91
90
|
/**
|
|
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
|
|
|
6
6
|
let cachedPackageInfo;
|
|
7
7
|
// The package build stamps the published version into `dist` so bundled
|
|
8
8
|
// deployments can still report package metadata without resolving package.json.
|
|
9
|
-
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.
|
|
9
|
+
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.19.0";
|
|
10
10
|
const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
|
|
11
11
|
const WORKFLOW_MODULE_ALIASES = {
|
|
12
12
|
"workflow/api": "src/compiled/@workflow/core/runtime.js",
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
|
|
1
2
|
import type { SandboxDefinition } from "#public/definitions/sandbox.js";
|
|
2
|
-
export type NormalizedSandboxDefinition = Readonly<SandboxDefinition
|
|
3
|
+
export type NormalizedSandboxDefinition = Readonly<Omit<SandboxDefinition, "backend">> & {
|
|
4
|
+
readonly backend?: SandboxBackend;
|
|
3
5
|
readonly description?: string;
|
|
4
6
|
};
|
|
5
7
|
/**
|
|
6
8
|
* Normalizes one authored sandbox definition into the canonical internal
|
|
7
|
-
* shape.
|
|
9
|
+
* shape. If the author supplied a `backend` callback (e.g.
|
|
10
|
+
* `backend: () => vercelBackend({...})`), it is wrapped via
|
|
11
|
+
* {@link lazyBackend} so downstream consumers always see a plain
|
|
12
|
+
* `SandboxBackend` value — the callback fires exactly once on first
|
|
13
|
+
* access and the resulting backend is memoized.
|
|
8
14
|
*/
|
|
9
15
|
export declare function normalizeSandboxDefinition(value: unknown, message: string): NormalizedSandboxDefinition;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { expectFunction, expectObjectRecord, expectOnlyKnownKeys, } from "#internal/authored-module.js";
|
|
2
|
+
import { lazyBackend } from "#execution/sandbox/lazy-backend.js";
|
|
2
3
|
/**
|
|
3
4
|
* Normalizes one authored sandbox definition into the canonical internal
|
|
4
|
-
* shape.
|
|
5
|
+
* shape. If the author supplied a `backend` callback (e.g.
|
|
6
|
+
* `backend: () => vercelBackend({...})`), it is wrapped via
|
|
7
|
+
* {@link lazyBackend} so downstream consumers always see a plain
|
|
8
|
+
* `SandboxBackend` value — the callback fires exactly once on first
|
|
9
|
+
* access and the resulting backend is memoized.
|
|
5
10
|
*/
|
|
6
11
|
export function normalizeSandboxDefinition(value, message) {
|
|
7
12
|
const record = expectObjectRecord(value, message);
|
|
@@ -25,7 +30,10 @@ export function normalizeSandboxDefinition(value, message) {
|
|
|
25
30
|
return definition;
|
|
26
31
|
}
|
|
27
32
|
function expectSandboxBackend(value, message) {
|
|
28
|
-
|
|
33
|
+
if (typeof value === "function") {
|
|
34
|
+
return lazyBackend(value);
|
|
35
|
+
}
|
|
36
|
+
const record = expectObjectRecord(value, `${message} The "backend" field must be a SandboxBackend value (use vercelBackend(), localBackend(), or your own factory) or a zero-arg function returning one.`);
|
|
29
37
|
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
30
38
|
throw new Error(`${message} The "backend" value must expose a non-empty string "name" identifier.`);
|
|
31
39
|
}
|
|
@@ -11,7 +11,6 @@ export const WORKFLOW_STEP_EXTERNAL_PACKAGES = ["@mongodb-js/zstd", "node-liblzm
|
|
|
11
11
|
* Nitro performs the final bundling/tracing pass for hosted output.
|
|
12
12
|
*/
|
|
13
13
|
export const WORKFLOW_BUILDER_DEFERRED_PACKAGES = ["@chat-adapter/slack", "chat"];
|
|
14
|
-
const WORKFLOW_FUNCTION_NODE_OPTIONS = "--experimental-require-module";
|
|
15
14
|
/**
|
|
16
15
|
* Builds the environment block every generated Vercel workflow function needs.
|
|
17
16
|
*/
|
|
@@ -20,7 +19,6 @@ export function createWorkflowFunctionEnvironment(environment) {
|
|
|
20
19
|
if (isRecord(environment)) {
|
|
21
20
|
Object.assign(nextEnvironment, environment);
|
|
22
21
|
}
|
|
23
|
-
nextEnvironment.NODE_OPTIONS = WORKFLOW_FUNCTION_NODE_OPTIONS;
|
|
24
22
|
return nextEnvironment;
|
|
25
23
|
}
|
|
26
24
|
function isRecord(value) {
|
|
@@ -272,7 +272,7 @@ export interface SlackBinding {
|
|
|
272
272
|
*
|
|
273
273
|
* Auto-anchor: when the binding starts without a `threadTs`, the first
|
|
274
274
|
* `chat.postMessage` adopts its own `ts` as the thread root; the live
|
|
275
|
-
* `threadTs` is updated and `
|
|
275
|
+
* `threadTs` is updated and `onThreadTsChanged` fires so the caller can persist
|
|
276
276
|
* the anchor. Ephemerals and files-only posts do not anchor.
|
|
277
277
|
*/
|
|
278
278
|
export declare function buildSlackBinding(input: {
|
|
@@ -280,31 +280,6 @@ export declare function buildSlackBinding(input: {
|
|
|
280
280
|
readonly channelId: string;
|
|
281
281
|
readonly threadTs: string;
|
|
282
282
|
readonly teamId: string | undefined;
|
|
283
|
-
readonly
|
|
283
|
+
readonly onThreadTsChanged?: (ts: string) => void;
|
|
284
284
|
}): SlackBinding;
|
|
285
|
-
/**
|
|
286
|
-
* Best-effort GFM → Slack mrkdwn converter used only in contexts that
|
|
287
|
-
* do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
|
|
288
|
-
* `initial_comment` field).
|
|
289
|
-
*
|
|
290
|
-
* The main `{ markdown }` post path sends `markdown_text` directly
|
|
291
|
-
* to `chat.postMessage` and does not go through this converter.
|
|
292
|
-
*/
|
|
293
|
-
export declare function gfmToSlackMrkdwn(input: string): string;
|
|
294
|
-
/**
|
|
295
|
-
* Best-effort Slack mrkdwn → GFM converter applied to the text of
|
|
296
|
-
* every inbound Slack message before the harness sees it.
|
|
297
|
-
*
|
|
298
|
-
* - `<@U123>` → `@U123`
|
|
299
|
-
* - `<#C123|name>` → `#name` (or `#C123` when no name)
|
|
300
|
-
* - `<!channel>` etc. → `@channel`
|
|
301
|
-
* - `<https://x|label>` → `[label](https://x)`
|
|
302
|
-
* - `<https://x>` → `https://x`
|
|
303
|
-
* - `*bold*` (paired) → `**bold**`
|
|
304
|
-
* - `~strike~` (paired) → `~~strike~~`
|
|
305
|
-
*
|
|
306
|
-
* Inline `_italic_` and code spans pass through unchanged because both
|
|
307
|
-
* formats render them identically.
|
|
308
|
-
*/
|
|
309
|
-
export declare function slackMrkdwnToGfm(input: string): string;
|
|
310
285
|
export {};
|
|
@@ -18,6 +18,7 @@ import { isCardElement } from "#compiled/chat/index.js";
|
|
|
18
18
|
import { createLogger } from "#internal/logging.js";
|
|
19
19
|
import { encodeSlackApiBody } from "#public/channels/slack/api-encoding.js";
|
|
20
20
|
import { cardToBlocks, cardToFallbackText } from "#public/channels/slack/blocks.js";
|
|
21
|
+
import { gfmToSlackMrkdwn, rewriteBareMentions, slackMrkdwnToGfm, } from "#public/channels/slack/mrkdwn.js";
|
|
21
22
|
const log = createLogger("slack.api");
|
|
22
23
|
/**
|
|
23
24
|
* Builds the Slack channel-local continuation token
|
|
@@ -72,18 +73,18 @@ export function createSlackRequester(botToken) {
|
|
|
72
73
|
*
|
|
73
74
|
* Auto-anchor: when the binding starts without a `threadTs`, the first
|
|
74
75
|
* `chat.postMessage` adopts its own `ts` as the thread root; the live
|
|
75
|
-
* `threadTs` is updated and `
|
|
76
|
+
* `threadTs` is updated and `onThreadTsChanged` fires so the caller can persist
|
|
76
77
|
* the anchor. Ephemerals and files-only posts do not anchor.
|
|
77
78
|
*/
|
|
78
79
|
export function buildSlackBinding(input) {
|
|
79
80
|
const request = createSlackRequester(input.botToken);
|
|
80
81
|
const messages = [];
|
|
81
82
|
let currentThreadTs = input.threadTs;
|
|
82
|
-
function
|
|
83
|
-
if (currentThreadTs
|
|
83
|
+
function handleMessageTs(ts) {
|
|
84
|
+
if (currentThreadTs || ts === currentThreadTs)
|
|
84
85
|
return;
|
|
85
86
|
currentThreadTs = ts;
|
|
86
|
-
input.
|
|
87
|
+
input.onThreadTsChanged?.(ts);
|
|
87
88
|
}
|
|
88
89
|
async function uploadFiles(files, options) {
|
|
89
90
|
if (files.length === 0) {
|
|
@@ -168,7 +169,7 @@ export function buildSlackBinding(input) {
|
|
|
168
169
|
throw new Error(`Slack chat.postMessage failed: ${response.error ?? "unknown_error"}`);
|
|
169
170
|
}
|
|
170
171
|
const id = typeof response.ts === "string" ? response.ts : "";
|
|
171
|
-
|
|
172
|
+
handleMessageTs(id);
|
|
172
173
|
// blocks / card + files: structured message lands first, then upload
|
|
173
174
|
// files as a follow-up post in the same thread.
|
|
174
175
|
if (files.length > 0 && hasStructured) {
|
|
@@ -313,70 +314,6 @@ function parseThreadMessage(raw, threadRootTs) {
|
|
|
313
314
|
raw,
|
|
314
315
|
};
|
|
315
316
|
}
|
|
316
|
-
const BARE_MENTION_RE = /(?<![<\w])@(\w+)/gu;
|
|
317
|
-
function rewriteBareMentions(text) {
|
|
318
|
-
return text.replace(BARE_MENTION_RE, "<@$1>");
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Best-effort GFM → Slack mrkdwn converter used only in contexts that
|
|
322
|
-
* do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
|
|
323
|
-
* `initial_comment` field).
|
|
324
|
-
*
|
|
325
|
-
* The main `{ markdown }` post path sends `markdown_text` directly
|
|
326
|
-
* to `chat.postMessage` and does not go through this converter.
|
|
327
|
-
*/
|
|
328
|
-
export function gfmToSlackMrkdwn(input) {
|
|
329
|
-
const segments = splitCodeFences(input);
|
|
330
|
-
return segments
|
|
331
|
-
.map((segment) => (segment.kind === "code" ? segment.text : convertInline(segment.text)))
|
|
332
|
-
.join("");
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Best-effort Slack mrkdwn → GFM converter applied to the text of
|
|
336
|
-
* every inbound Slack message before the harness sees it.
|
|
337
|
-
*
|
|
338
|
-
* - `<@U123>` → `@U123`
|
|
339
|
-
* - `<#C123|name>` → `#name` (or `#C123` when no name)
|
|
340
|
-
* - `<!channel>` etc. → `@channel`
|
|
341
|
-
* - `<https://x|label>` → `[label](https://x)`
|
|
342
|
-
* - `<https://x>` → `https://x`
|
|
343
|
-
* - `*bold*` (paired) → `**bold**`
|
|
344
|
-
* - `~strike~` (paired) → `~~strike~~`
|
|
345
|
-
*
|
|
346
|
-
* Inline `_italic_` and code spans pass through unchanged because both
|
|
347
|
-
* formats render them identically.
|
|
348
|
-
*/
|
|
349
|
-
export function slackMrkdwnToGfm(input) {
|
|
350
|
-
const segments = splitCodeFences(input);
|
|
351
|
-
return segments
|
|
352
|
-
.map((segment) => (segment.kind === "code" ? segment.text : decodeInline(segment.text)))
|
|
353
|
-
.join("");
|
|
354
|
-
}
|
|
355
|
-
function splitCodeFences(input) {
|
|
356
|
-
const segments = [];
|
|
357
|
-
const fenceRe = /```[\s\S]*?```|`[^`\n]+`/gu;
|
|
358
|
-
let lastIndex = 0;
|
|
359
|
-
for (const match of input.matchAll(fenceRe)) {
|
|
360
|
-
const start = match.index ?? 0;
|
|
361
|
-
if (start > lastIndex) {
|
|
362
|
-
segments.push({ kind: "text", text: input.slice(lastIndex, start) });
|
|
363
|
-
}
|
|
364
|
-
segments.push({ kind: "code", text: match[0] });
|
|
365
|
-
lastIndex = start + match[0].length;
|
|
366
|
-
}
|
|
367
|
-
if (lastIndex < input.length) {
|
|
368
|
-
segments.push({ kind: "text", text: input.slice(lastIndex) });
|
|
369
|
-
}
|
|
370
|
-
return segments;
|
|
371
|
-
}
|
|
372
|
-
function convertInline(input) {
|
|
373
|
-
let out = input;
|
|
374
|
-
out = out.replace(/\*\*([^*\n]+)\*\*/gu, "*$1*");
|
|
375
|
-
out = out.replace(/__([^_\n]+)__/gu, "*$1*");
|
|
376
|
-
out = out.replace(/~~([^~\n]+)~~/gu, "~$1~");
|
|
377
|
-
out = out.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/gu, "<$2|$1>");
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
380
317
|
/**
|
|
381
318
|
* Normalize a {@link FileUpload.data} value (`Buffer | Blob | ArrayBuffer`) to
|
|
382
319
|
* a contiguous `Buffer` we can both POST and length-prefix without
|
|
@@ -394,16 +331,3 @@ async function readFileBytes(data) {
|
|
|
394
331
|
}
|
|
395
332
|
throw new Error("FileUpload.data must be a Buffer, ArrayBuffer, or Blob.");
|
|
396
333
|
}
|
|
397
|
-
function decodeInline(input) {
|
|
398
|
-
let out = input;
|
|
399
|
-
out = out.replace(/<!(channel|here|everyone)>/gu, "@$1");
|
|
400
|
-
out = out.replace(/<@([A-Z0-9]+)\|([^>]+)>/gu, "@$2");
|
|
401
|
-
out = out.replace(/<@([A-Z0-9]+)>/gu, "@$1");
|
|
402
|
-
out = out.replace(/<#([A-Z0-9]+)\|([^>]+)>/gu, "#$2");
|
|
403
|
-
out = out.replace(/<#([A-Z0-9]+)>/gu, "#$1");
|
|
404
|
-
out = out.replace(/<(https?:\/\/[^|>\s]+)\|([^>]+)>/gu, "[$2]($1)");
|
|
405
|
-
out = out.replace(/<(https?:\/\/[^>\s]+)>/gu, "$1");
|
|
406
|
-
out = out.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/gu, "$1**$2**");
|
|
407
|
-
out = out.replace(/(^|[^~])~([^~\n]+)~(?!~)/gu, "$1~~$2~~");
|
|
408
|
-
return out;
|
|
409
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createLogger, extractErrorId, formatErrorHint } from "#internal/logging.js";
|
|
2
2
|
import { buildAuthCompletedText, buildAuthEphemeralBlocks, buildAuthRequiredPublicText, formatConnectionDisplayName, } from "#public/channels/slack/connections.js";
|
|
3
3
|
import { renderInputRequestBlocks } from "#public/channels/slack/hitl.js";
|
|
4
|
-
import { truncateTypingStatus } from "#public/channels/slack/limits.js";
|
|
4
|
+
import { truncateMessageText, truncateTypingStatus } from "#public/channels/slack/limits.js";
|
|
5
5
|
const log = createLogger("slack.defaults");
|
|
6
6
|
/**
|
|
7
7
|
* Workspace-scoped projection of the Slack actor that produced
|
|
@@ -84,7 +84,7 @@ export function defaultInputRequestedHandler() {
|
|
|
84
84
|
return async (data, ctx) => {
|
|
85
85
|
if (data.requests.length === 0)
|
|
86
86
|
return;
|
|
87
|
-
const promptText = data.requests.map((r) => r.prompt).join("\n");
|
|
87
|
+
const promptText = truncateMessageText(data.requests.map((r) => r.prompt).join("\n"));
|
|
88
88
|
await ctx.thread.post({
|
|
89
89
|
blocks: data.requests.flatMap(renderInputRequestBlocks),
|
|
90
90
|
text: promptText,
|