@sjawhar/pi-legion-envoy 5.1.1 → 5.2.1
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 +155 -41
- package/dist/legion.js +3 -3
- package/dist/skills/dispatch/SKILL.md +4 -1
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -30052,7 +30052,7 @@ var dispatchToolSpecs = [
|
|
|
30052
30052
|
{
|
|
30053
30053
|
name: "dispatch_issue",
|
|
30054
30054
|
example: { project: "DSP", title: "Native workspace" },
|
|
30055
|
-
description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
30055
|
+
description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
30056
30056
|
arguments: (z2) => ({
|
|
30057
30057
|
project: z2.string().describe("Project key for the new issue."),
|
|
30058
30058
|
title: z2.string().describe("Concise issue title."),
|
|
@@ -30298,7 +30298,7 @@ var dispatchToolSpecs = [
|
|
|
30298
30298
|
name: "dispatch_artifact",
|
|
30299
30299
|
example: { issue: "DSP-1", name: "design.md", content: `# Design
|
|
30300
30300
|
` },
|
|
30301
|
-
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " +
|
|
30301
|
+
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch_doc_edit instead. Exactly one of path or content is required; artifacts are limited to 25 MiB. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
|
|
30302
30302
|
arguments: (z2) => ({
|
|
30303
30303
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
30304
30304
|
project: z2.string().describe("Project key for an unlinked document.").optional(),
|
|
@@ -34979,8 +34979,15 @@ var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
|
34979
34979
|
var OPEN_ASKS_TIMEOUT_MS = 3000;
|
|
34980
34980
|
var LEGION_MANAGED_ENTRY = "legion-managed-session";
|
|
34981
34981
|
var ASK_REMINDER_MESSAGE = "dispatch-ask-reminder";
|
|
34982
|
-
var
|
|
34982
|
+
var ASK_SELF_CHECK_PROMPT = "Your run has just ended. Answer with exactly one word and nothing else: WAITING or PROCEEDING. " + "WAITING \u2014 you stopped because you need a decision, an approval, or information from a human. " + "PROCEEDING \u2014 you finished, you will carry on by yourself, or you are waiting only on tools, " + "subagents, or events.";
|
|
34983
|
+
var WAITING_VERDICT = /^\W*WAITING\b/;
|
|
34984
|
+
var ECHOED_CHOICE = /\bPROCEEDING\b/;
|
|
34985
|
+
var isWaitingVerdict = (reply) => WAITING_VERDICT.test(reply) && !ECHOED_CHOICE.test(reply);
|
|
34986
|
+
var ASK_SELF_CHECK_TIMEOUT_MS = 60000;
|
|
34987
|
+
var ASK_CHECKS_PER_PERIOD = 5;
|
|
34988
|
+
var UNASKED_WAIT_REMINDER = "You just said you are waiting on a human, but you have no open ask in Dispatch, so nobody knows you are waiting. Open it now with dispatch_ask \u2014 or dispatch_request_approval when what you need is approval of a document \u2014 naming exactly what you need and from whom. Do not reply just to acknowledge this reminder.";
|
|
34983
34989
|
var ASK_OPENING_TOOLS = ["dispatch_ask", "dispatch_request_approval"];
|
|
34990
|
+
var DISPATCH_TOOL_PREFIX = "dispatch_";
|
|
34984
34991
|
function isLegionManagedEntry(entry) {
|
|
34985
34992
|
if (typeof entry !== "object" || entry === null)
|
|
34986
34993
|
return false;
|
|
@@ -35051,11 +35058,23 @@ function envoyExtension(pi) {
|
|
|
35051
35058
|
session_id: "",
|
|
35052
35059
|
period: 0,
|
|
35053
35060
|
baseline_as_of: null,
|
|
35054
|
-
|
|
35055
|
-
|
|
35061
|
+
check_due: false,
|
|
35062
|
+
checks: 0
|
|
35056
35063
|
};
|
|
35057
35064
|
let awarenessGeneration = 0;
|
|
35058
35065
|
let askCheckInFlight = false;
|
|
35066
|
+
let settledDuringCheck;
|
|
35067
|
+
const takeSettledDuringCheck = () => {
|
|
35068
|
+
const settle = settledDuringCheck;
|
|
35069
|
+
settledDuringCheck = undefined;
|
|
35070
|
+
return settle;
|
|
35071
|
+
};
|
|
35072
|
+
let askCheckAbort;
|
|
35073
|
+
const abortSelfCheck = (reason) => {
|
|
35074
|
+
askCheckAbort?.abort(new Error(reason));
|
|
35075
|
+
askCheckAbort = undefined;
|
|
35076
|
+
};
|
|
35077
|
+
let runSeq = 0;
|
|
35059
35078
|
let legionManagedTranscript = false;
|
|
35060
35079
|
const legionManaged = (id) => legionManagedTranscript || legionRoleClaimBridge().managedSessions.has(id);
|
|
35061
35080
|
const availabilityWarningSessionIDs = new Set;
|
|
@@ -35066,6 +35085,19 @@ function envoyExtension(pi) {
|
|
|
35066
35085
|
availabilityWarningSessionIDs.add(sessionID2);
|
|
35067
35086
|
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error48)}); the stop-time ask reminder is off until it recovers`, "warning");
|
|
35068
35087
|
};
|
|
35088
|
+
const askEphemeral = pi.askEphemeral;
|
|
35089
|
+
const overriddenTimeout = Number(process.env.ENVOY_SELF_CHECK_TIMEOUT_MS);
|
|
35090
|
+
const selfCheckTimeoutMs = Number.isFinite(overriddenTimeout) && overriddenTimeout > 0 ? overriddenTimeout : ASK_SELF_CHECK_TIMEOUT_MS;
|
|
35091
|
+
const selfCheckWarningSessionIDs = new Set;
|
|
35092
|
+
const logSelfCheckFailure = (failedSessionID, error48) => {
|
|
35093
|
+
if (selfCheckWarningSessionIDs.has(failedSessionID))
|
|
35094
|
+
return;
|
|
35095
|
+
selfCheckWarningSessionIDs.add(failedSessionID);
|
|
35096
|
+
logger.warn("envoy: run-end waiting self-check failed; no reminder was sent", {
|
|
35097
|
+
sessionID: failedSessionID,
|
|
35098
|
+
error: messageFor(error48)
|
|
35099
|
+
});
|
|
35100
|
+
};
|
|
35069
35101
|
const queryOpenAsks = async (requestedSessionID, since) => {
|
|
35070
35102
|
const config2 = activeDispatchConfig();
|
|
35071
35103
|
if (config2 === null)
|
|
@@ -35086,15 +35118,18 @@ function envoyExtension(pi) {
|
|
|
35086
35118
|
sessionID = context.sessionManager.getSessionId();
|
|
35087
35119
|
activeSessionContext = context;
|
|
35088
35120
|
const branch = context.sessionManager.getBranch?.() ?? [];
|
|
35089
|
-
askAwareness
|
|
35090
|
-
session_id
|
|
35091
|
-
|
|
35092
|
-
|
|
35093
|
-
|
|
35094
|
-
|
|
35095
|
-
|
|
35121
|
+
if (askAwareness.session_id !== sessionID) {
|
|
35122
|
+
abortSelfCheck(`session changed from ${askAwareness.session_id || "none"} to ${sessionID}`);
|
|
35123
|
+
askAwareness = {
|
|
35124
|
+
session_id: sessionID,
|
|
35125
|
+
period: 0,
|
|
35126
|
+
baseline_as_of: null,
|
|
35127
|
+
check_due: false,
|
|
35128
|
+
checks: 0
|
|
35129
|
+
};
|
|
35130
|
+
awarenessGeneration++;
|
|
35131
|
+
}
|
|
35096
35132
|
legionManagedTranscript = branch.some(isLegionManagedEntry);
|
|
35097
|
-
awarenessGeneration++;
|
|
35098
35133
|
};
|
|
35099
35134
|
pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
|
|
35100
35135
|
registerEnvoyMessageRenderer(pi);
|
|
@@ -35610,69 +35645,148 @@ function envoyExtension(pi) {
|
|
|
35610
35645
|
registerEnvoyWhoamiCommand(pi, () => sessionID);
|
|
35611
35646
|
pi.on("before_agent_start", async (event, context) => {
|
|
35612
35647
|
const id = context.sessionManager.getSessionId();
|
|
35613
|
-
if (id === "" || event.prompt.trim() === "" || !context.hasUI || legionManaged(id)) {
|
|
35648
|
+
if (id === "" || event.prompt.trim() === "" || !context.hasUI || askEphemeral === undefined || legionManaged(id)) {
|
|
35614
35649
|
return;
|
|
35615
35650
|
}
|
|
35616
35651
|
if (await isSubagent(context))
|
|
35617
35652
|
return;
|
|
35653
|
+
awarenessGeneration++;
|
|
35654
|
+
abortSelfCheck("a new user turn superseded the self-check");
|
|
35618
35655
|
try {
|
|
35619
35656
|
const open = await queryOpenAsks(id);
|
|
35620
35657
|
if (open !== null)
|
|
35621
|
-
armAskAwareness(id,
|
|
35658
|
+
armAskAwareness(id, open.snapshot.as_of);
|
|
35622
35659
|
} catch (error48) {
|
|
35623
35660
|
warnAskAvailability(context, error48);
|
|
35624
35661
|
}
|
|
35625
35662
|
return;
|
|
35626
35663
|
});
|
|
35627
|
-
|
|
35628
|
-
|
|
35629
|
-
|
|
35630
|
-
|
|
35664
|
+
pi.on("agent_start", async () => {
|
|
35665
|
+
runSeq += 1;
|
|
35666
|
+
});
|
|
35667
|
+
function armAskAwareness(id, asOf) {
|
|
35631
35668
|
askAwareness = {
|
|
35632
35669
|
session_id: id,
|
|
35633
35670
|
period: (askAwareness.session_id === id ? askAwareness.period : 0) + 1,
|
|
35634
35671
|
baseline_as_of: asOf,
|
|
35635
|
-
|
|
35636
|
-
|
|
35672
|
+
check_due: true,
|
|
35673
|
+
checks: 0
|
|
35637
35674
|
};
|
|
35638
35675
|
}
|
|
35639
35676
|
pi.on("agent_end", async (event, context) => {
|
|
35640
35677
|
const id = context.sessionManager.getSessionId();
|
|
35641
35678
|
const lastReply = event.messages?.findLast((message) => message.role === "assistant");
|
|
35642
|
-
if (event.willContinue === true || lastReply?.stopReason !== "stop" ||
|
|
35679
|
+
if (event.willContinue === true || lastReply?.stopReason !== "stop" || !context.hasUI || id === "" || !checkOwed(id)) {
|
|
35680
|
+
return;
|
|
35681
|
+
}
|
|
35682
|
+
if (askCheckInFlight) {
|
|
35683
|
+
settledDuringCheck = { context, seenRun: runSeq };
|
|
35643
35684
|
return;
|
|
35644
35685
|
}
|
|
35645
|
-
const period = askAwareness.period;
|
|
35646
|
-
const generation = awarenessGeneration;
|
|
35647
35686
|
askCheckInFlight = true;
|
|
35648
35687
|
try {
|
|
35649
|
-
let
|
|
35650
|
-
|
|
35651
|
-
|
|
35652
|
-
|
|
35653
|
-
if (
|
|
35654
|
-
|
|
35655
|
-
|
|
35656
|
-
}
|
|
35657
|
-
if (open === null || shuttingDown || generation !== awarenessGeneration || sessionID !== id || context.sessionManager.getSessionId() !== id || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period !== period || askAwareness.fired || askAwareness.saw_ask) {
|
|
35658
|
-
return;
|
|
35688
|
+
let settle = { context, seenRun: runSeq };
|
|
35689
|
+
while (settle !== undefined) {
|
|
35690
|
+
await checkAtSettle(id, settle);
|
|
35691
|
+
settle = takeSettledDuringCheck();
|
|
35692
|
+
if (settle?.context.sessionManager.getSessionId() !== id || settle.seenRun !== runSeq || !checkOwed(id)) {
|
|
35693
|
+
settle = undefined;
|
|
35694
|
+
}
|
|
35659
35695
|
}
|
|
35660
|
-
if (open.snapshot.count > 0 || open.snapshot.opened_since)
|
|
35661
|
-
return;
|
|
35662
|
-
askAwareness = { ...askAwareness, fired: true };
|
|
35663
|
-
pi.sendMessage({ customType: ASK_REMINDER_MESSAGE, content: OPEN_ASKS_REMINDER, display: false }, { deliverAs: "steer", triggerTurn: true });
|
|
35664
35696
|
} finally {
|
|
35665
35697
|
askCheckInFlight = false;
|
|
35698
|
+
settledDuringCheck = undefined;
|
|
35666
35699
|
}
|
|
35667
35700
|
});
|
|
35701
|
+
function checkOwed(id) {
|
|
35702
|
+
return !(shuttingDown || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period === 0 || !askAwareness.check_due || askAwareness.checks >= ASK_CHECKS_PER_PERIOD || askAwareness.baseline_as_of === null || askEphemeral === undefined);
|
|
35703
|
+
}
|
|
35704
|
+
async function checkAtSettle(id, settle) {
|
|
35705
|
+
const { context, seenRun } = settle;
|
|
35706
|
+
const period = askAwareness.period;
|
|
35707
|
+
const generation = awarenessGeneration;
|
|
35708
|
+
const baseline = askAwareness.baseline_as_of;
|
|
35709
|
+
if (baseline === null || askEphemeral === undefined)
|
|
35710
|
+
return;
|
|
35711
|
+
const stale = () => shuttingDown || generation !== awarenessGeneration || context.sessionManager.getSessionId() !== id || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period !== period || !askAwareness.check_due;
|
|
35712
|
+
let open;
|
|
35713
|
+
try {
|
|
35714
|
+
open = await queryOpenAsks(id, baseline);
|
|
35715
|
+
} catch (error48) {
|
|
35716
|
+
if (generation === awarenessGeneration)
|
|
35717
|
+
warnAskAvailability(context, error48);
|
|
35718
|
+
return;
|
|
35719
|
+
}
|
|
35720
|
+
if (open === null || stale())
|
|
35721
|
+
return;
|
|
35722
|
+
if (runSeq !== seenRun)
|
|
35723
|
+
return;
|
|
35724
|
+
if (open.snapshot.count > 0 || open.snapshot.opened_since) {
|
|
35725
|
+
askAwareness = {
|
|
35726
|
+
...askAwareness,
|
|
35727
|
+
check_due: false,
|
|
35728
|
+
baseline_as_of: open.snapshot.as_of
|
|
35729
|
+
};
|
|
35730
|
+
return;
|
|
35731
|
+
}
|
|
35732
|
+
const abort = new AbortController;
|
|
35733
|
+
askCheckAbort = abort;
|
|
35734
|
+
let answered;
|
|
35735
|
+
try {
|
|
35736
|
+
answered = askEphemeral({ prompt: ASK_SELF_CHECK_PROMPT, signal: abort.signal }).then((reply) => reply.replyText, (error48) => {
|
|
35737
|
+
if (!abort.signal.aborted)
|
|
35738
|
+
logSelfCheckFailure(id, error48);
|
|
35739
|
+
return;
|
|
35740
|
+
});
|
|
35741
|
+
} catch (error48) {
|
|
35742
|
+
logSelfCheckFailure(id, error48);
|
|
35743
|
+
answered = Promise.resolve(undefined);
|
|
35744
|
+
}
|
|
35745
|
+
const expiry = Promise.withResolvers();
|
|
35746
|
+
const expire = setTimeout(() => {
|
|
35747
|
+
const timedOut = new Error(`self-check timed out after ${selfCheckTimeoutMs} ms`);
|
|
35748
|
+
abort.abort(timedOut);
|
|
35749
|
+
logSelfCheckFailure(id, timedOut);
|
|
35750
|
+
expiry.resolve(undefined);
|
|
35751
|
+
}, selfCheckTimeoutMs);
|
|
35752
|
+
let verdict;
|
|
35753
|
+
try {
|
|
35754
|
+
verdict = await Promise.race([answered, expiry.promise]);
|
|
35755
|
+
} finally {
|
|
35756
|
+
clearTimeout(expire);
|
|
35757
|
+
if (askCheckAbort === abort)
|
|
35758
|
+
askCheckAbort = undefined;
|
|
35759
|
+
}
|
|
35760
|
+
if (stale()) {
|
|
35761
|
+
abort.abort(new Error("the self-check was superseded before its verdict arrived"));
|
|
35762
|
+
return;
|
|
35763
|
+
}
|
|
35764
|
+
const superseded = runSeq !== seenRun;
|
|
35765
|
+
askAwareness = {
|
|
35766
|
+
...askAwareness,
|
|
35767
|
+
check_due: superseded,
|
|
35768
|
+
checks: askAwareness.checks + 1,
|
|
35769
|
+
baseline_as_of: open.snapshot.as_of
|
|
35770
|
+
};
|
|
35771
|
+
if (superseded)
|
|
35772
|
+
return;
|
|
35773
|
+
if (verdict === undefined || !isWaitingVerdict(verdict.trim()))
|
|
35774
|
+
return;
|
|
35775
|
+
pi.sendMessage({ customType: ASK_REMINDER_MESSAGE, content: UNASKED_WAIT_REMINDER, display: false }, { deliverAs: "steer", triggerTurn: true });
|
|
35776
|
+
}
|
|
35668
35777
|
const announceFollow = createFollowAnnouncer((text) => {
|
|
35669
35778
|
pi.sendMessage({ customType: "envoy-message", content: text, display: true, details: LOCAL_ENVOY_NOTICE }, { deliverAs: "steer", triggerTurn: false });
|
|
35670
35779
|
});
|
|
35671
|
-
pi.on("tool_result", async (event) => {
|
|
35780
|
+
pi.on("tool_result", async (event, context) => {
|
|
35672
35781
|
if (event.isError)
|
|
35673
35782
|
return;
|
|
35674
|
-
if (
|
|
35675
|
-
|
|
35783
|
+
if (askAwareness.session_id === context.sessionManager.getSessionId() && askAwareness.period > 0) {
|
|
35784
|
+
if (ASK_OPENING_TOOLS.includes(event.toolName)) {
|
|
35785
|
+
askAwareness = { ...askAwareness, check_due: false };
|
|
35786
|
+
abortSelfCheck("the agent opened the ask itself");
|
|
35787
|
+
} else if (!event.toolName.startsWith(DISPATCH_TOOL_PREFIX)) {
|
|
35788
|
+
askAwareness = { ...askAwareness, check_due: true };
|
|
35789
|
+
}
|
|
35676
35790
|
}
|
|
35677
35791
|
announceFollow(event.details);
|
|
35678
35792
|
});
|
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: "5.
|
|
16145
|
+
version: "5.2.1",
|
|
16146
16146
|
type: "module",
|
|
16147
16147
|
omp: {
|
|
16148
16148
|
extensions: [
|
|
@@ -30125,7 +30125,7 @@ var dispatchToolSpecs = [
|
|
|
30125
30125
|
{
|
|
30126
30126
|
name: "dispatch_issue",
|
|
30127
30127
|
example: { project: "DSP", title: "Native workspace" },
|
|
30128
|
-
description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
30128
|
+
description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
30129
30129
|
arguments: (z2) => ({
|
|
30130
30130
|
project: z2.string().describe("Project key for the new issue."),
|
|
30131
30131
|
title: z2.string().describe("Concise issue title."),
|
|
@@ -30371,7 +30371,7 @@ var dispatchToolSpecs = [
|
|
|
30371
30371
|
name: "dispatch_artifact",
|
|
30372
30372
|
example: { issue: "DSP-1", name: "design.md", content: `# Design
|
|
30373
30373
|
` },
|
|
30374
|
-
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " +
|
|
30374
|
+
description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch_doc_edit instead. Exactly one of path or content is required; artifacts are limited to 25 MiB. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
|
|
30375
30375
|
arguments: (z2) => ({
|
|
30376
30376
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
30377
30377
|
project: z2.string().describe("Project key for an unlinked document.").optional(),
|
|
@@ -688,7 +688,10 @@ its authoritative value at settlement.
|
|
|
688
688
|
|
|
689
689
|
Questions about a document must be `ask` blocks, never an `Open questions` prose section. An ask
|
|
690
690
|
body is one or more question paragraphs followed by an optional bullet list of options, where each
|
|
691
|
-
item is `Label: description`.
|
|
691
|
+
item is `Label: description`. A spec, an uploaded document or an uploaded version holding an ask that
|
|
692
|
+
breaks that shape - a code block, heading or quote in it, a paragraph after its options, a second
|
|
693
|
+
list - is refused with `INVALID_ASK_BLOCK`; so is an edit that writes one. An ask someone left
|
|
694
|
+
unreadable in the browser refuses nothing it is carried through unchanged by. For example:
|
|
692
695
|
|
|
693
696
|
```md
|
|
694
697
|
:::ask{urgency="high" multiple="false"}
|