@tyvm/knowhow 0.0.132 → 0.0.135
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 +1 -1
- package/src/agents/base/base.ts +63 -21
- package/src/cli.ts +2 -0
- package/src/commands/hash.ts +15 -7
- package/src/commands/sandbox.ts +540 -0
- package/src/tunnel.ts +11 -0
- package/src/worker.ts +102 -10
- package/tests/agents/interrupt.test.ts +207 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +2 -0
- package/ts_build/src/agents/base/base.js +30 -19
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/cli.js +2 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/commands/hash.js +12 -7
- package/ts_build/src/commands/hash.js.map +1 -1
- package/ts_build/src/commands/sandbox.d.ts +2 -0
- package/ts_build/src/commands/sandbox.js +353 -0
- package/ts_build/src/commands/sandbox.js.map +1 -0
- package/ts_build/src/tunnel.d.ts +1 -0
- package/ts_build/src/tunnel.js +6 -1
- package/ts_build/src/tunnel.js.map +1 -1
- package/ts_build/src/worker.js +56 -6
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/tests/agents/interrupt.test.d.ts +15 -0
- package/ts_build/tests/agents/interrupt.test.js +146 -0
- package/ts_build/tests/agents/interrupt.test.js.map +1 -0
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -75,6 +75,13 @@ export abstract class BaseAgent implements IAgent {
|
|
|
75
75
|
|
|
76
76
|
// Interrupt support: resolves the currently awaited tool call or completion
|
|
77
77
|
private _interruptResolve: (() => void) | null = null;
|
|
78
|
+
// A monotonically-increasing token identifying the active interruptible window.
|
|
79
|
+
// Used to ensure a stale (slow) operation completing in the background cannot
|
|
80
|
+
// clobber a newer interruptible window's resolver.
|
|
81
|
+
private _interruptToken = 0;
|
|
82
|
+
// Set when interrupt() is called with no active window, so the next
|
|
83
|
+
// makeInterruptible fires immediately instead of dropping the interrupt.
|
|
84
|
+
private _pendingInterrupt = false;
|
|
78
85
|
|
|
79
86
|
protected threads = [] as Message[][];
|
|
80
87
|
|
|
@@ -217,6 +224,11 @@ export abstract class BaseAgent implements IAgent {
|
|
|
217
224
|
this.startTimeMs = Date.now();
|
|
218
225
|
this.currentTaskId = taskId || this.startTimeMs.toString();
|
|
219
226
|
|
|
227
|
+
// Reset interrupt state so a queued/stale interrupt from a prior task
|
|
228
|
+
// can't fire against the new task.
|
|
229
|
+
this._interruptResolve = null;
|
|
230
|
+
this._pendingInterrupt = false;
|
|
231
|
+
|
|
220
232
|
// Emit event for plugin integration
|
|
221
233
|
const id = taskId || this.startTimeMs.toString();
|
|
222
234
|
this.events.emit(this.eventTypes.agentNewTask, {
|
|
@@ -740,32 +752,43 @@ export abstract class BaseAgent implements IAgent {
|
|
|
740
752
|
interruptValue: T
|
|
741
753
|
): Promise<T> {
|
|
742
754
|
return new Promise<T>((resolve, reject) => {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
755
|
+
// Each interruptible window gets a unique token. `settled` guards this
|
|
756
|
+
// specific window so it can only ever resolve/reject once, and the token
|
|
757
|
+
// ensures a stale (slow) background operation that finishes AFTER this
|
|
758
|
+
// window was interrupted cannot clobber a NEWER window's resolver.
|
|
759
|
+
const myToken = ++this._interruptToken;
|
|
760
|
+
let settled = false;
|
|
761
|
+
|
|
762
|
+
const finish = (fn: () => void) => {
|
|
763
|
+
if (settled) return;
|
|
764
|
+
settled = true;
|
|
765
|
+
// Only clear the shared resolver if it still belongs to this window.
|
|
766
|
+
if (this._interruptToken === myToken) {
|
|
767
|
+
this._interruptResolve = null;
|
|
768
|
+
}
|
|
769
|
+
fn();
|
|
749
770
|
};
|
|
750
771
|
|
|
772
|
+
this._interruptResolve = () => finish(() => resolve(interruptValue));
|
|
773
|
+
|
|
774
|
+
// If an interrupt arrived while there was no active window, honor it now.
|
|
775
|
+
if (this._pendingInterrupt) {
|
|
776
|
+
this._pendingInterrupt = false;
|
|
777
|
+
finish(() => resolve(interruptValue));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
751
781
|
promise
|
|
752
782
|
.then((result) => {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
783
|
+
// If this window was already interrupted/settled, drop the stale
|
|
784
|
+
// result — do NOT touch _interruptResolve (it may belong to a newer
|
|
785
|
+
// window now).
|
|
786
|
+
finish(() => resolve(result));
|
|
757
787
|
})
|
|
758
788
|
.catch((err) => {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// Only swallow the error if interrupt() was explicitly called.
|
|
763
|
-
// Otherwise re-throw so callers see the real error.
|
|
764
|
-
if (interrupted) {
|
|
765
|
-
resolve(interruptValue);
|
|
766
|
-
} else {
|
|
767
|
-
reject(err);
|
|
768
|
-
}
|
|
789
|
+
// If the window was already interrupted, swallow the late error.
|
|
790
|
+
if (settled) return;
|
|
791
|
+
finish(() => reject(err));
|
|
769
792
|
});
|
|
770
793
|
});
|
|
771
794
|
}
|
|
@@ -777,10 +800,29 @@ export abstract class BaseAgent implements IAgent {
|
|
|
777
800
|
*/
|
|
778
801
|
interrupt(message = "User interrupted this action you were waiting on") {
|
|
779
802
|
this.log(`Interrupting current operation: ${message}`);
|
|
803
|
+
|
|
804
|
+
// If the user supplied a real message with the poke (not the default
|
|
805
|
+
// interrupt reason), queue it as a pending user message so the agent
|
|
806
|
+
// actually sees what they said on its next step.
|
|
807
|
+
if (
|
|
808
|
+
message &&
|
|
809
|
+
message !== "User interrupted this action you were waiting on"
|
|
810
|
+
) {
|
|
811
|
+
this.addPendingUserMessage({ role: "user", content: message });
|
|
812
|
+
}
|
|
813
|
+
|
|
780
814
|
if (this._interruptResolve) {
|
|
781
815
|
this._interruptResolve();
|
|
782
816
|
} else {
|
|
783
|
-
|
|
817
|
+
// No active interruptible window right now (we're between the AI
|
|
818
|
+
// completion and the tool call, or in message processing). Queue the
|
|
819
|
+
// interrupt so the next makeInterruptible window fires immediately
|
|
820
|
+
// instead of silently dropping it.
|
|
821
|
+
this._pendingInterrupt = true;
|
|
822
|
+
this.log(
|
|
823
|
+
"No active interruptible operation — queued interrupt for next step",
|
|
824
|
+
"warn"
|
|
825
|
+
);
|
|
784
826
|
}
|
|
785
827
|
}
|
|
786
828
|
|
package/src/cli.ts
CHANGED
|
@@ -61,6 +61,7 @@ import { addBehaviorsCommand } from "./commands/behaviors";
|
|
|
61
61
|
import { addSkillsCommand } from "./commands/skills";
|
|
62
62
|
import { addAgentsCommand } from "./commands/agents";
|
|
63
63
|
import { addHashCommand } from "./commands/hash";
|
|
64
|
+
import { addSandboxCommand } from "./commands/sandbox";
|
|
64
65
|
|
|
65
66
|
// Handle unhandled promise rejections gracefully — particularly from MCP SDK
|
|
66
67
|
// which fires errors via event emitters that can bypass Promise.allSettled.
|
|
@@ -129,6 +130,7 @@ async function main() {
|
|
|
129
130
|
addSkillsCommand(program);
|
|
130
131
|
addAgentsCommand(program);
|
|
131
132
|
addHashCommand(program);
|
|
133
|
+
addSandboxCommand(program);
|
|
132
134
|
|
|
133
135
|
// Load global modules early (before parse) so they can register CLI subcommands.
|
|
134
136
|
// We pass only the Program in context — no services are spun up at this stage.
|
package/src/commands/hash.ts
CHANGED
|
@@ -43,7 +43,10 @@ export function addHashCommand(program: Command): void {
|
|
|
43
43
|
)
|
|
44
44
|
.option(
|
|
45
45
|
"--save",
|
|
46
|
-
"Save the current hash
|
|
46
|
+
"Save the current hash. In standalone mode, records the hash immediately (no command run). " +
|
|
47
|
+
"In --run mode, records the hash only if the command succeeds. " +
|
|
48
|
+
"Without --save in --run mode, the hash is never recorded — useful when --run is a " +
|
|
49
|
+
"side-effect trigger rather than the step that produces the tracked artifacts."
|
|
47
50
|
)
|
|
48
51
|
.option(
|
|
49
52
|
"--run <command>",
|
|
@@ -143,12 +146,17 @@ export function addHashCommand(program: Command): void {
|
|
|
143
146
|
process.exit(typeof err?.status === "number" ? err.status : 1);
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
// Command succeeded —
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (opts.
|
|
151
|
-
|
|
149
|
+
// Command succeeded — only save the hash if --save was also passed.
|
|
150
|
+
// Without --save, the hash is NOT recorded so the command will re-run next time.
|
|
151
|
+
// Use --save when --run produces the artifacts that the hash tracks.
|
|
152
|
+
// Omit --save when --run is a side-effect trigger (e.g. snapshot regeneration).
|
|
153
|
+
if (opts.save) {
|
|
154
|
+
if (!hashes[opts.name]) hashes[opts.name] = {};
|
|
155
|
+
hashes[opts.name][opts.input] = currentHash;
|
|
156
|
+
await saveHashes(hashes);
|
|
157
|
+
if (opts.verbose) {
|
|
158
|
+
console.error(`[knowhow hash] saved hash ${currentHash} for "${opts.name}"`);
|
|
159
|
+
}
|
|
152
160
|
}
|
|
153
161
|
process.exit(0);
|
|
154
162
|
}
|