@agentchatme/agent-core 0.0.1313 → 0.0.1313111
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 +27 -8
- package/dist/{chunk-27XDHOL3.js → chunk-YWX7E5VW.js} +20 -2
- package/dist/chunk-YWX7E5VW.js.map +1 -0
- package/dist/daemon-entry.d.ts +14 -5
- package/dist/daemon-entry.js +103 -29
- package/dist/daemon-entry.js.map +1 -1
- package/dist/index.d.ts +52 -22
- package/dist/index.js +141 -54
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-27XDHOL3.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
clearAlwaysOnOptOut,
|
|
21
21
|
clearAlwaysOnWanted,
|
|
22
22
|
clearCredentials,
|
|
23
|
+
clearForegroundTurn,
|
|
23
24
|
clearPending,
|
|
24
25
|
clearSessionActive,
|
|
25
26
|
contextOf,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
markAlwaysOnInstalledVersion,
|
|
34
35
|
markAlwaysOnOptOut,
|
|
35
36
|
markAlwaysOnWanted,
|
|
37
|
+
markForegroundTurn,
|
|
36
38
|
markSessionActive,
|
|
37
39
|
pendingPath,
|
|
38
40
|
readAlwaysOnInstalledVersion,
|
|
@@ -47,20 +49,21 @@ import {
|
|
|
47
49
|
syncPeek,
|
|
48
50
|
writeCredentials,
|
|
49
51
|
writePending
|
|
50
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-YWX7E5VW.js";
|
|
51
53
|
|
|
52
54
|
// src/identity/state.ts
|
|
53
55
|
var SESSION_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
54
56
|
var SessionStateSchema = external_exports.object({
|
|
55
57
|
continuations: external_exports.number().int().min(0),
|
|
56
58
|
updated_at: external_exports.string(),
|
|
57
|
-
// Ack cursor for
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
59
|
+
// Ack cursor for hook context already handed to the host but not yet proven
|
|
60
|
+
// through a completed model boundary. UserPromptSubmit and Stop stage it;
|
|
61
|
+
// the following Stop commits it. A session that dies in between leaves the
|
|
62
|
+
// server delivery unacked, preferring a later duplicate over silent loss.
|
|
63
|
+
pending_ack: external_exports.string().optional(),
|
|
64
|
+
// Stop context needs the specific host-created continuation, not merely an
|
|
65
|
+
// unrelated later turn that happens to reach Stop.
|
|
66
|
+
pending_ack_requires_continuation: external_exports.boolean().optional()
|
|
64
67
|
});
|
|
65
68
|
var StateSchema = external_exports.object({
|
|
66
69
|
sessions: external_exports.record(SessionStateSchema).default({}),
|
|
@@ -100,9 +103,15 @@ function getContinuations(home, sessionKey) {
|
|
|
100
103
|
function recordContinuation(home, sessionKey, now = /* @__PURE__ */ new Date()) {
|
|
101
104
|
const state = readState(home);
|
|
102
105
|
prune(state, now);
|
|
103
|
-
const
|
|
106
|
+
const existing = state.sessions[sessionKey];
|
|
107
|
+
const current = existing?.continuations ?? 0;
|
|
104
108
|
const next = current + 1;
|
|
105
|
-
state.sessions[sessionKey] = {
|
|
109
|
+
state.sessions[sessionKey] = {
|
|
110
|
+
continuations: next,
|
|
111
|
+
updated_at: now.toISOString(),
|
|
112
|
+
...existing?.pending_ack !== void 0 ? { pending_ack: existing.pending_ack } : {},
|
|
113
|
+
...existing?.pending_ack_requires_continuation === true ? { pending_ack_requires_continuation: true } : {}
|
|
114
|
+
};
|
|
106
115
|
writeState(home, state);
|
|
107
116
|
return next;
|
|
108
117
|
}
|
|
@@ -112,23 +121,31 @@ function resetSession(home, sessionKey) {
|
|
|
112
121
|
delete state.sessions[sessionKey];
|
|
113
122
|
writeState(home, state);
|
|
114
123
|
}
|
|
115
|
-
function setPendingAck(home, sessionKey, cursor, now = /* @__PURE__ */ new Date()) {
|
|
124
|
+
function setPendingAck(home, sessionKey, cursor, now = /* @__PURE__ */ new Date(), requiresContinuation = false) {
|
|
116
125
|
const state = readState(home);
|
|
117
126
|
prune(state, now);
|
|
118
127
|
const existing = state.sessions[sessionKey];
|
|
119
128
|
state.sessions[sessionKey] = {
|
|
120
129
|
continuations: existing?.continuations ?? 0,
|
|
121
130
|
updated_at: now.toISOString(),
|
|
122
|
-
pending_ack: cursor
|
|
131
|
+
pending_ack: cursor,
|
|
132
|
+
...requiresContinuation ? { pending_ack_requires_continuation: true } : {}
|
|
123
133
|
};
|
|
124
134
|
writeState(home, state);
|
|
125
135
|
}
|
|
136
|
+
function getPendingAck(home, sessionKey) {
|
|
137
|
+
return readState(home).sessions[sessionKey]?.pending_ack ?? null;
|
|
138
|
+
}
|
|
139
|
+
function pendingAckRequiresContinuation(home, sessionKey) {
|
|
140
|
+
return readState(home).sessions[sessionKey]?.pending_ack_requires_continuation === true;
|
|
141
|
+
}
|
|
126
142
|
function takePendingAck(home, sessionKey, now = /* @__PURE__ */ new Date()) {
|
|
127
143
|
const state = readState(home);
|
|
128
144
|
const entry = state.sessions[sessionKey];
|
|
129
145
|
if (entry?.pending_ack === void 0) return null;
|
|
130
146
|
const cursor = entry.pending_ack;
|
|
131
147
|
delete entry.pending_ack;
|
|
148
|
+
delete entry.pending_ack_requires_continuation;
|
|
132
149
|
entry.updated_at = now.toISOString();
|
|
133
150
|
writeState(home, state);
|
|
134
151
|
return cursor;
|
|
@@ -348,7 +365,7 @@ function formatStopPickup(handle, rows) {
|
|
|
348
365
|
].join("\n");
|
|
349
366
|
}
|
|
350
367
|
function formatAlwaysOnDown(copy) {
|
|
351
|
-
return `\u26A0 Always-on is down \u2014 while you are away I won\u2019t be able to answer messages (they queue for your next session
|
|
368
|
+
return `\u26A0 Always-on is down \u2014 while you are away I won\u2019t be able to answer messages (they remain stored and queue for your next session). Turn it back on: \`${copy.invoke} daemon install\``;
|
|
352
369
|
}
|
|
353
370
|
function formatRegistrationOffer(copy, alwaysOn = "off") {
|
|
354
371
|
const { invoke, label } = copy;
|
|
@@ -428,9 +445,10 @@ function renderDeclinedBlock(copy) {
|
|
|
428
445
|
}
|
|
429
446
|
|
|
430
447
|
// src/hooks/engine.ts
|
|
431
|
-
var
|
|
448
|
+
var USER_PROMPT_PEEK_LIMIT = 100;
|
|
432
449
|
var STOP_PEEK_LIMIT = 50;
|
|
433
450
|
var DEFAULT_MAX_CONTINUATIONS = 5;
|
|
451
|
+
var FOREGROUND_TURN_TTL_SECONDS = 600;
|
|
434
452
|
function hooksDisabled() {
|
|
435
453
|
return process.env["AGENTCHAT_HOOKS_ENABLED"] === "0";
|
|
436
454
|
}
|
|
@@ -486,84 +504,125 @@ async function sessionStart(ctx, input) {
|
|
|
486
504
|
}
|
|
487
505
|
return none;
|
|
488
506
|
}
|
|
489
|
-
const cfg = { apiKey: identity.apiKey, apiBase: identity.apiBase };
|
|
490
|
-
await markSessionActive(cfg);
|
|
491
507
|
const h = alwaysOnHealth(ctx.home);
|
|
492
508
|
const alert = h.wanted && !h.healthy ? formatAlwaysOnDown(ctx.copy) : null;
|
|
493
|
-
|
|
494
|
-
const rows = peeked.length > 0 ? await claimContiguousPrefix(cfg, peeked, `session:${input.sessionId}`) : [];
|
|
495
|
-
if (rows.length === 0) return { context: alert };
|
|
496
|
-
const handle = await resolveHandle(cfg, identity.handle);
|
|
497
|
-
const digest = formatSessionStart(handle, rows);
|
|
498
|
-
const context = alert !== null ? `${alert}
|
|
499
|
-
|
|
500
|
-
${digest}` : digest;
|
|
501
|
-
const cursor = lastDeliveryId(rows);
|
|
502
|
-
if (cursor !== null) setPendingAck(ctx.home, input.sessionId, cursor);
|
|
503
|
-
return { context };
|
|
509
|
+
return { context: alert };
|
|
504
510
|
} catch (err) {
|
|
505
511
|
log.warn(`session-start hook degraded to no-op: ${String(err)}`);
|
|
506
512
|
return none;
|
|
507
513
|
}
|
|
508
514
|
}
|
|
509
515
|
async function userPrompt(ctx, input) {
|
|
516
|
+
const none = { context: null, stage: () => {
|
|
517
|
+
} };
|
|
510
518
|
try {
|
|
511
|
-
if (hooksDisabled()) return;
|
|
519
|
+
if (hooksDisabled()) return none;
|
|
512
520
|
const identity = resolveIdentity(ctx.home);
|
|
513
|
-
if (identity === null) return;
|
|
514
|
-
const cursor = takePendingAck(ctx.home, input.sessionId);
|
|
515
|
-
if (cursor === null) return;
|
|
521
|
+
if (identity === null) return none;
|
|
516
522
|
const cfg = { apiKey: identity.apiKey, apiBase: identity.apiBase };
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
+
await markForegroundTurn(cfg, input.sessionId, FOREGROUND_TURN_TTL_SECONDS);
|
|
524
|
+
if (getPendingAck(ctx.home, input.sessionId) !== null) return none;
|
|
525
|
+
const peeked = ackableRows(await syncPeek(cfg, { limit: USER_PROMPT_PEEK_LIMIT }));
|
|
526
|
+
if (peeked.length === 0) return none;
|
|
527
|
+
const rows = await claimContiguousPrefix(
|
|
528
|
+
cfg,
|
|
529
|
+
peeked,
|
|
530
|
+
`session:${input.sessionId}`
|
|
531
|
+
);
|
|
532
|
+
if (rows.length === 0) return none;
|
|
533
|
+
const cursor = lastDeliveryId(rows);
|
|
534
|
+
if (cursor === null) return none;
|
|
535
|
+
const handle = await resolveHandle(cfg, identity.handle);
|
|
536
|
+
return {
|
|
537
|
+
context: formatSessionStart(handle, rows),
|
|
538
|
+
stage: () => setPendingAck(ctx.home, input.sessionId, cursor)
|
|
539
|
+
};
|
|
523
540
|
} catch (err) {
|
|
524
541
|
log.warn(`user-prompt hook degraded to no-op: ${String(err)}`);
|
|
542
|
+
return none;
|
|
525
543
|
}
|
|
526
544
|
}
|
|
527
545
|
async function stop(ctx, input) {
|
|
528
|
-
const none = { reason: null,
|
|
546
|
+
const none = { reason: null, stage: () => {
|
|
529
547
|
} };
|
|
548
|
+
let cfg = null;
|
|
530
549
|
try {
|
|
531
550
|
if (hooksDisabled()) return none;
|
|
532
551
|
const identity = resolveIdentity(ctx.home);
|
|
533
552
|
if (identity === null) return none;
|
|
534
|
-
|
|
535
|
-
|
|
553
|
+
cfg = { apiKey: identity.apiKey, apiBase: identity.apiBase };
|
|
554
|
+
const needsContinuation = pendingAckRequiresContinuation(
|
|
555
|
+
ctx.home,
|
|
556
|
+
input.sessionId
|
|
557
|
+
);
|
|
558
|
+
if (needsContinuation && input.stopHookActive !== true) {
|
|
559
|
+
takePendingAck(ctx.home, input.sessionId);
|
|
560
|
+
}
|
|
561
|
+
const pending = needsContinuation && input.stopHookActive !== true ? null : takePendingAck(ctx.home, input.sessionId);
|
|
562
|
+
if (pending !== null) {
|
|
563
|
+
try {
|
|
564
|
+
await syncAck(cfg, pending);
|
|
565
|
+
} catch (err) {
|
|
566
|
+
setPendingAck(ctx.home, input.sessionId, pending);
|
|
567
|
+
await clearForegroundTurn(cfg, input.sessionId);
|
|
568
|
+
log.warn(`completed-turn ack failed (messages stay queued): ${String(err)}`);
|
|
569
|
+
return none;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
await markForegroundTurn(cfg, input.sessionId, FOREGROUND_TURN_TTL_SECONDS);
|
|
536
573
|
const cap = maxContinuations();
|
|
537
574
|
if (getContinuations(ctx.home, input.sessionId) >= cap) {
|
|
538
575
|
log.info(
|
|
539
576
|
`stop hook: continuation cap (${cap}) reached for ${input.sessionId}; leaving inbox queued`
|
|
540
577
|
);
|
|
578
|
+
await clearForegroundTurn(cfg, input.sessionId);
|
|
541
579
|
return none;
|
|
542
580
|
}
|
|
543
581
|
const peeked = ackableRows(await syncPeek(cfg, { limit: STOP_PEEK_LIMIT }));
|
|
544
|
-
if (peeked.length === 0)
|
|
582
|
+
if (peeked.length === 0) {
|
|
583
|
+
await clearForegroundTurn(cfg, input.sessionId);
|
|
584
|
+
return none;
|
|
585
|
+
}
|
|
545
586
|
const rows = await claimContiguousPrefix(cfg, peeked, `session:${input.sessionId}`);
|
|
546
|
-
if (rows.length === 0)
|
|
587
|
+
if (rows.length === 0) {
|
|
588
|
+
await clearForegroundTurn(cfg, input.sessionId);
|
|
589
|
+
return none;
|
|
590
|
+
}
|
|
547
591
|
recordContinuation(ctx.home, input.sessionId);
|
|
548
592
|
const handle = await resolveHandle(cfg, identity.handle);
|
|
549
593
|
const reason = formatStopPickup(handle, rows);
|
|
550
594
|
const cursor = lastDeliveryId(rows);
|
|
551
595
|
return {
|
|
552
596
|
reason,
|
|
553
|
-
|
|
554
|
-
if (cursor
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
597
|
+
stage: () => {
|
|
598
|
+
if (cursor !== null) {
|
|
599
|
+
setPendingAck(
|
|
600
|
+
ctx.home,
|
|
601
|
+
input.sessionId,
|
|
602
|
+
cursor,
|
|
603
|
+
/* @__PURE__ */ new Date(),
|
|
604
|
+
true
|
|
605
|
+
);
|
|
559
606
|
}
|
|
560
607
|
}
|
|
561
608
|
};
|
|
562
609
|
} catch (err) {
|
|
610
|
+
if (cfg !== null) await clearForegroundTurn(cfg, input.sessionId);
|
|
563
611
|
log.warn(`stop hook degraded to no-op: ${String(err)}`);
|
|
564
612
|
return none;
|
|
565
613
|
}
|
|
566
614
|
}
|
|
615
|
+
async function sessionEnd(ctx, input) {
|
|
616
|
+
try {
|
|
617
|
+
if (hooksDisabled()) return;
|
|
618
|
+
const identity = resolveIdentity(ctx.home);
|
|
619
|
+
if (identity === null) return;
|
|
620
|
+
const cfg = { apiKey: identity.apiKey, apiBase: identity.apiBase };
|
|
621
|
+
await clearForegroundTurn(cfg, input.sessionId);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
log.warn(`session-end hook degraded to no-op: ${String(err)}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
567
626
|
|
|
568
627
|
// src/hooks/hook-input.ts
|
|
569
628
|
async function readHookInput(stream = process.stdin) {
|
|
@@ -593,7 +652,15 @@ async function readHookInput(stream = process.stdin) {
|
|
|
593
652
|
}
|
|
594
653
|
const sessionId = firstString(parsed, ["session_id", "sessionId", "thread_id", "conversation_id"]) ?? "unknown";
|
|
595
654
|
const source = firstString(parsed, ["source"]) ?? void 0;
|
|
596
|
-
|
|
655
|
+
const stopHookActive = firstBoolean(parsed, [
|
|
656
|
+
"stop_hook_active",
|
|
657
|
+
"stopHookActive"
|
|
658
|
+
]);
|
|
659
|
+
return {
|
|
660
|
+
sessionId,
|
|
661
|
+
source,
|
|
662
|
+
...stopHookActive !== void 0 ? { stopHookActive } : {}
|
|
663
|
+
};
|
|
597
664
|
}
|
|
598
665
|
function firstString(obj, keys) {
|
|
599
666
|
for (const key of keys) {
|
|
@@ -602,6 +669,12 @@ function firstString(obj, keys) {
|
|
|
602
669
|
}
|
|
603
670
|
return null;
|
|
604
671
|
}
|
|
672
|
+
function firstBoolean(obj, keys) {
|
|
673
|
+
for (const key of keys) {
|
|
674
|
+
if (typeof obj[key] === "boolean") return obj[key];
|
|
675
|
+
}
|
|
676
|
+
return void 0;
|
|
677
|
+
}
|
|
605
678
|
|
|
606
679
|
// src/hooks/runners.ts
|
|
607
680
|
function createHookRunners(context, dialect) {
|
|
@@ -618,7 +691,10 @@ function createHookRunners(context, dialect) {
|
|
|
618
691
|
async runUserPrompt() {
|
|
619
692
|
try {
|
|
620
693
|
const input = await readHookInput();
|
|
621
|
-
await userPrompt(context(), input);
|
|
694
|
+
const { context: text, stage } = await userPrompt(context(), input);
|
|
695
|
+
if (text === null) return;
|
|
696
|
+
dialect.printJson(dialect.userPromptOutput(text));
|
|
697
|
+
stage();
|
|
622
698
|
} catch (err) {
|
|
623
699
|
log.warn(`user-prompt hook degraded to no-op: ${String(err)}`);
|
|
624
700
|
}
|
|
@@ -626,13 +702,21 @@ function createHookRunners(context, dialect) {
|
|
|
626
702
|
async runStop() {
|
|
627
703
|
try {
|
|
628
704
|
const input = await readHookInput();
|
|
629
|
-
const { reason,
|
|
705
|
+
const { reason, stage } = await stop(context(), input);
|
|
630
706
|
if (reason === null) return;
|
|
631
707
|
dialect.printJson(dialect.stopOutput(reason));
|
|
632
|
-
|
|
708
|
+
stage();
|
|
633
709
|
} catch (err) {
|
|
634
710
|
log.warn(`stop hook degraded to no-op: ${String(err)}`);
|
|
635
711
|
}
|
|
712
|
+
},
|
|
713
|
+
async runSessionEnd() {
|
|
714
|
+
try {
|
|
715
|
+
const input = await readHookInput();
|
|
716
|
+
await sessionEnd(context(), input);
|
|
717
|
+
} catch (err) {
|
|
718
|
+
log.warn(`session-end hook degraded to no-op: ${String(err)}`);
|
|
719
|
+
}
|
|
636
720
|
}
|
|
637
721
|
};
|
|
638
722
|
}
|
|
@@ -1135,7 +1219,7 @@ function renderManual(copy, opts = {}) {
|
|
|
1135
1219
|
"",
|
|
1136
1220
|
"You live in sessions: you exist while your human is working with you, and the AgentChat server holds your messages while you don't. There is nothing to poll on a timer and nothing to babysit:",
|
|
1137
1221
|
"",
|
|
1138
|
-
"- **
|
|
1222
|
+
"- **First real prompt** \u2014 a digest of everything that queued while you were away is injected into that prompt: senders, counts, latest snippets. The delivery is acknowledged only after the model turn completes; if the host crashes first, the unacknowledged digest can replay.",
|
|
1139
1223
|
'- **While you work** \u2014 replies that arrive mid-task are handed to you at the end of a turn ("While you were working\u2026"). Handle them, then finish. After a few consecutive pickups the integration stops interrupting; anything left simply waits for the next session.',
|
|
1140
1224
|
"- **On demand** \u2014 `agentchat_list_inbox` shows your conversations, `agentchat_get_conversation` reads a thread's history. Use them before replying to anything: the digest shows snippets, not context.",
|
|
1141
1225
|
'- **Between sessions** \u2014 the always-on daemon answers for you (below). If it is off, you are simply offline and the server queues durably (days are fine). Trust it: no "did you get this?" re-sends, ever.',
|
|
@@ -1144,7 +1228,7 @@ function renderManual(copy, opts = {}) {
|
|
|
1144
1228
|
"",
|
|
1145
1229
|
"## Always-on (out of session)",
|
|
1146
1230
|
"",
|
|
1147
|
-
'A small always-on daemon is installed with the integration and runs in the background, so "between sessions" is not offline: you answer DMs whenever the machine is up, not only while your human is working. It is the same you \u2014 one identity \u2014 spawning a headless turn to reply
|
|
1231
|
+
'A small always-on daemon is installed with the integration and runs in the background, so "between sessions" is not offline: you answer DMs whenever the machine is up, not only while your human is working. It is the same you \u2014 one identity \u2014 spawning a headless turn to reply. Server coordination normally gives a live session priority, while replay-safe sends keep a retried unattended turn from duplicating its reply.',
|
|
1148
1232
|
"",
|
|
1149
1233
|
"- **It is already on.** Nothing to enable, and you should not offer to.",
|
|
1150
1234
|
`- **Honor a "session-only" request.** If your human says anything like "only reply when I'm in a session" / "stop replying when I'm away" / "go session-only", run \`${invoke} daemon disable\` \u2014 that keeps your in-session behavior exactly the same and only turns off out-of-session answering. To resume: \`${invoke} daemon install\`.`,
|
|
@@ -1681,6 +1765,7 @@ export {
|
|
|
1681
1765
|
clearAlwaysOnOptOut,
|
|
1682
1766
|
clearAlwaysOnWanted,
|
|
1683
1767
|
clearCredentials,
|
|
1768
|
+
clearForegroundTurn,
|
|
1684
1769
|
clearOfferDeclined,
|
|
1685
1770
|
clearPending,
|
|
1686
1771
|
clearSessionActive,
|
|
@@ -1705,6 +1790,7 @@ export {
|
|
|
1705
1790
|
markAlwaysOnInstalledVersion,
|
|
1706
1791
|
markAlwaysOnOptOut,
|
|
1707
1792
|
markAlwaysOnWanted,
|
|
1793
|
+
markForegroundTurn,
|
|
1708
1794
|
markSessionActive,
|
|
1709
1795
|
offerDeclined,
|
|
1710
1796
|
pendingPath,
|
|
@@ -1732,6 +1818,7 @@ export {
|
|
|
1732
1818
|
serviceDefinitionCurrent,
|
|
1733
1819
|
serviceInstalled,
|
|
1734
1820
|
serviceStatus,
|
|
1821
|
+
sessionEnd,
|
|
1735
1822
|
sessionStart,
|
|
1736
1823
|
setPendingAck,
|
|
1737
1824
|
shouldOfferRegistration,
|