experimental-ash 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # experimental-ash
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4d7af3a: Remove `SlackThread.threadId` from the public Slack channel API and clarify Slack continuation-token encoding. Slack callers should use `ctx.slack.channelId` and `ctx.slack.threadTs` for real Slack API identifiers, while Ash keeps continuation tokens as channel-local values before runtime namespacing.
8
+
9
+ ### Patch Changes
10
+
11
+ - 8e4067a: Remove Ash-owned AI Gateway reporting tags from harness model calls. Authored `providerOptions.gateway.tags` and `mergeGatewayAutoCaching` behavior are unchanged.
12
+
3
13
  ## 0.15.0
4
14
 
5
15
  ### Minor Changes
@@ -88,7 +98,7 @@
88
98
 
89
99
  The resulting `ctx` exposes two handles so call sites read naturally:
90
100
 
91
- - `ctx.thread` (`SlackThread`) — `post`, `postEphemeral`, `startTyping`, `refresh`, `recentMessages`, `threadId`, `mentionUser`. Reads as "post a reply to this thread".
101
+ - `ctx.thread` (`SlackThread`) — `post`, `postEphemeral`, `startTyping`, `refresh`, `recentMessages`, `mentionUser`. Reads as "post a reply to this thread".
92
102
  - `ctx.slack` (`SlackHandle`) — `channelId`, `threadTs`, `teamId`, `request(op, body)`, `uploadFiles(...)`. Reads as "raw Slack API, possibly not the bound thread".
93
103
 
94
104
  `ctx.thread.post(...)` accepts ergonomic bare forms (`string` → `{ markdown }`, `CardElement` → `{ card }`) in addition to the explicit `SlackPostInput` variants:
@@ -75,7 +75,7 @@ Channel and Slack types exported from `experimental-ash/channels/slack`:
75
75
  - `SlackHandle` - Slack identity handle with `channelId`, `threadTs`, `teamId`, `request()`,
76
76
  `uploadFiles()`
77
77
  - `SlackThread` - thread-scoped operations: `post`, `postEphemeral`, `startTyping`, `refresh`,
78
- `recentMessages`, `mentionUser`, `threadId`
78
+ `recentMessages`, `mentionUser`
79
79
  - `SlackMessage` - parsed inbound mention or DM payload (`text`, `markdown`, `author`,
80
80
  `attachments`, `threadTs`, `channelId`, ...)
81
81
  - `SlackInteractionAction` - action type for `onInteraction`
@@ -461,8 +461,8 @@ async function disposeHook(hook) {
461
461
  *
462
462
  * Used when no explicit continuation token was provided on RunInput
463
463
  * (schedule-initiated sessions). The token is derived from the
464
- * adapter's serialized thread identity — for Slack, this is the
465
- * thread id `slack:{channelId}:{threadTs}` when threadTs is populated.
464
+ * adapter's serialized resume identity — for Slack, this is the
465
+ * continuation token `slack:{channelId}:{threadTs}` when threadTs is populated.
466
466
  */
467
467
  function deriveAdapterContinuationToken(serializedContext) {
468
468
  const channel = serializedContext["ash.channel"];
@@ -47,12 +47,6 @@ export declare function getAnthropicCacheMarker(): AnthropicCacheMarker;
47
47
  * another value.
48
48
  */
49
49
  export declare function mergeGatewayAutoCaching(base: Readonly<Record<string, unknown>> | undefined): Record<string, unknown>;
50
- /**
51
- * Returns a new `providerOptions` object with `gateway.tags` merged into the
52
- * existing `gateway` sub-object. Preserves authored tags and de-dupes repeated
53
- * framework tags across retry and compaction paths.
54
- */
55
- export declare function mergeGatewayTags(base: Readonly<Record<string, unknown>> | undefined, tags: readonly string[]): Record<string, unknown>;
56
50
  /**
57
51
  * Returns a new ToolSet where the last tool entry carries the Anthropic
58
52
  * cache marker on `providerOptions`. Used on the `anthropic-direct` path
@@ -1,4 +1,3 @@
1
- import { createLogger } from "#internal/logging.js";
2
1
  /**
3
2
  * Shared frozen marker. All direct-Anthropic breakpoints in the harness share
4
3
  * this instance to avoid allocating per-message.
@@ -53,75 +52,6 @@ export function mergeGatewayAutoCaching(base) {
53
52
  gateway: mergedGateway,
54
53
  };
55
54
  }
56
- const MAX_GATEWAY_TAGS = 10;
57
- const MAX_GATEWAY_TAG_LENGTH = 64;
58
- const log = createLogger("harness.prompt-cache");
59
- function normalizeGatewayTag(tag) {
60
- const trimmed = tag.trim();
61
- if (!trimmed) {
62
- return undefined;
63
- }
64
- return trimmed
65
- .replace(/[^a-zA-Z0-9:_./-]+/g, "_")
66
- .replace(/_+/g, "_")
67
- .slice(0, MAX_GATEWAY_TAG_LENGTH);
68
- }
69
- function gatewayTagKey(tag) {
70
- const separatorIndex = tag.indexOf(":");
71
- return separatorIndex === -1 ? tag : tag.slice(0, separatorIndex);
72
- }
73
- /**
74
- * Returns a new `providerOptions` object with `gateway.tags` merged into the
75
- * existing `gateway` sub-object. Preserves authored tags and de-dupes repeated
76
- * framework tags across retry and compaction paths.
77
- */
78
- export function mergeGatewayTags(base, tags) {
79
- if (tags.length === 0) {
80
- return { ...base };
81
- }
82
- const baseGateway = base?.gateway !== undefined && typeof base.gateway === "object" && base.gateway !== null
83
- ? base.gateway
84
- : undefined;
85
- const authoredTags = Array.isArray(baseGateway?.tags)
86
- ? baseGateway.tags.filter((tag) => typeof tag === "string")
87
- : [];
88
- const mergedTags = [];
89
- for (const tag of authoredTags) {
90
- const normalized = normalizeGatewayTag(tag);
91
- if (!normalized || mergedTags.includes(normalized)) {
92
- continue;
93
- }
94
- mergedTags.push(normalized);
95
- if (mergedTags.length === MAX_GATEWAY_TAGS) {
96
- break;
97
- }
98
- }
99
- const droppedTags = [];
100
- for (const tag of tags) {
101
- const normalized = normalizeGatewayTag(tag);
102
- if (!normalized || mergedTags.includes(normalized)) {
103
- continue;
104
- }
105
- if (mergedTags.length === MAX_GATEWAY_TAGS) {
106
- droppedTags.push(normalized);
107
- continue;
108
- }
109
- mergedTags.push(normalized);
110
- }
111
- if (droppedTags.length > 0) {
112
- log.warn("dropped Ash AI Gateway reporting tags because Gateway tag limit was reached", {
113
- droppedTagKeys: [...new Set(droppedTags.map(gatewayTagKey))],
114
- limit: MAX_GATEWAY_TAGS,
115
- });
116
- }
117
- return {
118
- ...base,
119
- gateway: {
120
- ...baseGateway,
121
- tags: mergedTags,
122
- },
123
- };
124
- }
125
55
  /**
126
56
  * Returns a new ToolSet where the last tool entry carries the Anthropic
127
57
  * cache marker on `providerOptions`. Used on the `anthropic-direct` path
@@ -16,7 +16,6 @@ export interface StepHooksInput {
16
16
  readonly cachePath: PromptCachePath;
17
17
  readonly emit?: HarnessEmitFn;
18
18
  readonly emissionState: HarnessEmissionState;
19
- readonly gatewayTags?: readonly string[];
20
19
  readonly marker: AnthropicCacheMarker | undefined;
21
20
  readonly session: HarnessSession;
22
21
  }
@@ -2,7 +2,7 @@ import { createActionResultEvent, createActionsRequestedEvent, createStepComplet
2
2
  import { createRuntimeToolResultFromMessagePart, createRuntimeToolResultFromStepResult, } from "#harness/action-result-helpers.js";
3
3
  import { emitStepStarted, normalizeAssistantStepFinishReason } from "#harness/emission.js";
4
4
  import { extractToolApprovalInputRequests } from "#harness/input-extraction.js";
5
- import { applyConversationCacheControl, mergeGatewayAutoCaching, mergeGatewayTags, } from "#harness/prompt-cache.js";
5
+ import { applyConversationCacheControl, mergeGatewayAutoCaching, } from "#harness/prompt-cache.js";
6
6
  import { createRuntimeActionRequestFromToolCall } from "#harness/runtime-actions.js";
7
7
  // ---------------------------------------------------------------------------
8
8
  // Builder
@@ -41,9 +41,7 @@ export function buildStepHooks(input) {
41
41
  messages: processed,
42
42
  };
43
43
  if (input.cachePath.kind === "gateway-auto") {
44
- let providerOptions = mergeGatewayAutoCaching(session.agent.modelReference.providerOptions);
45
- providerOptions = mergeGatewayTags(providerOptions, input.gatewayTags ?? []);
46
- stepResult.providerOptions = providerOptions;
44
+ stepResult.providerOptions = mergeGatewayAutoCaching(session.agent.modelReference.providerOptions);
47
45
  }
48
46
  return stepResult;
49
47
  };
@@ -18,7 +18,7 @@ import { getInstrumentationConfig } from "#harness/instrumentation-config.js";
18
18
  import { resolveAssistantStepText } from "#harness/messages.js";
19
19
  import { classifyModelCallError, summarizeKnownModelCallConfigError, } from "#harness/model-call-error.js";
20
20
  import { ensureOtelIntegration } from "#harness/otel-integration.js";
21
- import { applyLastToolCacheBreakpoint, detectPromptCachePath, getAnthropicCacheMarker, mergeGatewayTags, } from "#harness/prompt-cache.js";
21
+ import { applyLastToolCacheBreakpoint, detectPromptCachePath, getAnthropicCacheMarker, } from "#harness/prompt-cache.js";
22
22
  import { createRuntimeActionRequestFromToolCall, resolvePendingRuntimeActions, setPendingRuntimeActionBatch, } from "#harness/runtime-actions.js";
23
23
  import { buildStepHooks, emitStepActions, isInvalidToolCall, } from "#harness/step-hooks.js";
24
24
  import { pruneToolResults } from "#harness/tool-result-pruning.js";
@@ -105,20 +105,6 @@ function buildGatewayAttributionHeaders(model, runtimeIdentity) {
105
105
  headers["http-referer"] = referer;
106
106
  return headers;
107
107
  }
108
- function buildGatewayReportingTags({ runtimeIdentity, session, turnId, }) {
109
- const tags = ["framework:ash"];
110
- if (session.sessionId.startsWith("wrun_")) {
111
- tags.push(`workflowRunId:${session.sessionId}`);
112
- }
113
- tags.push(`sessionId:${session.sessionId}`);
114
- if (turnId) {
115
- tags.push(`turnId:${turnId}`);
116
- }
117
- if (runtimeIdentity?.agentId) {
118
- tags.push(`agent:${runtimeIdentity.agentId}`);
119
- }
120
- return tags;
121
- }
122
108
  // ---------------------------------------------------------------------------
123
109
  // Turn trace state — survives step boundaries via session.state
124
110
  // ---------------------------------------------------------------------------
@@ -262,15 +248,9 @@ export function createToolLoopHarness(config) {
262
248
  // Runs before `agent.stream()` so the compacted messages flow through
263
249
  // `messages` (which the harness uses to rebuild session history).
264
250
  const attributionHeaders = buildGatewayAttributionHeaders(model, config.runtimeIdentity);
265
- const gatewayTags = buildGatewayReportingTags({
266
- runtimeIdentity: config.runtimeIdentity,
267
- session,
268
- turnId: emissionState.turnId,
269
- });
270
251
  ({ messages, session } = await maybeCompact({
271
252
  emit,
272
253
  emissionState,
273
- gatewayTags,
274
254
  headers: attributionHeaders,
275
255
  messages,
276
256
  model,
@@ -304,7 +284,6 @@ export function createToolLoopHarness(config) {
304
284
  cachePath,
305
285
  emit,
306
286
  emissionState,
307
- gatewayTags,
308
287
  marker,
309
288
  session,
310
289
  });
@@ -627,13 +606,7 @@ async function maybeCompact(input) {
627
606
  usageInputTokens: getInputTokenCount(messages, session.compaction),
628
607
  }));
629
608
  }
630
- const compactionProviderOptions = typeof compaction.model === "string"
631
- ? mergeGatewayTags(compaction.providerOptions, [
632
- "operation:compaction",
633
- ...(input.gatewayTags ?? []),
634
- ])
635
- : compaction.providerOptions;
636
- messages = await compactMessages(messages, compaction.model, session.compaction, compactionProviderOptions, input.telemetry, input.headers);
609
+ messages = await compactMessages(messages, compaction.model, session.compaction, compaction.providerOptions, input.telemetry, input.headers);
637
610
  if (input.onCompaction) {
638
611
  const compacted = await input.onCompaction(session);
639
612
  session = compacted.session;
@@ -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.15.0";
9
+ const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.16.0";
10
10
  const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
11
11
  const WORKFLOW_MODULE_ALIASES = {
12
12
  "workflow/api": "src/compiled/@workflow/core/runtime.js",
@@ -22,22 +22,12 @@ import { type CardElement, type FileUpload } from "#compiled/chat/index.js";
22
22
  */
23
23
  export type SlackBotToken = string | (() => string | Promise<string>);
24
24
  /**
25
- * Decodes a Slack thread id of the form `slack:<channelId>:<threadTs>`
26
- * back into its parts. Returns empty-string fields when the input does
27
- * not match used by the channel's fallback paths where the channel
28
- * id has not yet been observed.
25
+ * Builds the Slack channel-local continuation token (`<channelId>:<threadTs>`).
26
+ * Route `send()` namespaces this with the channel name before handing it to the
27
+ * runtime (`slack:<channelId>:<threadTs>`), so Slack routes should pass the raw
28
+ * channel-local form here.
29
29
  */
30
- export declare function decodeThreadId(id: string): {
31
- channelId: string;
32
- threadTs: string;
33
- };
34
- /**
35
- * Builds the Ash thread id (`slack:<channelId>:<threadTs>`) used as the
36
- * channel's continuation token. Keeps the encoding in one place so the
37
- * inbound route, the interaction handler, and the context rebuild path
38
- * cannot drift on the format.
39
- */
40
- export declare function encodeThreadId(channelId: string, threadTs: string): string;
30
+ export declare function encodeSlackContinuationToken(channelId: string, threadTs: string): string;
41
31
  /**
42
32
  * Materializes a {@link SlackBotToken} to a concrete string. Falls
43
33
  * back to `process.env.SLACK_BOT_TOKEN` when no value was passed.
@@ -176,8 +166,6 @@ export interface SlackThreadMessage {
176
166
  * use {@link SlackHandle.request} on `ctx.slack` instead.
177
167
  */
178
168
  export interface SlackThread {
179
- /** Ash continuation-token thread id — `slack:<channelId>:<threadTs>`. */
180
- readonly threadId: string;
181
169
  /** Recently fetched thread messages. Populated by {@link refresh}. */
182
170
  readonly recentMessages: readonly SlackThreadMessage[];
183
171
  /**
@@ -20,23 +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
- * Decodes a Slack thread id of the form `slack:<channelId>:<threadTs>`
24
- * back into its parts. Returns empty-string fields when the input does
25
- * not match used by the channel's fallback paths where the channel
26
- * id has not yet been observed.
23
+ * Builds the Slack channel-local continuation token (`<channelId>:<threadTs>`).
24
+ * Route `send()` namespaces this with the channel name before handing it to the
25
+ * runtime (`slack:<channelId>:<threadTs>`), so Slack routes should pass the raw
26
+ * channel-local form here.
27
27
  */
28
- export function decodeThreadId(id) {
29
- const parts = id.replace(/^slack:/u, "").split(":");
30
- return { channelId: parts[0] ?? "", threadTs: parts[1] ?? "" };
31
- }
32
- /**
33
- * Builds the Ash thread id (`slack:<channelId>:<threadTs>`) used as the
34
- * channel's continuation token. Keeps the encoding in one place so the
35
- * inbound route, the interaction handler, and the context rebuild path
36
- * cannot drift on the format.
37
- */
38
- export function encodeThreadId(channelId, threadTs) {
39
- return `slack:${channelId}:${threadTs}`;
28
+ export function encodeSlackContinuationToken(channelId, threadTs) {
29
+ return `${channelId}:${threadTs}`;
40
30
  }
41
31
  /**
42
32
  * Materializes a {@link SlackBotToken} to a concrete string. Falls
@@ -142,7 +132,6 @@ export function buildSlackBinding(input) {
142
132
  return { fileIds, raw: complete };
143
133
  }
144
134
  const thread = {
145
- threadId: encodeThreadId(input.channelId, input.threadTs),
146
135
  recentMessages: messages,
147
136
  async post(rawMessage) {
148
137
  const message = normalizePostInput(rawMessage);
@@ -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, encodeThreadId, resolveSlackBotToken, } from "#public/channels/slack/api.js";
20
+ import { buildSlackBinding, encodeSlackContinuationToken, resolveSlackBotToken, } 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
  /**
@@ -117,7 +117,7 @@ export async function handleInteractionPost(rawBody, ctx, deps) {
117
117
  await openFreeformModal({ payload, interaction, freeformAction, deps });
118
118
  return ack;
119
119
  }
120
- const continuationToken = encodeThreadId(interaction.channelId, interaction.threadTs);
120
+ const continuationToken = encodeSlackContinuationToken(interaction.channelId, interaction.threadTs);
121
121
  const inputResponses = interaction.actions
122
122
  .map(deriveHitlResponse)
123
123
  .filter((r) => r !== null);
@@ -177,7 +177,7 @@ async function openFreeformModal(input) {
177
177
  return;
178
178
  }
179
179
  const metadata = {
180
- continuationToken: encodeThreadId(input.interaction.channelId, input.interaction.threadTs),
180
+ continuationToken: encodeSlackContinuationToken(input.interaction.channelId, input.interaction.threadTs),
181
181
  channelId: input.interaction.channelId,
182
182
  threadTs: input.interaction.threadTs,
183
183
  messageTs,
@@ -18,7 +18,7 @@ type EventData<T extends HandleMessageStreamEvent["type"]> = Extract<HandleMessa
18
18
  *
19
19
  * - {@link thread} owns thread-scoped operations (`post`,
20
20
  * `postEphemeral`, `startTyping`, `refresh`, `recentMessages`,
21
- * `threadId`, `mentionUser`). Reads as "post a reply to this thread".
21
+ * `mentionUser`). Reads as "post a reply to this thread".
22
22
  * - {@link slack} owns Slack identity (`channelId`, `threadTs`,
23
23
  * `teamId`) plus the raw-API escape hatch (`request`, `uploadFiles`).
24
24
  */
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "#internal/logging.js";
2
- import { buildSlackBinding, encodeThreadId, } from "#public/channels/slack/api.js";
2
+ import { buildSlackBinding, encodeSlackContinuationToken, } from "#public/channels/slack/api.js";
3
3
  import { buildSlackTurnMessage, collectInboundFileParts, createSlackFetchFile, } from "#public/channels/slack/attachments.js";
4
4
  import { defaultEvents, defaultInputRequestedHandler, defaultOnAppMention, defaultOnDirectMessage, } from "#public/channels/slack/defaults.js";
5
5
  import { parseAppMentionEvent, parseDirectMessageEvent, prependSlackContext, } from "#public/channels/slack/inbound.js";
@@ -79,7 +79,7 @@ export function slackChannel(config = {}) {
79
79
  const threadTs = typeof input.args.threadTs === "string" ? input.args.threadTs : "";
80
80
  return send(input.message, {
81
81
  auth: input.auth,
82
- continuationToken: encodeThreadId(channelId, threadTs),
82
+ continuationToken: encodeSlackContinuationToken(channelId, threadTs),
83
83
  state: {
84
84
  channelId,
85
85
  threadTs: threadTs || null,
@@ -184,7 +184,7 @@ async function dispatchInboundMessage(input) {
184
184
  }
185
185
  if (result === null || result === undefined)
186
186
  return;
187
- const continuationToken = encodeThreadId(message.channelId, message.threadTs);
187
+ const continuationToken = encodeSlackContinuationToken(message.channelId, message.threadTs);
188
188
  const fileParts = await collectInboundFileParts({
189
189
  mention: message,
190
190
  thread,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-ash",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "bin": {
5
5
  "ash": "./bin/ash.js",
6
6
  "experimental-ash": "./bin/ash.js"