@dynamicagents/core 0.9.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/LICENSE +201 -0
- package/README.md +522 -0
- package/dist/a2a/agent-stub.d.ts +60 -0
- package/dist/a2a/agent-stub.js +1 -0
- package/dist/a2a/caller-token.d.ts +44 -0
- package/dist/a2a/caller-token.js +61 -0
- package/dist/a2a/caller.d.ts +23 -0
- package/dist/a2a/caller.js +33 -0
- package/dist/a2a/card.d.ts +158 -0
- package/dist/a2a/card.js +163 -0
- package/dist/a2a/context.d.ts +19 -0
- package/dist/a2a/context.js +61 -0
- package/dist/a2a/deliver.d.ts +159 -0
- package/dist/a2a/deliver.js +185 -0
- package/dist/a2a/executor.d.ts +84 -0
- package/dist/a2a/executor.js +106 -0
- package/dist/a2a/index.d.ts +35 -0
- package/dist/a2a/index.js +34 -0
- package/dist/a2a/notify.d.ts +106 -0
- package/dist/a2a/notify.js +180 -0
- package/dist/a2a/parts.d.ts +45 -0
- package/dist/a2a/parts.js +94 -0
- package/dist/a2a/push.d.ts +70 -0
- package/dist/a2a/push.js +53 -0
- package/dist/a2a/self-origin.d.ts +91 -0
- package/dist/a2a/self-origin.js +114 -0
- package/dist/a2a/task-store.d.ts +44 -0
- package/dist/a2a/task-store.js +99 -0
- package/dist/a2a/task.d.ts +85 -0
- package/dist/a2a/task.js +15 -0
- package/dist/a2a/verify.d.ts +80 -0
- package/dist/a2a/verify.js +143 -0
- package/dist/agent/budget.d.ts +46 -0
- package/dist/agent/budget.js +42 -0
- package/dist/agent/control.d.ts +109 -0
- package/dist/agent/control.js +115 -0
- package/dist/agent/errors.d.ts +85 -0
- package/dist/agent/errors.js +64 -0
- package/dist/agent/final-reply.d.ts +49 -0
- package/dist/agent/final-reply.js +68 -0
- package/dist/agent/history.d.ts +97 -0
- package/dist/agent/history.js +133 -0
- package/dist/agent/index.d.ts +29 -0
- package/dist/agent/index.js +29 -0
- package/dist/agent/inference.d.ts +110 -0
- package/dist/agent/inference.js +120 -0
- package/dist/agent/model.d.ts +90 -0
- package/dist/agent/model.js +1 -0
- package/dist/agent/session.d.ts +100 -0
- package/dist/agent/session.js +82 -0
- package/dist/agent/workers-ai/index.d.ts +23 -0
- package/dist/agent/workers-ai/index.js +23 -0
- package/dist/agent/workers-ai/runtime.d.ts +42 -0
- package/dist/agent/workers-ai/runtime.js +63 -0
- package/dist/alarm/index.d.ts +77 -0
- package/dist/alarm/index.js +116 -0
- package/dist/config.d.ts +202 -0
- package/dist/config.js +135 -0
- package/dist/contract/index.d.ts +9 -0
- package/dist/contract/index.js +8 -0
- package/dist/contract/plugin.d.ts +324 -0
- package/dist/contract/plugin.js +114 -0
- package/dist/contract/recipe.d.ts +180 -0
- package/dist/contract/recipe.js +1 -0
- package/dist/contract/validation.d.ts +91 -0
- package/dist/contract/validation.js +84 -0
- package/dist/db/db.d.ts +147 -0
- package/dist/db/db.js +90 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrations/index.d.ts +20 -0
- package/dist/db/migrations/index.js +60 -0
- package/dist/db/models/subtasks.d.ts +100 -0
- package/dist/db/models/subtasks.js +241 -0
- package/dist/db/models/tasks.d.ts +118 -0
- package/dist/db/models/tasks.js +274 -0
- package/dist/db/schema.d.ts +468 -0
- package/dist/db/schema.js +88 -0
- package/dist/env.d.ts +53 -0
- package/dist/env.js +47 -0
- package/dist/host/agent.d.ts +305 -0
- package/dist/host/agent.js +400 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/index.js +19 -0
- package/dist/host/plugin-host.d.ts +42 -0
- package/dist/host/plugin-host.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/job/index.d.ts +21 -0
- package/dist/job/index.js +21 -0
- package/dist/job/lifecycle.d.ts +176 -0
- package/dist/job/lifecycle.js +230 -0
- package/dist/job/state.d.ts +92 -0
- package/dist/job/state.js +40 -0
- package/dist/platform.d.ts +138 -0
- package/dist/platform.js +140 -0
- package/dist/round/agent.d.ts +271 -0
- package/dist/round/agent.js +678 -0
- package/dist/round/index.d.ts +25 -0
- package/dist/round/index.js +23 -0
- package/dist/round/policy.d.ts +98 -0
- package/dist/round/policy.js +1 -0
- package/dist/round/subagent.d.ts +87 -0
- package/dist/round/subagent.js +108 -0
- package/dist/round/turn.d.ts +249 -0
- package/dist/round/turn.js +564 -0
- package/dist/round/workflow.d.ts +147 -0
- package/dist/round/workflow.js +387 -0
- package/dist/runtime/index.d.ts +127 -0
- package/dist/runtime/index.js +186 -0
- package/dist/runtime/tool-families.d.ts +32 -0
- package/dist/runtime/tool-families.js +61 -0
- package/dist/subagent/fingerprint.d.ts +37 -0
- package/dist/subagent/fingerprint.js +92 -0
- package/dist/subagent/index.d.ts +169 -0
- package/dist/subagent/index.js +330 -0
- package/dist/subagent/prompt.d.ts +32 -0
- package/dist/subagent/prompt.js +37 -0
- package/dist/subagent/run.d.ts +157 -0
- package/dist/subagent/run.js +540 -0
- package/dist/subagent/workspace.d.ts +85 -0
- package/dist/subagent/workspace.js +127 -0
- package/dist/subtasks/catalog.d.ts +40 -0
- package/dist/subtasks/catalog.js +36 -0
- package/dist/subtasks/decomposition.d.ts +85 -0
- package/dist/subtasks/decomposition.js +156 -0
- package/dist/subtasks/delegate.d.ts +120 -0
- package/dist/subtasks/delegate.js +131 -0
- package/dist/subtasks/index.d.ts +9 -0
- package/dist/subtasks/index.js +9 -0
- package/dist/subtasks/subtask-types.d.ts +91 -0
- package/dist/subtasks/subtask-types.js +103 -0
- package/dist/subtasks/types.d.ts +295 -0
- package/dist/subtasks/types.js +15 -0
- package/dist/testing/auth.d.ts +34 -0
- package/dist/testing/auth.js +35 -0
- package/dist/testing/do.d.ts +29 -0
- package/dist/testing/do.js +25 -0
- package/dist/testing/fake-session.d.ts +26 -0
- package/dist/testing/fake-session.js +37 -0
- package/dist/testing/fixtures.d.ts +64 -0
- package/dist/testing/fixtures.js +104 -0
- package/dist/testing/harness.d.ts +97 -0
- package/dist/testing/harness.js +138 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.js +35 -0
- package/dist/testing/mock-model.d.ts +77 -0
- package/dist/testing/mock-model.js +136 -0
- package/dist/testing/node.d.ts +56 -0
- package/dist/testing/node.js +56 -0
- package/dist/testing/vcr-global-setup.d.ts +12 -0
- package/dist/testing/vcr-global-setup.js +15 -0
- package/dist/testing/vcr-shared.d.ts +38 -0
- package/dist/testing/vcr-shared.js +33 -0
- package/dist/testing/vcr-spec.d.ts +25 -0
- package/dist/testing/vcr-spec.js +124 -0
- package/dist/testing/vcr-store.d.ts +86 -0
- package/dist/testing/vcr-store.js +191 -0
- package/dist/testing/vcr.d.ts +117 -0
- package/dist/testing/vcr.js +275 -0
- package/dist/worker/define-agent.d.ts +123 -0
- package/dist/worker/define-agent.js +20 -0
- package/dist/worker/index.d.ts +218 -0
- package/dist/worker/index.js +369 -0
- package/eslint-rules/index.js +31 -0
- package/eslint-rules/no-deprecated-object-properties.js +81 -0
- package/package.json +178 -0
- package/scripts/generate-keys.mjs +48 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Task, TaskState } from "@a2a-js/sdk";
|
|
2
|
+
import { and, count, desc, eq, gte, lt } from "drizzle-orm";
|
|
3
|
+
import { taskStateLabel } from "../../a2a/task.js";
|
|
4
|
+
import { buildSubmittedTask } from "../../a2a/notify.js";
|
|
5
|
+
import { notifyTasks } from "../schema.js";
|
|
6
|
+
function nowIso() {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A task's state, tolerating the `status`-less task the SDK's generated type
|
|
11
|
+
* permits (`TaskStatus | undefined`). Nothing we build omits it, so an
|
|
12
|
+
* unspecified state means the row came from somewhere unexpected — and it
|
|
13
|
+
* compares equal to none of the states the callers switch on.
|
|
14
|
+
*/
|
|
15
|
+
export function stateOf(task) {
|
|
16
|
+
return task.status?.state ?? TaskState.TASK_STATE_UNSPECIFIED;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The states a task never leaves.
|
|
20
|
+
*
|
|
21
|
+
* `INPUT_REQUIRED` and `AUTH_REQUIRED` are deliberately absent: a turn parked on
|
|
22
|
+
* either is waiting, not finished, and will move again.
|
|
23
|
+
*/
|
|
24
|
+
const TERMINAL_STATES = new Set([
|
|
25
|
+
TaskState.TASK_STATE_COMPLETED,
|
|
26
|
+
TaskState.TASK_STATE_FAILED,
|
|
27
|
+
TaskState.TASK_STATE_CANCELED,
|
|
28
|
+
TaskState.TASK_STATE_REJECTED
|
|
29
|
+
]);
|
|
30
|
+
function isTerminal(state) {
|
|
31
|
+
return TERMINAL_STATES.has(state);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Query methods for the `notify_tasks` table (async A2A task state).
|
|
35
|
+
*
|
|
36
|
+
* Bound to a drizzle handle by {@link AgentDB} and reached as `db.tasks.*`.
|
|
37
|
+
* Migrations are owned by `AgentDB`, not this factory — it only issues queries.
|
|
38
|
+
*
|
|
39
|
+
* Rows hold the task in its **A2A wire form** (`Task.toJSON`), not the in-memory
|
|
40
|
+
* protobuf shape the SDK hands us. Those differ under v1.0 — enums are numbers
|
|
41
|
+
* in memory but `SCREAMING_SNAKE` strings on the wire, and `Part.content` is a
|
|
42
|
+
* `{ $case, value }` wrapper in memory but a bare named key on the wire — so a
|
|
43
|
+
* plain `JSON.stringify` would persist a shape that is neither valid A2A JSON
|
|
44
|
+
* nor stable across SDK versions. Encoding on write and decoding on read keeps
|
|
45
|
+
* the stored bytes the spec's own format, which is also exactly what
|
|
46
|
+
* `postNotification` puts on the wire.
|
|
47
|
+
*/
|
|
48
|
+
export function makeTasks(db) {
|
|
49
|
+
// Every row was written by the builders in `a2a/notify`, which produce exactly
|
|
50
|
+
// the narrowed {@link PlainTask} shape, so the decode lands back on it.
|
|
51
|
+
const parse = (row) => Task.fromJSON(JSON.parse(row.taskJson));
|
|
52
|
+
const serialize = (task) => JSON.stringify(Task.toJSON(task));
|
|
53
|
+
const readOne = (taskId) => {
|
|
54
|
+
const row = db
|
|
55
|
+
.select()
|
|
56
|
+
.from(notifyTasks)
|
|
57
|
+
.where(eq(notifyTasks.taskId, taskId))
|
|
58
|
+
.get();
|
|
59
|
+
return row ? parse(row) : null;
|
|
60
|
+
};
|
|
61
|
+
const upsert = (task) => {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
// Denormalized so `ListTasks` can filter on it without decoding every blob.
|
|
64
|
+
const columns = {
|
|
65
|
+
contextId: task.contextId,
|
|
66
|
+
state: taskStateLabel(stateOf(task)),
|
|
67
|
+
taskJson: serialize(task),
|
|
68
|
+
updatedAt: now
|
|
69
|
+
};
|
|
70
|
+
db.insert(notifyTasks)
|
|
71
|
+
.values({ taskId: task.id, messageId: null, createdAt: now, ...columns })
|
|
72
|
+
.onConflictDoUpdate({ target: notifyTasks.taskId, set: columns })
|
|
73
|
+
.run();
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
/**
|
|
77
|
+
* Accept a turn: return the `submitted` task for the given `messageId`,
|
|
78
|
+
* creating it on first sight. Idempotent on `messageId` (the gatekeeper's
|
|
79
|
+
* dedupe key, stable across dispatch retries).
|
|
80
|
+
*/
|
|
81
|
+
begin(input) {
|
|
82
|
+
const existing = db
|
|
83
|
+
.select()
|
|
84
|
+
.from(notifyTasks)
|
|
85
|
+
.where(eq(notifyTasks.messageId, input.messageId))
|
|
86
|
+
.get();
|
|
87
|
+
if (existing)
|
|
88
|
+
return parse(existing);
|
|
89
|
+
const task = buildSubmittedTask(input.taskId, input.contextId);
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
db.insert(notifyTasks)
|
|
92
|
+
.values({
|
|
93
|
+
taskId: task.id,
|
|
94
|
+
messageId: input.messageId,
|
|
95
|
+
contextId: task.contextId,
|
|
96
|
+
state: taskStateLabel(task.status.state),
|
|
97
|
+
taskJson: serialize(task),
|
|
98
|
+
createdAt: now,
|
|
99
|
+
updatedAt: now
|
|
100
|
+
})
|
|
101
|
+
.run();
|
|
102
|
+
return task;
|
|
103
|
+
},
|
|
104
|
+
/** Load a task by id (for `GetTask` via the Worker's `DurableTaskStore`). */
|
|
105
|
+
get(taskId) {
|
|
106
|
+
return readOne(taskId);
|
|
107
|
+
},
|
|
108
|
+
/**
|
|
109
|
+
* A page of this caller's tasks for `ListTasks`, newest first, with the
|
|
110
|
+
* total matching count so the store can decide whether a next page exists.
|
|
111
|
+
*/
|
|
112
|
+
list(query) {
|
|
113
|
+
const filters = [];
|
|
114
|
+
if (query.contextId) {
|
|
115
|
+
filters.push(eq(notifyTasks.contextId, query.contextId));
|
|
116
|
+
}
|
|
117
|
+
if (query.state !== undefined) {
|
|
118
|
+
filters.push(eq(notifyTasks.state, taskStateLabel(query.state)));
|
|
119
|
+
}
|
|
120
|
+
if (query.updatedAfter !== undefined) {
|
|
121
|
+
filters.push(gte(notifyTasks.updatedAt, query.updatedAfter));
|
|
122
|
+
}
|
|
123
|
+
const where = filters.length > 0 ? and(...filters) : undefined;
|
|
124
|
+
const rows = db
|
|
125
|
+
.select()
|
|
126
|
+
.from(notifyTasks)
|
|
127
|
+
.where(where)
|
|
128
|
+
.orderBy(desc(notifyTasks.createdAt))
|
|
129
|
+
.limit(query.limit)
|
|
130
|
+
.offset(query.offset)
|
|
131
|
+
.all();
|
|
132
|
+
const total = db
|
|
133
|
+
.select({ value: count() })
|
|
134
|
+
.from(notifyTasks)
|
|
135
|
+
.where(where)
|
|
136
|
+
.get();
|
|
137
|
+
return {
|
|
138
|
+
tasks: rows.map((row) => project(parse(row), query)),
|
|
139
|
+
totalSize: total?.value ?? 0
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Upsert a task by id, preserving the `message_id` set by {@link begin}.
|
|
144
|
+
* Returns whether the write applied.
|
|
145
|
+
*
|
|
146
|
+
* Guarded exactly like {@link markWorking}, and for the same reason: a
|
|
147
|
+
* `canceled` row is terminal, so nothing may write a non-canceled state over
|
|
148
|
+
* it. That closes the window between a workflow's terminal build and its
|
|
149
|
+
* callback — the read-check-write is synchronous here, so a `CancelTask`
|
|
150
|
+
* landing mid-delivery makes this return `false` and the notify never fires.
|
|
151
|
+
*
|
|
152
|
+
* The reverse direction is guarded too: writing `canceled` over an already
|
|
153
|
+
* `completed`/`failed` row is refused, mirroring {@link cancel}'s own source
|
|
154
|
+
* guard. Without it, a cancellation landing between the Workflow's `complete`
|
|
155
|
+
* and `notify` steps — separate, independently-retried steps — could flip
|
|
156
|
+
* storage to canceled while `deliver()` still posts the cached completed
|
|
157
|
+
* task it already built, the exact race this guard exists to close. Writing
|
|
158
|
+
* `canceled` onto a `submitted`/`working` row, or re-writing it onto an
|
|
159
|
+
* already-`canceled` one, stays allowed: that is how the a2a-js handler's own
|
|
160
|
+
* cancel branch records the cancellation.
|
|
161
|
+
*
|
|
162
|
+
* **And no terminal row may be replaced by a *different* terminal state.**
|
|
163
|
+
* The two rules above were written about cancellation and between them left
|
|
164
|
+
* `completed → failed` wide open, which is not hypothetical: a workflow whose
|
|
165
|
+
* `notify` step exhausts its retries throws *after* `complete` durably saved
|
|
166
|
+
* a completed Task, and an abandoned-task recovery above it would then write
|
|
167
|
+
* a generic failure over a real answer and post a callback contradicting it.
|
|
168
|
+
* A turn that succeeded would be recorded as having failed because a webhook
|
|
169
|
+
* was flaky.
|
|
170
|
+
*
|
|
171
|
+
* Same terminal state re-written is still allowed, and must be: a Workflow
|
|
172
|
+
* replay legitimately re-runs `complete` and saves what it already saved, and
|
|
173
|
+
* refusing that would suppress the callback that replay exists to send.
|
|
174
|
+
*/
|
|
175
|
+
save(task) {
|
|
176
|
+
const existing = readOne(task.id);
|
|
177
|
+
if (existing === null) {
|
|
178
|
+
upsert(task);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
const existingState = stateOf(existing);
|
|
182
|
+
const incomingState = stateOf(task);
|
|
183
|
+
if (existingState === TaskState.TASK_STATE_CANCELED &&
|
|
184
|
+
incomingState !== TaskState.TASK_STATE_CANCELED) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (incomingState === TaskState.TASK_STATE_CANCELED &&
|
|
188
|
+
existingState !== TaskState.TASK_STATE_SUBMITTED &&
|
|
189
|
+
existingState !== TaskState.TASK_STATE_WORKING &&
|
|
190
|
+
existingState !== TaskState.TASK_STATE_CANCELED) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (isTerminal(existingState) &&
|
|
194
|
+
isTerminal(incomingState) &&
|
|
195
|
+
incomingState !== existingState) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
upsert(task);
|
|
199
|
+
return true;
|
|
200
|
+
},
|
|
201
|
+
/**
|
|
202
|
+
* Move a task to `working` (a turn workflow's first step). Returns
|
|
203
|
+
* `"canceled"` when the row is already canceled, which is the caller's
|
|
204
|
+
* signal to stop the turn; every other outcome — unknown task, or a task
|
|
205
|
+
* already past `submitted` — is a no-op reported as `"ok"`, because a
|
|
206
|
+
* workflow replay legitimately re-runs this step.
|
|
207
|
+
*/
|
|
208
|
+
markWorking(taskId) {
|
|
209
|
+
const task = readOne(taskId);
|
|
210
|
+
if (!task)
|
|
211
|
+
return "ok";
|
|
212
|
+
if (stateOf(task) === TaskState.TASK_STATE_CANCELED)
|
|
213
|
+
return "canceled";
|
|
214
|
+
if (stateOf(task) !== TaskState.TASK_STATE_SUBMITTED)
|
|
215
|
+
return "ok";
|
|
216
|
+
task.status = {
|
|
217
|
+
...task.status,
|
|
218
|
+
state: TaskState.TASK_STATE_WORKING,
|
|
219
|
+
message: task.status?.message,
|
|
220
|
+
timestamp: nowIso()
|
|
221
|
+
};
|
|
222
|
+
upsert(task);
|
|
223
|
+
return "ok";
|
|
224
|
+
},
|
|
225
|
+
/**
|
|
226
|
+
* Flip the task to `canceled` and return it, or `null` if the row is not
|
|
227
|
+
* eligible — unknown, or already past `submitted`/`working`. Guarding the
|
|
228
|
+
* source state (not just the destination, as {@link save} does) matters
|
|
229
|
+
* because `complete`/`notify` are separate Workflow steps: without this, a
|
|
230
|
+
* cancellation landing between them would flip an already-`completed` or
|
|
231
|
+
* `failed` row to `canceled` right as `deliver()` posts the terminal
|
|
232
|
+
* callback it had already built, silently rewriting a delivered result.
|
|
233
|
+
* Terminal: once this lands, {@link save} refuses every non-canceled write,
|
|
234
|
+
* so no completed or failed callback can be built from this row afterwards.
|
|
235
|
+
*/
|
|
236
|
+
cancel(taskId) {
|
|
237
|
+
const task = readOne(taskId);
|
|
238
|
+
if (!task)
|
|
239
|
+
return null;
|
|
240
|
+
const state = stateOf(task);
|
|
241
|
+
if (state !== TaskState.TASK_STATE_SUBMITTED &&
|
|
242
|
+
state !== TaskState.TASK_STATE_WORKING) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
task.status = {
|
|
246
|
+
...task.status,
|
|
247
|
+
state: TaskState.TASK_STATE_CANCELED,
|
|
248
|
+
message: task.status?.message,
|
|
249
|
+
timestamp: nowIso()
|
|
250
|
+
};
|
|
251
|
+
upsert(task);
|
|
252
|
+
return task;
|
|
253
|
+
},
|
|
254
|
+
/** Delete all tasks older than 30 days (called by the maintenance cron). */
|
|
255
|
+
cleanup() {
|
|
256
|
+
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
257
|
+
db.delete(notifyTasks).where(lt(notifyTasks.createdAt, cutoff)).run();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Apply the `ListTasks` response-shaping options to one stored task. Rows are
|
|
263
|
+
* JSON blobs, so the repeated fields are defaulted rather than assumed present.
|
|
264
|
+
*/
|
|
265
|
+
function project(task, query) {
|
|
266
|
+
const history = task.history ?? [];
|
|
267
|
+
return {
|
|
268
|
+
...task,
|
|
269
|
+
artifacts: query.includeArtifacts ? (task.artifacts ?? []) : [],
|
|
270
|
+
history: query.historyLength === undefined
|
|
271
|
+
? history
|
|
272
|
+
: history.slice(Math.max(0, history.length - query.historyLength))
|
|
273
|
+
};
|
|
274
|
+
}
|