@netnodeag/kraftwerk 0.50.0 → 0.50.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.
|
@@ -476,7 +476,13 @@ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
|
|
|
476
476
|
_meta: fileChangeReportMeta(randomUUID()),
|
|
477
477
|
});
|
|
478
478
|
turns++;
|
|
479
|
-
|
|
479
|
+
// A provider condition that ends the turn itself (usage limit, rate
|
|
480
|
+
// limit, provider error) is not a notification: the adapter settles
|
|
481
|
+
// the prompt with stopReason "end_turn" and the typed failure in the
|
|
482
|
+
// response `_meta`. Without reading it here the turn would end
|
|
483
|
+
// silently — no text, no error (see failureOf).
|
|
484
|
+
const failure = failureOf(res._meta);
|
|
485
|
+
return failure ? { stopReason: res.stopReason, failure } : { stopReason: res.stopReason };
|
|
480
486
|
}
|
|
481
487
|
catch (err) {
|
|
482
488
|
// A resumed session that cannot even take its first prompt (the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentCommand, Attachment, ChatEvent, ConfigOption, ElicitationAnswer, ElicitationField } from "./types.js";
|
|
1
|
+
import type { AgentCommand, Attachment, ChatEvent, ConfigOption, ElicitationAnswer, ElicitationField, SessionFailure } from "./types.js";
|
|
2
2
|
/** An attachment as the backend gets it: where the file is, and what it is. */
|
|
3
3
|
export interface PromptFile extends Attachment {
|
|
4
4
|
path: string;
|
|
@@ -46,6 +46,17 @@ export interface BackendTuning {
|
|
|
46
46
|
/** ACP agents: the session id to continue (from ChatMeta.sessions) instead of opening a new one. */
|
|
47
47
|
resume?: string;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* How a turn ended. `failure` is the typed session failure the agent
|
|
51
|
+
* attached to the turn's own outcome (a usage limit, a rate limit, a
|
|
52
|
+
* provider error): ACP hands those back on the prompt response rather
|
|
53
|
+
* than as a mid-turn notification, and without it the turn would look
|
|
54
|
+
* like an ordinary empty `end_turn`.
|
|
55
|
+
*/
|
|
56
|
+
export interface TurnEnd {
|
|
57
|
+
stopReason: string;
|
|
58
|
+
failure?: SessionFailure;
|
|
59
|
+
}
|
|
49
60
|
export interface ChatBackend {
|
|
50
61
|
/** The agent's own session id, when it has one to resume later (ACP agents). */
|
|
51
62
|
sessionId?: string;
|
|
@@ -53,8 +64,8 @@ export interface ChatBackend {
|
|
|
53
64
|
resumed?: boolean;
|
|
54
65
|
/** The resumed session turned out unusable at its first prompt — its id must not be resumed again. */
|
|
55
66
|
resumeFailed?: boolean;
|
|
56
|
-
/** Send one user message (plus files dropped into it); resolves with the
|
|
57
|
-
prompt(text: string, files?: PromptFile[]): Promise<
|
|
67
|
+
/** Send one user message (plus files dropped into it); resolves with how the turn ended. */
|
|
68
|
+
prompt(text: string, files?: PromptFile[]): Promise<TurnEnd>;
|
|
58
69
|
/** Branch the conversation: a new session id with this session's history (ACP agents that offer session/fork). */
|
|
59
70
|
fork?(): Promise<string>;
|
|
60
71
|
/**
|
|
@@ -87,14 +87,14 @@ export function startPiBackend(cwd, hooks, tuning = {}) {
|
|
|
87
87
|
child = null;
|
|
88
88
|
handleLine(buffer);
|
|
89
89
|
if (cancelled)
|
|
90
|
-
return resolve("cancelled");
|
|
90
|
+
return resolve({ stopReason: "cancelled" });
|
|
91
91
|
if (failure || code !== 0 || !sawAssistant) {
|
|
92
92
|
reject(new Error(`pi failed (exit ${code})` +
|
|
93
93
|
(failure ? `: ${failure}` : "") +
|
|
94
94
|
(stderr.trim() ? `\nstderr: ${stderr.trim().slice(-1000)}` : "")));
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
|
-
resolve("end_turn");
|
|
97
|
+
resolve({ stopReason: "end_turn" });
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
100
|
},
|
|
@@ -67,6 +67,10 @@ function lastEventText(state, afterSeq, type) {
|
|
|
67
67
|
parts.push(e.text);
|
|
68
68
|
if (type === "error" && e.type === "error")
|
|
69
69
|
parts.push(e.message);
|
|
70
|
+
// A turn the agent ended on a provider failure (usage limit, rate
|
|
71
|
+
// limit) reports no error event — the failure card is the outcome.
|
|
72
|
+
if (type === "error" && e.type === "failure" && e.severity === "error")
|
|
73
|
+
parts.push(e.title);
|
|
70
74
|
}
|
|
71
75
|
const text = parts.join("").trim();
|
|
72
76
|
if (!text)
|
|
@@ -790,14 +794,18 @@ export async function postMessage(id, text, opts = {}) {
|
|
|
790
794
|
// the thread keeps the short form the user typed.
|
|
791
795
|
const body = await expandSkillInvocation(state.meta.scope, text);
|
|
792
796
|
const promptText = context ? `<context>\n${context}\n</context>\n\n${body}` : body;
|
|
793
|
-
const stopReason = await backend.prompt(promptText, promptFiles(id, attachments));
|
|
797
|
+
const { stopReason, failure } = await backend.prompt(promptText, promptFiles(id, attachments));
|
|
794
798
|
emit(state, { type: "turn_end", stopReason });
|
|
799
|
+
// After turn_end, so the card stays live (a turn_end settles the
|
|
800
|
+
// failures reported before it): this one is why the turn ended.
|
|
801
|
+
if (failure)
|
|
802
|
+
emit(state, { type: "failure", ...failure });
|
|
795
803
|
// Routine sessions are one-shot and unattended: release the agent
|
|
796
804
|
// process as soon as the turn ends instead of waiting for the reaper,
|
|
797
805
|
// and tell the bell how it went.
|
|
798
806
|
if (isUnattended(state.meta.scope)) {
|
|
799
807
|
dropBackend(state);
|
|
800
|
-
const failed = state.events.some((e) => e.seq > turnStart && e.type === "error");
|
|
808
|
+
const failed = state.events.some((e) => e.seq > turnStart && (e.type === "error" || (e.type === "failure" && e.severity === "error")));
|
|
801
809
|
void pushNotification({
|
|
802
810
|
kind: failed ? "routine_failed" : "routine_done",
|
|
803
811
|
title: `${chatLabel(state.meta)} ${failed ? "ended with an error" : "finished"}`,
|
|
@@ -923,8 +931,10 @@ async function runSeatTurn(state, channel, seat, hops) {
|
|
|
923
931
|
}
|
|
924
932
|
const prompt = (context ? `<context>\n${context}\n</context>\n\n` : "") +
|
|
925
933
|
(delta ? `New messages in #${channel.slug}:\n\n${delta}` : `You joined #${channel.slug}. Nothing addressed to you yet — reply with one short line saying you are here.`);
|
|
926
|
-
const stopReason = await backend.prompt(prompt);
|
|
934
|
+
const { stopReason, failure } = await backend.prompt(prompt);
|
|
927
935
|
emit(state, { type: "turn_end", stopReason }, me);
|
|
936
|
+
if (failure)
|
|
937
|
+
emit(state, { type: "failure", ...failure }, me);
|
|
928
938
|
// Handover: agents this one @mentioned get its message, within the hop budget.
|
|
929
939
|
const said = seatTextSince(state, start, seat.key);
|
|
930
940
|
const next = mentionTargets(said, channel, seat.key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netnodeag/kraftwerk",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.1",
|
|
4
4
|
"description": "Open-source agentic workspace for teams — where people and AI agents get work done together: agents (Claude Code, Codex, Pi), skills, shared knowledge, repeatable workflows, and deterministic verification",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|