@dudousxd/nestjs-agent 1.2.0 → 1.4.0
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/README.md +30 -0
- package/dist/{agent-deps.factory-OjrLyAZo.d.cts → agent-deps.factory-CUyVPsa1.d.cts} +10 -1
- package/dist/{agent-deps.factory-OjrLyAZo.d.ts → agent-deps.factory-CUyVPsa1.d.ts} +10 -1
- package/dist/durable/index.cjs +180 -12
- package/dist/durable/index.cjs.map +1 -1
- package/dist/durable/index.d.cts +1 -1
- package/dist/durable/index.d.ts +1 -1
- package/dist/durable/index.js +182 -14
- package/dist/durable/index.js.map +1 -1
- package/dist/index.cjs +161 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -4
- package/dist/index.d.ts +47 -4
- package/dist/index.js +163 -13
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -184,6 +184,9 @@ var AgentDepsFactory2 = class {
|
|
|
184
184
|
store: this.store,
|
|
185
185
|
sink: this.sink,
|
|
186
186
|
rolesPolicy: this.rolesPolicy,
|
|
187
|
+
...this.options.approvalPolicy !== void 0 ? {
|
|
188
|
+
approvalPolicy: this.options.approvalPolicy
|
|
189
|
+
} : {},
|
|
187
190
|
registry: this.registry,
|
|
188
191
|
promptContributors: this.promptContributors,
|
|
189
192
|
systemPrompt: definition?.systemPrompt ?? "You are a helpful assistant.",
|
|
@@ -485,18 +488,35 @@ var AgentService = class {
|
|
|
485
488
|
await this.assertOwnsActiveStream(actor, runId);
|
|
486
489
|
return this.subscribe(runId);
|
|
487
490
|
}
|
|
488
|
-
|
|
489
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Approve a parked action call as `actor`. Who may is the call's recorded approver's business
|
|
493
|
+
* (see {@link assertMayDecide}); `remember` approves later calls of the same tool in the same
|
|
494
|
+
* thread, and `via` names the surface the decision came through (`'web'`, `'slack'`, …) — both
|
|
495
|
+
* persisted with the call.
|
|
496
|
+
*/
|
|
497
|
+
async approve(actor, toolCallId2, opts = {}) {
|
|
498
|
+
await this.assertMayDecide(actor, toolCallId2);
|
|
490
499
|
return this.signalToolCall(toolCallId2, {
|
|
491
|
-
approved: true
|
|
500
|
+
approved: true,
|
|
501
|
+
executedByRef: actor.id,
|
|
502
|
+
...opts.remember === true ? {
|
|
503
|
+
remember: true
|
|
504
|
+
} : {},
|
|
505
|
+
...opts.via !== void 0 ? {
|
|
506
|
+
decidedVia: opts.via
|
|
507
|
+
} : {}
|
|
492
508
|
});
|
|
493
509
|
}
|
|
494
|
-
async reject(actor, toolCallId2, reason) {
|
|
495
|
-
await this.
|
|
510
|
+
async reject(actor, toolCallId2, reason, opts = {}) {
|
|
511
|
+
await this.assertMayDecide(actor, toolCallId2);
|
|
496
512
|
return this.signalToolCall(toolCallId2, {
|
|
497
513
|
approved: false,
|
|
514
|
+
executedByRef: actor.id,
|
|
498
515
|
...reason !== void 0 ? {
|
|
499
516
|
reason
|
|
517
|
+
} : {},
|
|
518
|
+
...opts.via !== void 0 ? {
|
|
519
|
+
decidedVia: opts.via
|
|
500
520
|
} : {}
|
|
501
521
|
});
|
|
502
522
|
}
|
|
@@ -508,6 +528,7 @@ var AgentService = class {
|
|
|
508
528
|
*/
|
|
509
529
|
async answer(actor, toolCallId2, answers2 = {}) {
|
|
510
530
|
await this.assertOwnsToolCall(actor, toolCallId2);
|
|
531
|
+
await this.assertAnswersFit(toolCallId2, answers2);
|
|
511
532
|
const reply = {
|
|
512
533
|
answers: answers2,
|
|
513
534
|
answeredByRef: actor.id
|
|
@@ -537,6 +558,7 @@ var AgentService = class {
|
|
|
537
558
|
* not the thread's own actor).
|
|
538
559
|
*/
|
|
539
560
|
async signalToolCall(toolCallId2, reply) {
|
|
561
|
+
await this.assertNotExpired(toolCallId2);
|
|
540
562
|
const runId = await this.resolveRunForToolCall(toolCallId2);
|
|
541
563
|
return this.runner.signal(runId, toolCallId2, reply);
|
|
542
564
|
}
|
|
@@ -775,6 +797,68 @@ var AgentService = class {
|
|
|
775
797
|
}
|
|
776
798
|
}
|
|
777
799
|
/**
|
|
800
|
+
* Check a reply against the questions it answers before it is signalled: every submitted value a
|
|
801
|
+
* question's own rules accept (a typed input's type, bounds and pattern; a pick from the offered
|
|
802
|
+
* options), and every `required` question answered — by the reply, or by the defaults an omitted
|
|
803
|
+
* question falls back to. A reply the loop would have to drop parts of is refused here instead, so
|
|
804
|
+
* the person who typed it hears why. Needs the store to hand back the call's recorded questions;
|
|
805
|
+
* without that seam the loop's own filtering is the only check.
|
|
806
|
+
*/
|
|
807
|
+
async assertAnswersFit(toolCallId2, answers2) {
|
|
808
|
+
if (this.store.toolCallInput === void 0) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const questions = (0, import_nestjs_agent_core3.readElicitationQuestions)(await this.store.toolCallInput(toolCallId2));
|
|
812
|
+
for (const question of questions) {
|
|
813
|
+
const submitted = Object.hasOwn(answers2, question.id) ? answers2[question.id] : void 0;
|
|
814
|
+
const problem = (0, import_nestjs_agent_core3.validateElicitationAnswer)(question, submitted ?? question.defaults ?? []);
|
|
815
|
+
if (problem !== null) {
|
|
816
|
+
throw new import_common2.BadRequestException(`answers["${question.id}"] ${problem}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Authorization seam for approve/reject. The call's recorded approver decides who may settle it:
|
|
822
|
+
* the requester (the default, and every call recorded before approvers existed) is the thread's
|
|
823
|
+
* own actor — the ownership check this always was; any other approver goes through the policy's
|
|
824
|
+
* `canDecide`, which by default asks whether the actor holds that role.
|
|
825
|
+
*/
|
|
826
|
+
async assertMayDecide(actor, toolCallId2) {
|
|
827
|
+
const approval = await this.store.toolCallApproval?.(toolCallId2);
|
|
828
|
+
const approver = approval?.approver ?? import_nestjs_agent_core3.REQUESTER_APPROVER;
|
|
829
|
+
if (approver === import_nestjs_agent_core3.REQUESTER_APPROVER) {
|
|
830
|
+
return this.assertOwnsToolCall(actor, toolCallId2);
|
|
831
|
+
}
|
|
832
|
+
const requesterRef = await this.store.ownerOfToolCall(toolCallId2);
|
|
833
|
+
if (requesterRef === null) {
|
|
834
|
+
throw new import_common2.NotFoundException(`tool call ${toolCallId2} not found`);
|
|
835
|
+
}
|
|
836
|
+
const allowed = await (0, import_nestjs_agent_core3.mayDecideApproval)(this.deps.forAgent().approvalPolicy, actor, {
|
|
837
|
+
toolCallId: toolCallId2,
|
|
838
|
+
approver,
|
|
839
|
+
requesterRef
|
|
840
|
+
});
|
|
841
|
+
if (!allowed) {
|
|
842
|
+
throw new import_common2.ForbiddenException(`this approval is for ${approver}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Refuse a decision on a request that has already lapsed — recorded `expired`, or past its
|
|
847
|
+
* `expiresAt` with the run's own timer about to fire. Signalling it anyway would race the timeout:
|
|
848
|
+
* a durable runtime buffers a signal nobody is waiting on, and a late approval must not be what a
|
|
849
|
+
* later replay of the run reads back.
|
|
850
|
+
*/
|
|
851
|
+
async assertNotExpired(toolCallId2) {
|
|
852
|
+
const approval = await this.store.toolCallApproval?.(toolCallId2);
|
|
853
|
+
if (approval === null || approval === void 0) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const lapsed = approval.status === "expired" || approval.status === "pending_approval" && approval.expiresAt !== null && Date.parse(approval.expiresAt) <= Date.now();
|
|
857
|
+
if (lapsed) {
|
|
858
|
+
throw new import_common2.GoneException(`the approval request for tool call ${toolCallId2} has expired`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
778
862
|
* Resolve the run awaiting `toolCallId`. Derived server-side from the tool call alone — the client
|
|
779
863
|
* never knows or supplies a runId, and that run is the sub-agent's own child run when the pending
|
|
780
864
|
* call belongs to a delegated agent. The store answers from the CALL'S OWN `runId` where it has
|
|
@@ -852,6 +936,12 @@ var AgentApprovalPortAdapter = class {
|
|
|
852
936
|
approved: true,
|
|
853
937
|
...opts?.executedByRef !== void 0 ? {
|
|
854
938
|
executedByRef: opts.executedByRef
|
|
939
|
+
} : {},
|
|
940
|
+
...opts?.remember === true ? {
|
|
941
|
+
remember: true
|
|
942
|
+
} : {},
|
|
943
|
+
...opts?.decidedVia !== void 0 ? {
|
|
944
|
+
decidedVia: opts.decidedVia
|
|
855
945
|
} : {}
|
|
856
946
|
});
|
|
857
947
|
}
|
|
@@ -863,6 +953,9 @@ var AgentApprovalPortAdapter = class {
|
|
|
863
953
|
} : {},
|
|
864
954
|
...opts?.reason !== void 0 ? {
|
|
865
955
|
reason: opts.reason
|
|
956
|
+
} : {},
|
|
957
|
+
...opts?.decidedVia !== void 0 ? {
|
|
958
|
+
decidedVia: opts.decidedVia
|
|
866
959
|
} : {}
|
|
867
960
|
});
|
|
868
961
|
}
|
|
@@ -1782,6 +1875,8 @@ var MAX_ANSWERED_QUESTIONS = 100;
|
|
|
1782
1875
|
var MAX_VALUES_PER_QUESTION = 100;
|
|
1783
1876
|
var MAX_ANSWER_VALUE_LENGTH = 4096;
|
|
1784
1877
|
var MAX_REJECT_REASON_LENGTH = 4096;
|
|
1878
|
+
var MAX_VIA_LENGTH = 64;
|
|
1879
|
+
var DEFAULT_VIA = "web";
|
|
1785
1880
|
function toolCallId(claimed) {
|
|
1786
1881
|
if (typeof claimed !== "string" || claimed.length === 0) {
|
|
1787
1882
|
throw new import_common11.BadRequestException("toolCallId must be a non-empty string");
|
|
@@ -1802,6 +1897,26 @@ function rejectReason(claimed) {
|
|
|
1802
1897
|
return claimed;
|
|
1803
1898
|
}
|
|
1804
1899
|
__name(rejectReason, "rejectReason");
|
|
1900
|
+
function remember(claimed) {
|
|
1901
|
+
if (claimed === void 0) {
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
if (typeof claimed !== "boolean") {
|
|
1905
|
+
throw new import_common11.BadRequestException("remember must be a boolean");
|
|
1906
|
+
}
|
|
1907
|
+
return claimed;
|
|
1908
|
+
}
|
|
1909
|
+
__name(remember, "remember");
|
|
1910
|
+
function via(claimed) {
|
|
1911
|
+
if (claimed === void 0) {
|
|
1912
|
+
return DEFAULT_VIA;
|
|
1913
|
+
}
|
|
1914
|
+
if (typeof claimed !== "string" || claimed.length === 0 || claimed.length > MAX_VIA_LENGTH) {
|
|
1915
|
+
throw new import_common11.BadRequestException(`via must be a string of 1-${MAX_VIA_LENGTH} characters`);
|
|
1916
|
+
}
|
|
1917
|
+
return claimed;
|
|
1918
|
+
}
|
|
1919
|
+
__name(via, "via");
|
|
1805
1920
|
function answers(claimed) {
|
|
1806
1921
|
if (claimed === void 0) {
|
|
1807
1922
|
return void 0;
|
|
@@ -1846,14 +1961,19 @@ var ToolCallController = class {
|
|
|
1846
1961
|
}
|
|
1847
1962
|
async approve(req, body) {
|
|
1848
1963
|
const actor = await this.actorResolver.resolve(req);
|
|
1849
|
-
await this.agent.approve(actor, toolCallId(body.toolCallId)
|
|
1964
|
+
await this.agent.approve(actor, toolCallId(body.toolCallId), {
|
|
1965
|
+
remember: remember(body.remember),
|
|
1966
|
+
via: via(body.via)
|
|
1967
|
+
});
|
|
1850
1968
|
return {
|
|
1851
1969
|
ok: true
|
|
1852
1970
|
};
|
|
1853
1971
|
}
|
|
1854
1972
|
async reject(req, body) {
|
|
1855
1973
|
const actor = await this.actorResolver.resolve(req);
|
|
1856
|
-
await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason)
|
|
1974
|
+
await this.agent.reject(actor, toolCallId(body.toolCallId), rejectReason(body.reason), {
|
|
1975
|
+
via: via(body.via)
|
|
1976
|
+
});
|
|
1857
1977
|
return {
|
|
1858
1978
|
ok: true
|
|
1859
1979
|
};
|
|
@@ -1919,7 +2039,7 @@ _ts_decorate11([
|
|
|
1919
2039
|
_ts_metadata11("design:type", Function),
|
|
1920
2040
|
_ts_metadata11("design:paramtypes", [
|
|
1921
2041
|
typeof Request === "undefined" ? Object : Request,
|
|
1922
|
-
|
|
2042
|
+
Object
|
|
1923
2043
|
]),
|
|
1924
2044
|
_ts_metadata11("design:returntype", Promise)
|
|
1925
2045
|
], ToolCallController.prototype, "skip", null);
|
|
@@ -2785,6 +2905,36 @@ var InlineAgentRunner = class _InlineAgentRunner {
|
|
|
2785
2905
|
* shape it is delivering — the loop asked for one and only ever gets what the caller sent. A
|
|
2786
2906
|
* cancel settles the wait too, by rejecting it.
|
|
2787
2907
|
*/
|
|
2908
|
+
/**
|
|
2909
|
+
* {@link park} for an approval, bounded by the policy's time to live when it has one: the timer
|
|
2910
|
+
* settles the wait as `expired` — the same Decision the durable runner builds from its signal
|
|
2911
|
+
* timeout — and a decision that arrives first disarms it.
|
|
2912
|
+
*/
|
|
2913
|
+
parkApproval(runId, toolCallId2, timeoutMs) {
|
|
2914
|
+
const waiting = this.park(runId, toolCallId2);
|
|
2915
|
+
if (timeoutMs === void 0) {
|
|
2916
|
+
return waiting;
|
|
2917
|
+
}
|
|
2918
|
+
const key = `${runId}:${toolCallId2}`;
|
|
2919
|
+
let timer;
|
|
2920
|
+
const lapse = new Promise((resolve) => {
|
|
2921
|
+
timer = setTimeout(() => {
|
|
2922
|
+
const entry = this.pending.get(key);
|
|
2923
|
+
if (entry !== void 0) {
|
|
2924
|
+
this.pending.delete(key);
|
|
2925
|
+
resolve({
|
|
2926
|
+
approved: false,
|
|
2927
|
+
expired: true
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
}, timeoutMs);
|
|
2931
|
+
timer.unref?.();
|
|
2932
|
+
});
|
|
2933
|
+
return Promise.race([
|
|
2934
|
+
waiting,
|
|
2935
|
+
lapse
|
|
2936
|
+
]).finally(() => clearTimeout(timer));
|
|
2937
|
+
}
|
|
2788
2938
|
park(runId, toolCallId2) {
|
|
2789
2939
|
return new Promise((resolve, reject) => {
|
|
2790
2940
|
this.pending.set(`${runId}:${toolCallId2}`, {
|
|
@@ -2798,7 +2948,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
|
|
|
2798
2948
|
return {
|
|
2799
2949
|
runId,
|
|
2800
2950
|
openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
|
|
2801
|
-
awaitApproval: /* @__PURE__ */ __name((call) => this.
|
|
2951
|
+
awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
|
|
2802
2952
|
awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
|
|
2803
2953
|
step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
|
|
2804
2954
|
parallel: import_nestjs_agent_core18.settleAll,
|
|
@@ -2849,7 +2999,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
|
|
|
2849
2999
|
const hooks = {
|
|
2850
3000
|
runId,
|
|
2851
3001
|
openSink: /* @__PURE__ */ __name(() => deps.sink.open(runId), "openSink"),
|
|
2852
|
-
awaitApproval: /* @__PURE__ */ __name((call) => this.
|
|
3002
|
+
awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
|
|
2853
3003
|
awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
|
|
2854
3004
|
step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
|
|
2855
3005
|
parallel: import_nestjs_agent_core18.settleAll,
|
|
@@ -2953,7 +3103,7 @@ var InlineAgentRunner = class _InlineAgentRunner {
|
|
|
2953
3103
|
runId,
|
|
2954
3104
|
// Forward into the top-level stream; the top-level run owns end/fail on that shared sink.
|
|
2955
3105
|
openSink: /* @__PURE__ */ __name(async () => childSinkWriter(await deps.sink.open(sinkRunId)), "openSink"),
|
|
2956
|
-
awaitApproval: /* @__PURE__ */ __name((call) => this.
|
|
3106
|
+
awaitApproval: /* @__PURE__ */ __name((call, _ctx, opts) => this.parkApproval(runId, call.id, opts?.timeoutMs), "awaitApproval"),
|
|
2957
3107
|
awaitAnswers: /* @__PURE__ */ __name((request) => this.park(runId, request.id), "awaitAnswers"),
|
|
2958
3108
|
step: /* @__PURE__ */ __name((_name, fn) => fn(), "step"),
|
|
2959
3109
|
parallel: import_nestjs_agent_core18.settleAll,
|