@sjawhar/pi-legion-envoy 3.1.0 → 3.2.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/dist/envoy.js CHANGED
@@ -29997,6 +29997,13 @@ var commentValidation = {
29997
29997
  },
29998
29998
  message: `${commentOwner.message} turn requires reply_to_ask.`
29999
29999
  };
30000
+ var messageValidation = {
30001
+ check: (value) => {
30002
+ const input = value;
30003
+ return typeof input.issue === "string" || typeof input.in_reply_to === "string";
30004
+ },
30005
+ message: "issue is required unless in_reply_to names a message delivered to this session, which is the one message with no issue."
30006
+ };
30000
30007
  var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
30001
30008
  function componentsArgument(z2) {
30002
30009
  return z2.object({
@@ -30217,12 +30224,13 @@ var dispatchToolSpecs = [
30217
30224
  {
30218
30225
  name: "dispatch_message",
30219
30226
  example: { issue: "DSP-1", body: "Implementation started." },
30220
- description: "Post a note humans must read now: a reply to a human's message, a deliverable that landed, or a blocker only " + "they can clear. Never progress or status updates - Dispatch is a high-signal record, not a log. Not a decision " + `(dispatch_ask) or document feedback (dispatch_comment). Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
30227
+ description: "Post a note humans must read now: a reply to a human's message, a deliverable that landed, or a blocker only " + "they can clear. Never progress or status updates - Dispatch is a high-signal record, not a log. Not a decision " + "(dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation, and a second call with the same in_reply_to posts nothing because " + "Dispatch keeps the one reply per message. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
30221
30228
  arguments: (z2) => ({
30222
- issue: z2.string().describe(ISSUE_REFERENCE),
30229
+ issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
30223
30230
  body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
30224
- in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation.").optional()
30225
- })
30231
+ in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional()
30232
+ }),
30233
+ validation: messageValidation
30226
30234
  },
30227
30235
  {
30228
30236
  name: "dispatch_doc_edit",
@@ -31879,12 +31887,14 @@ function renderInbound(raw, sessionID, subject2) {
31879
31887
  issueKey: frame.event.issue_key,
31880
31888
  body: message2.data.body
31881
31889
  };
31882
- if (frame.event.issue_key !== null) {
31883
- dispatchReply = {
31884
- tool: "dispatch_message",
31885
- args: { issue: frame.event.issue_key, in_reply_to: message2.data.id, body: "..." }
31886
- };
31887
- }
31890
+ dispatchReply = {
31891
+ tool: "dispatch_message",
31892
+ args: {
31893
+ ...frame.event.issue_key === null ? {} : { issue: frame.event.issue_key },
31894
+ in_reply_to: message2.data.id,
31895
+ body: "..."
31896
+ }
31897
+ };
31888
31898
  }
31889
31899
  } else if (frame.event.type === "comment.created") {
31890
31900
  const comment2 = DispatchTargetedCommentPayloadSchema.safeParse(frame.event.payload);
@@ -32371,6 +32381,9 @@ class DispatchClient {
32371
32381
  async message(issue2, input) {
32372
32382
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "messages"], input);
32373
32383
  }
32384
+ async messageReply(id, input) {
32385
+ return this.#json("POST", ["api", "v1", "messages", id, "reply"], input);
32386
+ }
32374
32387
  async getMessage(issue2, id) {
32375
32388
  return this.#json("GET", [
32376
32389
  "api",
@@ -33196,6 +33209,10 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
33196
33209
  owner: issue3 === undefined ? null : { kind: "issue", issue: issue3 }
33197
33210
  };
33198
33211
  }
33212
+ const replyTarget = args.in_reply_to;
33213
+ if (tool === "dispatch_message" && typeof replyTarget === "string" && !replyTarget.startsWith("dispatch://")) {
33214
+ return { args, ref, owner: null };
33215
+ }
33199
33216
  const legionIssue = env.LEGION_ISSUE;
33200
33217
  if (!legionIssue) {
33201
33218
  problems.push(ownerRequiredProblem);
@@ -33620,7 +33637,7 @@ async function executeDispatchTool(input) {
33620
33637
  const schema = toolSchema(input.tool);
33621
33638
  const parsed = schema.safeParse(ownerArguments.args, { reportInput: true });
33622
33639
  if (!parsed.success) {
33623
- const issues = parsed.error.issues.filter((issue3) => !ownerMissing || !(issue3.code === "invalid_type" && issue3.path.length === 1 && issue3.path[0] === "issue" && issue3.input === undefined || issue3.code === "custom" && issue3.path.length === 0 && issue3.message.startsWith("Exactly one of issue and project is required")));
33640
+ const issues = parsed.error.issues.filter((issue3) => !ownerMissing || !(issue3.code === "invalid_type" && issue3.path.length === 1 && issue3.path[0] === "issue" && issue3.input === undefined || issue3.code === "custom" && issue3.path.length === 0 && (issue3.message.startsWith("Exactly one of issue and project is required") || issue3.message.startsWith("issue is required unless in_reply_to"))));
33624
33641
  problems.push(...formatZodIssues(issues, schema));
33625
33642
  }
33626
33643
  problems.push(...argumentProblems(input.tool, ownerArguments.args));
@@ -34050,9 +34067,23 @@ ${followsAsk(askOwner)}`,
34050
34067
  }
34051
34068
  case "dispatch_message": {
34052
34069
  const inReplyTo = messageInReplyTo(args);
34070
+ const body = stringArg(args, "body");
34071
+ if (owner === null && inReplyTo !== undefined) {
34072
+ const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor });
34073
+ if (reply.body !== body) {
34074
+ return {
34075
+ text: `Message ${inReplyTo} was already answered by message ${reply.id}; Dispatch kept ` + "that reply and posted nothing. Wait for their next message rather than answering " + "this one again.",
34076
+ details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
34077
+ };
34078
+ }
34079
+ return {
34080
+ text: `Replied to message ${inReplyTo} with message ${reply.id}`,
34081
+ details: { message: reply.id, in_reply_to: inReplyTo, posted: true }
34082
+ };
34083
+ }
34053
34084
  const issueKey = issue2();
34054
34085
  const message = await client.message(issueKey, {
34055
- body: stringArg(args, "body"),
34086
+ body,
34056
34087
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
34057
34088
  actor
34058
34089
  });
package/dist/legion.js CHANGED
@@ -16142,7 +16142,7 @@ import { logger } from "@oh-my-pi/pi-utils";
16142
16142
  // package.json
16143
16143
  var package_default = {
16144
16144
  name: "@sjawhar/pi-legion-envoy",
16145
- version: "3.1.0",
16145
+ version: "3.2.0",
16146
16146
  type: "module",
16147
16147
  omp: {
16148
16148
  extensions: [
@@ -30070,6 +30070,13 @@ var commentValidation = {
30070
30070
  },
30071
30071
  message: `${commentOwner.message} turn requires reply_to_ask.`
30072
30072
  };
30073
+ var messageValidation = {
30074
+ check: (value) => {
30075
+ const input = value;
30076
+ return typeof input.issue === "string" || typeof input.in_reply_to === "string";
30077
+ },
30078
+ message: "issue is required unless in_reply_to names a message delivered to this session, which is the one message with no issue."
30079
+ };
30073
30080
  var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
30074
30081
  function componentsArgument(z2) {
30075
30082
  return z2.object({
@@ -30290,12 +30297,13 @@ var dispatchToolSpecs = [
30290
30297
  {
30291
30298
  name: "dispatch_message",
30292
30299
  example: { issue: "DSP-1", body: "Implementation started." },
30293
- description: "Post a note humans must read now: a reply to a human's message, a deliverable that landed, or a blocker only " + "they can clear. Never progress or status updates - Dispatch is a high-signal record, not a log. Not a decision " + `(dispatch_ask) or document feedback (dispatch_comment). Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
30300
+ description: "Post a note humans must read now: a reply to a human's message, a deliverable that landed, or a blocker only " + "they can clear. Never progress or status updates - Dispatch is a high-signal record, not a log. Not a decision " + "(dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation, and a second call with the same in_reply_to posts nothing because " + "Dispatch keeps the one reply per message. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
30294
30301
  arguments: (z2) => ({
30295
- issue: z2.string().describe(ISSUE_REFERENCE),
30302
+ issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
30296
30303
  body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
30297
- in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation.").optional()
30298
- })
30304
+ in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional()
30305
+ }),
30306
+ validation: messageValidation
30299
30307
  },
30300
30308
  {
30301
30309
  name: "dispatch_doc_edit",
@@ -807,6 +807,31 @@ through Envoy or the hub. A bearer that targets over HTTP names its own session
807
807
  target only a session that advertises the mode you want. Sending to a session with no issue
808
808
  (`POST /api/v1/agents/{session_id}/messages`) stays human-only.
809
809
 
810
+ ### Answering a direct message
811
+
812
+ A human can also message you directly from the **Agents** page, with no issue at all. That frame
813
+ names no issue and its `reply_with` hint carries none either; answer it with the message's bare
814
+ id in `in_reply_to`, alone:
815
+
816
+ ```ts
817
+ dispatch_message({
818
+ in_reply_to: "<the direct message's id>",
819
+ body: "The requested answer.",
820
+ })
821
+ ```
822
+
823
+ Leave `issue` out — there is no issue to post into, and naming one would file your answer on
824
+ unrelated work. Dispatch threads the reply under their message in the same conversation, and the
825
+ human sees it on your agent card. Every other message still names its issue, so keep the `issue`
826
+ the frame gave you whenever it gave you one; a `dispatch://KEY/message/<id>` reference names the
827
+ issue its message lives on, so that form is a reply on that issue, not a direct message.
828
+
829
+ One reply per message they send. A second call with the same `in_reply_to` posts nothing:
830
+ Dispatch answers it with the reply already stored, and the tool result says the message was
831
+ already answered rather than reporting a send. That happens most often when your host answered
832
+ the **BTW** automatically before you got here — read the result before writing again, and wait
833
+ for their next message instead.
834
+
810
835
  ## Following
811
836
 
812
837
  An ask has followers: every session that wrote to it — the session that opened it and every session that replied with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [