@jskit-ai/assistant-runtime 0.1.216 → 0.1.218
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.218",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"./server": "./src/server/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
15
|
+
"@jskit-ai/assistant-core": "0.1.225",
|
|
16
16
|
"json-rest-schema": "^1.0.17"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
@@ -84,7 +84,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
84
84
|
const error = ref("");
|
|
85
85
|
const loading = ref(true);
|
|
86
86
|
const stopping = ref(false);
|
|
87
|
-
const
|
|
87
|
+
const goalAction = ref("");
|
|
88
88
|
const goalTargetCurrent = ref(false);
|
|
89
89
|
const goalError = ref("");
|
|
90
90
|
const goalView = shallowRef(null);
|
|
@@ -115,7 +115,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
115
115
|
const steerable = computed(() => snapshot.value?.status === "working" && snapshot.value?.capabilities?.steering === true);
|
|
116
116
|
const workingPolicy = computed(() => resolveWorkingPolicy(snapshot.value, { deferWhileWorking, admitWhileWorking }));
|
|
117
117
|
const queueing = computed(() => queueWhileSending !== false && (steerable.value || workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking));
|
|
118
|
-
const canSubmit = computed(() => available.value && (snapshot.value.status === "ready" || steerable.value || (workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking) && snapshot.value.status === "working") &&
|
|
118
|
+
const canSubmit = computed(() => available.value && !stopping.value && (snapshot.value.status === "ready" || steerable.value || (workingPolicy.value.deferWhileWorking || workingPolicy.value.admitWhileWorking) && snapshot.value.status === "working") &&
|
|
119
119
|
(queueWhileSending !== false || !delivery.state.sending) &&
|
|
120
120
|
!delivery.state.messages.some(message => message.status === "uncertain" &&
|
|
121
121
|
(snapshot.value.status !== "ready" || !message.payload || text(message.payload.draft ?? message.payload.message) === text(draft))));
|
|
@@ -221,6 +221,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
221
221
|
// Keep the open goal menu while its first native identity arrives. The
|
|
222
222
|
// pending read disables commands until it supplies the new exact target.
|
|
223
223
|
goalTargetCurrent.value = false;
|
|
224
|
+
goalLoadError.value = "";
|
|
224
225
|
goalReadController?.abort();
|
|
225
226
|
}
|
|
226
227
|
if (readsGoals.value && (initial || goalChanged || state.segmentId !== goalSegment)) void refreshGoal();
|
|
@@ -338,7 +339,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
338
339
|
draftAttachments.value = [];
|
|
339
340
|
draftRetry.value = null;
|
|
340
341
|
goalError.value = "";
|
|
341
|
-
|
|
342
|
+
goalAction.value = "";
|
|
342
343
|
goalView.value = null;
|
|
343
344
|
goalLoadError.value = "";
|
|
344
345
|
goalReadQueued = false;
|
|
@@ -441,7 +442,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
441
442
|
if (pendingMessages.has(messageId)) return false;
|
|
442
443
|
// Register before the original serial tail so Stop/retirement also reaches
|
|
443
444
|
// a local follower whose deliver callback has not started yet.
|
|
444
|
-
const pending = { controller: new AbortController(), started: false, dispatched: false
|
|
445
|
+
const pending = { controller: new AbortController(), started: false, dispatched: false };
|
|
445
446
|
const { controller } = pending;
|
|
446
447
|
pendingMessages.set(messageId, pending);
|
|
447
448
|
try {
|
|
@@ -592,7 +593,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
592
593
|
const view = goalView.value;
|
|
593
594
|
const target = view?.target;
|
|
594
595
|
const command = target?.capabilities?.goalCommands?.[action];
|
|
595
|
-
if (!available.value || view?.status !== "available" || goalLoadError.value ||
|
|
596
|
+
if (!available.value || view?.status !== "available" || goalLoadError.value || goalAction.value || !goalTargetCurrent.value ||
|
|
596
597
|
!["message", "control"].includes(command?.delivery) ||
|
|
597
598
|
delivery.state.messages.some(message => message.status === "uncertain")) return false;
|
|
598
599
|
if (input.tokenBudget != null && !target.capabilities.goalBudgets) {
|
|
@@ -605,7 +606,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
605
606
|
expectedGoalId: view.goal?.id || null,
|
|
606
607
|
...(action === "set" ? { objective: String(input.objective || "").trim(),
|
|
607
608
|
...(input.tokenBudget == null ? {} : { tokenBudget: input.tokenBudget }) } : {}) };
|
|
608
|
-
|
|
609
|
+
goalAction.value = action;
|
|
609
610
|
goalError.value = "";
|
|
610
611
|
try {
|
|
611
612
|
if (retry || command.delivery === "message") {
|
|
@@ -630,7 +631,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
630
631
|
return false;
|
|
631
632
|
} finally {
|
|
632
633
|
if (current.value) {
|
|
633
|
-
|
|
634
|
+
goalAction.value = "";
|
|
634
635
|
await goalRead;
|
|
635
636
|
await refreshGoal();
|
|
636
637
|
subscription?.reload();
|
|
@@ -642,14 +643,19 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
642
643
|
if (!readsGoals.value) return null;
|
|
643
644
|
const capabilities = view?.target?.capabilities || {};
|
|
644
645
|
const commands = capabilities.goalCommands || {};
|
|
646
|
+
const loading = !goalTargetCurrent.value && !goalLoadError.value;
|
|
647
|
+
const goalAvailable = goalTargetCurrent.value && view?.status === "available" && !goalLoadError.value;
|
|
645
648
|
return {
|
|
646
|
-
enabled: active.value && Boolean((view?.status === "available" && !goalLoadError.value) || goalError.value),
|
|
649
|
+
enabled: active.value && Boolean(loading || (view?.status === "available" && !goalLoadError.value) || goalError.value || goalLoadError.value),
|
|
650
|
+
loading,
|
|
651
|
+
available: Boolean(goalAvailable),
|
|
652
|
+
action: goalAction.value,
|
|
647
653
|
tokenBudgetSupported: capabilities.goalBudgets === true,
|
|
648
|
-
goal: view?.goal ? { ...view.goal, elapsedSeconds: view.goal.timeUsedSeconds,
|
|
654
|
+
goal: goalAvailable && view?.goal ? { ...view.goal, elapsedSeconds: view.goal.timeUsedSeconds,
|
|
649
655
|
sampledAt: Date.parse(view.goal.updatedAt) } : null,
|
|
650
|
-
pending:
|
|
656
|
+
pending: Boolean(goalAction.value) || !goalAvailable || !available.value || delivery.state.sending ||
|
|
651
657
|
delivery.state.messages.some(message => message.status === "uncertain"),
|
|
652
|
-
error: goalError.value,
|
|
658
|
+
error: goalError.value || goalLoadError.value,
|
|
653
659
|
pauseInterruptsTurn: commands.pause?.interruptsTurn === true,
|
|
654
660
|
cancelInterruptsTurn: commands.cancel?.interruptsTurn === true,
|
|
655
661
|
...Object.fromEntries(["set", "resume", "pause", "cancel"].filter(action =>
|
|
@@ -660,7 +666,7 @@ function createConversation(identity, { api, socket, actorKey, placement, reader
|
|
|
660
666
|
if (!editable.value || stopping.value) return false;
|
|
661
667
|
stopping.value = true;
|
|
662
668
|
for (const pending of pendingMessages.values()) {
|
|
663
|
-
if (
|
|
669
|
+
if (!pending.dispatched) pending.controller.abort();
|
|
664
670
|
}
|
|
665
671
|
try { return await api.cancelConversation(identity.conversationId); }
|
|
666
672
|
catch (failure) { receiveError(failure); return false; }
|
|
@@ -295,6 +295,12 @@ test("a supplied goal view keeps its pinned target and refreshes through the ret
|
|
|
295
295
|
failedRead = true;
|
|
296
296
|
await f.page.evaluate(() => window.conversationFixture.current().refreshGoal());
|
|
297
297
|
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().goalLoadError.value)).toBe("Goal status is unavailable.");
|
|
298
|
+
await expect(f.primary.getByRole("button", { name: "Goal unavailable", exact: true })).toBeVisible();
|
|
299
|
+
assert.deepEqual(await f.page.evaluate(() => {
|
|
300
|
+
const state = window.conversationFixture.current().goalState.value;
|
|
301
|
+
return { available: state.available, goal: state.goal, pending: state.pending };
|
|
302
|
+
}), { available: false, goal: null, pending: true });
|
|
303
|
+
await expect(f.page.getByRole("button", { name: "Resume goal", exact: true })).toHaveCount(0);
|
|
298
304
|
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().error.value), "",
|
|
299
305
|
"Passive goal failure does not enter conversation or app-wide request recovery");
|
|
300
306
|
await expect(f.input).toBeEnabled();
|
|
@@ -877,6 +883,41 @@ test("opted-in nonsteerable follow-ups use the original serial queue and dispatc
|
|
|
877
883
|
assert.equal((await f.sent()).at(-1).input.steer, true);
|
|
878
884
|
});
|
|
879
885
|
|
|
886
|
+
for (const steer of [false, true]) {
|
|
887
|
+
test(`Stop cancels locally queued native ${steer ? "steering" : "startup"} follow-ups`, options, async t => {
|
|
888
|
+
const f = await fixture(t);
|
|
889
|
+
await f.command("steering", { id: "chat:1", enabled: true });
|
|
890
|
+
await f.command("mode", { mode: "hold" });
|
|
891
|
+
await f.page.evaluate(() => {
|
|
892
|
+
window.firstNative = window.conversationFixture.current().send({ message: "First held instruction" });
|
|
893
|
+
});
|
|
894
|
+
await expect.poll(async () => (await f.state()).held).toBe(1);
|
|
895
|
+
await f.command("question", { id: "chat:1", text: "Starting native work", pending: true });
|
|
896
|
+
await f.notify("chat:1");
|
|
897
|
+
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().steerable.value)).toBe(true);
|
|
898
|
+
await f.page.evaluate(steer => {
|
|
899
|
+
window.queuedNative = window.conversationFixture.current().send({ message: "Queued native follow-up",
|
|
900
|
+
request: { text: "Queued native follow-up", steer } });
|
|
901
|
+
}, steer);
|
|
902
|
+
assert.equal((await f.sent()).length, 1);
|
|
903
|
+
const stopGate = Promise.withResolvers();
|
|
904
|
+
t.after(() => stopGate.resolve());
|
|
905
|
+
await f.page.route("**/cancel", async route => { await stopGate.promise; await route.continue(); });
|
|
906
|
+
await f.page.evaluate(() => { window.stoppingNative = window.conversationFixture.current().cancel(); });
|
|
907
|
+
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().stopping.value)).toBe(true);
|
|
908
|
+
const canSendDuringStop = await f.page.evaluate(() => window.conversationFixture.current().canSubmit.value);
|
|
909
|
+
stopGate.resolve();
|
|
910
|
+
await f.page.evaluate(() => window.stoppingNative);
|
|
911
|
+
await f.command("mode", { mode: "accepted" });
|
|
912
|
+
await f.command("accept");
|
|
913
|
+
await f.page.evaluate(() => window.firstNative);
|
|
914
|
+
const queuedResult = await f.page.evaluate(() => window.queuedNative);
|
|
915
|
+
assert.equal((await f.sent()).length, 1, "Stop must prevent a local follower from dispatching after the first receipt");
|
|
916
|
+
assert.equal(queuedResult, false);
|
|
917
|
+
assert.equal(canSendDuringStop, false, "new sends wait for Stop confirmation");
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
880
921
|
for (const action of ["stop", "actor", "denied"]) {
|
|
881
922
|
test(`opted-in local followers cannot dispatch after ${action}`, options, async t => {
|
|
882
923
|
const f = await fixture(t);
|
|
@@ -2912,6 +2953,11 @@ test("changing the observed native segment blocks cached goal controls until the
|
|
|
2912
2953
|
await expect.poll(() => f.page.evaluate(() => window.conversationFixture.current().snapshot.value.segmentId)).toBe("replacement-native-observation");
|
|
2913
2954
|
await expect.poll(() => Boolean(releaseRead)).toBe(true);
|
|
2914
2955
|
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().goalState.value.pending), true);
|
|
2956
|
+
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().goalState.value.loading), true);
|
|
2957
|
+
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().goalState.value.goal), null);
|
|
2958
|
+
await expect(f.primary.getByRole("button", { name: "Loading goal…", exact: true })).toBeVisible();
|
|
2959
|
+
await expect(f.page.getByRole("button", { name: "Pause goal", exact: true })).toHaveCount(0);
|
|
2960
|
+
await expect(f.page.getByRole("button", { name: "Resuming…", exact: true })).toHaveCount(0);
|
|
2915
2961
|
assert.equal(await f.page.evaluate(() => window.conversationFixture.current().changeGoal("pause")), false);
|
|
2916
2962
|
assert.deepEqual(commands, [], "A previous observation cannot authorize a command for the replacement thread");
|
|
2917
2963
|
view.goal.id = "replacement-goal";
|
|
@@ -3011,3 +3057,31 @@ for (const width of [1280, 390]) test(`ten real messages per history load across
|
|
|
3011
3057
|
await expect(f.primary.getByRole("button", { name: "Load older messages", exact: true })).toHaveCount(0);
|
|
3012
3058
|
assert.deepEqual(await f.sent(), []);
|
|
3013
3059
|
});
|
|
3060
|
+
|
|
3061
|
+
|
|
3062
|
+
test("initial goal loading shows no goal commands until the empty goal is confirmed", options, async t => {
|
|
3063
|
+
const f = await fixture(t);
|
|
3064
|
+
const waiting = Promise.withResolvers();
|
|
3065
|
+
const entered = Promise.withResolvers();
|
|
3066
|
+
await f.page.route("**/conversations/*/goal", async route => {
|
|
3067
|
+
entered.resolve();
|
|
3068
|
+
await waiting.promise;
|
|
3069
|
+
return route.fulfill({ json: { status: "available", goal: null, target: { segmentId: "empty-goal",
|
|
3070
|
+
capabilities: { goals: true, goalBudgets: false, goalCommands: {
|
|
3071
|
+
set: { delivery: "message", interruptsTurn: false }, resume: { delivery: "message", interruptsTurn: false }
|
|
3072
|
+
} } } } });
|
|
3073
|
+
});
|
|
3074
|
+
try {
|
|
3075
|
+
await f.page.evaluate(() => window.conversationFixture.eventReaders([{ id: "goal-reader", goal: true }]));
|
|
3076
|
+
await entered.promise;
|
|
3077
|
+
const loading = f.primary.getByRole("button", { name: "Loading goal…", exact: true });
|
|
3078
|
+
await expect(loading).toBeVisible();
|
|
3079
|
+
await loading.click();
|
|
3080
|
+
await expect(f.page.getByRole("button", { name: "Start goal", exact: true })).toHaveCount(0);
|
|
3081
|
+
await expect(f.page.getByRole("button", { name: "Resume goal", exact: true })).toHaveCount(0);
|
|
3082
|
+
waiting.resolve();
|
|
3083
|
+
await expect(f.primary.getByRole("button", { name: "Set goal", exact: true })).toBeVisible();
|
|
3084
|
+
await expect(f.page.getByRole("button", { name: "Start goal", exact: true })).toBeVisible();
|
|
3085
|
+
await expect(f.page.getByRole("button", { name: "Resume goal", exact: true })).toHaveCount(0);
|
|
3086
|
+
} finally { waiting.resolve(); }
|
|
3087
|
+
});
|