@polos/sdk 0.2.2 → 0.2.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/dist/index.cjs CHANGED
@@ -748,6 +748,11 @@ var OrchestratorClient = class {
748
748
  if (request.waitForSubworkflow !== void 0)
749
749
  body["wait_for_subworkflow"] = request.waitForSubworkflow;
750
750
  if (request.otelTraceparent !== void 0) body["otel_traceparent"] = request.otelTraceparent;
751
+ if (request.channelContext !== void 0)
752
+ body["channel_context"] = {
753
+ channel_id: request.channelContext.channelId,
754
+ source: request.channelContext.source
755
+ };
751
756
  return this.request("POST", "/api/v1/workflows/batch_run", {
752
757
  body
753
758
  });
@@ -3381,6 +3386,187 @@ function defineAgent(config) {
3381
3386
  });
3382
3387
  return agentWorkflow;
3383
3388
  }
3389
+
3390
+ // src/channels/slack.ts
3391
+ var SlackChannel = class {
3392
+ id = "slack";
3393
+ outputMode = "per_step";
3394
+ config;
3395
+ constructor(config) {
3396
+ if (!config.botToken.startsWith("xoxb-")) {
3397
+ throw new Error(
3398
+ `Invalid Slack bot token: must start with "xoxb-". Use the Bot User OAuth Token from your Slack app's OAuth & Permissions page.`
3399
+ );
3400
+ }
3401
+ this.config = config;
3402
+ }
3403
+ async notify(notification) {
3404
+ const overrides = notification.channelOverrides;
3405
+ const channel = overrides?.["channel"] ?? this.config.defaultChannel;
3406
+ const threadTs = overrides?.["thread_ts"] ?? overrides?.["threadTs"];
3407
+ const blocks = this.buildBlocks(notification);
3408
+ const text = notification.title ?? "Agent needs your input";
3409
+ const messageTs = await this.postMessage(channel, threadTs, text, blocks);
3410
+ return {
3411
+ slack_channel: channel,
3412
+ slack_message_ts: messageTs,
3413
+ slack_blocks: blocks
3414
+ };
3415
+ }
3416
+ async sendOutput(context2, event) {
3417
+ const channel = context2.source["channel"];
3418
+ const threadTs = context2.source["threadTs"];
3419
+ if (!channel) return;
3420
+ const text = this.formatOutputEvent(event);
3421
+ if (!text) return;
3422
+ await this.postMessage(channel, threadTs, text);
3423
+ }
3424
+ async postMessage(channel, threadTs, text, blocks) {
3425
+ const body = { channel, text };
3426
+ if (threadTs) body["thread_ts"] = threadTs;
3427
+ if (blocks) body["blocks"] = blocks;
3428
+ const response = await fetch("https://slack.com/api/chat.postMessage", {
3429
+ method: "POST",
3430
+ headers: {
3431
+ Authorization: `Bearer ${this.config.botToken}`,
3432
+ "Content-Type": "application/json"
3433
+ },
3434
+ body: JSON.stringify(body)
3435
+ });
3436
+ const data = await response.json();
3437
+ if (!data.ok) {
3438
+ throw new Error(`Slack API error: ${data.error ?? "unknown"}`);
3439
+ }
3440
+ return data.ts ?? "";
3441
+ }
3442
+ formatOutputEvent(event) {
3443
+ const eventType = event.eventType;
3444
+ if (eventType === "workflow_finish" || eventType === "agent_finish") {
3445
+ const metadata = event.data["_metadata"];
3446
+ const rawResult = event.data["result"];
3447
+ const error = event.data["error"];
3448
+ const workflowId = metadata?.["workflow_id"];
3449
+ if (error) {
3450
+ return `\u274C *${workflowId ?? "Workflow"} failed:* ${error}`;
3451
+ }
3452
+ const result = typeof rawResult === "object" && rawResult !== null && "result" in rawResult ? rawResult["result"] : rawResult;
3453
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
3454
+ if (resultStr) {
3455
+ return resultStr;
3456
+ }
3457
+ return `\u2705 *${workflowId ?? "Workflow"} finished*`;
3458
+ }
3459
+ if (eventType === "tool_call") {
3460
+ const toolCall = event.data["tool_call"];
3461
+ if (toolCall) {
3462
+ const fn = toolCall["function"];
3463
+ const name = fn?.["name"];
3464
+ if (name) {
3465
+ return `\u{1F527} Calling tool: \`${name}\``;
3466
+ }
3467
+ }
3468
+ return null;
3469
+ }
3470
+ if (eventType === "step_finish") {
3471
+ const stepKey = event.data["step_key"];
3472
+ const error = event.data["error"];
3473
+ if (error) {
3474
+ return `\u26A0\uFE0F Step \`${stepKey ?? "unknown"}\` failed: ${error}`;
3475
+ }
3476
+ return null;
3477
+ }
3478
+ return null;
3479
+ }
3480
+ buildBlocks(n) {
3481
+ const blocks = [];
3482
+ blocks.push({
3483
+ type: "header",
3484
+ text: { type: "plain_text", text: n.title ?? "Agent needs your input" }
3485
+ });
3486
+ if (n.description) {
3487
+ blocks.push({
3488
+ type: "section",
3489
+ text: { type: "mrkdwn", text: n.description }
3490
+ });
3491
+ }
3492
+ if (n.source || n.tool) {
3493
+ const parts = [];
3494
+ if (n.source) parts.push(`*Source:* ${n.source}`);
3495
+ if (n.tool) parts.push(`*Tool:* \`${n.tool}\``);
3496
+ blocks.push({
3497
+ type: "context",
3498
+ elements: [{ type: "mrkdwn", text: parts.join(" | ") }]
3499
+ });
3500
+ }
3501
+ if (n.context && Object.keys(n.context).length > 0) {
3502
+ const contextText = JSON.stringify(n.context, null, 2);
3503
+ blocks.push({
3504
+ type: "section",
3505
+ text: { type: "mrkdwn", text: "```" + contextText + "```" }
3506
+ });
3507
+ }
3508
+ if (n.expiresAt) {
3509
+ blocks.push({
3510
+ type: "context",
3511
+ elements: [{ type: "mrkdwn", text: `Expires: ${n.expiresAt}` }]
3512
+ });
3513
+ }
3514
+ if (this.isSimpleApproval(n)) {
3515
+ const approveValue = JSON.stringify({
3516
+ executionId: n.executionId,
3517
+ stepKey: n.stepKey,
3518
+ approved: true
3519
+ });
3520
+ const rejectValue = JSON.stringify({
3521
+ executionId: n.executionId,
3522
+ stepKey: n.stepKey,
3523
+ approved: false
3524
+ });
3525
+ blocks.push({
3526
+ type: "actions",
3527
+ elements: [
3528
+ {
3529
+ type: "button",
3530
+ action_id: "polos_approve",
3531
+ text: { type: "plain_text", text: "Approve" },
3532
+ style: "primary",
3533
+ value: approveValue
3534
+ },
3535
+ {
3536
+ type: "button",
3537
+ action_id: "polos_reject",
3538
+ text: { type: "plain_text", text: "Reject" },
3539
+ style: "danger",
3540
+ value: rejectValue
3541
+ },
3542
+ {
3543
+ type: "button",
3544
+ text: { type: "plain_text", text: "View Details" },
3545
+ url: n.approvalUrl
3546
+ }
3547
+ ]
3548
+ });
3549
+ } else {
3550
+ blocks.push({
3551
+ type: "actions",
3552
+ elements: [
3553
+ {
3554
+ type: "button",
3555
+ text: { type: "plain_text", text: "Respond" },
3556
+ url: n.approvalUrl,
3557
+ style: "primary"
3558
+ }
3559
+ ]
3560
+ });
3561
+ }
3562
+ return blocks;
3563
+ }
3564
+ isSimpleApproval(n) {
3565
+ const fields = n.formFields;
3566
+ if (!fields || fields.length === 0) return false;
3567
+ return fields.some((f) => f["key"] === "approved" && f["type"] === "boolean");
3568
+ }
3569
+ };
3384
3570
  var logger4 = createLogger({ name: "worker-server" });
3385
3571
  var WorkerServer = class {
3386
3572
  app;
@@ -4234,7 +4420,8 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
4234
4420
  otelTraceparent: getCurrentTraceparent(),
4235
4421
  waitForSubworkflow: options?.waitForSubworkflow ?? false,
4236
4422
  initialState: options?.initialState,
4237
- runTimeoutSeconds: options?.runTimeoutSeconds
4423
+ runTimeoutSeconds: options?.runTimeoutSeconds,
4424
+ channelContext
4238
4425
  });
4239
4426
  if (options?.waitForSubworkflow) {
4240
4427
  return [void 0, false];
@@ -4422,7 +4609,8 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
4422
4609
  sessionId: execCtx.sessionId,
4423
4610
  userId: execCtx.userId,
4424
4611
  waitForSubworkflow: false,
4425
- otelTraceparent: getCurrentTraceparent()
4612
+ otelTraceparent: getCurrentTraceparent(),
4613
+ channelContext
4426
4614
  });
4427
4615
  const handleData = response.executions.map((exec, i) => {
4428
4616
  const item = items[i];
@@ -4498,7 +4686,8 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
4498
4686
  sessionId: execCtx.sessionId,
4499
4687
  userId: execCtx.userId,
4500
4688
  waitForSubworkflow: true,
4501
- otelTraceparent: getCurrentTraceparent()
4689
+ otelTraceparent: getCurrentTraceparent(),
4690
+ channelContext
4502
4691
  });
4503
4692
  const workflowIds = items.map((item) => typeof item.workflow === "string" ? item.workflow : item.workflow.id).join(", ");
4504
4693
  throw new WaitError(`Waiting for sub-workflows [${workflowIds}] to complete`, {
@@ -4694,7 +4883,7 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
4694
4883
  if (channelContext !== void 0) {
4695
4884
  notification.channelContext = channelContext;
4696
4885
  }
4697
- await Promise.allSettled(
4886
+ const notifyResults = await Promise.allSettled(
4698
4887
  channels.map(async (ch) => {
4699
4888
  try {
4700
4889
  let overrides = notifyConfig?.[ch.id];
@@ -4702,12 +4891,35 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
4702
4891
  overrides = { ...channelContext.source, ...overrides };
4703
4892
  }
4704
4893
  const n = overrides !== void 0 ? { ...notification, channelOverrides: overrides } : notification;
4705
- await ch.notify(n);
4894
+ const meta = await ch.notify(n);
4895
+ return meta ? { channelId: ch.id, ...meta } : void 0;
4706
4896
  } catch (err) {
4707
4897
  logger6.warn(`Channel ${ch.id} notification failed`, { error: String(err) });
4898
+ return void 0;
4708
4899
  }
4709
4900
  })
4710
4901
  );
4902
+ const metaEntries = [];
4903
+ for (const r of notifyResults) {
4904
+ if (r.status === "fulfilled" && r.value != null) {
4905
+ metaEntries.push(r.value);
4906
+ }
4907
+ }
4908
+ if (metaEntries.length > 0) {
4909
+ orchestratorClient.publishEvent({
4910
+ topic,
4911
+ events: [
4912
+ {
4913
+ eventType: `notification_meta_${key}`,
4914
+ data: { channels: metaEntries }
4915
+ }
4916
+ ],
4917
+ executionId: execCtx.executionId,
4918
+ rootExecutionId: execCtx.rootExecutionId
4919
+ }).catch((err) => {
4920
+ logger6.warn("Failed to publish notification metadata", { error: String(err) });
4921
+ });
4922
+ }
4711
4923
  }
4712
4924
  throw new WaitError(`Waiting for resume event: ${topic}`, { topic });
4713
4925
  },
@@ -6295,10 +6507,24 @@ var Worker = class {
6295
6507
  state = "stopped";
6296
6508
  heartbeatInterval = null;
6297
6509
  activeExecutions = /* @__PURE__ */ new Map();
6510
+ /** Tracks running channel bridges so we don't start duplicates on re-dispatch. */
6511
+ activeChannelBridges = /* @__PURE__ */ new Set();
6298
6512
  signalHandler = null;
6299
6513
  constructor(config) {
6300
6514
  this.config = config;
6301
6515
  this.channels = config.channels ?? [];
6516
+ if (!this.channels.some((ch) => ch.id === "slack")) {
6517
+ const slackBotToken = process.env["SLACK_BOT_TOKEN"];
6518
+ if (slackBotToken) {
6519
+ this.channels.push(
6520
+ new SlackChannel({
6521
+ botToken: slackBotToken,
6522
+ defaultChannel: process.env["SLACK_CHANNEL"] ?? "#general"
6523
+ })
6524
+ );
6525
+ logger9.info("Auto-registered SlackChannel from SLACK_BOT_TOKEN env var");
6526
+ }
6527
+ }
6302
6528
  this.maxConcurrentWorkflows = config.maxConcurrentWorkflows ?? 100;
6303
6529
  this.workerServerUrl = config.workerServerUrl ?? `http://localhost:${String(config.port ?? 8e3)}`;
6304
6530
  this.port = config.port ?? 8e3;
@@ -6832,6 +7058,11 @@ var Worker = class {
6832
7058
  };
6833
7059
  const hasWorkflowChannels = workflow.config.channels !== void 0;
6834
7060
  const resolvedChannels = workflow.config.channels ?? this.channels;
7061
+ if (data.channelContext && !data.parentExecutionId) {
7062
+ const rootExecId = data.rootExecutionId ?? executionId;
7063
+ const rootWfId = data.rootWorkflowId ?? workflowId;
7064
+ this.startChannelBridge(rootExecId, rootWfId, data.channelContext, workflow);
7065
+ }
6835
7066
  result = await executeWorkflow({
6836
7067
  workflow,
6837
7068
  payload: data.payload,
@@ -6843,9 +7074,6 @@ var Worker = class {
6843
7074
  sandboxManager: this.sandboxManager,
6844
7075
  channelContext: hasWorkflowChannels ? void 0 : data.channelContext
6845
7076
  });
6846
- if (data.channelContext) {
6847
- this.startChannelBridge(executionId, workflowId, data.channelContext, workflow);
6848
- }
6849
7077
  await this.handleExecutionResult(executionId, workflowId, context2, result);
6850
7078
  retryableFailure = !result.success && !result.waiting && (result.retryable ?? true);
6851
7079
  } catch (error) {
@@ -6988,15 +7216,20 @@ var Worker = class {
6988
7216
  /**
6989
7217
  * Start a channel bridge that streams execution events back to the originating channel.
6990
7218
  */
6991
- startChannelBridge(executionId, workflowId, channelCtx, workflow) {
7219
+ startChannelBridge(rootExecutionId, rootWorkflowId, channelCtx, workflow) {
7220
+ if (this.activeChannelBridges.has(rootExecutionId)) return;
6992
7221
  const channel = this.channels.find((ch) => ch.id === channelCtx.channelId);
6993
- if (!channel?.sendOutput) return;
7222
+ if (!channel) {
7223
+ logger9.error(
7224
+ `No channel registered for "${channelCtx.channelId}". ` + (channelCtx.channelId === "slack" ? "Set the SLACK_BOT_TOKEN environment variable so the worker can respond to Slack." : `Register a channel with id "${channelCtx.channelId}" on the worker.`),
7225
+ { executionId: rootExecutionId }
7226
+ );
7227
+ return;
7228
+ }
7229
+ if (!channel.sendOutput) return;
6994
7230
  const outputMode = workflow.config["channelOutputMode"] ?? channel.outputMode ?? "per_step";
6995
7231
  if (outputMode === "none") return;
6996
- const execution = this.activeExecutions.get(executionId);
6997
- if (!execution) return;
6998
- const rootExecutionId = execution.abortController.signal.aborted ? executionId : executionId;
6999
- const rootWorkflowId = workflowId;
7232
+ this.activeChannelBridges.add(rootExecutionId);
7000
7233
  void (async () => {
7001
7234
  try {
7002
7235
  const stream = this.orchestratorClient.streamEvents({
@@ -7004,13 +7237,14 @@ var Worker = class {
7004
7237
  workflowRunId: rootExecutionId
7005
7238
  });
7006
7239
  for await (const event of stream) {
7007
- if (execution.abortController.signal.aborted) break;
7008
7240
  if (shouldForwardEvent(event, outputMode)) {
7009
7241
  await channel.sendOutput?.(channelCtx, event);
7010
7242
  }
7011
7243
  }
7012
7244
  } catch (err) {
7013
- logger9.warn("Channel bridge error", { error: String(err), executionId });
7245
+ logger9.warn("Channel bridge error", { error: String(err), executionId: rootExecutionId });
7246
+ } finally {
7247
+ this.activeChannelBridges.delete(rootExecutionId);
7014
7248
  }
7015
7249
  })();
7016
7250
  }
@@ -7715,181 +7949,6 @@ function sandboxTools(config) {
7715
7949
  return tools;
7716
7950
  }
7717
7951
 
7718
- // src/channels/slack.ts
7719
- var SlackChannel = class {
7720
- id = "slack";
7721
- outputMode = "per_step";
7722
- config;
7723
- constructor(config) {
7724
- if (!config.botToken.startsWith("xoxb-")) {
7725
- throw new Error(
7726
- `Invalid Slack bot token: must start with "xoxb-". Use the Bot User OAuth Token from your Slack app's OAuth & Permissions page.`
7727
- );
7728
- }
7729
- this.config = config;
7730
- }
7731
- async notify(notification) {
7732
- const overrides = notification.channelOverrides;
7733
- const channel = overrides?.["channel"] ?? this.config.defaultChannel;
7734
- const threadTs = overrides?.["thread_ts"];
7735
- const blocks = this.buildBlocks(notification);
7736
- const text = notification.title ?? "Agent needs your input";
7737
- await this.postMessage(channel, threadTs, text, blocks);
7738
- }
7739
- async sendOutput(context2, event) {
7740
- const channel = context2.source["channel"];
7741
- const threadTs = context2.source["threadTs"];
7742
- if (!channel) return;
7743
- const text = this.formatOutputEvent(event);
7744
- if (!text) return;
7745
- await this.postMessage(channel, threadTs, text);
7746
- }
7747
- async postMessage(channel, threadTs, text, blocks) {
7748
- const body = { channel, text };
7749
- if (threadTs) body["thread_ts"] = threadTs;
7750
- if (blocks) body["blocks"] = blocks;
7751
- const response = await fetch("https://slack.com/api/chat.postMessage", {
7752
- method: "POST",
7753
- headers: {
7754
- Authorization: `Bearer ${this.config.botToken}`,
7755
- "Content-Type": "application/json"
7756
- },
7757
- body: JSON.stringify(body)
7758
- });
7759
- const data = await response.json();
7760
- if (!data.ok) {
7761
- throw new Error(`Slack API error: ${data.error ?? "unknown"}`);
7762
- }
7763
- }
7764
- formatOutputEvent(event) {
7765
- const eventType = event.eventType;
7766
- if (eventType === "workflow_finish" || eventType === "agent_finish") {
7767
- const metadata = event.data["_metadata"];
7768
- const result = event.data["result"];
7769
- const error = event.data["error"];
7770
- const workflowId = metadata?.["workflow_id"];
7771
- if (error) {
7772
- return `\u274C *${workflowId ?? "Workflow"} failed:* ${error}`;
7773
- }
7774
- const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
7775
- if (resultStr) {
7776
- return `\u2705 *${workflowId ?? "Workflow"} finished:*
7777
- ${resultStr}`;
7778
- }
7779
- return `\u2705 *${workflowId ?? "Workflow"} finished*`;
7780
- }
7781
- if (eventType === "tool_call") {
7782
- const toolCall = event.data["tool_call"];
7783
- if (toolCall) {
7784
- const fn = toolCall["function"];
7785
- const name = fn?.["name"];
7786
- if (name) {
7787
- return `\u{1F527} Calling tool: \`${name}\``;
7788
- }
7789
- }
7790
- return null;
7791
- }
7792
- if (eventType === "step_finish") {
7793
- const stepKey = event.data["step_key"];
7794
- const error = event.data["error"];
7795
- if (error) {
7796
- return `\u26A0\uFE0F Step \`${stepKey ?? "unknown"}\` failed: ${error}`;
7797
- }
7798
- return null;
7799
- }
7800
- return null;
7801
- }
7802
- buildBlocks(n) {
7803
- const blocks = [];
7804
- blocks.push({
7805
- type: "header",
7806
- text: { type: "plain_text", text: n.title ?? "Agent needs your input" }
7807
- });
7808
- if (n.description) {
7809
- blocks.push({
7810
- type: "section",
7811
- text: { type: "mrkdwn", text: n.description }
7812
- });
7813
- }
7814
- if (n.source || n.tool) {
7815
- const parts = [];
7816
- if (n.source) parts.push(`*Source:* ${n.source}`);
7817
- if (n.tool) parts.push(`*Tool:* \`${n.tool}\``);
7818
- blocks.push({
7819
- type: "context",
7820
- elements: [{ type: "mrkdwn", text: parts.join(" | ") }]
7821
- });
7822
- }
7823
- if (n.context && Object.keys(n.context).length > 0) {
7824
- const contextText = JSON.stringify(n.context, null, 2);
7825
- blocks.push({
7826
- type: "section",
7827
- text: { type: "mrkdwn", text: "```" + contextText + "```" }
7828
- });
7829
- }
7830
- if (n.expiresAt) {
7831
- blocks.push({
7832
- type: "context",
7833
- elements: [{ type: "mrkdwn", text: `Expires: ${n.expiresAt}` }]
7834
- });
7835
- }
7836
- if (this.isSimpleApproval(n)) {
7837
- const approveValue = JSON.stringify({
7838
- executionId: n.executionId,
7839
- stepKey: n.stepKey,
7840
- approved: true
7841
- });
7842
- const rejectValue = JSON.stringify({
7843
- executionId: n.executionId,
7844
- stepKey: n.stepKey,
7845
- approved: false
7846
- });
7847
- blocks.push({
7848
- type: "actions",
7849
- elements: [
7850
- {
7851
- type: "button",
7852
- action_id: "polos_approve",
7853
- text: { type: "plain_text", text: "Approve" },
7854
- style: "primary",
7855
- value: approveValue
7856
- },
7857
- {
7858
- type: "button",
7859
- action_id: "polos_reject",
7860
- text: { type: "plain_text", text: "Reject" },
7861
- style: "danger",
7862
- value: rejectValue
7863
- },
7864
- {
7865
- type: "button",
7866
- text: { type: "plain_text", text: "View Details" },
7867
- url: n.approvalUrl
7868
- }
7869
- ]
7870
- });
7871
- } else {
7872
- blocks.push({
7873
- type: "actions",
7874
- elements: [
7875
- {
7876
- type: "button",
7877
- text: { type: "plain_text", text: "Respond" },
7878
- url: n.approvalUrl,
7879
- style: "primary"
7880
- }
7881
- ]
7882
- });
7883
- }
7884
- return blocks;
7885
- }
7886
- isSimpleApproval(n) {
7887
- const fields = n.formFields;
7888
- if (!fields || fields.length === 0) return false;
7889
- return fields.some((f) => f["key"] === "approved" && f["type"] === "boolean");
7890
- }
7891
- };
7892
-
7893
7952
  exports.AgentRunConfig = AgentRunConfig;
7894
7953
  exports.DockerEnvironment = DockerEnvironment;
7895
7954
  exports.DuplicateWorkflowError = DuplicateWorkflowError;