@sjawhar/opencode-legion-envoy 5.8.2 → 5.9.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/src/server.js +630 -172
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +6 -0
- package/skills/dispatch/references/documents.md +23 -0
package/dist/src/server.js
CHANGED
|
@@ -13886,6 +13886,21 @@ function snippetSegments(snippet) {
|
|
|
13886
13886
|
function snippetText(snippet) {
|
|
13887
13887
|
return snippetSegments(snippet).map(({ text, mark }) => mark ? `**${text}**` : text).join("");
|
|
13888
13888
|
}
|
|
13889
|
+
// ../contracts/src/dispatch-text-rules.ts
|
|
13890
|
+
var SESSION_ID_PATTERN = "[^/?#\\s\\p{Z}\\p{Cc}<>\"'`\\[\\]|]+";
|
|
13891
|
+
var sessionIdOnly = new RegExp(`^${SESSION_ID_PATTERN}$`, "u");
|
|
13892
|
+
function pictureCaption(name) {
|
|
13893
|
+
return name.replace(/[\\[\]`]/g, "\\$&").replace(/\s+/g, " ");
|
|
13894
|
+
}
|
|
13895
|
+
var PICTURE_TYPES = {
|
|
13896
|
+
"image/png": true,
|
|
13897
|
+
"image/jpeg": true,
|
|
13898
|
+
"image/gif": true,
|
|
13899
|
+
"image/webp": true
|
|
13900
|
+
};
|
|
13901
|
+
function isPictureType(mime) {
|
|
13902
|
+
return PICTURE_TYPES[mime] === true;
|
|
13903
|
+
}
|
|
13889
13904
|
// ../contracts/src/dispatch-tools.ts
|
|
13890
13905
|
function dispatchToolSchema(spec, z2, opts) {
|
|
13891
13906
|
const shape = spec.arguments(z2);
|
|
@@ -13959,8 +13974,12 @@ function componentsArgument(z2) {
|
|
|
13959
13974
|
}).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
|
|
13960
13975
|
}
|
|
13961
13976
|
var SPEC_WRITING_POINTER = 'Write a spec as the "Writing a spec" section of skill://dispatch says.';
|
|
13977
|
+
function imagesArgument(z2, owner, cap) {
|
|
13978
|
+
return z2.array(z2.string({ min: 1 }), { min: 1 }).describe(`Optional local picture files to show inline: PNG, JPEG, GIF or WebP (by their bytes, not their name), each at most 25 MiB. Each is uploaded to ${owner} and appended after a blank line as one  line, in this order; those lines count toward ${cap}.`).optional();
|
|
13979
|
+
}
|
|
13962
13980
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
13963
13981
|
var ASK_QUESTION_MAX = 800;
|
|
13982
|
+
var DISPATCH_BODY_MAX = 2000;
|
|
13964
13983
|
var SEARCH_QUERY_MAX = 1000;
|
|
13965
13984
|
var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passage";
|
|
13966
13985
|
var SEARCH_KIND_DEPTH = 100;
|
|
@@ -14060,7 +14079,7 @@ var dispatchToolSpecs = [
|
|
|
14060
14079
|
}
|
|
14061
14080
|
]
|
|
14062
14081
|
},
|
|
14063
|
-
description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch_doc_edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch_request_approval. Do not use this tool for a status " + "update or discussion; use dispatch_message instead. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. " + "Anchor a to-do about a document passage, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". ` + `A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} characters ` + `and has at most 8 options. ${OWNER_REFERENCE}`,
|
|
14082
|
+
description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch_doc_edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch_request_approval. Do not use this tool for a status " + "update or discussion; use dispatch_message instead. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. " + "Anchor a to-do about a document passage, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". ` + `A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} characters, ` + `the lines images appends included, and has at most 8 options. ${OWNER_REFERENCE}`,
|
|
14064
14083
|
arguments: (z2) => ({
|
|
14065
14084
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14066
14085
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14077,7 +14096,8 @@ var dispatchToolSpecs = [
|
|
|
14077
14096
|
artifact: z2.string().describe("Artifact slug or id containing the quoted text."),
|
|
14078
14097
|
quote: z2.string().describe("Exact text the decision concerns."),
|
|
14079
14098
|
occurrence: z2.number({ int: true, min: 0 }).describe("Zero-based occurrence of the quote.").optional()
|
|
14080
|
-
}).describe("Optional document location for the question.").optional()
|
|
14099
|
+
}).describe("Optional document location for the question.").optional(),
|
|
14100
|
+
images: imagesArgument(z2, "the issue (or the project document's project)", `the ${ASK_QUESTION_MAX}-character question limit`)
|
|
14081
14101
|
}),
|
|
14082
14102
|
validation: documentOwnerValidation(true)
|
|
14083
14103
|
},
|
|
@@ -14152,7 +14172,7 @@ var dispatchToolSpecs = [
|
|
|
14152
14172
|
{
|
|
14153
14173
|
name: "dispatch_comment",
|
|
14154
14174
|
example: { issue: "DSP-1", body: "Looks good." },
|
|
14155
|
-
description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters. ${OWNER_REFERENCE}`,
|
|
14175
|
+
description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters, the lines images appends included. ${OWNER_REFERENCE}`,
|
|
14156
14176
|
arguments: (z2) => ({
|
|
14157
14177
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14158
14178
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14160,10 +14180,11 @@ var dispatchToolSpecs = [
|
|
|
14160
14180
|
ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
|
|
14161
14181
|
quote: z2.string().describe("Optional exact quoted document text.").optional(),
|
|
14162
14182
|
occurrence: z2.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
|
|
14163
|
-
body: z2.string({ max:
|
|
14183
|
+
body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Review comment, at most 2,000 characters."),
|
|
14164
14184
|
reply_to: z2.string().describe("A comment id (uuid); replying to any comment in a thread continues that thread (an " + "ask's clarification thread included). To reply to an ask, use reply_to_ask with the " + "ask id instead.").optional(),
|
|
14165
14185
|
reply_to_ask: z2.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
|
|
14166
|
-
turn: z2.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional()
|
|
14186
|
+
turn: z2.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional(),
|
|
14187
|
+
images: imagesArgument(z2, "the issue (or the project document's project)", "the 2,000-character body limit")
|
|
14167
14188
|
}),
|
|
14168
14189
|
validation: commentValidation
|
|
14169
14190
|
},
|
|
@@ -14191,11 +14212,12 @@ var dispatchToolSpecs = [
|
|
|
14191
14212
|
{
|
|
14192
14213
|
name: "dispatch_message",
|
|
14193
14214
|
example: { issue: "DSP-1", body: "Implementation started." },
|
|
14194
|
-
description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (dispatch_ask), so it reaches their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a design decision (write it as a decision block in the document) 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. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
|
|
14215
|
+
description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (dispatch_ask), so it reaches their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a design decision (write it as a decision block in the document) 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. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters, the lines images appends included. ${ISSUE_REFERENCE}`,
|
|
14195
14216
|
arguments: (z2) => ({
|
|
14196
14217
|
issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
|
|
14197
|
-
body: z2.string({ max:
|
|
14198
|
-
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()
|
|
14218
|
+
body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Update text, at most 2,000 characters."),
|
|
14219
|
+
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(),
|
|
14220
|
+
images: imagesArgument(z2, "the issue (a reply to a direct message: your own conversation, dispatch://agent/<your session id>)", "the 2,000-character body limit")
|
|
14199
14221
|
}),
|
|
14200
14222
|
validation: messageValidation
|
|
14201
14223
|
},
|
|
@@ -14240,13 +14262,13 @@ var dispatchToolSpecs = [
|
|
|
14240
14262
|
{
|
|
14241
14263
|
name: "dispatch_doc_read",
|
|
14242
14264
|
example: { issue: "DSP-1" },
|
|
14243
|
-
description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + "A file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
|
|
14265
|
+
description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
|
|
14244
14266
|
arguments: (z2) => ({
|
|
14245
14267
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14246
14268
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
14247
14269
|
artifact: z2.string().describe("Optional project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
|
|
14248
14270
|
version: z2.number({ int: true, min: 1 }).describe("Optional version number.").optional(),
|
|
14249
|
-
ref: z2.string().describe("Optional dispatch:// document reference.").optional()
|
|
14271
|
+
ref: z2.string().describe("Optional dispatch:// document reference; a picture in a conversation on the Agents page is dispatch://agent/<session id>/artifact/<slug>@vN.").optional()
|
|
14250
14272
|
}),
|
|
14251
14273
|
validation: documentOwnerValidation(true)
|
|
14252
14274
|
},
|
|
@@ -14286,7 +14308,7 @@ var dispatchToolSpecs = [
|
|
|
14286
14308
|
{
|
|
14287
14309
|
name: "dispatch_read",
|
|
14288
14310
|
example: { issue: "DSP-1" },
|
|
14289
|
-
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + OWNER_REFERENCE,
|
|
14311
|
+
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch_doc_read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch_doc_read shows it again. " + OWNER_REFERENCE,
|
|
14290
14312
|
arguments: (z2) => ({
|
|
14291
14313
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14292
14314
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
@@ -14630,7 +14652,7 @@ function resolveDispatchConfig(env, options = {}) {
|
|
|
14630
14652
|
}
|
|
14631
14653
|
|
|
14632
14654
|
// ../envoy-client/src/dispatch-execute.ts
|
|
14633
|
-
import { resolve as
|
|
14655
|
+
import { resolve as resolvePath2 } from "path";
|
|
14634
14656
|
|
|
14635
14657
|
// ../envoy-client/src/ask-answer.ts
|
|
14636
14658
|
var HEAD_LENGTH = 120;
|
|
@@ -14876,21 +14898,27 @@ class DispatchClient {
|
|
|
14876
14898
|
return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
|
|
14877
14899
|
}
|
|
14878
14900
|
async projectArtifact(project, input) {
|
|
14879
|
-
|
|
14880
|
-
if ("content" in input)
|
|
14881
|
-
return this.#json("POST", artifactPath, input);
|
|
14882
|
-
const form = new FormData;
|
|
14883
|
-
form.set("name", input.name);
|
|
14884
|
-
if (input.summary !== undefined)
|
|
14885
|
-
form.set("summary", input.summary);
|
|
14886
|
-
if (input.actor !== undefined)
|
|
14887
|
-
form.set("actor", JSON.stringify(input.actor));
|
|
14888
|
-
form.set("file", input.file, input.name);
|
|
14889
|
-
return this.#form("POST", artifactPath, form);
|
|
14901
|
+
return this.#upload(["api", "v1", "projects", project, "artifacts"], input);
|
|
14890
14902
|
}
|
|
14891
14903
|
async getProjectArtifact(project, slug) {
|
|
14892
14904
|
return this.#json("GET", ["api", "v1", "projects", project, "artifacts", slug]);
|
|
14893
14905
|
}
|
|
14906
|
+
async agentArtifact(sessionId, input) {
|
|
14907
|
+
return this.#upload(["api", "v1", "agents", sessionId, "artifacts"], input);
|
|
14908
|
+
}
|
|
14909
|
+
async getAgentArtifact(sessionId, slug) {
|
|
14910
|
+
return this.#json("GET", ["api", "v1", "agents", sessionId, "artifacts", slug]);
|
|
14911
|
+
}
|
|
14912
|
+
async getIssueArtifact(issue2, slug) {
|
|
14913
|
+
return this.#json("GET", [
|
|
14914
|
+
"api",
|
|
14915
|
+
"v1",
|
|
14916
|
+
"issues",
|
|
14917
|
+
await this.#resolveIssue(issue2),
|
|
14918
|
+
"artifacts",
|
|
14919
|
+
slug
|
|
14920
|
+
]);
|
|
14921
|
+
}
|
|
14894
14922
|
async search(query, options = {}) {
|
|
14895
14923
|
return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
|
|
14896
14924
|
}
|
|
@@ -15011,17 +15039,7 @@ class DispatchClient {
|
|
|
15011
15039
|
return this.#json("POST", ["api", "v1", "messages", id, "deliveries", String(attempt), "accept"], input);
|
|
15012
15040
|
}
|
|
15013
15041
|
async artifact(issue2, input) {
|
|
15014
|
-
|
|
15015
|
-
if ("content" in input)
|
|
15016
|
-
return this.#json("POST", artifactPath, input);
|
|
15017
|
-
const form = new FormData;
|
|
15018
|
-
form.set("name", input.name);
|
|
15019
|
-
if (input.summary !== undefined)
|
|
15020
|
-
form.set("summary", input.summary);
|
|
15021
|
-
if (input.actor !== undefined)
|
|
15022
|
-
form.set("actor", JSON.stringify(input.actor));
|
|
15023
|
-
form.set("file", input.file, input.name);
|
|
15024
|
-
return this.#form("POST", artifactPath, form);
|
|
15042
|
+
return this.#upload(["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"], input);
|
|
15025
15043
|
}
|
|
15026
15044
|
async getArtifact(id) {
|
|
15027
15045
|
return this.#json("GET", ["api", "v1", "artifacts", id]);
|
|
@@ -15128,6 +15146,18 @@ class DispatchClient {
|
|
|
15128
15146
|
});
|
|
15129
15147
|
return { status: response.status, payload: await this.#response(method, url2, response) };
|
|
15130
15148
|
}
|
|
15149
|
+
async#upload(path2, input) {
|
|
15150
|
+
if ("content" in input)
|
|
15151
|
+
return this.#json("POST", path2, input);
|
|
15152
|
+
const form = new FormData;
|
|
15153
|
+
form.set("name", input.name);
|
|
15154
|
+
if (input.summary !== undefined)
|
|
15155
|
+
form.set("summary", input.summary);
|
|
15156
|
+
if (input.actor !== undefined)
|
|
15157
|
+
form.set("actor", JSON.stringify(input.actor));
|
|
15158
|
+
form.set("file", input.file, input.name);
|
|
15159
|
+
return this.#form("POST", path2, form);
|
|
15160
|
+
}
|
|
15131
15161
|
async#form(method, path2, body) {
|
|
15132
15162
|
const url2 = this.#url(path2);
|
|
15133
15163
|
const response = await this.fetchImpl(url2, {
|
|
@@ -15208,70 +15238,115 @@ function dispatchDocumentRef(owner, slug) {
|
|
|
15208
15238
|
function dispatchChildRef(ownerRef, kind, id) {
|
|
15209
15239
|
return `${ownerRef}/${kind}/${id}`;
|
|
15210
15240
|
}
|
|
15211
|
-
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
15221
|
-
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
return `${label}: ${snippetText(result.snippet)} -> ${href}`;
|
|
15226
|
-
}
|
|
15227
|
-
function searchAnswer(search, query, offset, configUrl) {
|
|
15228
|
-
const results = search.results;
|
|
15229
|
-
const count = results.length;
|
|
15230
|
-
const lines = results.map((result) => searchResultLine(result, configUrl));
|
|
15231
|
-
const noun = count === 1 ? "result" : "results";
|
|
15232
|
-
const noResults = `No results for "${query}".`;
|
|
15233
|
-
if (typeof search.total !== "number") {
|
|
15234
|
-
if (offset !== undefined && offset > 0) {
|
|
15235
|
-
throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
|
|
15241
|
+
function parseDispatchRef(ref) {
|
|
15242
|
+
const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
|
|
15243
|
+
if (projectDocument) {
|
|
15244
|
+
const [, project, artifact2, version3, targetKind, targetID] = projectDocument;
|
|
15245
|
+
if (project === undefined || artifact2 === undefined || version3 !== undefined && Number(version3) < 1) {
|
|
15246
|
+
return null;
|
|
15247
|
+
}
|
|
15248
|
+
if (targetKind === undefined) {
|
|
15249
|
+
return {
|
|
15250
|
+
owner: { kind: "project", project },
|
|
15251
|
+
kind: "artifact",
|
|
15252
|
+
id: artifact2,
|
|
15253
|
+
...version3 === undefined ? {} : { version: Number(version3) }
|
|
15254
|
+
};
|
|
15236
15255
|
}
|
|
15256
|
+
if (targetID === undefined || targetKind !== "ask" && targetKind !== "comment")
|
|
15257
|
+
return null;
|
|
15237
15258
|
return {
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15259
|
+
owner: { kind: "project", project },
|
|
15260
|
+
kind: targetKind,
|
|
15261
|
+
id: targetID,
|
|
15262
|
+
artifact: artifact2
|
|
15241
15263
|
};
|
|
15242
15264
|
}
|
|
15243
|
-
const
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
const
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15254
|
-
if (
|
|
15265
|
+
const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
|
|
15266
|
+
if (!issueReference)
|
|
15267
|
+
return null;
|
|
15268
|
+
const [, issue2, spec, log, children, artifact, version2, ask, comment, message] = issueReference;
|
|
15269
|
+
if (!issue2 || version2 !== undefined && Number(version2) < 1)
|
|
15270
|
+
return null;
|
|
15271
|
+
const owner = { kind: "issue", issue: issue2 };
|
|
15272
|
+
if (spec)
|
|
15273
|
+
return { owner, kind: "spec", id: spec };
|
|
15274
|
+
if (log)
|
|
15275
|
+
return { owner, kind: "log", id: log };
|
|
15276
|
+
if (children)
|
|
15277
|
+
return { owner, kind: "children", id: children };
|
|
15278
|
+
if (artifact) {
|
|
15255
15279
|
return {
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
|
|
15260
|
-
`),
|
|
15261
|
-
details
|
|
15280
|
+
owner,
|
|
15281
|
+
kind: "artifact",
|
|
15282
|
+
id: artifact,
|
|
15283
|
+
...version2 === undefined ? {} : { version: Number(version2) }
|
|
15262
15284
|
};
|
|
15263
15285
|
}
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15286
|
+
if (ask)
|
|
15287
|
+
return { owner, kind: "ask", id: ask };
|
|
15288
|
+
if (comment)
|
|
15289
|
+
return { owner, kind: "comment", id: comment };
|
|
15290
|
+
if (message)
|
|
15291
|
+
return { owner, kind: "message", id: message };
|
|
15292
|
+
return { owner, kind: "issue", id: issue2 };
|
|
15293
|
+
}
|
|
15294
|
+
|
|
15295
|
+
// ../envoy-client/src/dispatch-picture-tools.ts
|
|
15296
|
+
import { basename, resolve as resolvePath } from "path";
|
|
15297
|
+
|
|
15298
|
+
// ../envoy-client/src/dispatch-pictures.ts
|
|
15299
|
+
var PICTURE_UPLOAD_MAX_BYTES = 25 * 1024 * 1024;
|
|
15300
|
+
var PICTURE_SHOWN_MAX_BYTES = 3750000;
|
|
15301
|
+
var pictureShownMaxText = `${PICTURE_SHOWN_MAX_BYTES.toLocaleString("en-US")} bytes (5 MB of base64)`;
|
|
15302
|
+
function oversizePictureText(name, bytes) {
|
|
15303
|
+
return `${name} is ${bytes.toLocaleString("en-US")} bytes, over the ${pictureShownMaxText} a model is shown`;
|
|
15304
|
+
}
|
|
15305
|
+
var PICTURES_SHOWN_MAX = 8;
|
|
15306
|
+
var PICTURES_SHOWN_MAX_BYTES = 10 * 1024 * 1024;
|
|
15307
|
+
var PICTURE_SNIFF_BYTES = 12;
|
|
15308
|
+
function bytesAt(bytes, offset, expected) {
|
|
15309
|
+
return expected.every((byte, index) => bytes[offset + index] === byte);
|
|
15310
|
+
}
|
|
15311
|
+
function sniffPictureType(bytes) {
|
|
15312
|
+
if (bytesAt(bytes, 0, [137, 80, 78, 71, 13, 10, 26, 10]))
|
|
15313
|
+
return "image/png";
|
|
15314
|
+
if (bytesAt(bytes, 0, [255, 216, 255]))
|
|
15315
|
+
return "image/jpeg";
|
|
15316
|
+
if (bytesAt(bytes, 0, [71, 73, 70, 56]) && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97) {
|
|
15317
|
+
return "image/gif";
|
|
15318
|
+
}
|
|
15319
|
+
if (bytesAt(bytes, 0, [82, 73, 70, 70]) && bytesAt(bytes, 8, [87, 69, 66, 80])) {
|
|
15320
|
+
return "image/webp";
|
|
15321
|
+
}
|
|
15322
|
+
return;
|
|
15323
|
+
}
|
|
15324
|
+
function toolImage(bytes) {
|
|
15325
|
+
const mimeType = sniffPictureType(bytes);
|
|
15326
|
+
if (mimeType === undefined || bytes.length > PICTURE_SHOWN_MAX_BYTES)
|
|
15327
|
+
return;
|
|
15328
|
+
return { data: Buffer.from(bytes).toString("base64"), mimeType };
|
|
15329
|
+
}
|
|
15330
|
+
function pictureLine(name, address) {
|
|
15331
|
+
return ``;
|
|
15332
|
+
}
|
|
15333
|
+
function withPictureLines(text, lines) {
|
|
15334
|
+
if (lines.length === 0)
|
|
15335
|
+
return text;
|
|
15336
|
+
const head = text.trimEnd();
|
|
15337
|
+
return head === "" ? lines.join(`
|
|
15338
|
+
`) : `${head}
|
|
15339
|
+
|
|
15340
|
+
${lines.join(`
|
|
15341
|
+
`)}`;
|
|
15342
|
+
}
|
|
15343
|
+
var PICTURE_SYNTAX = /!\[(?:\\.|[^\\\]])*\]\((dispatch:\/\/[^\s()]+@v[1-9][0-9]*)(?:\s+"(?:\\.|[^\\"])*")?\)/g;
|
|
15344
|
+
function pictureAddresses(text) {
|
|
15345
|
+
return [...text.matchAll(PICTURE_SYNTAX)].map((match) => match[1]);
|
|
15346
|
+
}
|
|
15347
|
+
function picturesNewestFirst(texts) {
|
|
15348
|
+
const ordered = texts.map((item, index) => ({ item, index, at: Date.parse(item.at) })).sort((left, right) => (Number.isNaN(right.at) ? 0 : right.at) - (Number.isNaN(left.at) ? 0 : left.at) || left.index - right.index);
|
|
15349
|
+
return [...new Set(ordered.flatMap(({ item }) => pictureAddresses(item.text)))];
|
|
15275
15350
|
}
|
|
15276
15351
|
|
|
15277
15352
|
// ../envoy-client/src/tool-input-errors.ts
|
|
@@ -15397,6 +15472,314 @@ function formatZodIssues(issues, schema) {
|
|
|
15397
15472
|
});
|
|
15398
15473
|
}
|
|
15399
15474
|
|
|
15475
|
+
// ../envoy-client/src/dispatch-picture-tools.ts
|
|
15476
|
+
var PICTURE_TYPE_NAMES = "a PNG, JPEG, GIF or WebP";
|
|
15477
|
+
async function localPictures(images, cwd, problems) {
|
|
15478
|
+
if (!Array.isArray(images))
|
|
15479
|
+
return [];
|
|
15480
|
+
const checked = await Promise.all(images.map((path2) => localPicture(path2, cwd)));
|
|
15481
|
+
const pictures = [];
|
|
15482
|
+
for (const outcome of checked) {
|
|
15483
|
+
if (typeof outcome === "string")
|
|
15484
|
+
problems.push(outcome);
|
|
15485
|
+
else if (outcome !== undefined)
|
|
15486
|
+
pictures.push(outcome);
|
|
15487
|
+
}
|
|
15488
|
+
return pictures;
|
|
15489
|
+
}
|
|
15490
|
+
async function localPicture(path2, cwd) {
|
|
15491
|
+
if (typeof path2 !== "string" || path2 === "")
|
|
15492
|
+
return;
|
|
15493
|
+
const absolute = resolvePath(cwd, path2);
|
|
15494
|
+
const file2 = Bun.file(absolute);
|
|
15495
|
+
let size;
|
|
15496
|
+
let head;
|
|
15497
|
+
try {
|
|
15498
|
+
const stats = await file2.stat();
|
|
15499
|
+
if (!stats.isFile())
|
|
15500
|
+
return `images: ${path2} is not a file`;
|
|
15501
|
+
size = stats.size;
|
|
15502
|
+
head = new Uint8Array(await file2.slice(0, PICTURE_SNIFF_BYTES).arrayBuffer());
|
|
15503
|
+
} catch (error48) {
|
|
15504
|
+
return `images: ${path2} cannot be read: ${messageFor(error48)}`;
|
|
15505
|
+
}
|
|
15506
|
+
if (size > PICTURE_UPLOAD_MAX_BYTES) {
|
|
15507
|
+
return `images: ${path2} is ${size.toLocaleString("en-US")} bytes, over the ${PICTURE_UPLOAD_MAX_BYTES / 1024 / 1024} MiB one Dispatch upload takes`;
|
|
15508
|
+
}
|
|
15509
|
+
const type = sniffPictureType(head);
|
|
15510
|
+
if (type === undefined) {
|
|
15511
|
+
return `images: ${path2} is not ${PICTURE_TYPE_NAMES} picture (judged by its bytes, not its name)`;
|
|
15512
|
+
}
|
|
15513
|
+
return { path: path2, name: basename(absolute), file: Bun.file(absolute, { type }) };
|
|
15514
|
+
}
|
|
15515
|
+
function uploadSlug(name) {
|
|
15516
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
15517
|
+
return slug === "" ? "artifact" : slug;
|
|
15518
|
+
}
|
|
15519
|
+
async function textWithPictures(tool, text, pictures, limit, owner, upload, actor) {
|
|
15520
|
+
if (pictures.length === 0)
|
|
15521
|
+
return text;
|
|
15522
|
+
const counted = `${limit.text} plus ${pictures.length} ${pictures.length === 1 ? "picture" : "pictures"}`;
|
|
15523
|
+
const fix = `${limit.shorten} or send fewer pictures`;
|
|
15524
|
+
const predicted = withPictureLines(text, pictures.map((picture) => pictureLine(picture.name, `dispatch://${owner}/artifact/${uploadSlug(picture.name)}@v1`)));
|
|
15525
|
+
if (predicted.length > limit.cap) {
|
|
15526
|
+
throw new ToolInputError(tool, [
|
|
15527
|
+
`${counted} ${overCapMessage(predicted.length, limit.cap)}; ${fix}`
|
|
15528
|
+
]);
|
|
15529
|
+
}
|
|
15530
|
+
const lines = [];
|
|
15531
|
+
const addresses = [];
|
|
15532
|
+
for (const picture of pictures) {
|
|
15533
|
+
let uploaded;
|
|
15534
|
+
try {
|
|
15535
|
+
uploaded = await upload({ name: picture.name, file: picture.file, actor });
|
|
15536
|
+
} catch (error48) {
|
|
15537
|
+
const earlier = addresses.length === 0 ? "" : `; the pictures before it are uploaded (${addresses.join(", ")})`;
|
|
15538
|
+
throw new Error(`images: ${picture.path} was not uploaded: ${messageFor(error48)}. Nothing was posted${earlier}.`);
|
|
15539
|
+
}
|
|
15540
|
+
const address = `dispatch://${owner}/artifact/${uploaded.artifact.slug}@v${uploaded.version.number}`;
|
|
15541
|
+
addresses.push(address);
|
|
15542
|
+
lines.push(pictureLine(picture.name, address));
|
|
15543
|
+
}
|
|
15544
|
+
const posted = withPictureLines(text, lines);
|
|
15545
|
+
if (posted.length > limit.cap) {
|
|
15546
|
+
throw new Error(`${counted} ${overCapMessage(posted.length, limit.cap)} with the addresses Dispatch gave ` + `the uploads (${addresses.join(", ")}), so nothing was posted; ${fix}.`);
|
|
15547
|
+
}
|
|
15548
|
+
return posted;
|
|
15549
|
+
}
|
|
15550
|
+
var AGENT_ARTIFACT_REF = new RegExp(`^dispatch://agent/(${SESSION_ID_PATTERN})/artifact/([^/@]+)(?:@v(\\d+))?$`, "u");
|
|
15551
|
+
function parseAgentArtifactRef(ref) {
|
|
15552
|
+
const match = ref.match(AGENT_ARTIFACT_REF);
|
|
15553
|
+
if (match === null)
|
|
15554
|
+
return null;
|
|
15555
|
+
const [, session, slug, version2] = match;
|
|
15556
|
+
if (session === undefined || slug === undefined || version2 !== undefined && Number(version2) < 1) {
|
|
15557
|
+
return null;
|
|
15558
|
+
}
|
|
15559
|
+
return { session, slug, ...version2 === undefined ? {} : { version: Number(version2) } };
|
|
15560
|
+
}
|
|
15561
|
+
function datedBodies(records) {
|
|
15562
|
+
return records.map((record2) => ({ text: record2.body, at: record2.created_at }));
|
|
15563
|
+
}
|
|
15564
|
+
function askTexts({ ask, replies }) {
|
|
15565
|
+
return [
|
|
15566
|
+
{ text: ask.question, at: ask.created_at },
|
|
15567
|
+
...ask.answer?.text ? [{ text: ask.answer.text, at: ask.answer.at }] : [],
|
|
15568
|
+
...ask.resolution === undefined ? [] : [{ text: ask.resolution.reason, at: ask.resolution.at }],
|
|
15569
|
+
...datedBodies(replies)
|
|
15570
|
+
];
|
|
15571
|
+
}
|
|
15572
|
+
function eventTexts(event) {
|
|
15573
|
+
let texts;
|
|
15574
|
+
switch (event.type) {
|
|
15575
|
+
case "ask.opened":
|
|
15576
|
+
case "ask.anchor_refreshed":
|
|
15577
|
+
case "ask.edited":
|
|
15578
|
+
case "ask.handed_back":
|
|
15579
|
+
case "ask.answered":
|
|
15580
|
+
case "ask.resolved":
|
|
15581
|
+
texts = [
|
|
15582
|
+
event.payload.question,
|
|
15583
|
+
event.payload.answer?.text,
|
|
15584
|
+
event.payload.resolution?.reason
|
|
15585
|
+
];
|
|
15586
|
+
break;
|
|
15587
|
+
case "comment.created":
|
|
15588
|
+
case "comment.answered":
|
|
15589
|
+
case "comment.anchor_refreshed":
|
|
15590
|
+
case "comment.edited":
|
|
15591
|
+
case "comment.resolved":
|
|
15592
|
+
case "comment.reopened":
|
|
15593
|
+
case "suggestion.accepted":
|
|
15594
|
+
case "suggestion.rejected":
|
|
15595
|
+
case "message.created":
|
|
15596
|
+
case "message.answered":
|
|
15597
|
+
texts = [event.payload.body];
|
|
15598
|
+
break;
|
|
15599
|
+
default:
|
|
15600
|
+
texts = [];
|
|
15601
|
+
}
|
|
15602
|
+
return texts.filter((text) => typeof text === "string" && text !== "").map((text) => ({ text, at: event.created_at }));
|
|
15603
|
+
}
|
|
15604
|
+
function shownPictureLine(index, address, name, mimeType, bytes) {
|
|
15605
|
+
return `- image ${index}: ${address} (${name}, ${mimeType}, ${bytes.toLocaleString("en-US")} bytes)`;
|
|
15606
|
+
}
|
|
15607
|
+
function pictureTarget(address) {
|
|
15608
|
+
const agent = parseAgentArtifactRef(address);
|
|
15609
|
+
if (agent !== null) {
|
|
15610
|
+
if (agent.version === undefined)
|
|
15611
|
+
return;
|
|
15612
|
+
return {
|
|
15613
|
+
key: `agent/${agent.session}/${agent.slug}`,
|
|
15614
|
+
read: (client) => client.getAgentArtifact(agent.session, agent.slug),
|
|
15615
|
+
version: agent.version
|
|
15616
|
+
};
|
|
15617
|
+
}
|
|
15618
|
+
const ref = parseDispatchRef(address);
|
|
15619
|
+
if (ref?.kind !== "artifact" || ref.version === undefined)
|
|
15620
|
+
return;
|
|
15621
|
+
const { owner, id: slug } = ref;
|
|
15622
|
+
return owner.kind === "issue" ? {
|
|
15623
|
+
key: `${owner.issue}/${slug}`,
|
|
15624
|
+
read: (client) => client.getIssueArtifact(owner.issue, slug),
|
|
15625
|
+
version: ref.version
|
|
15626
|
+
} : {
|
|
15627
|
+
key: `${owner.project}/${slug}`,
|
|
15628
|
+
read: (client) => client.getProjectArtifact(owner.project, slug),
|
|
15629
|
+
version: ref.version
|
|
15630
|
+
};
|
|
15631
|
+
}
|
|
15632
|
+
var SESSIONS_SHOWN_MAX = 64;
|
|
15633
|
+
var picturesShown = new Map;
|
|
15634
|
+
function shownPictures(sessionId) {
|
|
15635
|
+
const shown = picturesShown.get(sessionId) ?? new Set;
|
|
15636
|
+
picturesShown.delete(sessionId);
|
|
15637
|
+
picturesShown.set(sessionId, shown);
|
|
15638
|
+
const oldest = picturesShown.keys().next();
|
|
15639
|
+
if (picturesShown.size > SESSIONS_SHOWN_MAX && oldest.done !== true) {
|
|
15640
|
+
picturesShown.delete(oldest.value);
|
|
15641
|
+
}
|
|
15642
|
+
return shown;
|
|
15643
|
+
}
|
|
15644
|
+
async function readPictures(client, addresses, shown) {
|
|
15645
|
+
if (addresses.length === 0)
|
|
15646
|
+
return { lines: [], images: [], shown: [] };
|
|
15647
|
+
const artifacts = new Map;
|
|
15648
|
+
const images = [];
|
|
15649
|
+
const showing = [];
|
|
15650
|
+
const lines = ["Pictures:"];
|
|
15651
|
+
let shownBytes = 0;
|
|
15652
|
+
const overBudget = `past this read's ${PICTURES_SHOWN_MAX_BYTES / 1024 / 1024} MiB of pictures; dispatch_doc_read shows it`;
|
|
15653
|
+
for (const address of addresses) {
|
|
15654
|
+
const skip = (reason) => lines.push(`- not shown: ${address} (${reason})`);
|
|
15655
|
+
if (shown?.has(address)) {
|
|
15656
|
+
skip("shown earlier this session; dispatch_doc_read shows it again");
|
|
15657
|
+
continue;
|
|
15658
|
+
}
|
|
15659
|
+
if (images.length === PICTURES_SHOWN_MAX) {
|
|
15660
|
+
skip(`past this read's ${PICTURES_SHOWN_MAX} pictures; dispatch_doc_read shows it`);
|
|
15661
|
+
continue;
|
|
15662
|
+
}
|
|
15663
|
+
const target = pictureTarget(address);
|
|
15664
|
+
if (target === undefined) {
|
|
15665
|
+
skip("names no versioned Dispatch artifact");
|
|
15666
|
+
continue;
|
|
15667
|
+
}
|
|
15668
|
+
try {
|
|
15669
|
+
let read = artifacts.get(target.key);
|
|
15670
|
+
if (read === undefined) {
|
|
15671
|
+
read = target.read(client);
|
|
15672
|
+
artifacts.set(target.key, read);
|
|
15673
|
+
}
|
|
15674
|
+
const artifact = await read;
|
|
15675
|
+
const version2 = artifact.versions.find((candidate) => candidate.number === target.version);
|
|
15676
|
+
if (version2 === undefined) {
|
|
15677
|
+
skip(`${artifact.name} has no version ${target.version}`);
|
|
15678
|
+
continue;
|
|
15679
|
+
}
|
|
15680
|
+
if (artifact.kind !== "image") {
|
|
15681
|
+
skip(`${artifact.name} is a ${artifact.kind}, not a picture`);
|
|
15682
|
+
continue;
|
|
15683
|
+
}
|
|
15684
|
+
const mime = version2.mime?.split(";")[0]?.trim();
|
|
15685
|
+
if (mime !== undefined && !isPictureType(mime)) {
|
|
15686
|
+
skip(`${artifact.name} is ${mime}, not ${PICTURE_TYPE_NAMES} a model is shown`);
|
|
15687
|
+
continue;
|
|
15688
|
+
}
|
|
15689
|
+
if (version2.size !== undefined && version2.size > PICTURE_SHOWN_MAX_BYTES) {
|
|
15690
|
+
skip(oversizePictureText(artifact.name, version2.size));
|
|
15691
|
+
continue;
|
|
15692
|
+
}
|
|
15693
|
+
if (version2.size !== undefined && shownBytes + version2.size > PICTURES_SHOWN_MAX_BYTES) {
|
|
15694
|
+
skip(overBudget);
|
|
15695
|
+
continue;
|
|
15696
|
+
}
|
|
15697
|
+
const file2 = await client.fileVersion(artifact.id, target.version);
|
|
15698
|
+
const image = toolImage(file2.bytes);
|
|
15699
|
+
if (image === undefined) {
|
|
15700
|
+
skip(sniffPictureType(file2.bytes) === undefined ? `${artifact.name} is not ${PICTURE_TYPE_NAMES} a model is shown` : oversizePictureText(artifact.name, file2.bytes.length));
|
|
15701
|
+
continue;
|
|
15702
|
+
}
|
|
15703
|
+
if (shownBytes + file2.bytes.length > PICTURES_SHOWN_MAX_BYTES) {
|
|
15704
|
+
skip(overBudget);
|
|
15705
|
+
continue;
|
|
15706
|
+
}
|
|
15707
|
+
shownBytes += file2.bytes.length;
|
|
15708
|
+
images.push(image);
|
|
15709
|
+
showing.push(address);
|
|
15710
|
+
lines.push(shownPictureLine(images.length, address, artifact.name, image.mimeType, file2.bytes.length));
|
|
15711
|
+
} catch (error48) {
|
|
15712
|
+
skip(`unavailable: ${messageFor(error48)}`);
|
|
15713
|
+
}
|
|
15714
|
+
}
|
|
15715
|
+
return { lines, images, shown: showing };
|
|
15716
|
+
}
|
|
15717
|
+
|
|
15718
|
+
// ../envoy-client/src/search-answer.ts
|
|
15719
|
+
function pageSummaryText(offset, count, total) {
|
|
15720
|
+
return `showing ${offset + 1}-${offset + count} of ${total}`;
|
|
15721
|
+
}
|
|
15722
|
+
function searchResultLine(result, baseUrl) {
|
|
15723
|
+
const href = new URL(result.href, baseUrl).toString();
|
|
15724
|
+
const { owner } = result;
|
|
15725
|
+
if (owner.kind === "document") {
|
|
15726
|
+
const reference = dispatchDocumentRef(owner.project, owner.slug);
|
|
15727
|
+
return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
|
|
15728
|
+
}
|
|
15729
|
+
const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
|
|
15730
|
+
const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
|
|
15731
|
+
return `${label}: ${snippetText(result.snippet)} -> ${href}`;
|
|
15732
|
+
}
|
|
15733
|
+
function searchAnswer(search, query, offset, configUrl) {
|
|
15734
|
+
const results = search.results;
|
|
15735
|
+
const count = results.length;
|
|
15736
|
+
const lines = results.map((result) => searchResultLine(result, configUrl));
|
|
15737
|
+
const noun = count === 1 ? "result" : "results";
|
|
15738
|
+
const noResults = `No results for "${query}".`;
|
|
15739
|
+
if (typeof search.total !== "number") {
|
|
15740
|
+
if (offset !== undefined && offset > 0) {
|
|
15741
|
+
throw new Error(`Dispatch answered without a total: it predates search paging and ignored offset ${offset}, so this would be its first page again.`);
|
|
15742
|
+
}
|
|
15743
|
+
return {
|
|
15744
|
+
text: count === 0 ? noResults : [`${count} ${noun} for "${query}" (${search.took_ms} ms)`, ...lines].join(`
|
|
15745
|
+
`),
|
|
15746
|
+
details: { query, results }
|
|
15747
|
+
};
|
|
15748
|
+
}
|
|
15749
|
+
const { total, reachable, offset: pageOffset } = search;
|
|
15750
|
+
const end = pageOffset + count;
|
|
15751
|
+
const cut = reachable < total ? `Each kind lists only its best ${SEARCH_KIND_DEPTH} matches, so ${reachable} of the ${total} can be paged to; narrow the query or name a project to reach the rest.` : undefined;
|
|
15752
|
+
const details = {
|
|
15753
|
+
query,
|
|
15754
|
+
results,
|
|
15755
|
+
total,
|
|
15756
|
+
reachable,
|
|
15757
|
+
offset: pageOffset,
|
|
15758
|
+
limit: search.limit
|
|
15759
|
+
};
|
|
15760
|
+
if (count === 0) {
|
|
15761
|
+
return {
|
|
15762
|
+
text: total === 0 ? noResults : [
|
|
15763
|
+
`No results for "${query}" at offset ${pageOffset}: it matches ${total}, and the pages reach the first ${reachable}.`,
|
|
15764
|
+
...cut === undefined ? [] : [cut]
|
|
15765
|
+
].join(`
|
|
15766
|
+
`),
|
|
15767
|
+
details
|
|
15768
|
+
};
|
|
15769
|
+
}
|
|
15770
|
+
const showing = pageOffset === 0 && count === total ? "" : `${pageSummaryText(pageOffset, count, total)}, `;
|
|
15771
|
+
return {
|
|
15772
|
+
text: [
|
|
15773
|
+
`${count} ${noun} for "${query}" (${showing}${search.took_ms} ms)`,
|
|
15774
|
+
...cut === undefined ? [] : [cut],
|
|
15775
|
+
...lines,
|
|
15776
|
+
...end < reachable ? [`Next page: offset ${end}.`] : []
|
|
15777
|
+
].join(`
|
|
15778
|
+
`),
|
|
15779
|
+
details
|
|
15780
|
+
};
|
|
15781
|
+
}
|
|
15782
|
+
|
|
15400
15783
|
// ../envoy-client/src/dispatch-execute.ts
|
|
15401
15784
|
function documentTopic(artifact) {
|
|
15402
15785
|
return documentTopicOf(artifact.project, artifact.slug);
|
|
@@ -15624,59 +16007,11 @@ function askQuestionWithRef(args) {
|
|
|
15624
16007
|
function askQuestionProblem(withRef) {
|
|
15625
16008
|
return withRef.length > ASK_QUESTION_MAX ? `question plus ref ${overCapMessage(withRef.length, ASK_QUESTION_MAX)}; shorten the question or drop the ref` : undefined;
|
|
15626
16009
|
}
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
15631
|
-
|
|
15632
|
-
return null;
|
|
15633
|
-
}
|
|
15634
|
-
if (targetKind === undefined) {
|
|
15635
|
-
return {
|
|
15636
|
-
owner: { kind: "project", project },
|
|
15637
|
-
kind: "artifact",
|
|
15638
|
-
id: artifact2,
|
|
15639
|
-
...version3 === undefined ? {} : { version: Number(version3) }
|
|
15640
|
-
};
|
|
15641
|
-
}
|
|
15642
|
-
if (targetID === undefined || targetKind !== "ask" && targetKind !== "comment")
|
|
15643
|
-
return null;
|
|
15644
|
-
return {
|
|
15645
|
-
owner: { kind: "project", project },
|
|
15646
|
-
kind: targetKind,
|
|
15647
|
-
id: targetID,
|
|
15648
|
-
artifact: artifact2
|
|
15649
|
-
};
|
|
15650
|
-
}
|
|
15651
|
-
const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
|
|
15652
|
-
if (!issueReference)
|
|
15653
|
-
return null;
|
|
15654
|
-
const [, issue2, spec, log, children, artifact, version2, ask, comment, message] = issueReference;
|
|
15655
|
-
if (!issue2 || version2 !== undefined && Number(version2) < 1)
|
|
15656
|
-
return null;
|
|
15657
|
-
const owner = { kind: "issue", issue: issue2 };
|
|
15658
|
-
if (spec)
|
|
15659
|
-
return { owner, kind: "spec", id: spec };
|
|
15660
|
-
if (log)
|
|
15661
|
-
return { owner, kind: "log", id: log };
|
|
15662
|
-
if (children)
|
|
15663
|
-
return { owner, kind: "children", id: children };
|
|
15664
|
-
if (artifact) {
|
|
15665
|
-
return {
|
|
15666
|
-
owner,
|
|
15667
|
-
kind: "artifact",
|
|
15668
|
-
id: artifact,
|
|
15669
|
-
...version2 === undefined ? {} : { version: Number(version2) }
|
|
15670
|
-
};
|
|
15671
|
-
}
|
|
15672
|
-
if (ask)
|
|
15673
|
-
return { owner, kind: "ask", id: ask };
|
|
15674
|
-
if (comment)
|
|
15675
|
-
return { owner, kind: "comment", id: comment };
|
|
15676
|
-
if (message)
|
|
15677
|
-
return { owner, kind: "message", id: message };
|
|
15678
|
-
return { owner, kind: "issue", id: issue2 };
|
|
15679
|
-
}
|
|
16010
|
+
var pictureSendingTools = {
|
|
16011
|
+
dispatch_ask: true,
|
|
16012
|
+
dispatch_comment: true,
|
|
16013
|
+
dispatch_message: true
|
|
16014
|
+
};
|
|
15680
16015
|
function refTarget(ref, kind, id) {
|
|
15681
16016
|
const ownerRef = ref.owner.kind === "issue" ? dispatchIssueRef(ref.owner.issue) : dispatchDocumentRef(ref.owner.project, `${ref.artifact}`);
|
|
15682
16017
|
return dispatchChildRef(ownerRef, kind, id);
|
|
@@ -15856,6 +16191,15 @@ function dispatchRefFromUrl(value, serverUrl) {
|
|
|
15856
16191
|
return `dispatch://${key}/message/${message}`;
|
|
15857
16192
|
return `dispatch://${key}`;
|
|
15858
16193
|
}
|
|
16194
|
+
const agentArtifactPage = url2.pathname.match(/^\/agents\/([^/]+)\/artifacts\/([^/]+)\/?$/);
|
|
16195
|
+
if (agentArtifactPage) {
|
|
16196
|
+
const [, session, slug2] = agentArtifactPage;
|
|
16197
|
+
try {
|
|
16198
|
+
return `dispatch://agent/${decodeURIComponent(session ?? "")}/artifact/${decodeURIComponent(slug2 ?? "")}${versionOf("v")}`;
|
|
16199
|
+
} catch {
|
|
16200
|
+
return;
|
|
16201
|
+
}
|
|
16202
|
+
}
|
|
15859
16203
|
const documentPage = url2.pathname.match(/^\/projects\/([A-Z][A-Z0-9]{1,9})\/documents\/([^/]+)\/?$/);
|
|
15860
16204
|
if (!documentPage)
|
|
15861
16205
|
return;
|
|
@@ -15866,7 +16210,7 @@ function dispatchRefFromUrl(value, serverUrl) {
|
|
|
15866
16210
|
return;
|
|
15867
16211
|
return item === undefined ? document : `${document}/${item.kind}/${item.id}`;
|
|
15868
16212
|
}
|
|
15869
|
-
var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>,
|
|
16213
|
+
var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for dispatch_doc_read); a dashboard URL on this Dispatch server is accepted too";
|
|
15870
16214
|
var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
|
|
15871
16215
|
var toolSchemas = new Map;
|
|
15872
16216
|
function toolSchema(tool) {
|
|
@@ -15890,6 +16234,16 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
|
|
|
15890
16234
|
const refText = dispatchRefFromUrl(refArgument, serverUrl) ?? refArgument;
|
|
15891
16235
|
if (refText !== refArgument)
|
|
15892
16236
|
args = { ...input, ref: refText };
|
|
16237
|
+
const agentArtifact = parseAgentArtifactRef(refText);
|
|
16238
|
+
if (agentArtifact !== null) {
|
|
16239
|
+
if (tool !== "dispatch_doc_read") {
|
|
16240
|
+
problems.push(`ref ${refText} names a picture in a conversation on the Agents page; only dispatch_doc_read reads one`);
|
|
16241
|
+
}
|
|
16242
|
+
if (args.issue !== undefined || args.project !== undefined || args.artifact !== undefined) {
|
|
16243
|
+
problems.push("a dispatch://agent/... ref names its upload alone: name no issue, project, or artifact with it");
|
|
16244
|
+
}
|
|
16245
|
+
return { args, ref: null, owner: null, agentArtifact };
|
|
16246
|
+
}
|
|
15893
16247
|
ref = parseDispatchRef(refText);
|
|
15894
16248
|
if (ref === null) {
|
|
15895
16249
|
problems.push(tool === "dispatch_ask" && !refText.startsWith("dispatch://") ? "ref must be a dispatch:// reference" : refGrammarProblem);
|
|
@@ -16232,6 +16586,7 @@ function eventHead(event) {
|
|
|
16232
16586
|
case "ask.answered":
|
|
16233
16587
|
return `${textHead(event.payload.question)} -> ${textHead(askAnswerText(event.payload.answer))}`;
|
|
16234
16588
|
case "comment.created":
|
|
16589
|
+
case "comment.answered":
|
|
16235
16590
|
case "comment.anchor_refreshed":
|
|
16236
16591
|
case "comment.edited":
|
|
16237
16592
|
case "comment.resolved":
|
|
@@ -16417,20 +16772,42 @@ async function blockAsks(client, resolved, state) {
|
|
|
16417
16772
|
const asks = await (resolved.issue === undefined ? client.getArtifactAsks(resolved.artifact.id, state) : client.listIssueAsks(resolved.issue.key, state));
|
|
16418
16773
|
return asks.filter((ask) => typeof ask.block_id === "string" && ask.block_artifact?.id === resolved.artifact.id);
|
|
16419
16774
|
}
|
|
16420
|
-
async function readUploadedFile(client,
|
|
16421
|
-
const { artifact } = resolved;
|
|
16775
|
+
async function readUploadedFile(client, artifact, details, requested, owner, shown) {
|
|
16422
16776
|
const latest = Math.max(0, ...artifact.versions.map((version2) => version2.number));
|
|
16423
16777
|
const number4 = requested ?? latest;
|
|
16424
|
-
const file2 = await client.fileVersion(artifact.id, number4);
|
|
16425
16778
|
const of = number4 === latest ? "" : ` of ${latest}`;
|
|
16779
|
+
const bytesRoute = `GET /api/v1/artifacts/${artifact.id}/versions/${number4} serves its bytes.`;
|
|
16780
|
+
const tooLarge = (mime, bytes) => ({
|
|
16781
|
+
text: `${oversizePictureText(artifact.name, bytes)}, so dispatch_doc_read cannot show this ` + `uploaded ${mime} picture (version ${number4}${of}). ${bytesRoute}`,
|
|
16782
|
+
details
|
|
16783
|
+
});
|
|
16784
|
+
const stated = artifact.versions.find((version2) => version2.number === number4);
|
|
16785
|
+
if (artifact.kind === "image" && stated?.size !== undefined && stated.size > PICTURE_SHOWN_MAX_BYTES) {
|
|
16786
|
+
return tooLarge(stated.mime ?? "image", stated.size);
|
|
16787
|
+
}
|
|
16788
|
+
const file2 = await client.fileVersion(artifact.id, number4);
|
|
16426
16789
|
const size = `${file2.bytes.length.toLocaleString("en-US")} bytes`;
|
|
16427
|
-
|
|
16790
|
+
if (artifact.kind === "image") {
|
|
16791
|
+
const image = toolImage(file2.bytes);
|
|
16792
|
+
if (image !== undefined) {
|
|
16793
|
+
const address = `dispatch://${owner}/artifact/${artifact.slug}@v${number4}`;
|
|
16794
|
+
shown?.add(address);
|
|
16795
|
+
return {
|
|
16796
|
+
text: `Picture ${artifact.name}: ${image.mimeType}, version ${number4}${of}, ${size}.
|
|
16797
|
+
` + shownPictureLine(1, address, artifact.name, image.mimeType, file2.bytes.length),
|
|
16798
|
+
details,
|
|
16799
|
+
images: [image]
|
|
16800
|
+
};
|
|
16801
|
+
}
|
|
16802
|
+
if (sniffPictureType(file2.bytes) !== undefined)
|
|
16803
|
+
return tooLarge(file2.mime, file2.bytes.length);
|
|
16804
|
+
}
|
|
16428
16805
|
let text;
|
|
16429
16806
|
try {
|
|
16430
16807
|
text = new TextDecoder("utf-8", { fatal: true }).decode(file2.bytes);
|
|
16431
16808
|
} catch {
|
|
16432
16809
|
return {
|
|
16433
|
-
text: `${artifact.name} is an uploaded ${file2.mime} file (version ${number4}${of}, ${size}) that is not ` + `UTF-8 text, so dispatch_doc_read cannot show it.
|
|
16810
|
+
text: `${artifact.name} is an uploaded ${file2.mime} file (version ${number4}${of}, ${size}) that is not ` + `UTF-8 text, so dispatch_doc_read cannot show it. ${bytesRoute}`,
|
|
16434
16811
|
details
|
|
16435
16812
|
};
|
|
16436
16813
|
}
|
|
@@ -16571,6 +16948,7 @@ async function executeDispatchTool(input) {
|
|
|
16571
16948
|
problems.push(...formatZodIssues(issues, schema));
|
|
16572
16949
|
}
|
|
16573
16950
|
problems.push(...argumentProblems(input.tool, ownerArguments.args));
|
|
16951
|
+
const pictures = pictureSendingTools[input.tool] === true ? await localPictures(ownerArguments.args.images, input.cwd, problems) : [];
|
|
16574
16952
|
if (problems.length > 0)
|
|
16575
16953
|
throw new ToolInputError(input.tool, problems);
|
|
16576
16954
|
const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
@@ -16622,6 +17000,17 @@ async function executeDispatchTool(input) {
|
|
|
16622
17000
|
throw new Error("issue or project is required");
|
|
16623
17001
|
return owner;
|
|
16624
17002
|
};
|
|
17003
|
+
const postWithPictures = (text, limit, owner2, upload) => textWithPictures(input.tool, text, pictures, limit, owner2, upload, actor);
|
|
17004
|
+
const writePictures = (resolved, text, limit) => {
|
|
17005
|
+
if (resolved?.owner.kind === "project") {
|
|
17006
|
+
const { project } = resolved.owner;
|
|
17007
|
+
return postWithPictures(text, limit, project, (upload) => client.projectArtifact(project, upload));
|
|
17008
|
+
}
|
|
17009
|
+
const issueKey = issue2();
|
|
17010
|
+
return postWithPictures(text, limit, issueKey, (upload) => client.artifact(issueKey, upload));
|
|
17011
|
+
};
|
|
17012
|
+
const hostSession = input.sessionId?.trim();
|
|
17013
|
+
const shown = hostSession ? shownPictures(hostSession) : undefined;
|
|
16625
17014
|
switch (input.tool) {
|
|
16626
17015
|
case "dispatch_issue": {
|
|
16627
17016
|
const project = stringArg(args, "project");
|
|
@@ -16924,8 +17313,15 @@ async function executeDispatchTool(input) {
|
|
|
16924
17313
|
const multiple = optionalBoolean(args, "multiple");
|
|
16925
17314
|
const urgency = askUrgency(args);
|
|
16926
17315
|
const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
|
|
17316
|
+
const withRef = askQuestionWithRef(args);
|
|
17317
|
+
const refAppended = withRef !== stringArg(args, "question");
|
|
17318
|
+
const question = await writePictures(resolved, withRef, {
|
|
17319
|
+
cap: ASK_QUESTION_MAX,
|
|
17320
|
+
text: refAppended ? "question plus ref" : "question",
|
|
17321
|
+
shorten: refAppended ? "shorten the question, drop the ref" : "shorten the question"
|
|
17322
|
+
});
|
|
16927
17323
|
const askInput = {
|
|
16928
|
-
question
|
|
17324
|
+
question,
|
|
16929
17325
|
...Array.isArray(options) ? { options } : {},
|
|
16930
17326
|
...multiple === undefined ? {} : { multiple },
|
|
16931
17327
|
...urgency === undefined ? {} : { urgency },
|
|
@@ -16983,8 +17379,13 @@ ${followsAsk(askOwner)}`,
|
|
|
16983
17379
|
]);
|
|
16984
17380
|
}
|
|
16985
17381
|
const requestedTurn = optionalString(args, "turn");
|
|
17382
|
+
const body = await writePictures(resolved, stringArg(args, "body"), {
|
|
17383
|
+
cap: DISPATCH_BODY_MAX,
|
|
17384
|
+
text: "body",
|
|
17385
|
+
shorten: "shorten the body"
|
|
17386
|
+
});
|
|
16986
17387
|
const commentInput = {
|
|
16987
|
-
body
|
|
17388
|
+
body,
|
|
16988
17389
|
...anchored === undefined ? {} : { anchor: anchored },
|
|
16989
17390
|
...replyTo === undefined ? {} : { reply_to: replyTo },
|
|
16990
17391
|
...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
|
|
@@ -17044,16 +17445,29 @@ ${followsAsk(askOwner)}`,
|
|
|
17044
17445
|
}
|
|
17045
17446
|
case "dispatch_message": {
|
|
17046
17447
|
const inReplyTo = messageInReplyTo(args);
|
|
17047
|
-
const
|
|
17448
|
+
const written = stringArg(args, "body");
|
|
17449
|
+
const bodyLimit = {
|
|
17450
|
+
cap: DISPATCH_BODY_MAX,
|
|
17451
|
+
text: "body",
|
|
17452
|
+
shorten: "shorten the body"
|
|
17453
|
+
};
|
|
17048
17454
|
if (owner === null && inReplyTo !== undefined) {
|
|
17049
|
-
|
|
17050
|
-
if (
|
|
17455
|
+
let body2 = written;
|
|
17456
|
+
if (pictures.length > 0) {
|
|
17457
|
+
const sessionId = input.sessionId?.trim();
|
|
17458
|
+
if (!sessionId) {
|
|
17459
|
+
throw new Error("host session id is required to send pictures in a direct-message reply");
|
|
17460
|
+
}
|
|
17461
|
+
body2 = await postWithPictures(written, bodyLimit, `agent/${sessionId}`, (upload) => client.agentArtifact(sessionId, upload));
|
|
17462
|
+
}
|
|
17463
|
+
const reply = await client.messageReply(inReplyTo, { body: body2, attempt: 1, actor }, { followUp: true });
|
|
17464
|
+
if (reply.duplicate === true && reply.body === body2) {
|
|
17051
17465
|
return {
|
|
17052
17466
|
text: `Dispatch already has this exact text in the conversation (message ${reply.id}); ` + "nothing new was posted. Send different text if you have more to say.",
|
|
17053
17467
|
details: { message: reply.id, in_reply_to: inReplyTo, posted: false, duplicate: true }
|
|
17054
17468
|
};
|
|
17055
17469
|
}
|
|
17056
|
-
if (reply.body !==
|
|
17470
|
+
if (reply.body !== body2) {
|
|
17057
17471
|
return {
|
|
17058
17472
|
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.",
|
|
17059
17473
|
details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
|
|
@@ -17073,6 +17487,7 @@ ${followsAsk(askOwner)}`,
|
|
|
17073
17487
|
};
|
|
17074
17488
|
}
|
|
17075
17489
|
const issueKey = issue2();
|
|
17490
|
+
const body = await postWithPictures(written, bodyLimit, issueKey, (upload) => client.artifact(issueKey, upload));
|
|
17076
17491
|
const message = await client.message(issueKey, {
|
|
17077
17492
|
body,
|
|
17078
17493
|
...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
|
|
@@ -17136,11 +17551,20 @@ ${followsAsk(askOwner)}`,
|
|
|
17136
17551
|
};
|
|
17137
17552
|
}
|
|
17138
17553
|
case "dispatch_doc_read": {
|
|
17554
|
+
const { agentArtifact } = ownerArguments;
|
|
17555
|
+
if (agentArtifact !== undefined) {
|
|
17556
|
+
const artifact = await client.getAgentArtifact(agentArtifact.session, agentArtifact.slug);
|
|
17557
|
+
return readUploadedFile(client, artifact, { session: agentArtifact.session, artifact: artifact.slug }, optionalNumber(args, "version") ?? agentArtifact.version, `agent/${agentArtifact.session}`, shown);
|
|
17558
|
+
}
|
|
17139
17559
|
const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
|
|
17140
17560
|
const resolved = await resolveDocument(documentOwner(), artifactReference);
|
|
17141
17561
|
const version2 = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
|
|
17142
17562
|
if (resolved.artifact.kind === "file" || resolved.artifact.kind === "image") {
|
|
17143
|
-
|
|
17563
|
+
const { artifact } = resolved;
|
|
17564
|
+
return readUploadedFile(client, artifact, resolved.owner.kind === "project" ? {
|
|
17565
|
+
project: artifact.project,
|
|
17566
|
+
document: documentLabel(artifact.project, artifact.slug)
|
|
17567
|
+
} : { issue: resolved.issue?.key }, version2, resolved.owner.kind === "project" ? resolved.owner.project : resolved.owner.issue, shown);
|
|
17144
17568
|
}
|
|
17145
17569
|
const documentPromise = client.docRead(resolved.artifact.id, version2);
|
|
17146
17570
|
const marksPromise = openArtifactMarks(client, resolved);
|
|
@@ -17198,7 +17622,7 @@ ${trailer.join(`
|
|
|
17198
17622
|
const content = optionalString(args, "content");
|
|
17199
17623
|
const artifactInput = content === undefined ? {
|
|
17200
17624
|
name,
|
|
17201
|
-
file: Bun.file(
|
|
17625
|
+
file: Bun.file(resolvePath2(input.cwd, stringArg(args, "path"))),
|
|
17202
17626
|
...summary === undefined ? {} : { summary },
|
|
17203
17627
|
actor
|
|
17204
17628
|
} : {
|
|
@@ -17251,18 +17675,29 @@ ${trailer.join(`
|
|
|
17251
17675
|
}
|
|
17252
17676
|
case "dispatch_read": {
|
|
17253
17677
|
const message = optionalString(args, "message");
|
|
17678
|
+
const picturesOf = (texts) => readPictures(client, picturesNewestFirst(texts), shown);
|
|
17679
|
+
const showImages = (pictures3) => {
|
|
17680
|
+
for (const address of pictures3.shown)
|
|
17681
|
+
shown?.add(address);
|
|
17682
|
+
return pictures3.images.length === 0 ? {} : { images: pictures3.images };
|
|
17683
|
+
};
|
|
17254
17684
|
if (message !== undefined) {
|
|
17255
17685
|
const sessionId = input.sessionId?.trim();
|
|
17256
17686
|
if (!sessionId)
|
|
17257
17687
|
throw new Error("host session id is required for dispatch_read({message})");
|
|
17258
17688
|
const thread = await client.getMessageThread(messageIdOf(message), sessionId);
|
|
17259
17689
|
const issueKey2 = thread.message.issue_key;
|
|
17690
|
+
const [pictures3, graph2] = await Promise.all([
|
|
17691
|
+
picturesOf(datedBodies([thread.message, ...thread.replies])),
|
|
17692
|
+
issueKey2 === null ? [] : graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))
|
|
17693
|
+
]);
|
|
17260
17694
|
return {
|
|
17261
|
-
text: messageSummary(thread,
|
|
17695
|
+
text: messageSummary(thread, [...pictures3.lines, ...graph2]),
|
|
17262
17696
|
details: {
|
|
17263
17697
|
message: thread.message.id,
|
|
17264
17698
|
...issueKey2 === null ? {} : { issue: issueKey2 }
|
|
17265
|
-
}
|
|
17699
|
+
},
|
|
17700
|
+
...showImages(pictures3)
|
|
17266
17701
|
};
|
|
17267
17702
|
}
|
|
17268
17703
|
if (ownerArguments.ref?.kind === "ask") {
|
|
@@ -17270,9 +17705,14 @@ ${trailer.join(`
|
|
|
17270
17705
|
const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveDocument(ref.owner, ref.artifact)).artifact.id, "all"));
|
|
17271
17706
|
const askRead = await client.getAsk(id);
|
|
17272
17707
|
const askRef = refTarget(ref, "ask", id);
|
|
17708
|
+
const [pictures3, graph2] = await Promise.all([
|
|
17709
|
+
picturesOf(askTexts(askRead)),
|
|
17710
|
+
graphSections(client, askRef)
|
|
17711
|
+
]);
|
|
17273
17712
|
return {
|
|
17274
|
-
text: askSummary(askRead,
|
|
17275
|
-
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue }
|
|
17713
|
+
text: askSummary(askRead, [...pictures3.lines, ...graph2]),
|
|
17714
|
+
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue },
|
|
17715
|
+
...showImages(pictures3)
|
|
17276
17716
|
};
|
|
17277
17717
|
}
|
|
17278
17718
|
if (ownerArguments.ref?.kind === "comment") {
|
|
@@ -17280,9 +17720,14 @@ ${trailer.join(`
|
|
|
17280
17720
|
const id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveDocument(ref.owner, ref.artifact)).artifact.id));
|
|
17281
17721
|
const comment = await client.getComment(id);
|
|
17282
17722
|
const commentRef = refTarget(ref, "comment", id);
|
|
17723
|
+
const [pictures3, graph2] = await Promise.all([
|
|
17724
|
+
picturesOf(datedBodies([comment.comment, ...comment.replies])),
|
|
17725
|
+
graphSections(client, commentRef)
|
|
17726
|
+
]);
|
|
17283
17727
|
return {
|
|
17284
|
-
text: commentSummary(comment,
|
|
17285
|
-
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key }
|
|
17728
|
+
text: commentSummary(comment, [...pictures3.lines, ...graph2]),
|
|
17729
|
+
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key },
|
|
17730
|
+
...showImages(pictures3)
|
|
17286
17731
|
};
|
|
17287
17732
|
}
|
|
17288
17733
|
if (ownerArguments.ref?.kind === "message") {
|
|
@@ -17291,9 +17736,14 @@ ${trailer.join(`
|
|
|
17291
17736
|
}
|
|
17292
17737
|
const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
|
|
17293
17738
|
const messageRef = dispatchChildRef(dispatchIssueRef(ownerArguments.ref.owner.issue), "message", ownerArguments.ref.id);
|
|
17739
|
+
const [pictures3, graph2] = await Promise.all([
|
|
17740
|
+
picturesOf(datedBodies([messageRead.message, ...messageRead.replies])),
|
|
17741
|
+
graphSections(client, messageRef)
|
|
17742
|
+
]);
|
|
17294
17743
|
return {
|
|
17295
|
-
text: messageSummary(messageRead,
|
|
17296
|
-
details: { issue: messageRead.message.issue_key }
|
|
17744
|
+
text: messageSummary(messageRead, [...pictures3.lines, ...graph2]),
|
|
17745
|
+
details: { issue: messageRead.message.issue_key },
|
|
17746
|
+
...showImages(pictures3)
|
|
17297
17747
|
};
|
|
17298
17748
|
}
|
|
17299
17749
|
if (documentOwner().kind === "project") {
|
|
@@ -17317,9 +17767,12 @@ ${trailer.join(`
|
|
|
17317
17767
|
const issueKey = issue2();
|
|
17318
17768
|
if (ownerArguments.ref?.kind === "log") {
|
|
17319
17769
|
const read2 = await client.read(issueKey);
|
|
17770
|
+
const pictures3 = await picturesOf(read2.events.flatMap(eventTexts));
|
|
17320
17771
|
return {
|
|
17321
|
-
text: logSummary(read2.issue, read2.events),
|
|
17322
|
-
|
|
17772
|
+
text: [logSummary(read2.issue, read2.events), ...pictures3.lines].join(`
|
|
17773
|
+
`),
|
|
17774
|
+
details: { issue: read2.issue.key },
|
|
17775
|
+
...showImages(pictures3)
|
|
17323
17776
|
};
|
|
17324
17777
|
}
|
|
17325
17778
|
if (ownerArguments.ref?.kind === "children") {
|
|
@@ -17332,14 +17785,19 @@ ${trailer.join(`
|
|
|
17332
17785
|
const readPromise = client.read(issueKey);
|
|
17333
17786
|
const referencesPromise = issueReferencesOrUnavailable(client, issueKey);
|
|
17334
17787
|
const read = await readPromise;
|
|
17335
|
-
const [references, graph, titles] = await Promise.all([
|
|
17788
|
+
const [references, graph, titles, pictures2] = await Promise.all([
|
|
17336
17789
|
referencesPromise,
|
|
17337
17790
|
graphSections(client, dispatchIssueRef(read.issue.key)),
|
|
17338
|
-
liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue))
|
|
17791
|
+
liveSessionTitles(client, holdsSession(read.issue.claim) || routeHeldBySession(read.issue)),
|
|
17792
|
+
picturesOf([
|
|
17793
|
+
...read.issue.open_asks.map((ask) => ({ text: ask.question, at: ask.created_at })),
|
|
17794
|
+
...read.events.flatMap(eventTexts)
|
|
17795
|
+
])
|
|
17339
17796
|
]);
|
|
17340
17797
|
return {
|
|
17341
|
-
text: issueSummary(read.issue, read.events, references, graph, titles),
|
|
17342
|
-
details: { issue: read.issue.key }
|
|
17798
|
+
text: issueSummary(read.issue, read.events, references, [...pictures2.lines, ...graph], titles),
|
|
17799
|
+
details: { issue: read.issue.key },
|
|
17800
|
+
...showImages(pictures2)
|
|
17343
17801
|
};
|
|
17344
17802
|
}
|
|
17345
17803
|
default:
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -416,6 +416,9 @@ It returns live or versioned markdown with open marks. A live read ends with a d
|
|
|
416
416
|
`artifact` reads the issue specification; a project needs `artifact`; and a `dispatch://PROJECT/artifact/<document-ref>`
|
|
417
417
|
ref supplies both, where `document-ref` is the slug (an id or a filename resolves when no document has that slug). A file
|
|
418
418
|
`dispatch_artifact` uploaded reads its text at the latest or named version, or a description when it is not UTF-8 text.
|
|
419
|
+
A picture (PNG, JPEG, GIF or WebP under 3,750,000 bytes, 5 MB of base64) comes back as an image you see, with its name, type, size and version;
|
|
420
|
+
that includes `dispatch://agent/<session id>/artifact/<slug>@vN`, one a person sent you on the Agents page. A session is shown each picture once,
|
|
421
|
+
since the provider refuses a request over 32 MB: later reads and deliveries name it, and `dispatch_doc_read` shows it again (after compaction too).
|
|
419
422
|
|
|
420
423
|
Editing one is [Editing a document](skill://dispatch/references/document-edits.md): the shape of `dispatch_doc_edit`,
|
|
421
424
|
how to quote the text you mean, one `replace` per paragraph, preconditions against a stale edit, and
|
|
@@ -474,6 +477,9 @@ dispatch_message({ issue, body })
|
|
|
474
477
|
It returns `details` `{ issue, message }`. `body` is capped at 2,000 characters. A message is not a decision
|
|
475
478
|
(a decision block, or `dispatch_ask` for a human to-do) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
|
|
476
479
|
|
|
480
|
+
**Pictures.** To show a screenshot inline, pass its path in `images` (`dispatch_message`, `dispatch_comment`, `dispatch_ask`);
|
|
481
|
+
`dispatch_read` hands you the pictures what it shows embeds. Both are in [Documents](skill://dispatch/references/documents.md), "Artifacts".
|
|
482
|
+
|
|
477
483
|
A BTW, Aside or Steer frame, or a message from the Agents page, is answered as
|
|
478
484
|
[Targeted and direct messages](skill://dispatch/references/messages.md) says.
|
|
479
485
|
|
|
@@ -146,6 +146,29 @@ slug also arrives on `artifact.created` events. Dispatch suffixes a slug two doc
|
|
|
146
146
|
filename can be another's slug (`plan v2` takes `plan-v2`, then a document named `plan-v2` takes `plan-v2-2`): a bare
|
|
147
147
|
`artifact` that names both is refused with each one's id, while a `dispatch://` reference's document part is always the slug.
|
|
148
148
|
|
|
149
|
+
A picture meant to be seen inline in a message, comment or ask is not uploaded with `dispatch_artifact`: pass its local
|
|
150
|
+
path in that tool's `images` (PNG, JPEG, GIF or WebP by its bytes, at most 25 MiB each). Each is uploaded to the issue (a
|
|
151
|
+
project document's project; for a reply to a direct message, your own conversation) and appended after a blank line as
|
|
152
|
+
one `` line, about 60 characters that count toward the body or
|
|
153
|
+
question cap; a call the lines would carry over the cap is refused with the count before anything uploads, and again,
|
|
154
|
+
with nothing posted, in the rare case where Dispatch gives an upload a longer address than that (a later version of a
|
|
155
|
+
name the owner already holds, or a slug another file took first). The `@vN` pins the version the upload made (the same
|
|
156
|
+
file name again makes the next version). A plain `dispatch://KEY/artifact/<slug>` reference to an image stays a link
|
|
157
|
+
with a thumbnail.
|
|
158
|
+
|
|
159
|
+
`dispatch_read` hands you the pictures the messages, asks and comments it shows embed, newest first, at most 8 and
|
|
160
|
+
10 MiB per read, and its `Pictures:` section names each one shown, in order, and the rest by reference for
|
|
161
|
+
`dispatch_doc_read`, which shows one picture (at most 3,750,000 bytes, the 5 MB of base64 a model provider takes; a
|
|
162
|
+
larger one or an SVG is described). A picture in a
|
|
163
|
+
conversation on the Agents page belongs to that conversation, `dispatch://agent/<session id>/artifact/<slug>@vN`, and
|
|
164
|
+
`dispatch_doc_read` reads it by that reference alone. On Oh My Pi an Inbox delivery carries its pictures the same way; in
|
|
165
|
+
Claude Code it stays text with the reference.
|
|
166
|
+
|
|
167
|
+
A session is shown each picture once. A later read or delivery that embeds a picture you were already shown names it in
|
|
168
|
+
`Pictures:` as `shown earlier this session` and sends no bytes, since every request carries the session's whole history
|
|
169
|
+
and a model provider refuses one over 32 MB; `dispatch_doc_read` always shows the picture, so ask it for one you need
|
|
170
|
+
again, after compaction too.
|
|
171
|
+
|
|
149
172
|
Documents are CommonMark. A bare `<https://example.com|text>` is a CommonMark autolink and is normalised: the angle brackets are
|
|
150
173
|
dropped and the URL keeps `|text`. A backslash-escaped `\<https://example.com|text>` displays as `<https://example.com|text>` in the
|
|
151
174
|
document but comes back re-escaped (`\<`) from `dispatch_doc_read`. A Slack mrkdwn draft, or any other payload that is not Markdown,
|