experimental-ash 0.18.1 → 0.18.3
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 +25 -2
- package/dist/docs/public/channels/README.md +75 -9
- package/dist/docs/public/schedules.md +13 -4
- package/dist/src/channel/adapter-context.d.ts +4 -0
- package/dist/src/channel/adapter-context.js +6 -0
- package/dist/src/channel/adapter.d.ts +10 -10
- package/dist/src/channel/cross-channel-receive.d.ts +1 -1
- package/dist/src/channel/cross-channel-receive.js +40 -0
- package/dist/src/channel/routes.d.ts +7 -0
- package/dist/src/channel/send.js +1 -1
- package/dist/src/channel/session.d.ts +47 -1
- package/dist/src/channel/session.js +46 -0
- package/dist/src/channel/types.d.ts +6 -5
- package/dist/src/chunks/{dev-authored-source-watcher-DtLxnrXI.js → dev-authored-source-watcher-j7YWh2Gx.js} +1 -1
- package/dist/src/chunks/{host-Dor4C8jo.js → host-C19hLVqS.js} +3 -3
- package/dist/src/chunks/{paths-AVYgVLR3.js → paths-Dwv0Eash.js} +21 -21
- package/dist/src/chunks/{prewarm-DsMkM8wg.js → prewarm-CQYfka30.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +2 -2
- package/dist/src/execution/sandbox/bindings/vercel.js +1 -34
- package/dist/src/execution/workflow-entry.js +35 -31
- package/dist/src/execution/workflow-steps.d.ts +16 -0
- package/dist/src/execution/workflow-steps.js +32 -4
- package/dist/src/harness/attachment-staging.js +2 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/workflow-bundle/vercel-workflow-output.js +0 -2
- package/dist/src/public/channels/slack/api.d.ts +13 -33
- package/dist/src/public/channels/slack/api.js +32 -94
- 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 +2 -2
- package/dist/src/public/channels/slack/index.js +1 -0
- package/dist/src/public/channels/slack/interactions.js +3 -3
- 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 +5 -3
- package/dist/src/public/channels/slack/slackChannel.js +20 -15
- package/dist/src/public/channels/twilio/api.d.ts +9 -0
- package/dist/src/public/channels/twilio/api.js +11 -0
- package/dist/src/public/channels/twilio/index.d.ts +1 -1
- package/dist/src/public/channels/twilio/index.js +1 -1
- package/dist/src/public/channels/twilio/twilioChannel.d.ts +2 -0
- package/dist/src/public/channels/twilio/twilioChannel.js +8 -11
- package/dist/src/public/definitions/defineChannel.d.ts +9 -1
- package/dist/src/public/definitions/defineChannel.js +7 -11
- package/dist/src/public/definitions/sandbox.d.ts +2 -3
- package/dist/src/public/sandbox/backends/vercel.d.ts +4 -4
- package/dist/src/public/sandbox/backends/vercel.js +2 -2
- package/dist/src/public/sandbox/index.d.ts +1 -1
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +3 -28
- package/dist/src/runtime/types.d.ts +1 -2
- package/dist/src/shared/sandbox-backend.d.ts +4 -4
- package/dist/src/shared/sandbox-definition.d.ts +6 -6
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"../../chunks/paths-
|
|
1
|
+
import{n as e}from"../../chunks/paths-Dwv0Eash.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-C19hLVqS.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,7 +1,7 @@
|
|
|
1
1
|
import type * as VercelSandboxSdk from "#compiled/@vercel/sandbox/index.js";
|
|
2
2
|
import type { Sandbox as SdkSandbox } from "#compiled/@vercel/sandbox/index.js";
|
|
3
3
|
import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions } from "#public/sandbox/vercel-sandbox.js";
|
|
5
5
|
type VercelSandboxModule = typeof VercelSandboxSdk;
|
|
6
6
|
/**
|
|
7
7
|
* User-controllable subset of `Sandbox.create` parameters.
|
|
@@ -18,5 +18,5 @@ export interface CreateVercelSandboxBackendInput {
|
|
|
18
18
|
/**
|
|
19
19
|
* Creates the Vercel-backed sandbox backend.
|
|
20
20
|
*/
|
|
21
|
-
export declare function createVercelSandboxBackend(input?: CreateVercelSandboxBackendInput): SandboxBackend<
|
|
21
|
+
export declare function createVercelSandboxBackend(input?: CreateVercelSandboxBackendInput): SandboxBackend<VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions>;
|
|
22
22
|
export {};
|
|
@@ -168,7 +168,7 @@ function createHandle(sandbox, sessionKey) {
|
|
|
168
168
|
if (options !== undefined) {
|
|
169
169
|
await sandbox.update(options);
|
|
170
170
|
}
|
|
171
|
-
return
|
|
171
|
+
return buildSandboxSession(createVercelInternalSandboxSession(sandbox, sessionKey));
|
|
172
172
|
},
|
|
173
173
|
async captureState() {
|
|
174
174
|
return {
|
|
@@ -180,39 +180,6 @@ function createHandle(sandbox, sessionKey) {
|
|
|
180
180
|
async dispose() { },
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
|
-
function buildVercelSandbox(sandbox, id) {
|
|
184
|
-
const session = buildSandboxSession(createVercelInternalSandboxSession(sandbox, id));
|
|
185
|
-
return {
|
|
186
|
-
...session,
|
|
187
|
-
get name() {
|
|
188
|
-
return sandbox.name;
|
|
189
|
-
},
|
|
190
|
-
get persistent() {
|
|
191
|
-
return sandbox.persistent;
|
|
192
|
-
},
|
|
193
|
-
get status() {
|
|
194
|
-
return sandbox.status;
|
|
195
|
-
},
|
|
196
|
-
get networkPolicy() {
|
|
197
|
-
return sandbox.networkPolicy;
|
|
198
|
-
},
|
|
199
|
-
get tags() {
|
|
200
|
-
return sandbox.tags;
|
|
201
|
-
},
|
|
202
|
-
async update(params) {
|
|
203
|
-
await sandbox.update(params);
|
|
204
|
-
},
|
|
205
|
-
async stop(opts) {
|
|
206
|
-
await sandbox.stop(opts);
|
|
207
|
-
},
|
|
208
|
-
async snapshot(opts) {
|
|
209
|
-
return await sandbox.snapshot(opts);
|
|
210
|
-
},
|
|
211
|
-
domain(port) {
|
|
212
|
-
return sandbox.domain(port);
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
183
|
function createVercelInternalSandboxSession(sandbox, id) {
|
|
217
184
|
return {
|
|
218
185
|
id,
|
|
@@ -108,16 +108,21 @@ async function runWorkflowLoop(input) {
|
|
|
108
108
|
});
|
|
109
109
|
return { output: currentResult.output ?? "" };
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
// The first turn may have re-keyed the session via
|
|
112
|
+
// `ctx.session.setContinuationToken(...)` (e.g. Slack auto-anchor on
|
|
113
|
+
// first post). `currentSession.continuationToken` already reflects
|
|
114
|
+
// the latest token thanks to `reconcileSessionContinuationToken`
|
|
115
|
+
// inside `durableRunStep`, so creating the park hook here lands at
|
|
116
|
+
// the right token from the start.
|
|
117
|
+
if (!currentSession.continuationToken) {
|
|
118
|
+
throw new Error("Cannot park: no continuation token available. The channel must " +
|
|
119
|
+
"post the first message during the initial turn (anchoring the " +
|
|
120
|
+
"session) or `send()` must be called with an explicit " +
|
|
121
|
+
"continuationToken.");
|
|
116
122
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const iterator = hook[Symbol.asyncIterator]();
|
|
123
|
+
let parkToken = currentSession.continuationToken;
|
|
124
|
+
let hook = createHook({ token: parkToken });
|
|
125
|
+
let iterator = hook[Symbol.asyncIterator]();
|
|
121
126
|
let pendingNext = null;
|
|
122
127
|
const getNextPromise = () => {
|
|
123
128
|
pendingNext ??= iterator.next();
|
|
@@ -126,6 +131,24 @@ async function runWorkflowLoop(input) {
|
|
|
126
131
|
const consumeNext = () => {
|
|
127
132
|
pendingNext = null;
|
|
128
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* Disposes the current park hook and creates a new one at
|
|
136
|
+
* {@link nextToken}. Called after a turn whose handlers
|
|
137
|
+
* re-keyed the session via `setContinuationToken(...)`. In-flight
|
|
138
|
+
* deliveries to the old token after this returns are silently
|
|
139
|
+
* dropped — channels that re-key mid-session must coordinate with
|
|
140
|
+
* their senders so follow-up traffic uses the new token.
|
|
141
|
+
*/
|
|
142
|
+
const rekeyHook = async (nextToken) => {
|
|
143
|
+
if (nextToken === parkToken || !nextToken)
|
|
144
|
+
return;
|
|
145
|
+
await closeHookIterator(iterator);
|
|
146
|
+
await disposeHook(hook);
|
|
147
|
+
parkToken = nextToken;
|
|
148
|
+
hook = createHook({ token: parkToken });
|
|
149
|
+
iterator = hook[Symbol.asyncIterator]();
|
|
150
|
+
pendingNext = null;
|
|
151
|
+
};
|
|
129
152
|
try {
|
|
130
153
|
while (true) {
|
|
131
154
|
if (hasPendingRuntimeActionBatch(currentSession)) {
|
|
@@ -137,6 +160,7 @@ async function runWorkflowLoop(input) {
|
|
|
137
160
|
bufferedDeliveries,
|
|
138
161
|
getNextPromise,
|
|
139
162
|
consumeNext,
|
|
163
|
+
rekeyHook,
|
|
140
164
|
serializedContext: currentSerializedContext,
|
|
141
165
|
session: currentSession,
|
|
142
166
|
});
|
|
@@ -184,6 +208,7 @@ async function runWorkflowLoop(input) {
|
|
|
184
208
|
});
|
|
185
209
|
return { output: currentResult.output ?? "" };
|
|
186
210
|
}
|
|
211
|
+
await rekeyHook(currentSession.continuationToken);
|
|
187
212
|
}
|
|
188
213
|
}
|
|
189
214
|
finally {
|
|
@@ -292,6 +317,7 @@ async function waitForPendingRuntimeActionResults(input) {
|
|
|
292
317
|
});
|
|
293
318
|
currentSession = proxyResult.session;
|
|
294
319
|
currentSerializedContext = proxyResult.serializedContext;
|
|
320
|
+
await input.rekeyHook(currentSession.continuationToken);
|
|
295
321
|
}
|
|
296
322
|
},
|
|
297
323
|
session: currentSession,
|
|
@@ -456,25 +482,3 @@ async function disposeHook(hook) {
|
|
|
456
482
|
}
|
|
457
483
|
await symbolDispose.call(hook);
|
|
458
484
|
}
|
|
459
|
-
/**
|
|
460
|
-
* Derives the continuation token from the serialized adapter state.
|
|
461
|
-
*
|
|
462
|
-
* Used when no explicit continuation token was provided on RunInput
|
|
463
|
-
* (schedule-initiated sessions). The token is derived from the
|
|
464
|
-
* adapter's serialized resume identity — for Slack, this is the
|
|
465
|
-
* continuation token `slack:{channelId}:{threadTs}` when threadTs is populated.
|
|
466
|
-
*/
|
|
467
|
-
function deriveAdapterContinuationToken(serializedContext) {
|
|
468
|
-
const channel = serializedContext["ash.channel"];
|
|
469
|
-
if (!channel?.state)
|
|
470
|
-
return undefined;
|
|
471
|
-
const serializedThread = channel.state.serializedThread;
|
|
472
|
-
if (!serializedThread?.id)
|
|
473
|
-
return undefined;
|
|
474
|
-
const id = serializedThread.id;
|
|
475
|
-
const lastColon = id.lastIndexOf(":");
|
|
476
|
-
if (lastColon === -1)
|
|
477
|
-
return undefined;
|
|
478
|
-
const threadTs = id.slice(lastColon + 1);
|
|
479
|
-
return threadTs.length > 0 ? id : undefined;
|
|
480
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DeliverPayload, HookPayload, SessionAuthContext, SubagentInputRequestHookPayload } from "#channel/types.js";
|
|
2
|
+
import { deserializeContext } from "#context/serialize.js";
|
|
2
3
|
import type { HarnessSession } from "#harness/types.js";
|
|
3
4
|
import type { RuntimeCompiledArtifactsSource } from "#runtime/compiled-artifacts-source.js";
|
|
4
5
|
import type { DurableStepResult } from "#execution/types.js";
|
|
@@ -6,6 +7,21 @@ import type { DurableStepResult } from "#execution/types.js";
|
|
|
6
7
|
* Runs one atomic harness step inside a durable `"use step"` boundary.
|
|
7
8
|
*/
|
|
8
9
|
export declare function durableRunStep(serializedContext: Record<string, unknown>, session: HarnessSession, input: HookPayload | undefined): Promise<DurableStepResult>;
|
|
10
|
+
/**
|
|
11
|
+
* Reconciles `session.continuationToken` with the live
|
|
12
|
+
* `ContinuationTokenKey` value in context.
|
|
13
|
+
*
|
|
14
|
+
* Channels mutate the token by calling
|
|
15
|
+
* `ctx.session.setContinuationToken(...)`, which writes through to
|
|
16
|
+
* `ContinuationTokenKey`. The serialized context picks the new value
|
|
17
|
+
* up automatically, but the in-memory {@link HarnessSession} the
|
|
18
|
+
* workflow body carries needs to be re-stamped so its `parkToken`
|
|
19
|
+
* read after the step reflects the re-key.
|
|
20
|
+
*
|
|
21
|
+
* Exported for unit testing; the workflow runtime is the only
|
|
22
|
+
* production caller.
|
|
23
|
+
*/
|
|
24
|
+
export declare function reconcileSessionContinuationToken(ctx: Awaited<ReturnType<typeof deserializeContext>>, session: HarnessSession): HarnessSession;
|
|
9
25
|
/**
|
|
10
26
|
* Starts every pending runtime action for the parked parent session.
|
|
11
27
|
*
|
|
@@ -3,7 +3,7 @@ import { buildAdapterContext } from "#channel/adapter-context.js";
|
|
|
3
3
|
import { callAdapterEventHandler, defaultDeliverResult } from "#channel/adapter.js";
|
|
4
4
|
import { toContextAccessor } from "#context/container.js";
|
|
5
5
|
import { dispatchStreamEventHooks, runHookLifecycleStep } from "#context/hook-lifecycle.js";
|
|
6
|
-
import { AuthKey, BundleKey, CapabilitiesKey, ChannelKey, InitiatorAuthKey, ModeKey, } from "#context/keys.js";
|
|
6
|
+
import { AuthKey, BundleKey, CapabilitiesKey, ChannelKey, ContinuationTokenKey, InitiatorAuthKey, ModeKey, } from "#context/keys.js";
|
|
7
7
|
import { runStep } from "#context/run-step.js";
|
|
8
8
|
import { deserializeContext, serializeContext } from "#context/serialize.js";
|
|
9
9
|
import { getHarnessEmissionState, isHarnessBetweenTurns } from "#harness/emission.js";
|
|
@@ -65,6 +65,7 @@ export async function durableRunStep(serializedContext, session, input) {
|
|
|
65
65
|
// only edits a message). Persist any context mutations and re-park
|
|
66
66
|
// without running a model turn.
|
|
67
67
|
if (input?.kind === "deliver" && resolved === undefined) {
|
|
68
|
+
const rekeyed = reconcileSessionContinuationToken(ctx, session);
|
|
68
69
|
const nextSerializedContext = serializeContext(ctx);
|
|
69
70
|
const writable = getWritable();
|
|
70
71
|
const writer = writable.getWriter();
|
|
@@ -72,7 +73,7 @@ export async function durableRunStep(serializedContext, session, input) {
|
|
|
72
73
|
return {
|
|
73
74
|
action: "park",
|
|
74
75
|
serializedContext: nextSerializedContext,
|
|
75
|
-
session,
|
|
76
|
+
session: rekeyed,
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
const writable = getWritable();
|
|
@@ -84,7 +85,7 @@ export async function durableRunStep(serializedContext, session, input) {
|
|
|
84
85
|
await writer.write(encodeMessageStreamEvent(timestampHandleMessageStreamEvent(toEmit)));
|
|
85
86
|
await dispatchStreamEventHooks({ ctx, registry: hookRegistry, event: toEmit });
|
|
86
87
|
};
|
|
87
|
-
|
|
88
|
+
let stepResult = await runStep(ctx, session, async (enrichedSession) => {
|
|
88
89
|
const bundle = ctx.require(BundleKey);
|
|
89
90
|
const capabilities = ctx.get(CapabilitiesKey);
|
|
90
91
|
const mode = ctx.require(ModeKey);
|
|
@@ -129,7 +130,13 @@ export async function durableRunStep(serializedContext, session, input) {
|
|
|
129
130
|
}
|
|
130
131
|
return runHarnessStep(enrichedSession, resolved);
|
|
131
132
|
});
|
|
133
|
+
// Reconcile the session's continuation token with `ContinuationTokenKey`
|
|
134
|
+
// in case a handler called `session.setContinuationToken(...)` during
|
|
135
|
+
// the turn (e.g. Slack's auto-anchor on first post). Idempotent when
|
|
136
|
+
// the token is unchanged.
|
|
137
|
+
const rekeyed = reconcileSessionContinuationToken(ctx, stepResult.session);
|
|
132
138
|
const nextSerializedContext = serializeContext(ctx);
|
|
139
|
+
stepResult = { ...stepResult, session: rekeyed };
|
|
133
140
|
// Interactive OAuth. When the step left one or more pending
|
|
134
141
|
// connection tool calls (placeholders written to history) and at
|
|
135
142
|
// least one matching pending authorization, the workflow body owns
|
|
@@ -177,6 +184,26 @@ export async function durableRunStep(serializedContext, session, input) {
|
|
|
177
184
|
session: stepResult.session,
|
|
178
185
|
};
|
|
179
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Reconciles `session.continuationToken` with the live
|
|
189
|
+
* `ContinuationTokenKey` value in context.
|
|
190
|
+
*
|
|
191
|
+
* Channels mutate the token by calling
|
|
192
|
+
* `ctx.session.setContinuationToken(...)`, which writes through to
|
|
193
|
+
* `ContinuationTokenKey`. The serialized context picks the new value
|
|
194
|
+
* up automatically, but the in-memory {@link HarnessSession} the
|
|
195
|
+
* workflow body carries needs to be re-stamped so its `parkToken`
|
|
196
|
+
* read after the step reflects the re-key.
|
|
197
|
+
*
|
|
198
|
+
* Exported for unit testing; the workflow runtime is the only
|
|
199
|
+
* production caller.
|
|
200
|
+
*/
|
|
201
|
+
export function reconcileSessionContinuationToken(ctx, session) {
|
|
202
|
+
const next = ctx.get(ContinuationTokenKey);
|
|
203
|
+
if (next === undefined || next === session.continuationToken)
|
|
204
|
+
return session;
|
|
205
|
+
return { ...session, continuationToken: next };
|
|
206
|
+
}
|
|
180
207
|
/**
|
|
181
208
|
* Starts every pending runtime action for the parked parent session.
|
|
182
209
|
*
|
|
@@ -333,11 +360,12 @@ export async function runProxyInputRequestStep(input) {
|
|
|
333
360
|
// adapter and later text-reply deliveries miss the cached batch.
|
|
334
361
|
ctx.set(ChannelKey, { ...adapter, state: { ...adapterCtx.state } });
|
|
335
362
|
const nextSerializedContext = serializeContext(ctx);
|
|
336
|
-
const
|
|
363
|
+
const sessionWithProxyEntries = upsertProxyInputRequests({
|
|
337
364
|
entries: proxyResult.entries,
|
|
338
365
|
forChildContinuationToken: input.hookPayload.childContinuationToken,
|
|
339
366
|
session: proxyResult.session,
|
|
340
367
|
});
|
|
368
|
+
const nextSession = reconcileSessionContinuationToken(ctx, sessionWithProxyEntries);
|
|
341
369
|
return {
|
|
342
370
|
serializedContext: nextSerializedContext,
|
|
343
371
|
session: nextSession,
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { buildAdapterContext } from "#channel/adapter-context.js";
|
|
4
4
|
import { getAdapterKind } from "#channel/adapter.js";
|
|
5
|
+
import { buildSessionHandle } from "#channel/session.js";
|
|
5
6
|
import { loadContext } from "#context/container.js";
|
|
6
7
|
import { ChannelKey, SandboxKey } from "#context/keys.js";
|
|
7
8
|
import { fileDataToBytes } from "#internal/attachments/data.js";
|
|
@@ -83,7 +84,7 @@ export async function stageAttachmentsToSandbox(message) {
|
|
|
83
84
|
const adapter = container.get(ChannelKey);
|
|
84
85
|
const adapterCtx = adapter
|
|
85
86
|
? buildAdapterContext(adapter, container)
|
|
86
|
-
: { ctx: container, state: {} };
|
|
87
|
+
: { ctx: container, state: {}, session: buildSessionHandle(container) };
|
|
87
88
|
return stageAttachmentsForAdapter(message, sandbox, adapterCtx);
|
|
88
89
|
}
|
|
89
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.18.
|
|
9
|
+
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.18.3";
|
|
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",
|
|
@@ -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) {
|
|
@@ -22,12 +22,13 @@ import { type CardElement, type FileUpload } from "#compiled/chat/index.js";
|
|
|
22
22
|
*/
|
|
23
23
|
export type SlackBotToken = string | (() => string | Promise<string>);
|
|
24
24
|
/**
|
|
25
|
-
* Builds the Slack channel-local continuation token
|
|
26
|
-
* Route `send()` namespaces this with the
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* Builds the Slack channel-local continuation token
|
|
26
|
+
* (`<channelId>:<threadTs>`). Route `send()` namespaces this with the
|
|
27
|
+
* channel name before handing it to the runtime
|
|
28
|
+
* (`slack:<channelId>:<threadTs>`). `threadTs` may be empty for
|
|
29
|
+
* threadless sessions; the channel auto-anchors on its first post.
|
|
29
30
|
*/
|
|
30
|
-
export declare function
|
|
31
|
+
export declare function slackContinuationToken(channelId: string, threadTs: string): string;
|
|
31
32
|
/**
|
|
32
33
|
* Materializes a {@link SlackBotToken} to a concrete string. Falls
|
|
33
34
|
* back to `process.env.SLACK_BOT_TOKEN` when no value was passed.
|
|
@@ -267,39 +268,18 @@ export interface SlackBinding {
|
|
|
267
268
|
readonly slack: SlackHandle;
|
|
268
269
|
}
|
|
269
270
|
/**
|
|
270
|
-
* Internal
|
|
271
|
-
*
|
|
272
|
-
*
|
|
271
|
+
* Internal constructor for the `{ thread, slack }` pair.
|
|
272
|
+
*
|
|
273
|
+
* Auto-anchor: when the binding starts without a `threadTs`, the first
|
|
274
|
+
* `chat.postMessage` adopts its own `ts` as the thread root; the live
|
|
275
|
+
* `threadTs` is updated and `onThreadTsChanged` fires so the caller can persist
|
|
276
|
+
* the anchor. Ephemerals and files-only posts do not anchor.
|
|
273
277
|
*/
|
|
274
278
|
export declare function buildSlackBinding(input: {
|
|
275
279
|
readonly botToken: SlackBotToken | undefined;
|
|
276
280
|
readonly channelId: string;
|
|
277
281
|
readonly threadTs: string;
|
|
278
282
|
readonly teamId: string | undefined;
|
|
283
|
+
readonly onThreadTsChanged?: (ts: string) => void;
|
|
279
284
|
}): SlackBinding;
|
|
280
|
-
/**
|
|
281
|
-
* Best-effort GFM → Slack mrkdwn converter used only in contexts that
|
|
282
|
-
* do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
|
|
283
|
-
* `initial_comment` field).
|
|
284
|
-
*
|
|
285
|
-
* The main `{ markdown }` post path sends `markdown_text` directly
|
|
286
|
-
* to `chat.postMessage` and does not go through this converter.
|
|
287
|
-
*/
|
|
288
|
-
export declare function gfmToSlackMrkdwn(input: string): string;
|
|
289
|
-
/**
|
|
290
|
-
* Best-effort Slack mrkdwn → GFM converter applied to the text of
|
|
291
|
-
* every inbound Slack message before the harness sees it.
|
|
292
|
-
*
|
|
293
|
-
* - `<@U123>` → `@U123`
|
|
294
|
-
* - `<#C123|name>` → `#name` (or `#C123` when no name)
|
|
295
|
-
* - `<!channel>` etc. → `@channel`
|
|
296
|
-
* - `<https://x|label>` → `[label](https://x)`
|
|
297
|
-
* - `<https://x>` → `https://x`
|
|
298
|
-
* - `*bold*` (paired) → `**bold**`
|
|
299
|
-
* - `~strike~` (paired) → `~~strike~~`
|
|
300
|
-
*
|
|
301
|
-
* Inline `_italic_` and code spans pass through unchanged because both
|
|
302
|
-
* formats render them identically.
|
|
303
|
-
*/
|
|
304
|
-
export declare function slackMrkdwnToGfm(input: string): string;
|
|
305
285
|
export {};
|
|
@@ -18,14 +18,16 @@ 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
|
-
* Builds the Slack channel-local continuation token
|
|
24
|
-
* Route `send()` namespaces this with the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Builds the Slack channel-local continuation token
|
|
25
|
+
* (`<channelId>:<threadTs>`). Route `send()` namespaces this with the
|
|
26
|
+
* channel name before handing it to the runtime
|
|
27
|
+
* (`slack:<channelId>:<threadTs>`). `threadTs` may be empty for
|
|
28
|
+
* threadless sessions; the channel auto-anchors on its first post.
|
|
27
29
|
*/
|
|
28
|
-
export function
|
|
30
|
+
export function slackContinuationToken(channelId, threadTs) {
|
|
29
31
|
return `${channelId}:${threadTs}`;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
@@ -67,19 +69,29 @@ export function createSlackRequester(botToken) {
|
|
|
67
69
|
return (operation, body) => callSlackApi({ botToken, operation, body });
|
|
68
70
|
}
|
|
69
71
|
/**
|
|
70
|
-
* Internal
|
|
71
|
-
*
|
|
72
|
-
*
|
|
72
|
+
* Internal constructor for the `{ thread, slack }` pair.
|
|
73
|
+
*
|
|
74
|
+
* Auto-anchor: when the binding starts without a `threadTs`, the first
|
|
75
|
+
* `chat.postMessage` adopts its own `ts` as the thread root; the live
|
|
76
|
+
* `threadTs` is updated and `onThreadTsChanged` fires so the caller can persist
|
|
77
|
+
* the anchor. Ephemerals and files-only posts do not anchor.
|
|
73
78
|
*/
|
|
74
79
|
export function buildSlackBinding(input) {
|
|
75
80
|
const request = createSlackRequester(input.botToken);
|
|
76
81
|
const messages = [];
|
|
82
|
+
let currentThreadTs = input.threadTs;
|
|
83
|
+
function handleMessageTs(ts) {
|
|
84
|
+
if (currentThreadTs || ts === currentThreadTs)
|
|
85
|
+
return;
|
|
86
|
+
currentThreadTs = ts;
|
|
87
|
+
input.onThreadTsChanged?.(ts);
|
|
88
|
+
}
|
|
77
89
|
async function uploadFiles(files, options) {
|
|
78
90
|
if (files.length === 0) {
|
|
79
91
|
return { fileIds: [], raw: { ok: true } };
|
|
80
92
|
}
|
|
81
93
|
const channelId = options?.channelId ?? input.channelId;
|
|
82
|
-
const threadTs = options?.threadTs ??
|
|
94
|
+
const threadTs = options?.threadTs ?? currentThreadTs;
|
|
83
95
|
const token = await resolveSlackBotToken(input.botToken);
|
|
84
96
|
const fileIds = [];
|
|
85
97
|
for (const file of files) {
|
|
@@ -151,12 +163,13 @@ export function buildSlackBinding(input) {
|
|
|
151
163
|
: "";
|
|
152
164
|
return { id, raw: result.raw };
|
|
153
165
|
}
|
|
154
|
-
const body = buildPostMessageBody(message, input.channelId,
|
|
166
|
+
const body = buildPostMessageBody(message, input.channelId, currentThreadTs);
|
|
155
167
|
const response = await request("chat.postMessage", body);
|
|
156
168
|
if (response.ok !== true) {
|
|
157
169
|
throw new Error(`Slack chat.postMessage failed: ${response.error ?? "unknown_error"}`);
|
|
158
170
|
}
|
|
159
171
|
const id = typeof response.ts === "string" ? response.ts : "";
|
|
172
|
+
handleMessageTs(id);
|
|
160
173
|
// blocks / card + files: structured message lands first, then upload
|
|
161
174
|
// files as a follow-up post in the same thread.
|
|
162
175
|
if (files.length > 0 && hasStructured) {
|
|
@@ -171,7 +184,7 @@ export function buildSlackBinding(input) {
|
|
|
171
184
|
},
|
|
172
185
|
async postEphemeral(userId, rawMessage) {
|
|
173
186
|
const message = normalizePostInput(rawMessage);
|
|
174
|
-
const body = buildPostMessageBody(message, input.channelId,
|
|
187
|
+
const body = buildPostMessageBody(message, input.channelId, currentThreadTs);
|
|
175
188
|
body.user = userId;
|
|
176
189
|
const response = await request("chat.postEphemeral", body);
|
|
177
190
|
if (response.ok !== true) {
|
|
@@ -181,12 +194,12 @@ export function buildSlackBinding(input) {
|
|
|
181
194
|
return { id, raw: response };
|
|
182
195
|
},
|
|
183
196
|
async startTyping(status) {
|
|
184
|
-
if (!input.channelId || !
|
|
197
|
+
if (!input.channelId || !currentThreadTs)
|
|
185
198
|
return;
|
|
186
199
|
try {
|
|
187
200
|
const body = {
|
|
188
201
|
channel_id: input.channelId,
|
|
189
|
-
thread_ts:
|
|
202
|
+
thread_ts: currentThreadTs,
|
|
190
203
|
status: status ?? "",
|
|
191
204
|
};
|
|
192
205
|
if (status !== undefined && status.length > 0) {
|
|
@@ -205,12 +218,12 @@ export function buildSlackBinding(input) {
|
|
|
205
218
|
},
|
|
206
219
|
async refresh() {
|
|
207
220
|
messages.length = 0;
|
|
208
|
-
if (!input.channelId || !
|
|
221
|
+
if (!input.channelId || !currentThreadTs)
|
|
209
222
|
return;
|
|
210
223
|
try {
|
|
211
224
|
const response = await request("conversations.replies", {
|
|
212
225
|
channel: input.channelId,
|
|
213
|
-
ts:
|
|
226
|
+
ts: currentThreadTs,
|
|
214
227
|
limit: 50,
|
|
215
228
|
});
|
|
216
229
|
if (response.ok !== true || !Array.isArray(response.messages)) {
|
|
@@ -218,7 +231,7 @@ export function buildSlackBinding(input) {
|
|
|
218
231
|
return;
|
|
219
232
|
}
|
|
220
233
|
for (const raw of response.messages) {
|
|
221
|
-
messages.push(parseThreadMessage(raw,
|
|
234
|
+
messages.push(parseThreadMessage(raw, currentThreadTs));
|
|
222
235
|
}
|
|
223
236
|
}
|
|
224
237
|
catch (error) {
|
|
@@ -231,7 +244,9 @@ export function buildSlackBinding(input) {
|
|
|
231
244
|
};
|
|
232
245
|
const slack = {
|
|
233
246
|
channelId: input.channelId,
|
|
234
|
-
threadTs
|
|
247
|
+
get threadTs() {
|
|
248
|
+
return currentThreadTs;
|
|
249
|
+
},
|
|
235
250
|
teamId: input.teamId,
|
|
236
251
|
request,
|
|
237
252
|
uploadFiles,
|
|
@@ -299,70 +314,6 @@ function parseThreadMessage(raw, threadRootTs) {
|
|
|
299
314
|
raw,
|
|
300
315
|
};
|
|
301
316
|
}
|
|
302
|
-
const BARE_MENTION_RE = /(?<![<\w])@(\w+)/gu;
|
|
303
|
-
function rewriteBareMentions(text) {
|
|
304
|
-
return text.replace(BARE_MENTION_RE, "<@$1>");
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Best-effort GFM → Slack mrkdwn converter used only in contexts that
|
|
308
|
-
* do not support `markdown_text` (e.g. `files.completeUploadExternal`'s
|
|
309
|
-
* `initial_comment` field).
|
|
310
|
-
*
|
|
311
|
-
* The main `{ markdown }` post path sends `markdown_text` directly
|
|
312
|
-
* to `chat.postMessage` and does not go through this converter.
|
|
313
|
-
*/
|
|
314
|
-
export function gfmToSlackMrkdwn(input) {
|
|
315
|
-
const segments = splitCodeFences(input);
|
|
316
|
-
return segments
|
|
317
|
-
.map((segment) => (segment.kind === "code" ? segment.text : convertInline(segment.text)))
|
|
318
|
-
.join("");
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Best-effort Slack mrkdwn → GFM converter applied to the text of
|
|
322
|
-
* every inbound Slack message before the harness sees it.
|
|
323
|
-
*
|
|
324
|
-
* - `<@U123>` → `@U123`
|
|
325
|
-
* - `<#C123|name>` → `#name` (or `#C123` when no name)
|
|
326
|
-
* - `<!channel>` etc. → `@channel`
|
|
327
|
-
* - `<https://x|label>` → `[label](https://x)`
|
|
328
|
-
* - `<https://x>` → `https://x`
|
|
329
|
-
* - `*bold*` (paired) → `**bold**`
|
|
330
|
-
* - `~strike~` (paired) → `~~strike~~`
|
|
331
|
-
*
|
|
332
|
-
* Inline `_italic_` and code spans pass through unchanged because both
|
|
333
|
-
* formats render them identically.
|
|
334
|
-
*/
|
|
335
|
-
export function slackMrkdwnToGfm(input) {
|
|
336
|
-
const segments = splitCodeFences(input);
|
|
337
|
-
return segments
|
|
338
|
-
.map((segment) => (segment.kind === "code" ? segment.text : decodeInline(segment.text)))
|
|
339
|
-
.join("");
|
|
340
|
-
}
|
|
341
|
-
function splitCodeFences(input) {
|
|
342
|
-
const segments = [];
|
|
343
|
-
const fenceRe = /```[\s\S]*?```|`[^`\n]+`/gu;
|
|
344
|
-
let lastIndex = 0;
|
|
345
|
-
for (const match of input.matchAll(fenceRe)) {
|
|
346
|
-
const start = match.index ?? 0;
|
|
347
|
-
if (start > lastIndex) {
|
|
348
|
-
segments.push({ kind: "text", text: input.slice(lastIndex, start) });
|
|
349
|
-
}
|
|
350
|
-
segments.push({ kind: "code", text: match[0] });
|
|
351
|
-
lastIndex = start + match[0].length;
|
|
352
|
-
}
|
|
353
|
-
if (lastIndex < input.length) {
|
|
354
|
-
segments.push({ kind: "text", text: input.slice(lastIndex) });
|
|
355
|
-
}
|
|
356
|
-
return segments;
|
|
357
|
-
}
|
|
358
|
-
function convertInline(input) {
|
|
359
|
-
let out = input;
|
|
360
|
-
out = out.replace(/\*\*([^*\n]+)\*\*/gu, "*$1*");
|
|
361
|
-
out = out.replace(/__([^_\n]+)__/gu, "*$1*");
|
|
362
|
-
out = out.replace(/~~([^~\n]+)~~/gu, "~$1~");
|
|
363
|
-
out = out.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/gu, "<$2|$1>");
|
|
364
|
-
return out;
|
|
365
|
-
}
|
|
366
317
|
/**
|
|
367
318
|
* Normalize a {@link FileUpload.data} value (`Buffer | Blob | ArrayBuffer`) to
|
|
368
319
|
* a contiguous `Buffer` we can both POST and length-prefix without
|
|
@@ -380,16 +331,3 @@ async function readFileBytes(data) {
|
|
|
380
331
|
}
|
|
381
332
|
throw new Error("FileUpload.data must be a Buffer, ArrayBuffer, or Blob.");
|
|
382
333
|
}
|
|
383
|
-
function decodeInline(input) {
|
|
384
|
-
let out = input;
|
|
385
|
-
out = out.replace(/<!(channel|here|everyone)>/gu, "@$1");
|
|
386
|
-
out = out.replace(/<@([A-Z0-9]+)\|([^>]+)>/gu, "@$2");
|
|
387
|
-
out = out.replace(/<@([A-Z0-9]+)>/gu, "@$1");
|
|
388
|
-
out = out.replace(/<#([A-Z0-9]+)\|([^>]+)>/gu, "#$2");
|
|
389
|
-
out = out.replace(/<#([A-Z0-9]+)>/gu, "#$1");
|
|
390
|
-
out = out.replace(/<(https?:\/\/[^|>\s]+)\|([^>]+)>/gu, "[$2]($1)");
|
|
391
|
-
out = out.replace(/<(https?:\/\/[^>\s]+)>/gu, "$1");
|
|
392
|
-
out = out.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/gu, "$1**$2**");
|
|
393
|
-
out = out.replace(/(^|[^~])~([^~\n]+)~(?!~)/gu, "$1~~$2~~");
|
|
394
|
-
return out;
|
|
395
|
-
}
|
|
@@ -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,
|