@geminixiang/mikan 1.0.0-beta.67 → 1.0.0-beta.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/adapters/slack/auto-reply-context.d.ts +7 -5
- package/dist/adapters/slack/auto-reply-context.d.ts.map +1 -1
- package/dist/adapters/slack/auto-reply-context.js +11 -53
- package/dist/adapters/slack/auto-reply-context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +8 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +65 -11
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/jev-context.d.ts +30 -0
- package/dist/adapters/slack/jev-context.d.ts.map +1 -0
- package/dist/adapters/slack/jev-context.js +67 -0
- package/dist/adapters/slack/jev-context.js.map +1 -0
- package/dist/adapters/slack/task-intent.d.ts +32 -0
- package/dist/adapters/slack/task-intent.d.ts.map +1 -0
- package/dist/adapters/slack/task-intent.js +72 -0
- package/dist/adapters/slack/task-intent.js.map +1 -0
- package/dist/adapters/slack/task-status.d.ts.map +1 -1
- package/dist/adapters/slack/task-status.js +6 -0
- package/dist/adapters/slack/task-status.js.map +1 -1
- package/dist/env-manifest.js +1 -1
- package/dist/env-manifest.js.map +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/jev.d.ts +32 -11
- package/dist/harness/jev.d.ts.map +1 -1
- package/dist/harness/jev.js +103 -45
- package/dist/harness/jev.js.map +1 -1
- package/dist/harness/presenter.d.ts.map +1 -1
- package/dist/harness/presenter.js +4 -3
- package/dist/harness/presenter.js.map +1 -1
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js +1 -0
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/subagent-profiles.js +5 -5
- package/dist/harness/subagent-profiles.js.map +1 -1
- package/dist/harness/tools/index.d.ts.map +1 -1
- package/dist/harness/tools/index.js +3 -0
- package/dist/harness/tools/index.js.map +1 -1
- package/dist/harness/tools/jev.d.ts +21 -0
- package/dist/harness/tools/jev.d.ts.map +1 -0
- package/dist/harness/tools/jev.js +109 -0
- package/dist/harness/tools/jev.js.map +1 -0
- package/dist/sessions/session-store.d.ts +2 -0
- package/dist/sessions/session-store.d.ts.map +1 -1
- package/dist/sessions/session-store.js +1 -0
- package/dist/sessions/session-store.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,26 @@ any release.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.0.0-beta.69]
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `harness/jev.ts` is now backed by [`@geminixiang/jev`](https://github.com/geminixiang/jev), a standalone SDK shaped like pi-ai (providers own auth, a model catalog, and a wire implementation) instead of a hand-rolled OpenRouter fetch client. `evaluateWithJev`'s signature, types, and error classes are unchanged, so the auto-reply gate, DM task-intent classification, and the `jev` tool are unaffected. The swap adds request retries and timeouts, neither of which existed before, and a path to other Jev backends (TypeSafe, Vercel AI Gateway, Cloudflare Workers AI) without touching call sites.
|
|
17
|
+
|
|
18
|
+
## [1.0.0-beta.68]
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `jev` agent tool: the model can now call Jev directly with a `state` (text or JSON) and any number of boolean / choice / score `questions`, mirroring the decisions API one-to-one — structured instructions and criteria, per-option probabilities, confidence, and score legends all pass through. Available to the main agent and to the `worker`, `software-engineer`, `data-scientist`, and `summarizer` subagent profiles. A missing `OPENROUTER_API_KEY` is reported as a tool error so the model judges for itself.
|
|
23
|
+
- Slack: DM messages in a task thread (and top-level DMs while a task is running) are classified by Jev as `status`, `steer`, or `request` before spending a model turn. Status questions are answered from recorded task state; steering is delivered into the running task; anything else starts a normal run. The regex status matcher remains as the fallback.
|
|
24
|
+
- `task_status` reports `queued` for tasks that were admitted but have not started executing yet.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `harness/jev.ts` accepts structured JSON for state, instructions, and criteria, and returns `confidence` and `legend` on choice / score answers.
|
|
29
|
+
- Jev auto-reply and task-intent state renders `<@U…>` mentions as `@Name` (the bot as `@mikan`) so Jev can tell a message addressed to someone else from one addressed to mikan.
|
|
30
|
+
- Tool progress lines for `jev` calls are prefixed with `jev ·` so users can see which steps came from a calibrated judgment.
|
|
31
|
+
|
|
12
32
|
## [1.0.0-beta.67]
|
|
13
33
|
|
|
14
34
|
### Changed
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { type SlackNameResolver } from "./jev-context.js";
|
|
1
2
|
/** The one question Jev answers for an unaddressed shared-channel message. */
|
|
2
|
-
export declare const JEV_ADDRESSED_INSTRUCTIONS = "Given the recent Slack conversation, does the NEW message address, ask, or request the mikan AI assistant to do something (including continuing something mikan was already helping with), rather than being conversation between humans that needs no reply from mikan?";
|
|
3
|
+
export declare const JEV_ADDRESSED_INSTRUCTIONS = "Given the recent Slack conversation, does the NEW message address, ask, or request the mikan AI assistant to do something (including continuing something mikan was already helping with), rather than being conversation between humans that needs no reply from mikan? A message that @-mentions a specific person other than mikan is directed at that person.";
|
|
3
4
|
/**
|
|
4
5
|
* Build the shared `state` Jev scores when deciding whether an unaddressed
|
|
5
|
-
* shared-channel message is meant for mikan
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* mikan has already taken part in it.
|
|
6
|
+
* shared-channel message is meant for mikan: the surrounding scope plus, for
|
|
7
|
+
* a thread, whether mikan has already taken part in it. Mentions are shown
|
|
8
|
+
* as names so "@someone else" reads as such.
|
|
9
9
|
*/
|
|
10
10
|
export declare function buildAutoReplyState(conversationDir: string, event: {
|
|
11
11
|
ts: string;
|
|
@@ -15,5 +15,7 @@ export declare function buildAutoReplyState(conversationDir: string, event: {
|
|
|
15
15
|
}, options?: {
|
|
16
16
|
speaker?: string;
|
|
17
17
|
limit?: number;
|
|
18
|
+
resolveName?: SlackNameResolver;
|
|
19
|
+
botUserId?: string | null;
|
|
18
20
|
}): string;
|
|
19
21
|
//# sourceMappingURL=auto-reply-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-reply-context.d.ts","sourceRoot":"","sources":["../../../src/adapters/slack/auto-reply-context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auto-reply-context.d.ts","sourceRoot":"","sources":["../../../src/adapters/slack/auto-reply-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AAE1B,8EAA8E;AAC9E,eAAO,MAAM,0BAA0B,sWAC8T,CAAC;AAEtW;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACrE,OAAO,GAAE;IACP,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,GACL,MAAM,CAiBR"}
|
|
@@ -1,70 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isRecord, readTextFileIfExists } from "../../file-guards.js";
|
|
1
|
+
import { formatRecentScope, humanizeMentions, readRecentScope, } from "./jev-context.js";
|
|
3
2
|
/** The one question Jev answers for an unaddressed shared-channel message. */
|
|
4
|
-
export const JEV_ADDRESSED_INSTRUCTIONS = "Given the recent Slack conversation, does the NEW message address, ask, or request the mikan AI assistant to do something (including continuing something mikan was already helping with), rather than being conversation between humans that needs no reply from mikan?";
|
|
5
|
-
/** Recent messages in the same scope as `event` (channel top level or its thread), oldest first. */
|
|
6
|
-
function readRecentScope(conversationDir, event, limit) {
|
|
7
|
-
const raw = readTextFileIfExists(join(conversationDir, "log.jsonl"));
|
|
8
|
-
if (raw === undefined)
|
|
9
|
-
return [];
|
|
10
|
-
const lines = [];
|
|
11
|
-
for (const line of raw.split("\n")) {
|
|
12
|
-
if (!line)
|
|
13
|
-
continue;
|
|
14
|
-
let entry;
|
|
15
|
-
try {
|
|
16
|
-
entry = JSON.parse(line);
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (!isRecord(entry) || typeof entry.ts !== "string" || typeof entry.text !== "string") {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
const threadTs = typeof entry.threadTs === "string" ? entry.threadTs : undefined;
|
|
25
|
-
// Thread replies belong to their thread; everything else is the channel's top level.
|
|
26
|
-
const inScope = event.thread_ts
|
|
27
|
-
? threadTs === event.thread_ts || entry.ts === event.thread_ts
|
|
28
|
-
: threadTs === undefined;
|
|
29
|
-
if (!inScope || entry.ts === event.ts)
|
|
30
|
-
continue;
|
|
31
|
-
const isMikan = entry.isMessagingBot === true && entry.user === "bot";
|
|
32
|
-
const speaker = isMikan
|
|
33
|
-
? "mikan"
|
|
34
|
-
: String(entry.displayName ?? entry.userName ?? entry.user ?? "unknown");
|
|
35
|
-
const previous = lines.at(-1);
|
|
36
|
-
// Streamed bot responses are logged in chunks sharing one ts; merge them.
|
|
37
|
-
if (previous && previous.isMikan && isMikan && previous.ts === entry.ts) {
|
|
38
|
-
previous.text += entry.text;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
lines.push({ ts: entry.ts, threadTs, speaker, text: entry.text, isMikan });
|
|
42
|
-
}
|
|
43
|
-
return lines.slice(-limit);
|
|
44
|
-
}
|
|
3
|
+
export const JEV_ADDRESSED_INSTRUCTIONS = "Given the recent Slack conversation, does the NEW message address, ask, or request the mikan AI assistant to do something (including continuing something mikan was already helping with), rather than being conversation between humans that needs no reply from mikan? A message that @-mentions a specific person other than mikan is directed at that person.";
|
|
45
4
|
/**
|
|
46
5
|
* Build the shared `state` Jev scores when deciding whether an unaddressed
|
|
47
|
-
* shared-channel message is meant for mikan
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* mikan has already taken part in it.
|
|
6
|
+
* shared-channel message is meant for mikan: the surrounding scope plus, for
|
|
7
|
+
* a thread, whether mikan has already taken part in it. Mentions are shown
|
|
8
|
+
* as names so "@someone else" reads as such.
|
|
51
9
|
*/
|
|
52
10
|
export function buildAutoReplyState(conversationDir, event, options = {}) {
|
|
53
|
-
const
|
|
11
|
+
const resolve = options.resolveName ?? (() => undefined);
|
|
12
|
+
const humanize = (text) => humanizeMentions(text, resolve, options.botUserId ?? null);
|
|
13
|
+
const recent = readRecentScope(conversationDir, event, { limit: options.limit, humanize });
|
|
54
14
|
const header = event.thread_ts
|
|
55
15
|
? `Slack thread reply. mikan has ${recent.some((l) => l.isMikan) ? "already replied" : "not replied"} in this thread.`
|
|
56
16
|
: "Slack channel top-level message.";
|
|
57
|
-
const context = recent.length
|
|
58
|
-
? recent.map((l) => `- ${l.speaker}: ${l.text}`).join("\n")
|
|
59
|
-
: "(none)";
|
|
60
17
|
return [
|
|
61
18
|
header,
|
|
19
|
+
"The AI assistant is named mikan; mentions of other people appear as @Name.",
|
|
62
20
|
"",
|
|
63
21
|
"Recent messages (oldest first):",
|
|
64
|
-
|
|
22
|
+
formatRecentScope(recent),
|
|
65
23
|
"",
|
|
66
24
|
`NEW message from ${options.speaker ?? event.user}:`,
|
|
67
|
-
event.text,
|
|
25
|
+
humanize(event.text),
|
|
68
26
|
].join("\n");
|
|
69
27
|
}
|
|
70
28
|
//# sourceMappingURL=auto-reply-context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-reply-context.js","sourceRoot":"","sources":["../../../src/adapters/slack/auto-reply-context.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"auto-reply-context.js","sourceRoot":"","sources":["../../../src/adapters/slack/auto-reply-context.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAEhB,MAAM,kBAAkB,CAAC;AAE1B,8EAA8E;AAC9E,MAAM,CAAC,MAAM,0BAA0B,GACrC,mWAAmW,CAAC;AAEtW;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,eAAuB,EACvB,KAAqE,EACrE,OAAO,GAKH,EAAE;IAEN,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;IAC9F,MAAM,MAAM,GAAG,eAAe,CAAC,eAAe,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC3F,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS;QAC5B,CAAC,CAAC,iCAAiC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,aAAa,kBAAkB;QACtH,CAAC,CAAC,kCAAkC,CAAC;IACvC,OAAO;QACL,MAAM;QACN,4EAA4E;QAC5E,EAAE;QACF,iCAAiC;QACjC,iBAAiB,CAAC,MAAM,CAAC;QACzB,EAAE;QACF,oBAAoB,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG;QACpD,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;KACrB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC","sourcesContent":["import {\n formatRecentScope,\n humanizeMentions,\n readRecentScope,\n type SlackNameResolver,\n} from \"./jev-context.js\";\n\n/** The one question Jev answers for an unaddressed shared-channel message. */\nexport const JEV_ADDRESSED_INSTRUCTIONS =\n \"Given the recent Slack conversation, does the NEW message address, ask, or request the mikan AI assistant to do something (including continuing something mikan was already helping with), rather than being conversation between humans that needs no reply from mikan? A message that @-mentions a specific person other than mikan is directed at that person.\";\n\n/**\n * Build the shared `state` Jev scores when deciding whether an unaddressed\n * shared-channel message is meant for mikan: the surrounding scope plus, for\n * a thread, whether mikan has already taken part in it. Mentions are shown\n * as names so \"@someone else\" reads as such.\n */\nexport function buildAutoReplyState(\n conversationDir: string,\n event: { ts: string; thread_ts?: string; user: string; text: string },\n options: {\n speaker?: string;\n limit?: number;\n resolveName?: SlackNameResolver;\n botUserId?: string | null;\n } = {},\n): string {\n const resolve = options.resolveName ?? (() => undefined);\n const humanize = (text: string) => humanizeMentions(text, resolve, options.botUserId ?? null);\n const recent = readRecentScope(conversationDir, event, { limit: options.limit, humanize });\n const header = event.thread_ts\n ? `Slack thread reply. mikan has ${recent.some((l) => l.isMikan) ? \"already replied\" : \"not replied\"} in this thread.`\n : \"Slack channel top-level message.\";\n return [\n header,\n \"The AI assistant is named mikan; mentions of other people appear as @Name.\",\n \"\",\n \"Recent messages (oldest first):\",\n formatRecentScope(recent),\n \"\",\n `NEW message from ${options.speaker ?? event.user}:`,\n humanize(event.text),\n ].join(\"\\n\");\n}\n"]}
|
|
@@ -146,6 +146,7 @@ export declare class SlackMessagingBot implements MessagingBot {
|
|
|
146
146
|
private getQueue;
|
|
147
147
|
private resolveQueueKey;
|
|
148
148
|
private isTaskThread;
|
|
149
|
+
private hasRunningTaskThread;
|
|
149
150
|
private hasKnownThreadSession;
|
|
150
151
|
/**
|
|
151
152
|
* Slack's own conversation vocabulary for this channel, from the metadata
|
|
@@ -184,6 +185,13 @@ export declare class SlackMessagingBot implements MessagingBot {
|
|
|
184
185
|
private setupEventHandlers;
|
|
185
186
|
private handleAppMention;
|
|
186
187
|
private admitHumanMessage;
|
|
188
|
+
/**
|
|
189
|
+
* Handle a DM message that may be about a background task without spending
|
|
190
|
+
* a model turn: a pure status question is answered from observation, and a
|
|
191
|
+
* supplement is steered into the running task. Jev decides which (falling
|
|
192
|
+
* back to the regex status shortcut); anything else returns false so the
|
|
193
|
+
* message becomes an ordinary conversation turn.
|
|
194
|
+
*/
|
|
187
195
|
private deliverTaskUpdate;
|
|
188
196
|
/**
|
|
189
197
|
* Ask Jev a single typed yes/no question about whether this unaddressed
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/slack/bot.ts"],"names":[],"mappings":"AAMA,OAAO,EAGL,KAAK,YAAY,EAEjB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAI1B,KAAK,aAAa,EAElB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAA8C,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAOnG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,KAAK,EAGV,YAAY,EAEZ,SAAS,EACV,MAAM,YAAY,CAAC;AAiBpB,OAAO,EASL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAMxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/slack/bot.ts"],"names":[],"mappings":"AAMA,OAAO,EAGL,KAAK,YAAY,EAEjB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAI1B,KAAK,aAAa,EAElB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAA8C,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAOnG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,KAAK,EAGV,YAAY,EAEZ,SAAS,EACV,MAAM,YAAY,CAAC;AAiBpB,OAAO,EASL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAMxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAyEzD,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,SAAwB,GAAG,MAAM,EAAE,CAOrF;AAuDD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5D;AAMD,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAoBtE,qBAAa,iBAAkB,YAAW,YAAY;IACpD,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,SAAS,CAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAY;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoC;IAClE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,SAAS,CAAuB;IAExC,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,QAAQ,CAAmC;IACnD,gFAAgF;IAChF,OAAO,CAAC,gBAAgB,CAAiC;IACzD;;;;;OAKG;IACH,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,cAAc,CAA+B;IAErD,gDAAgD;IAChD,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,aAAa;IAoGrB,YACE,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAA;KAAE,EAiBrE;IAED,iBAAiB,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAEjD;IAMK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAyB3B;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ1B;IAED,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE7C;IAED,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtD;IAED,WAAW,IAAI,SAAS,EAAE,CAEzB;IAED,cAAc,IAAI,YAAY,EAAE,CAE/B;IAED,OAAO,CAAC,eAAe;IASvB;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAIjB,WAAW,CACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAWjB;IAEK,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYlF;IAEK,aAAa,CACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAUf;IAEK,mBAAmB,CACvB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAEf;IAEK,iBAAiB,CACrB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAWjB;IAEK,mBAAmB,CACvB,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,GACf,OAAO,CAAC,IAAI,CAAC,CAIf;IAEK,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErF;IAEK,qBAAqB,CACzB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,IAAI,CAAC,CAUf;IAEK,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS5D;IAED;;;;;;OAMG;IACG,YAAY,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,sBAAsB,EAAE,CAAC,CA8CnC;IAED,oEAAoE;IAC9D,SAAS,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAQ7C;IAEK,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5E;IAED;;;;OAIG;IACH,qBAAqB,IAAI,OAAO,CAE/B;IAED;;;;;;;;;;;;;OAaG;IACG,kBAAkB,CACtB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EACjB,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,CAAC,CAgBjB;IAED,+DAA+D;IACzD,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIlF;YAEa,iBAAiB;IAYzB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIlE;IAEK,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI9D;IAMD,sEAAsE;IAChE,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBzF;IAED,uDAAuD;IACjD,4BAA4B,CAChC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,EAAE,eAAe,EAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAUf;IAED,mFAAmF;IAC7E,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQvF;IAED,kFAAkF;IAClF,OAAO,CAAC,YAAY;IAUpB;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IAWpC;;;;;OAKG;IACH,OAAO,CAAC,8BAA8B;IA2BhC,YAAY,CAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAIjB;IAEK,kBAAkB,CACtB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,GACf,OAAO,CAAC,MAAM,CAAC,CAEjB;IAEK,UAAU,CACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAYf;IAED,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;IAED,cAAc,CACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,QAAQ,CAAC,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,IAAI,CAWN;IAED,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAkB3E;IAED,gBAAgB,IAAI,aAAa,CAiBhC;IAMD;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CA4E9C;IAMD,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,oBAAoB;IAa5B,OAAO,CAAC,qBAAqB;IAO7B;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAWtB;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;IAkB5B,OAAO,CAAC,yBAAyB;IAmDjC,OAAO,CAAC,kBAAkB;IA4D1B;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IA+C3B,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,qBAAqB;IAqE7B,OAAO,CAAC,sBAAsB;IA6D9B;;;;;OAKG;YACW,iBAAiB;IAyB/B,OAAO,CAAC,kBAAkB;YA8CZ,gBAAgB;IAkE9B,OAAO,CAAC,iBAAiB;IAmCzB;;;;;;OAMG;YACW,iBAAiB;IAsE/B;;;;;;;;;;OAUG;YACW,oBAAoB;YAqCpB,kBAAkB;YA2GlB,kBAAkB;IAyChC,OAAO,CAAC,mBAAmB;YA0Bb,iBAAiB;IAmD/B,OAAO,CAAC,sBAAsB;IAkF9B;;;;OAIG;YACW,kBAAkB;IA+BhC,6EAA6E;YAC/D,iBAAiB;IAmB/B;;OAEG;YACW,cAAc;YA4Bd,8BAA8B;YAwC9B,qBAAqB;IAqBnC,OAAO,CAAC,qBAAqB;YAcf,eAAe;YAuEf,mBAAmB;YAsCnB,UAAU;YA0BV,aAAa;CAmD5B"}
|
|
@@ -22,8 +22,9 @@ import { isSlackThreadSessionKey, planSlackAdapterSession, planSlackEventAnchorR
|
|
|
22
22
|
import { reportUserFacingError } from "../../observability/index.js";
|
|
23
23
|
import { recordSlackUpdate } from "./update-diagnostics.js";
|
|
24
24
|
import { renderSlackBlocks, resolveSlackMentions } from "./blocks.js";
|
|
25
|
-
import { querySlackTasks,
|
|
25
|
+
import { querySlackTasks, formatTaskStatus, readTaskRoots, isTaskStatusQuestion, } from "./task-status.js";
|
|
26
26
|
import { buildAutoReplyState, JEV_ADDRESSED_INSTRUCTIONS } from "./auto-reply-context.js";
|
|
27
|
+
import { buildTaskIntentState, classifyTaskIntent } from "./task-intent.js";
|
|
27
28
|
import { StreamStartLimiter } from "./stream-limits.js";
|
|
28
29
|
const SLACK_EVENT_ANCHOR_TEXT = "Working on it...";
|
|
29
30
|
function hasMessageContent(message, includeBlocks = true) {
|
|
@@ -798,6 +799,16 @@ export class SlackMessagingBot {
|
|
|
798
799
|
isTaskThread(channel, root) {
|
|
799
800
|
return readTaskRoots(this.conversationDir(channel)).has(root);
|
|
800
801
|
}
|
|
802
|
+
hasRunningTaskThread(channel) {
|
|
803
|
+
const roots = readTaskRoots(this.conversationDir(channel));
|
|
804
|
+
if (!roots.size)
|
|
805
|
+
return false;
|
|
806
|
+
return this.handler
|
|
807
|
+
.getRunningSessions()
|
|
808
|
+
.some((s) => s.address.platform === "slack" &&
|
|
809
|
+
s.address.conversationId === channel &&
|
|
810
|
+
[...roots.keys()].some((root) => resolveSlackSessionKey(channel, root) === s.sessionKey));
|
|
811
|
+
}
|
|
801
812
|
hasKnownThreadSession(conversationId, sessionKey) {
|
|
802
813
|
return hasMaterializedChatSession({
|
|
803
814
|
conversationDir: this.conversationDir(conversationId),
|
|
@@ -1258,6 +1269,13 @@ export class SlackMessagingBot {
|
|
|
1258
1269
|
ack();
|
|
1259
1270
|
return false;
|
|
1260
1271
|
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Handle a DM message that may be about a background task without spending
|
|
1274
|
+
* a model turn: a pure status question is answered from observation, and a
|
|
1275
|
+
* supplement is steered into the running task. Jev decides which (falling
|
|
1276
|
+
* back to the regex status shortcut); anything else returns false so the
|
|
1277
|
+
* message becomes an ordinary conversation turn.
|
|
1278
|
+
*/
|
|
1261
1279
|
async deliverTaskUpdate(event, attachmentsPromise) {
|
|
1262
1280
|
if (matchMagicWord(event.text) === "stop" || event.text.startsWith("/"))
|
|
1263
1281
|
return false;
|
|
@@ -1267,11 +1285,24 @@ export class SlackMessagingBot {
|
|
|
1267
1285
|
? this.postInThread(event.channel, event.thread_ts, text)
|
|
1268
1286
|
: this.postMessage(event.channel, text);
|
|
1269
1287
|
try {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1288
|
+
const tasks = await context.responder.getTaskStatus(event.thread_ts ? event.sessionKey : undefined);
|
|
1289
|
+
if (!tasks.length)
|
|
1290
|
+
return false;
|
|
1291
|
+
const active = tasks.filter((t) => t.status === "running" || t.status === "stopping");
|
|
1292
|
+
const user = this.users.get(event.user);
|
|
1293
|
+
// A message carrying files is never a pure status question; in a task
|
|
1294
|
+
// thread it is a supplement, at top level an ordinary turn.
|
|
1295
|
+
const intent = event.attachments?.length
|
|
1296
|
+
? event.thread_ts
|
|
1297
|
+
? "steer"
|
|
1298
|
+
: "request"
|
|
1299
|
+
: await classifyTaskIntent(buildTaskIntentState(this.conversationDir(event.channel), event, {
|
|
1300
|
+
speaker: user?.displayName ?? user?.userName,
|
|
1301
|
+
tasks,
|
|
1302
|
+
resolveName: (id) => this.users.get(id)?.displayName ?? this.users.get(id)?.userName,
|
|
1303
|
+
botUserId: this.botUserId,
|
|
1304
|
+
}), event.text, { conversationId: event.channel, inTaskThread: !!event.thread_ts });
|
|
1305
|
+
if (intent === "status") {
|
|
1275
1306
|
const selection = event.thread_ts ? tasks : active.length ? active : tasks.slice(0, 1);
|
|
1276
1307
|
const text = selection.length > 1
|
|
1277
1308
|
? "目前有多個任務在處理,請到你想查詢的任務對話串詢問,避免弄錯。"
|
|
@@ -1280,9 +1311,24 @@ export class SlackMessagingBot {
|
|
|
1280
1311
|
this.logBotResponse(event.channel, text, ts, event.thread_ts);
|
|
1281
1312
|
return true;
|
|
1282
1313
|
}
|
|
1283
|
-
if (
|
|
1284
|
-
|
|
1285
|
-
|
|
1314
|
+
if (intent === "steer") {
|
|
1315
|
+
// In a thread the message already targets that task; at top level it
|
|
1316
|
+
// can only be steered when exactly one task is running.
|
|
1317
|
+
const target = event.thread_ts
|
|
1318
|
+
? context.message
|
|
1319
|
+
: active.length === 1
|
|
1320
|
+
? {
|
|
1321
|
+
...context.message,
|
|
1322
|
+
sessionKey: active[0].sessionKey,
|
|
1323
|
+
threadTs: active[0].threadTs,
|
|
1324
|
+
}
|
|
1325
|
+
: undefined;
|
|
1326
|
+
if (target && (await this.handler.steer?.(target))) {
|
|
1327
|
+
const text = "收到補充,會在下一個處理步驟納入;目前的操作不會立即中斷。";
|
|
1328
|
+
const ts = await post(text);
|
|
1329
|
+
this.logBotResponse(event.channel, text, ts, event.thread_ts);
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1286
1332
|
}
|
|
1287
1333
|
}
|
|
1288
1334
|
catch (error) {
|
|
@@ -1309,6 +1355,8 @@ export class SlackMessagingBot {
|
|
|
1309
1355
|
const user = this.users.get(event.user);
|
|
1310
1356
|
const state = buildAutoReplyState(this.conversationDir(event.channel), event, {
|
|
1311
1357
|
speaker: user?.displayName ?? user?.userName,
|
|
1358
|
+
resolveName: (id) => this.users.get(id)?.displayName ?? this.users.get(id)?.userName,
|
|
1359
|
+
botUserId: this.botUserId,
|
|
1312
1360
|
});
|
|
1313
1361
|
const result = await evaluateWithJev(state, {
|
|
1314
1362
|
addressed: { type: "boolean", instructions: JEV_ADDRESSED_INSTRUCTIONS },
|
|
@@ -1380,8 +1428,14 @@ export class SlackMessagingBot {
|
|
|
1380
1428
|
}
|
|
1381
1429
|
const activeSessionKey = slackEvent.sessionKey ?? resolveSlackSessionKey(e.channel, e.thread_ts);
|
|
1382
1430
|
slackEvent.sessionKey = activeSessionKey;
|
|
1383
|
-
|
|
1384
|
-
|
|
1431
|
+
// Task threads always go through the task gate. Top-level DMs do only
|
|
1432
|
+
// while a task is running (so supplements can be steered) or when the
|
|
1433
|
+
// regex already recognises a status question about a finished one, so
|
|
1434
|
+
// idle DMs never pay a Jev round-trip.
|
|
1435
|
+
const taskControl = isDM &&
|
|
1436
|
+
(e.thread_ts
|
|
1437
|
+
? this.isTaskThread(e.channel, e.thread_ts)
|
|
1438
|
+
: this.hasRunningTaskThread(e.channel) || isTaskStatusQuestion(slackEvent.text));
|
|
1385
1439
|
if (taskControl) {
|
|
1386
1440
|
ack();
|
|
1387
1441
|
if (await this.deliverTaskUpdate(slackEvent, attachmentsPromise))
|