eve 0.15.0 → 0.15.2
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 +16 -0
- package/dist/src/cli/dev/tui/runner.js +1 -1
- package/dist/src/cli/dev/tui/status-line.d.ts +6 -3
- package/dist/src/cli/dev/tui/status-line.js +1 -1
- package/dist/src/cli/dev/tui/terminal-renderer.js +1 -1
- package/dist/src/cli/dev/tui/vercel-status.d.ts +2 -2
- package/dist/src/cli/dev/tui/vercel-status.js +1 -1
- package/dist/src/cli/run.d.ts +2 -2
- package/dist/src/cli/run.js +2 -2
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/deliver-payloads.d.ts +3 -0
- package/dist/src/execution/deliver-payloads.js +1 -0
- package/dist/src/execution/dispatch-runtime-actions-step.d.ts +2 -0
- package/dist/src/execution/dispatch-runtime-actions-step.js +1 -1
- package/dist/src/execution/dispatch-workflow-runtime-actions-step.d.ts +1 -0
- package/dist/src/execution/dispatch-workflow-runtime-actions-step.js +1 -1
- package/dist/src/execution/durable-session-migrations/turn-workflow.d.ts +7 -0
- package/dist/src/execution/durable-session-migrations/turn-workflow.js +1 -1
- package/dist/src/execution/forward-turn-delivery-step.d.ts +6 -0
- package/dist/src/execution/forward-turn-delivery-step.js +1 -0
- package/dist/src/execution/hook-ownership.d.ts +6 -0
- package/dist/src/execution/hook-ownership.js +1 -1
- package/dist/src/execution/remote-agent-dispatch.d.ts +1 -0
- package/dist/src/execution/remote-agent-dispatch.js +1 -1
- package/dist/src/execution/route-child-delivery.d.ts +18 -0
- package/dist/src/execution/route-child-delivery.js +1 -0
- package/dist/src/execution/session-delivery-hook.d.ts +20 -0
- package/dist/src/execution/session-delivery-hook.js +1 -0
- package/dist/src/execution/subagent-tool.d.ts +2 -0
- package/dist/src/execution/subagent-tool.js +1 -1
- package/dist/src/execution/turn-control-protocol.d.ts +36 -0
- package/dist/src/execution/turn-control-protocol.js +1 -0
- package/dist/src/execution/turn-control-receiver.d.ts +35 -0
- package/dist/src/execution/turn-control-receiver.js +1 -0
- package/dist/src/execution/turn-dispatch.d.ts +17 -0
- package/dist/src/execution/turn-dispatch.js +1 -0
- package/dist/src/execution/turn-execution-cursor.d.ts +48 -0
- package/dist/src/execution/turn-execution-cursor.js +1 -0
- package/dist/src/execution/turn-workflow.d.ts +1 -27
- package/dist/src/execution/turn-workflow.js +1 -1
- package/dist/src/execution/workflow-callback-url.d.ts +8 -0
- package/dist/src/execution/workflow-callback-url.js +1 -1
- package/dist/src/execution/workflow-entry.d.ts +6 -4
- package/dist/src/execution/workflow-entry.js +1 -1
- package/dist/src/execution/workflow-steps.js +1 -1
- package/dist/src/harness/runtime-actions.d.ts +4 -22
- package/dist/src/harness/runtime-actions.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/nitro/host/dev-server-state.d.ts +20 -0
- package/dist/src/internal/nitro/host/dev-server-state.js +1 -0
- package/dist/src/internal/nitro/host/start-development-server.d.ts +12 -5
- package/dist/src/internal/nitro/host/start-development-server.js +2 -2
- package/dist/src/internal/nitro/host/types.d.ts +25 -3
- package/dist/src/internal/nitro/host.d.ts +2 -2
- package/dist/src/internal/nitro/host.js +1 -1
- package/dist/src/internal/workflow-bundle/workflow-builders.js +1 -1
- package/dist/src/setup/scaffold/create/project.js +1 -1
- package/dist/src/shared/eve-server-health.d.ts +5 -0
- package/dist/src/shared/eve-server-health.js +1 -0
- package/dist/src/shared/network-address.d.ts +12 -0
- package/dist/src/shared/network-address.js +1 -1
- package/dist/src/shared/result.d.ts +12 -0
- package/dist/src/shared/result.js +1 -0
- package/docs/connections/overview.mdx +11 -11
- package/docs/guides/dev-tui.md +1 -1
- package/docs/meta.json +1 -0
- package/docs/patterns/dynamic-scheduling.md +257 -0
- package/docs/patterns/meta.json +9 -0
- package/docs/patterns/multi-tenant-approvals.md +191 -0
- package/docs/patterns/multi-tenant-auth.md +177 -0
- package/docs/patterns/multi-tenant-memory.md +188 -0
- package/docs/reference/cli.md +1 -1
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
2
|
-
`))}
|
|
1
|
+
import{isLoopbackServerUrl}from"#shared/network-address.js";import{detectPackageManager}from"#setup/package-manager.js";import{eveDevArguments}from"#setup/primitives/index.js";import{toErrorMessage}from"#shared/errors.js";import{loadDevelopmentEnvironmentFiles}from"#cli/dev/environment.js";import{startDevelopmentSandboxPrewarmInBackground}from"#execution/sandbox/development-prewarm.js";import{devBootPhase}from"#internal/dev-boot-progress.js";import{resolveDiscoveryProject}from"#discover/project.js";import{EVE_DEV_ENV_FLAG}from"#internal/application/optional-package-install.js";import{EVE_DEVELOPMENT_SANDBOX_RUN_ID_ENV,clearInitializedDevelopmentSandboxBackendNames,createDevelopmentSandboxRunId,getInitializedDevelopmentSandboxBackendNames}from"#execution/sandbox/development-run.js";import{pruneDevelopmentRuntimeArtifactsSnapshotsInBackground}from"#internal/nitro/dev-runtime-artifacts.js";import{build,createDevServer,prepare}from"nitro/builder";import{createApplicationNitro}from"#internal/nitro/host/create-application-nitro.js";import{prepareApplicationHost}from"#internal/nitro/host/prepare-application-host.js";import{createNitroArtifactsConfig}from"#internal/nitro/host/artifacts-config.js";import{resolveNitroCompiledArtifactsSource}from"#internal/nitro/routes/runtime-artifacts.js";import{DevelopmentServerState}from"#internal/nitro/host/dev-server-state.js";import{isEveServerHealthy}from"#shared/eve-server-health.js";import{pruneLocalSandboxTemplatesInBackground,stopDevelopmentSandboxResources}from"#execution/sandbox/bindings/local.js";import{DEFAULT_DEVELOPMENT_SERVER_PORT,MAX_DEVELOPMENT_SERVER_PORT_ATTEMPTS}from"#internal/nitro/host/ports.js";const MAX_ALLOWED_DEVELOPMENT_SERVER_PORT=65535,WORKFLOW_LOCAL_BASE_URL_ENV=`WORKFLOW_LOCAL_BASE_URL`,PORT_ENV=`PORT`,DEFAULT_DEVELOPMENT_SERVER_HOST=`127.0.0.1`,IPV6_WILDCARD_LISTEN_HOSTNAMES=new Set([`[::]`,`::`]);function normalizeDevelopmentServerClientUrl(e){let t=new URL(e);return IPV6_WILDCARD_LISTEN_HOSTNAMES.has(t.hostname)?(t.hostname=`[::1]`,t.toString()):t.hostname===`0.0.0.0`?(t.hostname=DEFAULT_DEVELOPMENT_SERVER_HOST,t.toString()):e}function isAddressInUseError(e){return e instanceof Error&&`code`in e&&e.code===`EADDRINUSE`}function resolveDevelopmentServerPort(e){let t=typeof e==`string`?Number(e):e??DEFAULT_DEVELOPMENT_SERVER_PORT;if(!Number.isInteger(t)||t<0||t>MAX_ALLOWED_DEVELOPMENT_SERVER_PORT)throw Error(`Invalid development server port "${String(e)}". Expected an integer between 0 and ${MAX_ALLOWED_DEVELOPMENT_SERVER_PORT}.`);return t}function readEnvironmentPort(){let e=process.env[PORT_ENV];if(e===void 0||e.trim()===``)return;let t=Number(e);if(!Number.isInteger(t)||t<0||t>MAX_ALLOWED_DEVELOPMENT_SERVER_PORT)throw Error(`Invalid ${PORT_ENV} environment variable "${e}". Expected an integer between 0 and ${MAX_ALLOWED_DEVELOPMENT_SERVER_PORT}.`);return t}async function detectDevelopmentCommandPackageManager(e){try{return(await detectPackageManager(e)).kind}catch{return`pnpm`}}async function formatDevelopmentServerConnectCommand(e,t){let r=await detectDevelopmentCommandPackageManager(e);return[r,...eveDevArguments(r),t].join(` `)}async function createDevelopmentServerAlreadyRunningError(e,t){let n=await formatDevelopmentServerConnectCommand(e,t);return Error([`A dev server is already running for this eve agent.`,`To connect to the existing instance, run: ${n}`].join(`
|
|
2
|
+
`))}function resolveDevelopmentServerPorts(e){let t=resolveDevelopmentServerPort(e.port);if(t===0||!e.retryOnAddressInUse)return[t];let n=[];for(let e=0;e<MAX_DEVELOPMENT_SERVER_PORT_ATTEMPTS;e+=1){let r=t+e;if(r>65535)break;n.push(r)}return n}function installWorkflowLocalQueueEnvironment(e){let t=process.env[WORKFLOW_LOCAL_BASE_URL_ENV],n=process.env[PORT_ENV],r=new URL(normalizeDevelopmentServerClientUrl(e));return process.env[WORKFLOW_LOCAL_BASE_URL_ENV]=r.origin,r.port&&(process.env[PORT_ENV]=r.port),()=>{t===void 0?delete process.env[WORKFLOW_LOCAL_BASE_URL_ENV]:process.env[WORKFLOW_LOCAL_BASE_URL_ENV]=t,n===void 0?delete process.env[PORT_ENV]:process.env[PORT_ENV]=n}}function attachTemporarySocketErrorHandler(e){let onSocketError=()=>{};return e.once(`error`,onSocketError),()=>{e.off(`error`,onSocketError)}}function shouldProxyDevelopmentServerWebSocketUpgrades(e){return e.options.features.websocket===!0||e.options.experimental.websocket===!0}function guardDevelopmentServerWebSocketUpgrades(e,t){let n=t.upgrade.bind(t),r=shouldProxyDevelopmentServerWebSocketUpgrades(e);t.upgrade=async(e,t,i)=>{if(!r){t.destroyed||t.destroy();return}let a=attachTemporarySocketErrorHandler(t);try{await n(e,t,i)}catch{t.destroyed||t.destroy()}finally{a()}}}async function closeDevelopmentServerResources(e){let t=[],attempt=async e=>{try{return await e(),!0}catch(e){return t.push(e),!1}},n=e.authoredSourceWatcher;n!==void 0&&await attempt(()=>n.close());let r=e.devServer,i=r===void 0?!0:await attempt(()=>r.close()),a=e.nitro;return a!==void 0&&await attempt(()=>a.close()),await attempt(()=>stopDevelopmentSandboxResources({backendNames:getInitializedDevelopmentSandboxBackendNames(e.developmentSandboxRunId),devRunId:e.developmentSandboxRunId,log:e=>console.warn(`[eve:dev] ${e}`)})),{errors:t,listenerClosed:i}}function createDevelopmentServerCleanupError(e){if(e.length!==0){if(e.length===1){let t=e[0];return t instanceof Error?t:Error(`Failed to close the development server: ${toErrorMessage(t)}`,{cause:t})}return AggregateError(e,`Multiple development-server resources failed to close.`)}}function createDevelopmentServerStartupCleanupError(e,t){return AggregateError([e,...t],`${toErrorMessage(e)} Cleanup also failed.`,{cause:e})}async function listenForDevelopmentServer(e){let t=resolveDevelopmentServerPorts({port:e.port,retryOnAddressInUse:e.retryOnAddressInUse}),n;for(let r of t){let t=e.devServer.listen({hostname:e.host,port:r,silent:!0});try{return await t.ready(),t}catch(r){if(n=r,await t.close().catch(()=>{}),!isAddressInUseError(r)||!e.retryOnAddressInUse)throw r}}throw Error(`Failed to start Nitro dev server after ${t.length} attempts. Tried ports ${t.join(`, `)}.`,{cause:n})}async function startNitroDevelopmentServer(t,n){process.env[EVE_DEV_ENV_FLAG]??=`1`;let r=await resolveDiscoveryProject(t);loadDevelopmentEnvironmentFiles(r.appRoot);let u=readEnvironmentPort(),p=n.port??u,m=n.host!==void 0||n.port!==void 0||u!==void 0,h=new DevelopmentServerState(r),g=await h.read();if(g!==void 0&&isLoopbackServerUrl(g)&&await isEveServerHealthy(g)){if(n.existing===`attach-if-unconfigured`&&!m)return{handle:{kind:`existing`,appRoot:r.appRoot,url:g},close:void 0};throw await createDevelopmentServerAlreadyRunningError(r.appRoot,g)}let _=process.env[EVE_DEVELOPMENT_SANDBOX_RUN_ID_ENV],v=createDevelopmentSandboxRunId();process.env[EVE_DEVELOPMENT_SANDBOX_RUN_ID_ENV]=v;let y,b,x,S;try{let e=await devBootPhase(`compiling agent`,()=>prepareApplicationHost(r.appRoot,{dev:!0}),n.onBootProgress);pruneDevelopmentRuntimeArtifactsSnapshotsInBackground(e.appRoot);let t=resolveNitroCompiledArtifactsSource(createNitroArtifactsConfig({appRoot:e.appRoot,dev:!0}));startDevelopmentSandboxPrewarmInBackground({appRoot:e.appRoot,compiledArtifactsSource:t}),pruneLocalSandboxTemplatesInBackground(e.appRoot);let i=await devBootPhase(`creating dev server`,()=>createApplicationNitro(e,!0),n.onBootProgress);y=i,b=createDevServer(i);let o=b;guardDevelopmentServerWebSocketUpgrades(i,b);let s=n.host??i.options.devServer.hostname??DEFAULT_DEVELOPMENT_SERVER_HOST,c=p===void 0,l=await devBootPhase(`binding port`,()=>listenForDevelopmentServer({devServer:o,host:s,port:p,retryOnAddressInUse:c}),n.onBootProgress);if(!l.url)throw Error(`Nitro dev server did not expose a URL.`);let u=normalizeDevelopmentServerClientUrl(l.url);x=installWorkflowLocalQueueEnvironment(u),await devBootPhase(`building dev bundle`,async()=>{await prepare(i),await build(i)},n.onBootProgress),S=await devBootPhase(`starting file watcher`,async()=>{let{startAuthoredSourceWatcher:t}=await import(`#internal/nitro/host/dev-authored-source-watcher.js`);return t({nitro:i,preparedHost:e})},n.onBootProgress),await h.write(u);let d=x;if(d===void 0)throw Error(`Workflow local queue environment was not initialized.`);let f=S,m=b,g=i,C;return{handle:{kind:`started`,appRoot:r.appRoot,url:u},close:()=>(C??=(async()=>{let e=await closeDevelopmentServerResources({authoredSourceWatcher:f,devServer:m,developmentSandboxRunId:v,nitro:g});e.listenerClosed&&await h.remove().catch(()=>{});try{let t=createDevelopmentServerCleanupError(e.errors);if(t!==void 0)throw t}finally{clearInitializedDevelopmentSandboxBackendNames(v),d(),restoreDevelopmentSandboxRunId(_)}})(),C)}}catch(e){let t=await closeDevelopmentServerResources({authoredSourceWatcher:S,devServer:b,developmentSandboxRunId:v,nitro:y}),n=[...t.errors];throw x?.(),clearInitializedDevelopmentSandboxBackendNames(v),t.listenerClosed&&await h.remove().catch(()=>{}),restoreDevelopmentSandboxRunId(_),n.length>0?createDevelopmentServerStartupCleanupError(e,n):e}}function createDevelopmentServer(e,t={}){let n,r;return{start(){if(n!==void 0)throw Error(`DevelopmentServer.start() was already called.`);return n=startNitroDevelopmentServer(e,t).then(({handle:e,close:t})=>(r=t,e)),n},async close(){n!==void 0&&(await n.catch(()=>void 0),await r?.())}}}function restoreDevelopmentSandboxRunId(e){if(e===void 0){delete process.env[EVE_DEVELOPMENT_SANDBOX_RUN_ID_ENV];return}process.env[EVE_DEVELOPMENT_SANDBOX_RUN_ID_ENV]=e}export{createDevelopmentServer,normalizeDevelopmentServerClientUrl};
|
|
@@ -7,14 +7,36 @@ import type { DevBootProgressReporter } from "#internal/dev-boot-progress.js";
|
|
|
7
7
|
* Route surface included in one programmatic Nitro host build.
|
|
8
8
|
*/
|
|
9
9
|
export type NitroBuildSurface = "all" | "app" | "flow";
|
|
10
|
+
/** Outcome of starting a Nitro development server the current process owns. */
|
|
11
|
+
export interface StartedDevelopmentServer {
|
|
12
|
+
readonly kind: "started";
|
|
13
|
+
readonly appRoot: string;
|
|
14
|
+
readonly url: string;
|
|
15
|
+
}
|
|
16
|
+
/** A live development server owned by another process. */
|
|
17
|
+
export interface ExistingDevelopmentServer {
|
|
18
|
+
readonly kind: "existing";
|
|
19
|
+
readonly appRoot: string;
|
|
20
|
+
readonly url: string;
|
|
21
|
+
}
|
|
22
|
+
/** Result of starting a development server for an app root. */
|
|
23
|
+
export type DevelopmentServerHandle = StartedDevelopmentServer | ExistingDevelopmentServer;
|
|
10
24
|
/**
|
|
11
|
-
*
|
|
25
|
+
* Lifecycle for one in-process Nitro development server.
|
|
26
|
+
*
|
|
27
|
+
* `start()` either boots a server this process owns or attaches to a running
|
|
28
|
+
* owner; the {@link DevelopmentServerHandle} result discriminates which.
|
|
29
|
+
* `close()` waits for an in-progress `start()`, then tears down only a server
|
|
30
|
+
* this instance started. It resolves after startup cleanup when `start()`
|
|
31
|
+
* fails, and is a no-op when the instance attached to an existing owner or was
|
|
32
|
+
* never started.
|
|
12
33
|
*/
|
|
13
|
-
export interface DevelopmentServerHandle {
|
|
34
|
+
export interface DevelopmentServer<H extends DevelopmentServerHandle = DevelopmentServerHandle> {
|
|
35
|
+
start(): Promise<H>;
|
|
14
36
|
close(): Promise<void>;
|
|
15
|
-
url: string;
|
|
16
37
|
}
|
|
17
38
|
export interface DevelopmentServerOptions {
|
|
39
|
+
readonly existing?: "attach-if-unconfigured" | "reject";
|
|
18
40
|
readonly host?: string;
|
|
19
41
|
readonly onBootProgress?: DevBootProgressReporter;
|
|
20
42
|
readonly port?: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { buildApplication } from "#internal/nitro/host/build-application.js";
|
|
2
|
-
export {
|
|
2
|
+
export { createDevelopmentServer } from "#internal/nitro/host/start-development-server.js";
|
|
3
3
|
export { startProductionServer } from "#internal/nitro/host/start-production-server.js";
|
|
4
|
-
export type { DevelopmentServerHandle, DevelopmentServerOptions, ProductionServerHandle, } from "#internal/nitro/host/types.js";
|
|
4
|
+
export type { DevelopmentServer, DevelopmentServerHandle, DevelopmentServerOptions, ExistingDevelopmentServer, ProductionServerHandle, StartedDevelopmentServer, } from "#internal/nitro/host/types.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildApplication}from"#internal/nitro/host/build-application.js";import{
|
|
1
|
+
import{buildApplication}from"#internal/nitro/host/build-application.js";import{createDevelopmentServer}from"#internal/nitro/host/start-development-server.js";import{startProductionServer}from"#internal/nitro/host/start-production-server.js";export{buildApplication,createDevelopmentServer,startProductionServer};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{transformWorkflowDirectives}from"./workflow-transformer.js";import{existsSync,readFileSync}from"node:fs";import{dirname,isAbsolute,join,relative,resolve,sep}from"node:path";import{readFile}from"node:fs/promises";import{STABLE_WORKFLOW_NAMES}from"#execution/workflow-runtime.js";import{EVE_WORKFLOW_QUEUE_TOPIC}from"#internal/workflow/queue-namespace.js";const WORKFLOW_QUEUE_TRIGGER={type:`queue/v2beta`,topic:EVE_WORKFLOW_QUEUE_TOPIC,consumer:`default`,retryAfterSeconds:5,initialDelaySeconds:0},packageJsonCache=new Map,projectDepsCache=new Map;async function applyWorkflowTransform(t,n,r,o,s,c=STABLE_WORKFLOW_NAMES){let l=s??process.cwd(),{moduleSpecifier:u,stableModuleSpecifier:d}=resolveModuleSpecifier(o===void 0?isAbsolute(t)?t:join(l,t):o,l);return transformWorkflowDirectives({filename:t,mode:r,moduleSpecifier:u,source:n,stableModuleSpecifier:d,stableWorkflowNames:c})}function detectWorkflowPatterns(e){return{hasSerde:e.includes(`workflow.serde`)||e.includes(`@serde`)||e.includes(`workflowSerde`)||e.includes(`__workflow_serde`),hasUseStep:/["']use step["']/.test(e),hasUseWorkflow:/["']use workflow["']/.test(e)}}function getImportPath(e,t){let n=isInNodeModules(e),r=!n&&isWorkspacePackage(e,t);if(n||r){let n=findPackageJson(e);if(n!==null){let i=getProjectDependencies(t).has(n.name);if(!(r||i))return{importPath:toRelativeImportPath(e,t),isPackage:!1};let a=resolveExportSubpath(e,n);return a?{importPath:`${n.name}${a}`,isPackage:!0}:isRootEntrypointFile(e,n)?{importPath:n.name,isPackage:!0}:{importPath:toRelativeImportPath(e,t),isPackage:!1}}}return{importPath:toRelativeImportPath(e,t),isPackage:!1}}function resolveModuleSpecifier(e,t){let n=isInNodeModules(e),r=!n&&isWorkspacePackage(e,t);if(!n&&!r)return{moduleSpecifier:void 0,stableModuleSpecifier:void 0};
|
|
1
|
+
import{transformWorkflowDirectives}from"./workflow-transformer.js";import{existsSync,readFileSync}from"node:fs";import{dirname,isAbsolute,join,relative,resolve,sep}from"node:path";import{readFile}from"node:fs/promises";import{STABLE_WORKFLOW_NAMES}from"#execution/workflow-runtime.js";import{EVE_PACKAGE_NAME}from"#internal/package-name.js";import{EVE_WORKFLOW_QUEUE_TOPIC}from"#internal/workflow/queue-namespace.js";const WORKFLOW_QUEUE_TRIGGER={type:`queue/v2beta`,topic:EVE_WORKFLOW_QUEUE_TOPIC,consumer:`default`,retryAfterSeconds:5,initialDelaySeconds:0},packageJsonCache=new Map,projectDepsCache=new Map;async function applyWorkflowTransform(t,n,r,o,s,c=STABLE_WORKFLOW_NAMES){let l=s??process.cwd(),{moduleSpecifier:u,stableModuleSpecifier:d}=resolveModuleSpecifier(o===void 0?isAbsolute(t)?t:join(l,t):o,l);return transformWorkflowDirectives({filename:t,mode:r,moduleSpecifier:u,source:n,stableModuleSpecifier:d,stableWorkflowNames:c})}function detectWorkflowPatterns(e){return{hasSerde:e.includes(`workflow.serde`)||e.includes(`@serde`)||e.includes(`workflowSerde`)||e.includes(`__workflow_serde`),hasUseStep:/["']use step["']/.test(e),hasUseWorkflow:/["']use workflow["']/.test(e)}}function getImportPath(e,t){let n=isInNodeModules(e),r=!n&&isWorkspacePackage(e,t);if(n||r){let n=findPackageJson(e);if(n!==null){let i=getProjectDependencies(t).has(n.name);if(!(r||i))return{importPath:toRelativeImportPath(e,t),isPackage:!1};let a=resolveExportSubpath(e,n);return a?{importPath:`${n.name}${a}`,isPackage:!0}:isRootEntrypointFile(e,n)?{importPath:n.name,isPackage:!0}:{importPath:toRelativeImportPath(e,t),isPackage:!1}}}return{importPath:toRelativeImportPath(e,t),isPackage:!1}}function resolveModuleSpecifier(e,t){let n=isInNodeModules(e),r=!n&&isWorkspacePackage(e,t),i=findPackageJson(e);if(!n&&!r)return{moduleSpecifier:void 0,stableModuleSpecifier:i?.name===EVE_PACKAGE_NAME?EVE_PACKAGE_NAME:void 0};if(i===null)return{moduleSpecifier:void 0,stableModuleSpecifier:void 0};let a=resolveExportSubpath(e,i),o=a?`${i.name}${a}`:i.name;return{moduleSpecifier:`${o}@${i.version}`,stableModuleSpecifier:o}}function findPackageJson(e){let i=dirname(e),o=[];for(;i!==dirname(i);){let e=packageJsonCache.get(i);if(e!==void 0){for(let t of o)packageJsonCache.set(t,e);return e}o.push(i);let s=join(i,`package.json`);if(existsSync(s))try{let e=JSON.parse(readFileSync(s,`utf8`));if(typeof e.name==`string`&&typeof e.version==`string`){let t={dir:i,exports:e.exports,main:e.main,module:e.module,name:e.name,version:e.version};packageJsonCache.set(i,t);for(let e of o)packageJsonCache.set(e,t);return t}}catch{}i=dirname(i)}for(let e of o)packageJsonCache.set(e,null);return null}function resolveExportSubpath(e,t){if(t.exports===null||typeof t.exports!=`object`||Array.isArray(t.exports))return``;let n=e.replace(/\\/g,`/`),r=t.dir.replace(/\\/g,`/`),i=n.startsWith(`${r}/`)?`./${n.substring(r.length+1)}`:null;if(i===null)return``;for(let[e,n]of Object.entries(t.exports)){let t=resolveExportTarget(n);if(t!==null&&normalizeExportPath(t)===i)return e===`.`?``:e.substring(1)}return``}function resolveExportTarget(e){if(typeof e==`string`)return e;if(Array.isArray(e)){for(let t of e){let e=resolveExportTarget(t);if(e!==null)return e}return null}if(typeof e==`object`&&e)for(let t of[`workflow`,`default`,`require`,`import`,`node`]){let n=e[t],r=resolveExportTarget(n);if(r!==null)return r}return null}function normalizeExportPath(e){return e.startsWith(`./`)?e:`./${e}`}function isInNodeModules(e){return e.split(sep).join(`/`).includes(`/node_modules/`)}function getProjectDependencies(e){let r=projectDepsCache.get(e);if(r!==void 0)return r;let i=new Set,o=join(e,`package.json`);if(existsSync(o))try{let e=JSON.parse(readFileSync(o,`utf8`));for(let t of[`dependencies`,`devDependencies`,`peerDependencies`,`optionalDependencies`]){let n=e[t];if(typeof n==`object`&&n&&!Array.isArray(n))for(let e of Object.keys(n))i.add(e)}}catch{}return projectDepsCache.set(e,i),i}function isWorkspacePackage(e,t){if(isInNodeModules(e))return!1;let n=findPackageJson(e);return n===null||resolve(n.dir)===resolve(t)?!1:getProjectDependencies(t).has(n.name)}function toRelativeImportPath(e,t){let n=t.replace(/\\/g,`/`),r=e.replace(/\\/g,`/`),i=r.startsWith(`${n}/`)?r.substring(n.length+1):relative(t,e).replace(/\\/g,`/`);return i.startsWith(`.`)||(i=`./${i}`),i}function hasRootExport(e){if(typeof e==`string`||Array.isArray(e))return!0;if(typeof e!=`object`||!e)return!1;let t=Object.keys(e);return t.length>0&&t.every(e=>!e.startsWith(`.`))?!0:`.`in e}function normalizePackageTargetPath(e){let t=e.replace(/\\/g,`/`);return t.startsWith(`./`)?t.substring(2):t.startsWith(`/`)?t.substring(1):t}function isRootEntrypointFile(e,t){let n=e.replace(/\\/g,`/`),r=t.dir.replace(/\\/g,`/`);if(!n.startsWith(`${r}/`))return!1;let i=n.substring(r.length+1);if(t.exports!==void 0){let e;if(t.exports!==null&&typeof t.exports==`object`&&`.`in t.exports)e=t.exports[`.`];else if(hasRootExport(t.exports))e=t.exports;else return!1;let n=resolveExportTarget(e);return n!==null&&normalizePackageTargetPath(n)===i}return[t.module,t.main,`index.js`,`index.mjs`,`index.cjs`,`index.ts`,`index.mts`,`index.cts`].flatMap(e=>typeof e==`string`?[normalizePackageTargetPath(e)]:[]).includes(i)}async function readSourceFile(e){return await readFile(e,`utf8`)}export{WORKFLOW_QUEUE_TRIGGER,applyWorkflowTransform,detectWorkflowPatterns,getImportPath,readSourceFile};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{pinnedNodeEngineMajor}from"../../node-engine.js";import{pathExists,writeTextFile}from"../files.js";import{resolveVersionToken}from"../version-tokens.js";import{applyPackageManagerWorkspaceConfiguration,isPackageManagerWorkspaceMember,patchWorkspaceRootPackageJson}from"../workspace-root.js";import{SUPPORTED_AUTHORED_MODULE_FILE_EXTENSIONS}from"../update/module-files.js";import{WEB_APP_TEMPLATE_FILES}from"./web-template.js";import{basename,join,resolve}from"node:path";import{mkdir,readdir,stat}from"node:fs/promises";const CURRENT_DIRECTORY_PROJECT_NAME=`.`,ALLOWED_CREATE_IN_PLACE_ENTRIES=new Set([`.DS_Store`,`.git`,`.gitkeep`,`.hg`]),DEFAULT_AI_PACKAGE_VERSION=`^7.0.0`,DEFAULT_CONNECT_PACKAGE_VERSION=`0.2.2`,DEFAULT_ZOD_PACKAGE_VERSION=`4.4.3`,DEFAULT_EVE_PACKAGE_CONTRACT={version:`0.15.
|
|
1
|
+
import{pinnedNodeEngineMajor}from"../../node-engine.js";import{pathExists,writeTextFile}from"../files.js";import{resolveVersionToken}from"../version-tokens.js";import{applyPackageManagerWorkspaceConfiguration,isPackageManagerWorkspaceMember,patchWorkspaceRootPackageJson}from"../workspace-root.js";import{SUPPORTED_AUTHORED_MODULE_FILE_EXTENSIONS}from"../update/module-files.js";import{WEB_APP_TEMPLATE_FILES}from"./web-template.js";import{basename,join,resolve}from"node:path";import{mkdir,readdir,stat}from"node:fs/promises";const CURRENT_DIRECTORY_PROJECT_NAME=`.`,ALLOWED_CREATE_IN_PLACE_ENTRIES=new Set([`.DS_Store`,`.git`,`.gitkeep`,`.hg`]),DEFAULT_AI_PACKAGE_VERSION=`^7.0.0`,DEFAULT_CONNECT_PACKAGE_VERSION=`0.2.2`,DEFAULT_ZOD_PACKAGE_VERSION=`4.4.3`,DEFAULT_EVE_PACKAGE_CONTRACT={version:`0.15.2`,nodeEngine:`>=24`};function resolveEvePackageContract(e=DEFAULT_EVE_PACKAGE_CONTRACT){return{version:resolveVersionToken(`evePackage.version`,e.version),nodeEngine:resolveVersionToken(`evePackage.nodeEngine`,e.nodeEngine)}}function modelProviderSlug(e){let t=(e.split(`/`)[0]??``).replaceAll(/[^A-Za-z0-9._-]/gu,``);return t.length>0?t:`anthropic`}function byokProviderEnvVar(e){let t=modelProviderSlug(e).toUpperCase().replaceAll(/[^A-Z0-9]/gu,`_`);return`${/^[0-9]/.test(t)?`_`:``}${t}_API_KEY`}function agentTemplateFiles(e){return{"agent/agent.ts":BASE_AGENT_TEMPLATE.replaceAll(`__EVE_INIT_MODEL__`,e),"agent/channels/eve.ts":WEB_APP_TEMPLATE_FILES[`agent/channels/eve.ts`],"agent/instructions.md":AGENT_INSTRUCTIONS_TEMPLATE}}function renderTemplate(e,t){return e.replaceAll(`__EVE_INIT_APP_NAME__`,t.appName).replaceAll(`__EVE_INIT_MODEL__`,t.model).replaceAll(`__EVE_INIT_BYOK_PROVIDER__`,modelProviderSlug(t.model)).replaceAll(`__EVE_INIT_BYOK_ENV_VAR__`,byokProviderEnvVar(t.model)).replaceAll(`__EVE_INIT_PACKAGE_VERSION__`,formatEveDependencySpecifier(t.eveVersion)).replaceAll(`__EVE_INIT_AI_SDK_VERSION__`,t.aiPackageVersion).replaceAll(`__EVE_INIT_CONNECT_VERSION__`,t.connectPackageVersion).replaceAll(`__EVE_INIT_ZOD_VERSION__`,t.zodPackageVersion).replaceAll(`__EVE_INIT_TYPESCRIPT_VERSION__`,t.typescriptPackageVersion).replaceAll(`__EVE_INIT_TYPES_NODE_VERSION__`,t.nodeTypesVersion).replaceAll(`__EVE_INIT_NODE_ENGINE__`,t.nodeEngine)}function formatEveDependencySpecifier(e){return/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/.test(e)?`^${e}`:e}const BASE_AGENT_TEMPLATE=`import { defineAgent } from "eve";
|
|
2
2
|
|
|
3
3
|
export default defineAgent({
|
|
4
4
|
model: "__EVE_INIT_MODEL__",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{EVE_HEALTH_ROUTE_PATH}from"#protocol/routes.js";async function isEveServerHealthy(e,t={}){let n=AbortSignal.timeout(t.timeoutMs??1e3),r=t.signal===void 0?n:AbortSignal.any([t.signal,n]);try{let t=new URL(EVE_HEALTH_ROUTE_PATH,e).toString();return(await fetch(t,{redirect:`error`,signal:r})).ok}catch{return!1}}export{isEveServerHealthy};
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import { z } from "#compiled/zod/index.js";
|
|
2
|
+
/** HTTP(S) URL accepted as a development-server endpoint. */
|
|
3
|
+
export declare const httpServerUrlSchema: z.ZodURL;
|
|
4
|
+
/**
|
|
5
|
+
* Returns whether `hostname` names the current machine's loopback interface.
|
|
6
|
+
* Accepts the full IPv4 loopback block, IPv6 loopback, and the RFC 6761
|
|
7
|
+
* `localhost` namespace. Wildcard bind addresses such as `0.0.0.0` are not
|
|
8
|
+
* loopback connect targets.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isLoopbackHostname(hostname: string): boolean;
|
|
11
|
+
/** Returns whether `urlText` is an HTTP(S) URL with a loopback hostname. */
|
|
12
|
+
export declare function isLoopbackServerUrl(urlText: string): boolean;
|
|
1
13
|
/**
|
|
2
14
|
* Whether `host` is an IP literal in a private, link-local, or otherwise
|
|
3
15
|
* reserved range that an outbound framework request must not target — an SSRF
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BlockList,isIP}from"node:net";const reservedRanges=new BlockList;reservedRanges.addSubnet(`0.0.0.0`,8,`ipv4`),reservedRanges.addSubnet(`10.0.0.0`,8,`ipv4`),reservedRanges.addSubnet(`100.64.0.0`,10,`ipv4`),reservedRanges.addSubnet(`169.254.0.0`,16,`ipv4`),reservedRanges.addSubnet(`172.16.0.0`,12,`ipv4`),reservedRanges.addSubnet(`192.168.0.0`,16,`ipv4`),reservedRanges.addSubnet(`198.18.0.0`,15,`ipv4`),reservedRanges.addAddress(`::`,`ipv6`),reservedRanges.addSubnet(`fc00::`,7,`ipv6`),reservedRanges.addSubnet(`fe80::`,10,`ipv6`);function normalizeAddress(e){let t=e.trim().replace(/^\[(.*)\]$/u,`$1`),n=t.indexOf(`%`),r=n===-1?t:t.slice(0,n);if(r.toLowerCase().startsWith(`::ffff:`)){let e=r.slice(7);if(isIP(e)===4)return e}return r}function
|
|
1
|
+
import{z}from"#compiled/zod/index.js";import{BlockList,isIP}from"node:net";const httpServerUrlSchema=z.url({protocol:/^https?$/}),reservedRanges=new BlockList;reservedRanges.addSubnet(`0.0.0.0`,8,`ipv4`),reservedRanges.addSubnet(`10.0.0.0`,8,`ipv4`),reservedRanges.addSubnet(`100.64.0.0`,10,`ipv4`),reservedRanges.addSubnet(`169.254.0.0`,16,`ipv4`),reservedRanges.addSubnet(`172.16.0.0`,12,`ipv4`),reservedRanges.addSubnet(`192.168.0.0`,16,`ipv4`),reservedRanges.addSubnet(`198.18.0.0`,15,`ipv4`),reservedRanges.addAddress(`::`,`ipv6`),reservedRanges.addSubnet(`fc00::`,7,`ipv6`),reservedRanges.addSubnet(`fe80::`,10,`ipv6`);function normalizeAddress(e){let t=e.trim().replace(/^\[(.*)\]$/u,`$1`),n=t.indexOf(`%`),r=n===-1?t:t.slice(0,n);if(r.toLowerCase().startsWith(`::ffff:`)){let e=r.slice(7);if(isIP(e)===4)return e}return r}function isLoopbackHostname(e){let t=normalizeAddress(e).toLowerCase();if(t===`localhost`||t.endsWith(`.localhost`))return!0;let n=isIP(t);return n===4?t.startsWith(`127.`):n===6&&t===`::1`}function isLoopbackServerUrl(e){let t=httpServerUrlSchema.safeParse(e);return t.success&&isLoopbackHostname(new URL(t.data).hostname)}function isReservedIpAddress(e){let t=normalizeAddress(e),n=isIP(t);return n===0?!1:reservedRanges.check(t,n===4?`ipv4`:`ipv6`)}export{httpServerUrlSchema,isLoopbackHostname,isLoopbackServerUrl,isReservedIpAddress};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A typed success-or-failure value for expected, recoverable outcomes. */
|
|
2
|
+
export type Result<T, E> = {
|
|
3
|
+
readonly ok: true;
|
|
4
|
+
readonly value: T;
|
|
5
|
+
} | {
|
|
6
|
+
readonly ok: false;
|
|
7
|
+
readonly error: E;
|
|
8
|
+
};
|
|
9
|
+
/** Wraps a value as a successful {@link Result}. */
|
|
10
|
+
export declare function ok<T>(value: T): Result<T, never>;
|
|
11
|
+
/** Wraps an error as a failed {@link Result}. */
|
|
12
|
+
export declare function err<E>(error: E): Result<never, E>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function ok(e){return{ok:!0,value:e}}function err(e){return{ok:!1,error:e}}export{err,ok};
|
|
@@ -32,11 +32,11 @@ eve resolves and caches connection tokens per step; they never land in conversat
|
|
|
32
32
|
|
|
33
33
|
A connection credential can belong to the agent or to the person using it. This choice is separate from route auth, but user-scoped connection auth depends on route auth: eve can only resolve a user token when the active session has `ctx.session.auth.current?.principalType === "user"`.
|
|
34
34
|
|
|
35
|
-
| Credential owner | Use when
|
|
36
|
-
| ---------------- |
|
|
37
|
-
| App | The agent should use one shared service, bot, installation, or app credential.
|
|
38
|
-
| User | Each end-user should authorize and use their own third-party account.
|
|
39
|
-
| User from a job | Background work should use the same user's OAuth grant that started the work.
|
|
35
|
+
| Credential owner | Use when | Auth shape |
|
|
36
|
+
| ---------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
37
|
+
| App | The agent should use one shared service, bot, installation, or app credential. | `auth: { getToken }` defaults to `principalType: "app"`, or use `connect({ connector: "linear/myagent", principalType: "app" })` with Vercel Connect. |
|
|
38
|
+
| User | Each end-user should authorize and use their own third-party account. | `connect("linear/myagent")`, `connect({ connector: "linear/myagent", principalType: "user" })`, or `auth: { principalType: "user", getToken }`. |
|
|
39
|
+
| User from a job | Background work should use the same user's OAuth grant that started the work. | Start or resume the session through a channel whose route auth resolved that user, or pass an explicit user auth context when dispatching through a channel. |
|
|
40
40
|
|
|
41
41
|
`principalType: "user"` does not mean "ask any human later." It means "key this credential to the authenticated user already attached to the eve session." If the run was started by a schedule, a same-project runtime token, `localDev()`, or another internal runtime path without an end-user principal, a user-scoped connection fails with `reason: "principal_required"` instead of starting OAuth. In that case, either authenticate the inbound channel as a user or configure the connection as app-scoped.
|
|
42
42
|
|
|
@@ -157,12 +157,12 @@ App-scoped Connect auth is non-interactive. eve asks Vercel Connect for an app t
|
|
|
157
157
|
|
|
158
158
|
### Troubleshooting Vercel Connect auth
|
|
159
159
|
|
|
160
|
-
| Symptom | What it means
|
|
161
|
-
| -------------------------------------------- |
|
|
162
|
-
| `reason: "principal_required"` | A user-scoped connection ran without an authenticated user on the active session. | Return `principalType: "user"` from the channel's route auth, or change the connection to `principalType: "app"` if it should be shared.
|
|
163
|
-
| `authorization.required` appears but no UI | eve parked the turn for OAuth, but the channel or frontend is not rendering the challenge. | Render the challenge from the stream event and continue the same session after the callback.
|
|
164
|
-
| OAuth works locally but fails after deploy | The project may not be linked to the Connect client, or the deployed runtime may not have the expected Vercel OIDC/project scope. | Run Connect setup from the consuming project directory, link the project, deploy again, and verify the connector UID in `connect("...")`.
|
|
165
|
-
| A scheduled or internal run needs user OAuth | Schedules and runtime callers do not automatically carry an end-user principal. | Dispatch through a user-authenticated channel when work is user-owned, or use app-scoped auth for agent-owned background work.
|
|
160
|
+
| Symptom | What it means | Fix |
|
|
161
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| `reason: "principal_required"` | A user-scoped connection ran without an authenticated user on the active session. | Return `principalType: "user"` from the channel's route auth, or change the connection to `principalType: "app"` if it should be shared. |
|
|
163
|
+
| `authorization.required` appears but no UI | eve parked the turn for OAuth, but the channel or frontend is not rendering the challenge. | Render the challenge from the stream event and continue the same session after the callback. |
|
|
164
|
+
| OAuth works locally but fails after deploy | The project may not be linked to the Connect client, or the deployed runtime may not have the expected Vercel OIDC/project scope. | Run Connect setup from the consuming project directory, link the project, deploy again, and verify the connector UID in `connect("...")`. |
|
|
165
|
+
| A scheduled or internal run needs user OAuth | Schedules and runtime callers do not automatically carry an end-user principal. | Dispatch through a user-authenticated channel when work is user-owned, or use app-scoped auth for agent-owned background work. |
|
|
166
166
|
|
|
167
167
|
## Self-hosted interactive OAuth
|
|
168
168
|
|
package/docs/guides/dev-tui.md
CHANGED
|
@@ -24,7 +24,7 @@ The conversation streams straight into your terminal's normal scrollback, so you
|
|
|
24
24
|
|
|
25
25
|
Each turn renders without boxes. A colored gutter glyph marks who is speaking, tool calls collapse to a one-line summary (`✓ get_weather city="SF" → 73°F`), and a subagent's work is indented beneath its `◆` header. When input is ready, the prompt stays bare until you type. A green circle-dot pulses while the agent is waiting to answer and disappears when reasoning or answer content begins.
|
|
26
26
|
|
|
27
|
-
A persistent line beneath the prompt or status shows the model, the session's token flow (`↑ 394.4K ↓ 4.3K`), the linked Vercel project, and a yellow `/deploy pending` marker once a channel added this session still needs `/deploy`. The Vercel segment stays hidden until the directory is linked. Remote sessions lead with a padded `↗ project (environment)` badge, or the host when Vercel cannot resolve the deployment. The badge is gray while checking or unavailable, yellow while authentication is required or failed, and blue when connected. Remote status lines omit AI Gateway endpoint state.
|
|
27
|
+
A persistent line beneath the prompt or status shows the model, the session's token flow (`↑ 394.4K ↓ 4.3K`), the linked Vercel project, and a yellow `/deploy pending` marker once a channel added this session still needs `/deploy`. The Vercel segment stays hidden until the directory is linked. Local sessions lead with a gray `:port` badge. Remote sessions lead with a padded `↗ project (environment)` badge, or the host when Vercel cannot resolve the deployment. The badge is gray while checking or unavailable, yellow while authentication is required or failed, and blue when connected. Other status segments use `·` separators. Remote status lines omit AI Gateway endpoint state.
|
|
28
28
|
|
|
29
29
|
Errors render compactly with docs links highlighted. A code bug escaping your agent's own code shows its stack trace dim beneath the error headline. Dev-server rebuilds condense into one status row that updates in place (`tui/setup-panel.ts changed · rebuilding…`, then `· rebuilt`); only the latest rebuild shows, and paths shrink to their last two components.
|
|
30
30
|
|
package/docs/meta.json
CHANGED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Dynamic scheduling"
|
|
3
|
+
description: "Compose one minute-level eve schedule, proactive channel handoff, and CRUD tools into application-managed schedules."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Authored eve schedules are static files discovered at build time. You can build dynamic scheduling today by putting schedule rows in your application store and using one authored schedule as a dispatcher:
|
|
7
|
+
|
|
8
|
+
1. CRUD tools let the agent create and manage rows for the current tenant;
|
|
9
|
+
2. `defineSchedule({ cron: "* * * * *" })` wakes once a minute;
|
|
10
|
+
3. the handler atomically claims due rows;
|
|
11
|
+
4. `receive(...)` starts a normal durable agent session for each row.
|
|
12
|
+
|
|
13
|
+
PostgreSQL or a durable KV store can back the adapter. The important storage capability is an atomic lease, not a particular schema.
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
agent/
|
|
17
|
+
channels/slack.ts
|
|
18
|
+
lib/schedule-store.ts # your storage adapter
|
|
19
|
+
lib/tenant.ts
|
|
20
|
+
schedules/dynamic.ts
|
|
21
|
+
tools/create_schedule.ts
|
|
22
|
+
tools/delete_schedule.ts
|
|
23
|
+
tools/list_schedules.ts
|
|
24
|
+
tools/update_schedule.ts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Dispatch due schedules every minute
|
|
28
|
+
|
|
29
|
+
This is the only authored schedule. It looks up due application-managed rows and hands each one to Slack as a proactive session:
|
|
30
|
+
|
|
31
|
+
```ts title="agent/schedules/dynamic.ts"
|
|
32
|
+
import { defineSchedule } from "eve/schedules";
|
|
33
|
+
import slack from "../channels/slack.js";
|
|
34
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
35
|
+
|
|
36
|
+
export default defineSchedule({
|
|
37
|
+
cron: "* * * * *",
|
|
38
|
+
run({ receive, waitUntil }) {
|
|
39
|
+
waitUntil(
|
|
40
|
+
(async () => {
|
|
41
|
+
const jobs = await scheduleStore.claimDue({
|
|
42
|
+
now: new Date(),
|
|
43
|
+
limit: 25,
|
|
44
|
+
leaseForMs: 5 * 60_000,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await Promise.all(
|
|
48
|
+
jobs.map(async (job) => {
|
|
49
|
+
try {
|
|
50
|
+
await receive(slack, {
|
|
51
|
+
message: [
|
|
52
|
+
`Run dynamic schedule ${job.id}.`,
|
|
53
|
+
"Complete this tenant-owned task:",
|
|
54
|
+
job.prompt,
|
|
55
|
+
].join("\n\n"),
|
|
56
|
+
target: { channelId: job.channelId },
|
|
57
|
+
auth: {
|
|
58
|
+
attributes: {
|
|
59
|
+
tenantId: job.tenantId,
|
|
60
|
+
role: job.ownerRole,
|
|
61
|
+
scheduleId: job.id,
|
|
62
|
+
},
|
|
63
|
+
authenticator: job.authenticator,
|
|
64
|
+
...(job.issuer ? { issuer: job.issuer } : {}),
|
|
65
|
+
principalId: job.ownerId,
|
|
66
|
+
principalType: "user",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
await scheduleStore.complete(job);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
await scheduleStore.release(job, { error, retryAt: new Date(Date.now() + 300_000) });
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
})(),
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`waitUntil` keeps the cron invocation alive until claiming and handoff settle. `receive` starts the same durable runtime used by inbound channel messages.
|
|
82
|
+
|
|
83
|
+
This example uses Slack because it has a proactive target of `{ channelId }`. Any channel that implements `receive` can replace it.
|
|
84
|
+
|
|
85
|
+
Configure Slack normally:
|
|
86
|
+
|
|
87
|
+
```ts title="agent/channels/slack.ts"
|
|
88
|
+
import { connectSlackCredentials } from "@vercel/connect/eve";
|
|
89
|
+
import { slackChannel } from "eve/channels/slack";
|
|
90
|
+
|
|
91
|
+
export default slackChannel({
|
|
92
|
+
credentials: connectSlackCredentials("slack/my-agent"),
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Give the agent CRUD tools
|
|
97
|
+
|
|
98
|
+
Tenant and owner identity come from `ctx.session`, never the model:
|
|
99
|
+
|
|
100
|
+
```ts title="agent/lib/tenant.ts"
|
|
101
|
+
import type { SessionAuthContext, SessionContext } from "eve/context";
|
|
102
|
+
|
|
103
|
+
export function requireScheduleOwner(ctx: SessionContext): {
|
|
104
|
+
tenantId: string;
|
|
105
|
+
userId: string;
|
|
106
|
+
auth: SessionAuthContext;
|
|
107
|
+
} {
|
|
108
|
+
const auth = ctx.session.auth.current;
|
|
109
|
+
const tenantId = auth?.attributes.tenantId;
|
|
110
|
+
if (auth?.principalType !== "user" || typeof tenantId !== "string") {
|
|
111
|
+
throw new Error("An authenticated tenant user is required.");
|
|
112
|
+
}
|
|
113
|
+
return { tenantId, userId: auth.principalId, auth };
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Create a one-time schedule with `everyMinutes: null`, or a recurring one with an interval:
|
|
118
|
+
|
|
119
|
+
```ts title="agent/tools/create_schedule.ts"
|
|
120
|
+
import { defineTool } from "eve/tools";
|
|
121
|
+
import { z } from "zod";
|
|
122
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
123
|
+
import { requireScheduleOwner } from "../lib/tenant.js";
|
|
124
|
+
|
|
125
|
+
export default defineTool({
|
|
126
|
+
description: "Create a one-time or repeating scheduled agent run for this tenant.",
|
|
127
|
+
inputSchema: z.object({
|
|
128
|
+
prompt: z.string().min(1).max(8000),
|
|
129
|
+
channelId: z.string().min(1),
|
|
130
|
+
firstRunAt: z.string().datetime({ offset: true }),
|
|
131
|
+
everyMinutes: z.number().int().min(1).max(525600).nullable().default(null),
|
|
132
|
+
}),
|
|
133
|
+
async execute(input, ctx) {
|
|
134
|
+
return await scheduleStore.create(requireScheduleOwner(ctx), {
|
|
135
|
+
...input,
|
|
136
|
+
firstRunAt: new Date(input.firstRunAt),
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```ts title="agent/tools/list_schedules.ts"
|
|
143
|
+
import { defineTool } from "eve/tools";
|
|
144
|
+
import { z } from "zod";
|
|
145
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
146
|
+
import { requireScheduleOwner } from "../lib/tenant.js";
|
|
147
|
+
|
|
148
|
+
export default defineTool({
|
|
149
|
+
description: "List this tenant's dynamic schedules and their latest status.",
|
|
150
|
+
inputSchema: z.object({}),
|
|
151
|
+
async execute(_input, ctx) {
|
|
152
|
+
return await scheduleStore.list(requireScheduleOwner(ctx));
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```ts title="agent/tools/update_schedule.ts"
|
|
158
|
+
import { defineTool } from "eve/tools";
|
|
159
|
+
import { z } from "zod";
|
|
160
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
161
|
+
import { requireScheduleOwner } from "../lib/tenant.js";
|
|
162
|
+
|
|
163
|
+
export default defineTool({
|
|
164
|
+
description: "Change, pause, or resume one of this tenant's schedules.",
|
|
165
|
+
inputSchema: z.object({
|
|
166
|
+
id: z.string().uuid(),
|
|
167
|
+
prompt: z.string().min(1).max(8000).optional(),
|
|
168
|
+
channelId: z.string().min(1).optional(),
|
|
169
|
+
nextRunAt: z.string().datetime({ offset: true }).optional(),
|
|
170
|
+
everyMinutes: z.number().int().min(1).max(525600).nullable().optional(),
|
|
171
|
+
enabled: z.boolean().optional(),
|
|
172
|
+
}),
|
|
173
|
+
async execute({ id, nextRunAt, ...patch }, ctx) {
|
|
174
|
+
return await scheduleStore.update(requireScheduleOwner(ctx), id, {
|
|
175
|
+
...patch,
|
|
176
|
+
...(nextRunAt ? { nextRunAt: new Date(nextRunAt) } : {}),
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```ts title="agent/tools/delete_schedule.ts"
|
|
183
|
+
import { defineTool } from "eve/tools";
|
|
184
|
+
import { always } from "eve/tools/approval";
|
|
185
|
+
import { z } from "zod";
|
|
186
|
+
import { scheduleStore } from "../lib/schedule-store.js";
|
|
187
|
+
import { requireScheduleOwner } from "../lib/tenant.js";
|
|
188
|
+
|
|
189
|
+
export default defineTool({
|
|
190
|
+
description: "Permanently delete one of this tenant's schedules.",
|
|
191
|
+
inputSchema: z.object({ id: z.string().uuid() }),
|
|
192
|
+
approval: always(),
|
|
193
|
+
async execute({ id }, ctx) {
|
|
194
|
+
return { deleted: await scheduleStore.delete(requireScheduleOwner(ctx), id) };
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Supply the schedule adapter
|
|
200
|
+
|
|
201
|
+
The eve-facing implementation depends on this shape, not a database schema:
|
|
202
|
+
|
|
203
|
+
```ts title="agent/lib/schedule-store.ts"
|
|
204
|
+
import type { SessionAuthContext } from "eve/context";
|
|
205
|
+
|
|
206
|
+
export interface ScheduleOwner {
|
|
207
|
+
tenantId: string;
|
|
208
|
+
userId: string;
|
|
209
|
+
auth: SessionAuthContext;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface ClaimedSchedule {
|
|
213
|
+
id: string;
|
|
214
|
+
leaseToken: string;
|
|
215
|
+
tenantId: string;
|
|
216
|
+
ownerId: string;
|
|
217
|
+
ownerRole: string;
|
|
218
|
+
authenticator: string;
|
|
219
|
+
issuer?: string;
|
|
220
|
+
prompt: string;
|
|
221
|
+
channelId: string;
|
|
222
|
+
everyMinutes: number | null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface ScheduleStore {
|
|
226
|
+
create(owner: ScheduleOwner, input: unknown): Promise<unknown>;
|
|
227
|
+
list(owner: ScheduleOwner): Promise<unknown[]>;
|
|
228
|
+
update(owner: ScheduleOwner, id: string, patch: unknown): Promise<unknown>;
|
|
229
|
+
delete(owner: ScheduleOwner, id: string): Promise<boolean>;
|
|
230
|
+
claimDue(options: { now: Date; limit: number; leaseForMs: number }): Promise<ClaimedSchedule[]>;
|
|
231
|
+
complete(job: ClaimedSchedule): Promise<void>;
|
|
232
|
+
release(job: ClaimedSchedule, failure: { error: unknown; retryAt: Date }): Promise<void>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export { scheduleStore } from "../../lib/schedule-store.js";
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Implement that adapter with whichever durable store already belongs to your application. It must preserve a few semantics:
|
|
239
|
+
|
|
240
|
+
- user-facing CRUD is always tenant-scoped;
|
|
241
|
+
- `claimDue` atomically leases rows so overlapping minute ticks do not claim the same work;
|
|
242
|
+
- dispatch revalidates the owner and destination before returning a job;
|
|
243
|
+
- `complete` disables one-time rows or computes the next recurring run;
|
|
244
|
+
- expired leases are recoverable.
|
|
245
|
+
|
|
246
|
+
Delivery is at least once. A crash after `receive` succeeds but before `complete` can dispatch again, so side-effecting tasks need application-level idempotency.
|
|
247
|
+
|
|
248
|
+
## Scheduling instructions
|
|
249
|
+
|
|
250
|
+
```md title="agent/instructions.md"
|
|
251
|
+
Before creating a schedule, confirm the user's time zone and destination.
|
|
252
|
+
Convert the first run to ISO 8601 with an explicit offset. Use everyMinutes only
|
|
253
|
+
for repeating work and null for a one-time run. List schedules before changing
|
|
254
|
+
an ambiguous one.
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The eve-specific core is small: four tools, one one-minute `defineSchedule`, and proactive `receive`. Storage and recurrence policy stay behind the application's adapter.
|