experimental-ash 0.18.1 → 0.18.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 +18 -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-DkTSR6YJ.js} +2 -2
- 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/public/channels/slack/api.d.ts +13 -8
- package/dist/src/public/channels/slack/api.js +31 -17
- 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/slackChannel.d.ts +5 -3
- package/dist/src/public/channels/slack/slackChannel.js +26 -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-DkTSR6YJ.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.2";
|
|
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",
|
|
@@ -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,15 +268,19 @@ 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 `onAnchor` 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 onAnchor?: (ts: string) => void;
|
|
279
284
|
}): SlackBinding;
|
|
280
285
|
/**
|
|
281
286
|
* Best-effort GFM → Slack mrkdwn converter used only in contexts that
|
|
@@ -20,12 +20,13 @@ import { encodeSlackApiBody } from "#public/channels/slack/api-encoding.js";
|
|
|
20
20
|
import { cardToBlocks, cardToFallbackText } from "#public/channels/slack/blocks.js";
|
|
21
21
|
const log = createLogger("slack.api");
|
|
22
22
|
/**
|
|
23
|
-
* Builds the Slack channel-local continuation token
|
|
24
|
-
* Route `send()` namespaces this with the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Builds the Slack channel-local continuation token
|
|
24
|
+
* (`<channelId>:<threadTs>`). Route `send()` namespaces this with the
|
|
25
|
+
* channel name before handing it to the runtime
|
|
26
|
+
* (`slack:<channelId>:<threadTs>`). `threadTs` may be empty for
|
|
27
|
+
* threadless sessions; the channel auto-anchors on its first post.
|
|
27
28
|
*/
|
|
28
|
-
export function
|
|
29
|
+
export function slackContinuationToken(channelId, threadTs) {
|
|
29
30
|
return `${channelId}:${threadTs}`;
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
@@ -67,19 +68,29 @@ export function createSlackRequester(botToken) {
|
|
|
67
68
|
return (operation, body) => callSlackApi({ botToken, operation, body });
|
|
68
69
|
}
|
|
69
70
|
/**
|
|
70
|
-
* Internal
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* Internal constructor for the `{ thread, slack }` pair.
|
|
72
|
+
*
|
|
73
|
+
* Auto-anchor: when the binding starts without a `threadTs`, the first
|
|
74
|
+
* `chat.postMessage` adopts its own `ts` as the thread root; the live
|
|
75
|
+
* `threadTs` is updated and `onAnchor` fires so the caller can persist
|
|
76
|
+
* the anchor. Ephemerals and files-only posts do not anchor.
|
|
73
77
|
*/
|
|
74
78
|
export function buildSlackBinding(input) {
|
|
75
79
|
const request = createSlackRequester(input.botToken);
|
|
76
80
|
const messages = [];
|
|
81
|
+
let currentThreadTs = input.threadTs;
|
|
82
|
+
function anchorIfUnset(ts) {
|
|
83
|
+
if (currentThreadTs.length > 0 || ts.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
currentThreadTs = ts;
|
|
86
|
+
input.onAnchor?.(ts);
|
|
87
|
+
}
|
|
77
88
|
async function uploadFiles(files, options) {
|
|
78
89
|
if (files.length === 0) {
|
|
79
90
|
return { fileIds: [], raw: { ok: true } };
|
|
80
91
|
}
|
|
81
92
|
const channelId = options?.channelId ?? input.channelId;
|
|
82
|
-
const threadTs = options?.threadTs ??
|
|
93
|
+
const threadTs = options?.threadTs ?? currentThreadTs;
|
|
83
94
|
const token = await resolveSlackBotToken(input.botToken);
|
|
84
95
|
const fileIds = [];
|
|
85
96
|
for (const file of files) {
|
|
@@ -151,12 +162,13 @@ export function buildSlackBinding(input) {
|
|
|
151
162
|
: "";
|
|
152
163
|
return { id, raw: result.raw };
|
|
153
164
|
}
|
|
154
|
-
const body = buildPostMessageBody(message, input.channelId,
|
|
165
|
+
const body = buildPostMessageBody(message, input.channelId, currentThreadTs);
|
|
155
166
|
const response = await request("chat.postMessage", body);
|
|
156
167
|
if (response.ok !== true) {
|
|
157
168
|
throw new Error(`Slack chat.postMessage failed: ${response.error ?? "unknown_error"}`);
|
|
158
169
|
}
|
|
159
170
|
const id = typeof response.ts === "string" ? response.ts : "";
|
|
171
|
+
anchorIfUnset(id);
|
|
160
172
|
// blocks / card + files: structured message lands first, then upload
|
|
161
173
|
// files as a follow-up post in the same thread.
|
|
162
174
|
if (files.length > 0 && hasStructured) {
|
|
@@ -171,7 +183,7 @@ export function buildSlackBinding(input) {
|
|
|
171
183
|
},
|
|
172
184
|
async postEphemeral(userId, rawMessage) {
|
|
173
185
|
const message = normalizePostInput(rawMessage);
|
|
174
|
-
const body = buildPostMessageBody(message, input.channelId,
|
|
186
|
+
const body = buildPostMessageBody(message, input.channelId, currentThreadTs);
|
|
175
187
|
body.user = userId;
|
|
176
188
|
const response = await request("chat.postEphemeral", body);
|
|
177
189
|
if (response.ok !== true) {
|
|
@@ -181,12 +193,12 @@ export function buildSlackBinding(input) {
|
|
|
181
193
|
return { id, raw: response };
|
|
182
194
|
},
|
|
183
195
|
async startTyping(status) {
|
|
184
|
-
if (!input.channelId || !
|
|
196
|
+
if (!input.channelId || !currentThreadTs)
|
|
185
197
|
return;
|
|
186
198
|
try {
|
|
187
199
|
const body = {
|
|
188
200
|
channel_id: input.channelId,
|
|
189
|
-
thread_ts:
|
|
201
|
+
thread_ts: currentThreadTs,
|
|
190
202
|
status: status ?? "",
|
|
191
203
|
};
|
|
192
204
|
if (status !== undefined && status.length > 0) {
|
|
@@ -205,12 +217,12 @@ export function buildSlackBinding(input) {
|
|
|
205
217
|
},
|
|
206
218
|
async refresh() {
|
|
207
219
|
messages.length = 0;
|
|
208
|
-
if (!input.channelId || !
|
|
220
|
+
if (!input.channelId || !currentThreadTs)
|
|
209
221
|
return;
|
|
210
222
|
try {
|
|
211
223
|
const response = await request("conversations.replies", {
|
|
212
224
|
channel: input.channelId,
|
|
213
|
-
ts:
|
|
225
|
+
ts: currentThreadTs,
|
|
214
226
|
limit: 50,
|
|
215
227
|
});
|
|
216
228
|
if (response.ok !== true || !Array.isArray(response.messages)) {
|
|
@@ -218,7 +230,7 @@ export function buildSlackBinding(input) {
|
|
|
218
230
|
return;
|
|
219
231
|
}
|
|
220
232
|
for (const raw of response.messages) {
|
|
221
|
-
messages.push(parseThreadMessage(raw,
|
|
233
|
+
messages.push(parseThreadMessage(raw, currentThreadTs));
|
|
222
234
|
}
|
|
223
235
|
}
|
|
224
236
|
catch (error) {
|
|
@@ -231,7 +243,9 @@ export function buildSlackBinding(input) {
|
|
|
231
243
|
};
|
|
232
244
|
const slack = {
|
|
233
245
|
channelId: input.channelId,
|
|
234
|
-
threadTs
|
|
246
|
+
get threadTs() {
|
|
247
|
+
return currentThreadTs;
|
|
248
|
+
},
|
|
235
249
|
teamId: input.teamId,
|
|
236
250
|
request,
|
|
237
251
|
uploadFiles,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { slackChannel, type SlackApiResponse, type SlackBotToken, type SlackChannel, type SlackChannelConfig, type SlackChannelCredentials, type SlackChannelEvents, type SlackChannelState, type SlackContext, type SlackEventContext, type SlackHandle, type SlackInboundResult, type SlackInboundResultOrPromise, type
|
|
1
|
+
export { slackChannel, type SlackApiResponse, type SlackBotToken, type SlackChannel, type SlackChannelConfig, type SlackChannelCredentials, type SlackChannelEvents, type SlackChannelState, type SlackContext, type SlackEventContext, type SlackHandle, type SlackInboundResult, type SlackInboundResultOrPromise, type SlackInitialMessage, type SlackInteractionAction, type SlackMentionResult, type SlackMentionResultOrPromise, type SlackReceiveArgs, type SlackThread, type SlackWebhookVerifier, } from "#public/channels/slack/slackChannel.js";
|
|
2
2
|
export type { SlackAttachment, SlackAuthor, SlackInboundContext, SlackMessage, } from "#public/channels/slack/inbound.js";
|
|
3
|
-
export type
|
|
3
|
+
export { slackContinuationToken, type SlackPostInput, type SlackPostedMessage, type SlackThreadMessage, type SlackUploadFilesOptions, type SlackUploadFilesResult, } from "#public/channels/slack/api.js";
|
|
4
4
|
export { cardToBlocks, cardToFallbackText, type BlockKitBlock, } from "#public/channels/slack/blocks.js";
|
|
5
5
|
/**
|
|
6
6
|
* Card builders and element types re-exported from the vendored chat
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { slackChannel, } from "#public/channels/slack/slackChannel.js";
|
|
2
|
+
export { slackContinuationToken, } from "#public/channels/slack/api.js";
|
|
2
3
|
export { cardToBlocks, cardToFallbackText, } from "#public/channels/slack/blocks.js";
|
|
3
4
|
/**
|
|
4
5
|
* Card builders and element types re-exported from the vendored chat
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* work runs under `waitUntil` so the webhook ACK is immediate.
|
|
18
18
|
*/
|
|
19
19
|
import { createLogger } from "#internal/logging.js";
|
|
20
|
-
import { buildSlackBinding,
|
|
20
|
+
import { buildSlackBinding, resolveSlackBotToken, slackContinuationToken, } from "#public/channels/slack/api.js";
|
|
21
21
|
import { buildAnsweredBlocks, buildFreeformModalView, deriveHitlResponse, freeformRequestIdFromActionId, HITL_FREEFORM_MODAL_ACTION_ID, HITL_FREEFORM_MODAL_BLOCK_ID, HITL_FREEFORM_MODAL_CALLBACK_ID, isFreeformAction, isHitlAction, } from "#public/channels/slack/hitl.js";
|
|
22
22
|
const log = createLogger("slack.interactions");
|
|
23
23
|
/**
|
|
@@ -124,7 +124,7 @@ export async function handleInteractionPost(rawBody, ctx, deps) {
|
|
|
124
124
|
await openFreeformModal({ payload, interaction, freeformAction, deps });
|
|
125
125
|
return ack;
|
|
126
126
|
}
|
|
127
|
-
const continuationToken =
|
|
127
|
+
const continuationToken = slackContinuationToken(interaction.channelId, interaction.threadTs);
|
|
128
128
|
const inputResponses = interaction.actions
|
|
129
129
|
.map(deriveHitlResponse)
|
|
130
130
|
.filter((r) => r !== null);
|
|
@@ -184,7 +184,7 @@ async function openFreeformModal(input) {
|
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
186
|
const metadata = {
|
|
187
|
-
continuationToken:
|
|
187
|
+
continuationToken: slackContinuationToken(input.interaction.channelId, input.interaction.threadTs),
|
|
188
188
|
channelId: input.interaction.channelId,
|
|
189
189
|
threadTs: input.interaction.threadTs,
|
|
190
190
|
messageTs,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TypedReceiveRoute } from "#channel/receive-args.js";
|
|
2
|
+
import type { SessionHandle } from "#channel/session.js";
|
|
2
3
|
import type { SessionAuthContext } from "#channel/types.js";
|
|
3
4
|
import type { CardElement } from "#compiled/chat/index.js";
|
|
4
5
|
import type { HandleMessageStreamEvent } from "#protocol/message.js";
|
|
@@ -35,6 +36,7 @@ export interface SlackContext {
|
|
|
35
36
|
* boundaries.
|
|
36
37
|
*/
|
|
37
38
|
export interface SlackEventContext extends SlackContext {
|
|
39
|
+
readonly session: SessionHandle;
|
|
38
40
|
state: SlackChannelState;
|
|
39
41
|
}
|
|
40
42
|
export type { SlackApiResponse, SlackBotToken, SlackHandle, SlackThread, } from "#public/channels/slack/api.js";
|
|
@@ -103,14 +105,14 @@ export interface SlackReceiveArgs {
|
|
|
103
105
|
* the caller wants a visible "this is why we're here" anchor before
|
|
104
106
|
* the model speaks. Mutually exclusive with {@link threadTs}.
|
|
105
107
|
*/
|
|
106
|
-
readonly
|
|
108
|
+
readonly initialMessage?: SlackInitialMessage;
|
|
107
109
|
}
|
|
108
110
|
/**
|
|
109
111
|
* Pre-agent post issued by `slackChannel().receive` when the caller
|
|
110
|
-
* provides `args.
|
|
112
|
+
* provides `args.initialMessage`. The shape mirrors `ctx.thread.post`'s
|
|
111
113
|
* card variant so the same `Card({...})` construction can be reused.
|
|
112
114
|
*/
|
|
113
|
-
export interface
|
|
115
|
+
export interface SlackInitialMessage {
|
|
114
116
|
readonly card: CardElement;
|
|
115
117
|
readonly fallbackText?: string;
|
|
116
118
|
}
|