@c4t4/heyamigo 0.9.19 → 0.9.20
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/dist/estimates/async-task.js +29 -0
- package/dist/estimates/browser-task.js +49 -0
- package/dist/estimates/image-gen.js +7 -0
- package/dist/estimates/index.js +7 -1
- package/dist/estimates/registry.js +4 -1
- package/dist/gateway/outgoing.js +19 -1
- package/dist/queue/worker.js +42 -2
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Generic async-task estimator (non-browser background work).
|
|
2
|
+
//
|
|
3
|
+
// The general async lane is still on in-memory fastq (no durable
|
|
4
|
+
// table). Real duration samples aren't queryable yet → the estimate
|
|
5
|
+
// uses defaultMs every time until/unless that lane gets migrated to
|
|
6
|
+
// SQLite. Cards still surface useful "long task incoming" UX.
|
|
7
|
+
import { aggregateMean, humanDur, registerEstimator, } from './registry.js';
|
|
8
|
+
class AsyncTaskEstimator {
|
|
9
|
+
kind = 'async-task';
|
|
10
|
+
// 3 min — generic background work tends to be moderate. A deeper
|
|
11
|
+
// research task might run longer; a quick one shorter. Single
|
|
12
|
+
// ballpark until we have real samples.
|
|
13
|
+
defaultMs = 3 * 60 * 1000;
|
|
14
|
+
matches(ctx) {
|
|
15
|
+
return ctx.taskKind === 'async';
|
|
16
|
+
}
|
|
17
|
+
// No durable samples (general async lane is still in-memory fastq).
|
|
18
|
+
// Returning [] forces aggregateMean to fall back to defaultMs.
|
|
19
|
+
querySamples() {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
estimate(samples) {
|
|
23
|
+
return aggregateMean(samples, this.defaultMs);
|
|
24
|
+
}
|
|
25
|
+
format(estimate) {
|
|
26
|
+
return `background task, ~${humanDur(estimate.pointMs)}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
registerEstimator(new AsyncTaskEstimator());
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Generic browser-task estimator. Matches any agent-delegated
|
|
2
|
+
// [ASYNC-BROWSER:] task. Pulls duration samples from the durable
|
|
3
|
+
// browser_tasks table so the average reflects real observed runtimes.
|
|
4
|
+
import { and, desc, eq, isNotNull } from 'drizzle-orm';
|
|
5
|
+
import { getDb } from '../db/index.js';
|
|
6
|
+
import { browserTasks } from '../db/schema.js';
|
|
7
|
+
import { aggregateMean, humanDur, registerEstimator, } from './registry.js';
|
|
8
|
+
class BrowserTaskEstimator {
|
|
9
|
+
kind = 'browser-task';
|
|
10
|
+
// 5 min is a reasonable ballpark for IG/TT scrapes. Real samples
|
|
11
|
+
// dominate after the first 1-2 jobs.
|
|
12
|
+
defaultMs = 5 * 60 * 1000;
|
|
13
|
+
matches(ctx) {
|
|
14
|
+
return ctx.taskKind === 'async-browser';
|
|
15
|
+
}
|
|
16
|
+
querySamples(limit = 20) {
|
|
17
|
+
const db = getDb();
|
|
18
|
+
// All done browser tasks — single bucket. Could be sliced further
|
|
19
|
+
// (per-domain) later via more-specific estimators registered ahead
|
|
20
|
+
// of this catch-all.
|
|
21
|
+
const rows = db
|
|
22
|
+
.select({
|
|
23
|
+
claimedAt: browserTasks.claimedAt,
|
|
24
|
+
updatedAt: browserTasks.updatedAt,
|
|
25
|
+
})
|
|
26
|
+
.from(browserTasks)
|
|
27
|
+
.where(and(eq(browserTasks.status, 'done'), isNotNull(browserTasks.claimedAt)))
|
|
28
|
+
.orderBy(desc(browserTasks.id))
|
|
29
|
+
.limit(limit)
|
|
30
|
+
.all();
|
|
31
|
+
return rows
|
|
32
|
+
.filter((r) => r.claimedAt !== null)
|
|
33
|
+
.map((r) => ({
|
|
34
|
+
durationMs: (r.updatedAt - r.claimedAt) * 1000,
|
|
35
|
+
finishedAt: r.updatedAt,
|
|
36
|
+
}))
|
|
37
|
+
.filter((s) => s.durationMs > 0);
|
|
38
|
+
}
|
|
39
|
+
estimate(samples) {
|
|
40
|
+
return aggregateMean(samples, this.defaultMs);
|
|
41
|
+
}
|
|
42
|
+
format(estimate) {
|
|
43
|
+
if (estimate.rangeMs) {
|
|
44
|
+
return `browser task, ~${humanDur(estimate.rangeMs.lowMs)} to ~${humanDur(estimate.rangeMs.highMs)}`;
|
|
45
|
+
}
|
|
46
|
+
return `browser task, ~${humanDur(estimate.pointMs)}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
registerEstimator(new BrowserTaskEstimator());
|
|
@@ -15,6 +15,13 @@ class ImageGenEstimator {
|
|
|
15
15
|
// observations.
|
|
16
16
|
defaultMs = 30_000;
|
|
17
17
|
matches(ctx) {
|
|
18
|
+
// Only match direct user input. When taskKind is set, the context
|
|
19
|
+
// is an agent-delegated task — those go through the browser/async
|
|
20
|
+
// estimators below, not here. Prevents an agent's
|
|
21
|
+
// "[ASYNC-BROWSER: generate marketing image of X]" from being
|
|
22
|
+
// mis-classified as a user-typed image-gen request.
|
|
23
|
+
if (ctx.taskKind)
|
|
24
|
+
return false;
|
|
18
25
|
return IMAGE_GEN_RE.test(ctx.description);
|
|
19
26
|
}
|
|
20
27
|
estimate(samples) {
|
package/dist/estimates/index.js
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Adding a new kind = drop a file alongside image-gen.ts and import
|
|
8
8
|
// it below. No other code in the codebase needs to change.
|
|
9
|
+
// Order matters: more-specific estimators register first so they win
|
|
10
|
+
// classify() over the catch-all task estimators. image-gen and other
|
|
11
|
+
// user-input matchers can run first because they explicitly DON'T
|
|
12
|
+
// match when ctx.taskKind is set.
|
|
9
13
|
import './image-gen.js';
|
|
10
|
-
|
|
14
|
+
import './browser-task.js'; // catches all [ASYNC-BROWSER:] tasks
|
|
15
|
+
import './async-task.js'; // catches all [ASYNC:] tasks
|
|
16
|
+
// future: import './browser-ig.js' // more specific than browser-task
|
|
11
17
|
// future: import './voice-gen.js'
|
|
12
18
|
export { classify, estimate, formatEstimateDefault, humanDur, listEstimators, querySamplesForKind, registerEstimator, } from './registry.js';
|
|
@@ -61,7 +61,10 @@ export function estimate(ctx) {
|
|
|
61
61
|
const e = classify(ctx);
|
|
62
62
|
if (!e)
|
|
63
63
|
return null;
|
|
64
|
-
|
|
64
|
+
// Estimator's own querySamples (if provided) takes precedence —
|
|
65
|
+
// browser/async estimators pull from their dedicated tables. Otherwise
|
|
66
|
+
// fall back to the inbound-by-kind default.
|
|
67
|
+
const samples = e.querySamples ? e.querySamples() : querySamplesForKind(e.kind);
|
|
65
68
|
const result = e.estimate(samples);
|
|
66
69
|
const text = (e.format ?? formatEstimateDefault)(result);
|
|
67
70
|
return { kind: e.kind, result, text };
|
package/dist/gateway/outgoing.js
CHANGED
|
@@ -108,7 +108,25 @@ export async function handleReply(job, result, _originalMsg) {
|
|
|
108
108
|
enqueuePiece({ address, kind: 'text', text: chunkForSend });
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
// Job cards (ETAs for delegated async/browser tasks) go LAST so
|
|
112
|
+
// they arrive after the agent's reply chunks in chat. Each card
|
|
113
|
+
// has its own producer-supplied idempotencyKey; we don't slot them
|
|
114
|
+
// into the piece-numbered key space.
|
|
115
|
+
for (const card of result.jobCards ?? []) {
|
|
116
|
+
enqueueOutbound({
|
|
117
|
+
address,
|
|
118
|
+
kind: 'text',
|
|
119
|
+
text: card.text,
|
|
120
|
+
idempotencyKey: card.idempotencyKey,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
logger.info({
|
|
124
|
+
jid: job.jid,
|
|
125
|
+
files: files.length,
|
|
126
|
+
chars: text.length,
|
|
127
|
+
pieces: pieceIdx,
|
|
128
|
+
cards: result.jobCards?.length ?? 0,
|
|
129
|
+
}, 'reply enqueued for outbound');
|
|
112
130
|
}
|
|
113
131
|
// Proactive outbound: send a message to a chat without an incoming
|
|
114
132
|
// trigger. Same parsing as handleReply; enqueues outbound rows.
|
package/dist/queue/worker.js
CHANGED
|
@@ -4,6 +4,7 @@ import { config } from '../config.js';
|
|
|
4
4
|
import { formatAddress, jidToAddress } from '../db/address.js';
|
|
5
5
|
import { logger } from '../logger.js';
|
|
6
6
|
import { addDailyTokens } from '../store/usage.js';
|
|
7
|
+
import { estimate as estimateJob } from '../estimates/index.js';
|
|
7
8
|
import { extractFlags, filterFlagsByRole } from '../memory/digest-flag.js';
|
|
8
9
|
import { isValidSlug } from '../memory/journals.js';
|
|
9
10
|
import { enqueueAsyncTask, enqueueBrowserTask } from './async-tasks.js';
|
|
@@ -171,7 +172,15 @@ async function callClaude(job) {
|
|
|
171
172
|
// [ASYNC:...] → general lane, stateless, concurrency 3, non-browser work
|
|
172
173
|
// [ASYNC-BROWSER:...] → browser lane, persistent session, concurrency 1
|
|
173
174
|
// Both report back via initiate() when done.
|
|
174
|
-
|
|
175
|
+
//
|
|
176
|
+
// For each delegation, we also build a "job card" — a short ETA
|
|
177
|
+
// message that handleReply will emit after the agent's reply
|
|
178
|
+
// chunks. Gives the user a visible "doing X, ~Y min" instead of
|
|
179
|
+
// wondering whether anything's happening.
|
|
180
|
+
const jobCards = [];
|
|
181
|
+
const cardBase = `card-${job.jid}-${Date.now()}`;
|
|
182
|
+
for (let i = 0; i < asyncTasks.length; i++) {
|
|
183
|
+
const t = asyncTasks[i];
|
|
175
184
|
enqueueAsyncTask({
|
|
176
185
|
jid: job.jid,
|
|
177
186
|
senderNumber: job.senderNumber,
|
|
@@ -179,8 +188,19 @@ async function callClaude(job) {
|
|
|
179
188
|
originatingMessage: job.text,
|
|
180
189
|
allowedTools: job.allowedTools ?? 'all',
|
|
181
190
|
});
|
|
191
|
+
const est = estimateJob({
|
|
192
|
+
description: t.description,
|
|
193
|
+
taskKind: 'async',
|
|
194
|
+
});
|
|
195
|
+
if (est) {
|
|
196
|
+
jobCards.push({
|
|
197
|
+
text: formatJobCard(est.text, t.description),
|
|
198
|
+
idempotencyKey: `${cardBase}-async-${i}`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
182
201
|
}
|
|
183
|
-
for (
|
|
202
|
+
for (let i = 0; i < asyncBrowserTasks.length; i++) {
|
|
203
|
+
const t = asyncBrowserTasks[i];
|
|
184
204
|
enqueueBrowserTask({
|
|
185
205
|
jid: job.jid,
|
|
186
206
|
senderNumber: job.senderNumber,
|
|
@@ -188,6 +208,16 @@ async function callClaude(job) {
|
|
|
188
208
|
originatingMessage: job.text,
|
|
189
209
|
allowedTools: job.allowedTools ?? 'all',
|
|
190
210
|
});
|
|
211
|
+
const est = estimateJob({
|
|
212
|
+
description: t.description,
|
|
213
|
+
taskKind: 'async-browser',
|
|
214
|
+
});
|
|
215
|
+
if (est) {
|
|
216
|
+
jobCards.push({
|
|
217
|
+
text: formatJobCard(est.text, t.description),
|
|
218
|
+
idempotencyKey: `${cardBase}-browser-${i}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
191
221
|
}
|
|
192
222
|
// SEND-TEXT: cross-chat text send. Agent specified the destination
|
|
193
223
|
// address explicitly. Just drops a row in outbound; sender worker
|
|
@@ -250,8 +280,18 @@ async function callClaude(job) {
|
|
|
250
280
|
journalSlugs: journals.map((j) => j.slug),
|
|
251
281
|
asyncCount: asyncTasks.length + asyncBrowserTasks.length,
|
|
252
282
|
},
|
|
283
|
+
jobCards: jobCards.length > 0 ? jobCards : undefined,
|
|
253
284
|
};
|
|
254
285
|
}
|
|
286
|
+
// Compact card text. Emoji + ETA + a brief excerpt of what the agent
|
|
287
|
+
// delegated, so the user knows which job each card refers to when
|
|
288
|
+
// multiple are running.
|
|
289
|
+
function formatJobCard(etaText, description) {
|
|
290
|
+
const excerpt = description.length > 100
|
|
291
|
+
? description.slice(0, 97) + '...'
|
|
292
|
+
: description;
|
|
293
|
+
return `🔄 ${etaText}\n${excerpt}`;
|
|
294
|
+
}
|
|
255
295
|
function titleCase(slug) {
|
|
256
296
|
return slug
|
|
257
297
|
.split('-')
|