@zooid/core 0.12.0 → 0.14.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/dist/index.d.ts +222 -5
- package/dist/index.js +262 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/acp-registry.test.ts +14 -0
- package/src/acp-registry.ts +14 -17
- package/src/acp-types.ts +2 -1
- package/src/config.test.ts +1 -0
- package/src/config.ts +314 -0
- package/src/index.ts +12 -11
- package/src/match-expression.test.ts +61 -0
- package/src/match-expression.ts +33 -0
- package/src/render-template.test.ts +38 -0
- package/src/render-template.ts +32 -0
- package/src/task-actions.ts +84 -0
- package/src/transport-context.test.ts +11 -4
- package/src/transport-context.ts +18 -2
- package/src/triggers-config.test.ts +168 -0
- package/src/triggers-messages-config.test.ts +146 -0
- package/src/triggers-webhook-config.test.ts +143 -0
- package/src/types.ts +65 -0
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ import { EventEmitter } from 'node:events';
|
|
|
7
7
|
* Per-agent ACP block in zooid.yaml. XOR: either a known preset or an
|
|
8
8
|
* explicit command. The schema parser rejects both/neither.
|
|
9
9
|
*
|
|
10
|
-
* Built-in presets: `claude`, `codex`, `opencode`, `cline`, `kiro`,
|
|
10
|
+
* Built-in presets: `claude`, `codex`, `opencode`, `pi`, `cline`, `kiro`,
|
|
11
|
+
* `gemini`.
|
|
11
12
|
* See `@zooid/acp-client`'s preset registry for the current list.
|
|
12
13
|
*/
|
|
13
14
|
type AcpAgentSpec = {
|
|
@@ -318,12 +319,74 @@ interface ZooidConfig {
|
|
|
318
319
|
pre_turn?: string;
|
|
319
320
|
post_turn?: string;
|
|
320
321
|
};
|
|
322
|
+
/** Optional. Map of trigger name → trigger. Empty map when the block is absent. */
|
|
323
|
+
triggers: Record<string, TriggerConfig>;
|
|
321
324
|
}
|
|
322
325
|
interface CliFlags {
|
|
323
326
|
runtime?: string;
|
|
324
327
|
/** Container image override (shorthand for container.image). */
|
|
325
328
|
image?: string;
|
|
326
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* A webhook trigger: exposes `POST /_zooid/webhooks/<name>`, verifies an HMAC
|
|
332
|
+
* signature over the raw request body, and fires like a schedule trigger
|
|
333
|
+
* once accepted. See [[ZOD082]] §Design 4 and [[ZOD086]].
|
|
334
|
+
*/
|
|
335
|
+
interface WebhookTriggerConfig {
|
|
336
|
+
provider: 'github' | 'stripe' | 'slack' | 'standard' | 'custom';
|
|
337
|
+
/** Required. Interpolated from env — see [[ZOD081]] §Design 4, Secrets. */
|
|
338
|
+
secret: string;
|
|
339
|
+
/**
|
|
340
|
+
* `custom` only, required: path to a module exporting the verifier
|
|
341
|
+
* function, resolved to an absolute path against the zooid.yaml directory
|
|
342
|
+
* at parse time. The escape hatch for any scheme without a named provider
|
|
343
|
+
* — ed25519, SHA-1 over sorted params, bespoke timestamped base strings.
|
|
344
|
+
* Code rather than declarative fields, because a config language for
|
|
345
|
+
* signing schemes is a security-critical mini-DSL that still would not
|
|
346
|
+
* cover them all. The daemon imports it at startup — see
|
|
347
|
+
* `loadCustomVerifiers` in the CLI.
|
|
348
|
+
*/
|
|
349
|
+
verify?: string;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* One message a trigger can post: a room, the agent to mention, the body
|
|
353
|
+
* template, and — webhook triggers only — a CEL `match:` predicate gating
|
|
354
|
+
* whether this message fires for a given delivery. See [[ZOD085]] §Design 4.
|
|
355
|
+
*/
|
|
356
|
+
interface TriggerMessage {
|
|
357
|
+
/** Room id or alias the message goes to. */
|
|
358
|
+
room: string;
|
|
359
|
+
/** Agent name (key in `agents`) to mention. Structural, never templated — §Design 3. */
|
|
360
|
+
mention: string;
|
|
361
|
+
/** The message body. Literal for a scheduled trigger; `${...}` is CEL-interpolated for a webhook trigger. */
|
|
362
|
+
text: string;
|
|
363
|
+
/**
|
|
364
|
+
* CEL predicate over the delivery (`event`, `body`, `headers`, `output`).
|
|
365
|
+
* Absent means always fire. Only valid on a `webhook:` trigger — a
|
|
366
|
+
* schedule trigger has no delivery to evaluate.
|
|
367
|
+
*/
|
|
368
|
+
match?: string;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* A trigger: fires on a cron or a webhook delivery and posts one or more
|
|
372
|
+
* `messages`, each structurally mentioning its agent so the turn starts
|
|
373
|
+
* through the ordinary message path. No emitter/command tier — see
|
|
374
|
+
* [[ZOD081]] §Concept.
|
|
375
|
+
*/
|
|
376
|
+
interface TriggerConfig {
|
|
377
|
+
/** Cron expression. Mutually exclusive with `webhook:` — exactly one is required. */
|
|
378
|
+
schedule?: string;
|
|
379
|
+
/** Webhook ingress config. Mutually exclusive with `schedule:` — [[ZOD082]]. */
|
|
380
|
+
webhook?: WebhookTriggerConfig;
|
|
381
|
+
/** Full MXID the trigger posts as, e.g. `@cron:example.org`. */
|
|
382
|
+
as: string;
|
|
383
|
+
/**
|
|
384
|
+
* The messages this trigger can post. A flat `room:`/`mention:`/`text:`/
|
|
385
|
+
* `match:` at the trigger level desugars into a single-entry list here at
|
|
386
|
+
* config load — nothing downstream branches on which spelling was used.
|
|
387
|
+
*/
|
|
388
|
+
messages: TriggerMessage[];
|
|
389
|
+
}
|
|
327
390
|
|
|
328
391
|
interface LoadZooidConfigOptions {
|
|
329
392
|
/**
|
|
@@ -333,6 +396,13 @@ interface LoadZooidConfigOptions {
|
|
|
333
396
|
* path.
|
|
334
397
|
*/
|
|
335
398
|
configDir?: string;
|
|
399
|
+
/**
|
|
400
|
+
* Cron-expression validator, called as `validateCron(name, expr)` and
|
|
401
|
+
* expected to throw on an invalid expression. `core` takes no cron
|
|
402
|
+
* dependency — `cli` passes croner's parser at load time. Defaults to a
|
|
403
|
+
* field-count check sufficient to catch malformed input at parse time.
|
|
404
|
+
*/
|
|
405
|
+
validateCron?: (name: string, expr: string) => void;
|
|
336
406
|
}
|
|
337
407
|
declare function loadZooidConfig(yamlText: string, opts?: LoadZooidConfigOptions): ZooidConfig;
|
|
338
408
|
declare function findTransport(cfg: ZooidConfig, name: string): TransportConfig | undefined;
|
|
@@ -350,6 +420,42 @@ interface FoundConfigFile {
|
|
|
350
420
|
declare function findConfigFile(cwd: string): FoundConfigFile | null;
|
|
351
421
|
declare function mergeCliFlags(base: ZooidConfig, flags: CliFlags): ZooidConfig;
|
|
352
422
|
|
|
423
|
+
interface MatchContext {
|
|
424
|
+
event: string | undefined;
|
|
425
|
+
body: unknown;
|
|
426
|
+
headers: Record<string, string>;
|
|
427
|
+
output: string;
|
|
428
|
+
}
|
|
429
|
+
/** Parse-check an expression. Throws on a syntax error, so a typo fails at config load. */
|
|
430
|
+
declare function compileMatch(expr: string): string;
|
|
431
|
+
/**
|
|
432
|
+
* Only an actual `true` fires. Everything else is "no match":
|
|
433
|
+
* - a CelError value (missing field, undeclared variable) — the library
|
|
434
|
+
* returns these rather than throwing, and a merged-PR filter legitimately
|
|
435
|
+
* errors on every issues delivery;
|
|
436
|
+
* - a truthy non-boolean (a string, a number), which must never be coerced;
|
|
437
|
+
* - a throw.
|
|
438
|
+
* Failing closed matters more here than anywhere else in the ingress: this is
|
|
439
|
+
* the only place an operator's typo could otherwise open a filter.
|
|
440
|
+
*/
|
|
441
|
+
declare function evaluateMatch(expr: string, ctx: MatchContext): boolean;
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Fill `${...}` placeholders in a trigger's `text:`. Each placeholder is a
|
|
445
|
+
* CEL expression over the same bindings `match:` sees — one expression
|
|
446
|
+
* language, not two. `${output}` is not a special case: `output` is just
|
|
447
|
+
* another bound variable, so a whole-payload dump still works for senders we
|
|
448
|
+
* control ([[ZOD081]] §2).
|
|
449
|
+
*
|
|
450
|
+
* An unresolvable placeholder (missing field, bad expression) renders as
|
|
451
|
+
* empty rather than throwing or pasting an error object into a room — a bad
|
|
452
|
+
* placeholder must not take the message down.
|
|
453
|
+
*
|
|
454
|
+
* The result is never re-scanned, so a payload that itself contains the
|
|
455
|
+
* literal string `${...}` cannot inject a placeholder.
|
|
456
|
+
*/
|
|
457
|
+
declare function renderTemplate(template: string, ctx: MatchContext): string;
|
|
458
|
+
|
|
353
459
|
interface RegisteredApproval {
|
|
354
460
|
approvalId: string;
|
|
355
461
|
agentName: string;
|
|
@@ -469,8 +575,14 @@ interface AcpAgentRegistryOptions {
|
|
|
469
575
|
* when the workspace mount is active; falls back to `agent.workdir`.
|
|
470
576
|
*/
|
|
471
577
|
cwd?: Record<string, string>;
|
|
578
|
+
/**
|
|
579
|
+
* Called after an ACP session is created or recovered. Context adapters
|
|
580
|
+
* which cannot receive an MCP spawn id (Pi) use this to resolve their
|
|
581
|
+
* daemon-side binding by the ACP session id instead.
|
|
582
|
+
*/
|
|
583
|
+
onSessionEstablished?: (agentName: string, sessionKey: string, sessionId: string) => void;
|
|
472
584
|
}
|
|
473
|
-
type ContextSpawnFactory = (threadId: string, channelId?: string) => Promise<{
|
|
585
|
+
type ContextSpawnFactory = (threadId: string, channelId?: string, sessionKey?: string) => Promise<{
|
|
474
586
|
name: 'zooid-context';
|
|
475
587
|
command: string;
|
|
476
588
|
args: string[];
|
|
@@ -531,11 +643,21 @@ interface Member {
|
|
|
531
643
|
is_agent: boolean;
|
|
532
644
|
agent_name?: string;
|
|
533
645
|
}
|
|
534
|
-
|
|
646
|
+
/** Renamed from ChannelInfo — "channel" is not a Matrix primitive ([[ZOD084]]). */
|
|
647
|
+
interface RoomInfo {
|
|
535
648
|
id: string;
|
|
536
649
|
name: string;
|
|
537
650
|
transport: 'http' | 'matrix';
|
|
538
651
|
}
|
|
652
|
+
interface SendMessageInput {
|
|
653
|
+
room: string;
|
|
654
|
+
thread_id?: string;
|
|
655
|
+
text: string;
|
|
656
|
+
}
|
|
657
|
+
interface SendMessageResult {
|
|
658
|
+
event_id: string;
|
|
659
|
+
thread_id?: string;
|
|
660
|
+
}
|
|
539
661
|
interface HistoryPage {
|
|
540
662
|
messages: Message[];
|
|
541
663
|
next_before?: string;
|
|
@@ -583,7 +705,102 @@ interface TransportContextProvider {
|
|
|
583
705
|
getRecentThreads(channelId: string, opts: HistoryOptions): Promise<ThreadOverviewPage>;
|
|
584
706
|
getThreadHistory(channelId: string, threadId: string, opts: HistoryOptions): Promise<HistoryPage>;
|
|
585
707
|
getChannelMembers(channelId: string): Promise<Member[]>;
|
|
586
|
-
getChannelInfo
|
|
708
|
+
/** Renamed from getChannelInfo. */
|
|
709
|
+
getRoomInfo(channelId: string): Promise<RoomInfo>;
|
|
710
|
+
/** Rooms this agent is bound to. Targets for sendMessage. */
|
|
711
|
+
getRooms(): Promise<RoomInfo[]>;
|
|
712
|
+
sendMessage(input: SendMessageInput): Promise<SendMessageResult>;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Harness-independent delegated-task contracts ([[ZOD072]]). */
|
|
716
|
+
interface TaskCallerRef {
|
|
717
|
+
agentName: string;
|
|
718
|
+
channelId: string;
|
|
719
|
+
threadRoot: string;
|
|
720
|
+
sessionKey: string;
|
|
721
|
+
}
|
|
722
|
+
interface StartTaskSpec {
|
|
723
|
+
agent: string;
|
|
724
|
+
prompt: string;
|
|
725
|
+
}
|
|
726
|
+
interface StartTasksInput {
|
|
727
|
+
tasks: StartTaskSpec[];
|
|
728
|
+
notify?: 'caller' | 'none';
|
|
729
|
+
}
|
|
730
|
+
type StartTaskResult = {
|
|
731
|
+
agent: string;
|
|
732
|
+
status: 'started';
|
|
733
|
+
thread_id: string;
|
|
734
|
+
} | {
|
|
735
|
+
agent: string;
|
|
736
|
+
status: 'refused' | 'failed';
|
|
737
|
+
reason: string;
|
|
738
|
+
attempt_id?: string;
|
|
739
|
+
};
|
|
740
|
+
interface StartTasksOutput {
|
|
741
|
+
results: StartTaskResult[];
|
|
742
|
+
notify: 'caller' | 'none';
|
|
743
|
+
/** Stated at the point of decision, where the model actually reads it. */
|
|
744
|
+
delivery: string;
|
|
745
|
+
}
|
|
746
|
+
interface CompleteTaskInput {
|
|
747
|
+
summary: string;
|
|
748
|
+
}
|
|
749
|
+
interface CompleteTaskOutput {
|
|
750
|
+
status: 'recorded' | 'already_recorded' | 'refused';
|
|
751
|
+
reason?: string;
|
|
752
|
+
}
|
|
753
|
+
/** What this session is, so the surface can gate itself instead of refusing later. */
|
|
754
|
+
interface TaskRole {
|
|
755
|
+
is_task_assignee: boolean;
|
|
756
|
+
can_start_task_threads: boolean;
|
|
757
|
+
}
|
|
758
|
+
interface TaskActions {
|
|
759
|
+
startTasks(caller: TaskCallerRef, input: StartTasksInput): Promise<StartTasksOutput>;
|
|
760
|
+
completeTask(caller: TaskCallerRef, input: CompleteTaskInput): Promise<CompleteTaskOutput>;
|
|
761
|
+
describeRole(caller: TaskCallerRef): Promise<TaskRole>;
|
|
762
|
+
}
|
|
763
|
+
/** One in-thread handoff inside a delegated task. */
|
|
764
|
+
type InvocationState = 'outstanding' | 'returned' | 'cancelled';
|
|
765
|
+
interface InvocationRecord {
|
|
766
|
+
invocationId: string;
|
|
767
|
+
taskId: string;
|
|
768
|
+
callerAgent: string;
|
|
769
|
+
callerSessionKey: string;
|
|
770
|
+
calleeAgent: string;
|
|
771
|
+
callEventId?: string;
|
|
772
|
+
calleeSessionKey?: string;
|
|
773
|
+
state: InvocationState;
|
|
774
|
+
}
|
|
775
|
+
/** Open human-input requests for a session. ZOD078 supplies the implementation. */
|
|
776
|
+
interface PendingInputRegistry {
|
|
777
|
+
countFor(sessionKey: string): number;
|
|
778
|
+
cancelFor(sessionKeys: string[]): void;
|
|
779
|
+
}
|
|
780
|
+
declare const NO_PENDING_INPUT: PendingInputRegistry;
|
|
781
|
+
declare const THREAD_START_FIELD = "dev.zooid.thread_start";
|
|
782
|
+
declare const THREAD_RESULT_FIELD = "dev.zooid.thread_result";
|
|
783
|
+
interface ThreadStartContent {
|
|
784
|
+
version: 1;
|
|
785
|
+
assignee: string;
|
|
786
|
+
attempt_id: string;
|
|
787
|
+
parent: {
|
|
788
|
+
agent: string;
|
|
789
|
+
thread_root: string;
|
|
790
|
+
session_key: string;
|
|
791
|
+
};
|
|
792
|
+
notify: 'caller' | 'none';
|
|
793
|
+
}
|
|
794
|
+
interface ThreadCompletion {
|
|
795
|
+
agent: string;
|
|
796
|
+
thread_id: string;
|
|
797
|
+
status: 'complete' | 'failed' | 'cancelled' | 'partial';
|
|
798
|
+
output?: {
|
|
799
|
+
type: 'message';
|
|
800
|
+
text: string;
|
|
801
|
+
};
|
|
802
|
+
reason?: string;
|
|
803
|
+
error?: string;
|
|
587
804
|
}
|
|
588
805
|
|
|
589
|
-
export { AcpAgentRegistry, type AcpAgentRegistryOptions, type AcpAgentSpec, type AcpMount, type AcpRegistry, type AcpRegistryApprovalHandler, type AcpRegistryEventHandler, type AcpRuntime, type AcpSpawnSpec, type AgentConfig, ApprovalCorrelator, type
|
|
806
|
+
export { AcpAgentRegistry, type AcpAgentRegistryOptions, type AcpAgentSpec, type AcpMount, type AcpRegistry, type AcpRegistryApprovalHandler, type AcpRegistryEventHandler, type AcpRuntime, type AcpSpawnSpec, type AgentConfig, ApprovalCorrelator, type CliFlags, type CompleteTaskInput, type CompleteTaskOutput, type ContainerConfig, type ContextSpawnFactory, type HistoryOptions, type HistoryPage, type HttpBinding, type HttpTransportConfig, type InboundMessage, type InvocationRecord, type InvocationState, type LoadZooidConfigOptions, type MatchContext, type MatrixBinding, type MatrixTransportConfig, type Member, type Message, type MountConfig, NO_PENDING_INPUT, type PendingInputRegistry, type RegisterOptions, type RegisteredApproval, type RoomBinding, type RoomInfo, type SendMessageInput, type SendMessageResult, type StartTaskResult, type StartTaskSpec, type StartTasksInput, type StartTasksOutput, THREAD_RESULT_FIELD, THREAD_START_FIELD, type TaskActions, type TaskCallerRef, type TaskRole, type ThreadCompletion, type ThreadOverview, type ThreadOverviewPage, type ThreadRef, type ThreadStartContent, type Transport, type TransportConfig, type TransportContextProvider, type TriggerConfig, type TriggerMessage, type WebhookTriggerConfig, type ZooidConfig, type ZooidContainerConfig, compileMatch, evaluateMatch, findConfigFile, findHttpTransport, findMatrixTransport, findTransport, loadZooidConfig, mergeCliFlags, renderTemplate, resolveAcpAgentSpec };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { isAbsolute, join, resolve as pathResolve } from "path";
|
|
4
|
-
import { parse } from "yaml";
|
|
4
|
+
import { parse as parse2 } from "yaml";
|
|
5
5
|
import { isPreset } from "@zooid/acp-client";
|
|
6
6
|
|
|
7
7
|
// src/env-interpolation.ts
|
|
@@ -54,7 +54,23 @@ function interpolateString(value, processEnv) {
|
|
|
54
54
|
return result.parsed?.[sentinel] ?? "";
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// src/match-expression.ts
|
|
58
|
+
import { parse, run } from "@bufbuild/cel";
|
|
59
|
+
function compileMatch(expr) {
|
|
60
|
+
parse(expr);
|
|
61
|
+
return expr;
|
|
62
|
+
}
|
|
63
|
+
function evaluateMatch(expr, ctx) {
|
|
64
|
+
try {
|
|
65
|
+
const result = run(expr, { ...ctx });
|
|
66
|
+
return result === true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
// src/config.ts
|
|
73
|
+
var WEBHOOK_PROVIDERS = ["github", "stripe", "slack", "standard", "custom"];
|
|
58
74
|
var SLUG_RE = /^[a-z0-9-]+$/;
|
|
59
75
|
var AGENT_NAME_RE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
60
76
|
var MATRIX_USER_ID_RE = /^@[A-Za-z0-9._\-=/+]+:[A-Za-z0-9.\-]+$/;
|
|
@@ -707,6 +723,203 @@ See [ZOD043].`
|
|
|
707
723
|
}
|
|
708
724
|
return result;
|
|
709
725
|
}
|
|
726
|
+
function defaultValidateCron(name, expr) {
|
|
727
|
+
const parts = expr.trim().split(/\s+/);
|
|
728
|
+
if (parts.length < 5 || parts.length > 7) {
|
|
729
|
+
throw new Error(
|
|
730
|
+
`triggers.${name}.schedule: invalid cron expression ${JSON.stringify(expr)}`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function expandTriggerAs(name, as, transports) {
|
|
735
|
+
if (as.includes(":")) {
|
|
736
|
+
if (!MATRIX_USER_ID_RE.test(as)) {
|
|
737
|
+
throw new Error(`triggers.${name}.as: must be a full MXID (got ${JSON.stringify(as)})`);
|
|
738
|
+
}
|
|
739
|
+
return as;
|
|
740
|
+
}
|
|
741
|
+
const bare = as.startsWith("@") ? as : `@${as}`;
|
|
742
|
+
if (!MATRIX_USER_LOCALPART_RE.test(bare)) {
|
|
743
|
+
throw new Error(`triggers.${name}.as: must be a full MXID (got ${JSON.stringify(as)})`);
|
|
744
|
+
}
|
|
745
|
+
const matrixTransports = Object.values(transports).filter(
|
|
746
|
+
(t) => t.type === "matrix"
|
|
747
|
+
);
|
|
748
|
+
if (matrixTransports.length !== 1) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
`triggers.${name}.as: "${as}" is a bare localpart, which requires exactly one matrix transport to expand against (found ${matrixTransports.length}). Use a full MXID instead.`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
const serverName = deriveServerName(matrixTransports[0].user_namespace);
|
|
754
|
+
return `${bare}:${serverName}`;
|
|
755
|
+
}
|
|
756
|
+
function parseTriggers(raw, agents, transports, validateCron, processEnv, configDir) {
|
|
757
|
+
if (raw === void 0 || raw === null) return {};
|
|
758
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
759
|
+
throw new Error("triggers: must be a mapping");
|
|
760
|
+
}
|
|
761
|
+
const result = {};
|
|
762
|
+
for (const [name, val] of Object.entries(raw)) {
|
|
763
|
+
if (!val || typeof val !== "object" || Array.isArray(val)) {
|
|
764
|
+
throw new Error(`triggers.${name} must be a mapping`);
|
|
765
|
+
}
|
|
766
|
+
const t = val;
|
|
767
|
+
if (t.run !== void 0) {
|
|
768
|
+
throw new Error(
|
|
769
|
+
`triggers.${name}.run: is not supported \u2014 a trigger posts a message and the agent runs what needs running. See [ZOD081] \xA7Concept.`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
if (t.schedule === void 0 && t.webhook === void 0) {
|
|
773
|
+
throw new Error(`triggers.${name}: must specify schedule: or webhook:`);
|
|
774
|
+
}
|
|
775
|
+
if (t.schedule !== void 0 && t.webhook !== void 0) {
|
|
776
|
+
throw new Error(`triggers.${name}: specify either schedule: or webhook:, not both`);
|
|
777
|
+
}
|
|
778
|
+
let schedule;
|
|
779
|
+
if (t.schedule !== void 0) {
|
|
780
|
+
if (typeof t.schedule !== "string" || t.schedule.length === 0) {
|
|
781
|
+
throw new Error(`triggers.${name}.schedule: must be a non-empty string`);
|
|
782
|
+
}
|
|
783
|
+
validateCron(name, t.schedule);
|
|
784
|
+
schedule = t.schedule;
|
|
785
|
+
}
|
|
786
|
+
let webhook;
|
|
787
|
+
if (t.webhook !== void 0) {
|
|
788
|
+
webhook = parseWebhookTrigger(name, t.webhook, processEnv, configDir);
|
|
789
|
+
}
|
|
790
|
+
if (typeof t.as !== "string" || t.as.length === 0) {
|
|
791
|
+
throw new Error(`triggers.${name}.as: must be a non-empty string`);
|
|
792
|
+
}
|
|
793
|
+
const as = expandTriggerAs(name, t.as, transports);
|
|
794
|
+
const hasFlat = t.room !== void 0 || t.mention !== void 0 || t.text !== void 0 || t.match !== void 0;
|
|
795
|
+
const hasMessages = t.messages !== void 0;
|
|
796
|
+
if (hasFlat && hasMessages) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`triggers.${name}: specify either room:/mention:/text: or messages:, not both`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
if (!hasFlat && !hasMessages) {
|
|
802
|
+
throw new Error(`triggers.${name}: must specify room:/mention:/text: or messages:`);
|
|
803
|
+
}
|
|
804
|
+
let rawMessages;
|
|
805
|
+
if (hasFlat) {
|
|
806
|
+
if (t.match !== void 0 && !webhook) {
|
|
807
|
+
throw new Error(`triggers.${name}.match: only applies to a webhook: trigger`);
|
|
808
|
+
}
|
|
809
|
+
rawMessages = [{ room: t.room, mention: t.mention, text: t.text, match: t.match }];
|
|
810
|
+
} else {
|
|
811
|
+
if (!Array.isArray(t.messages)) {
|
|
812
|
+
throw new Error(`triggers.${name}.messages: must be a list`);
|
|
813
|
+
}
|
|
814
|
+
if (t.messages.length === 0) {
|
|
815
|
+
throw new Error(`triggers.${name}.messages: must not be empty`);
|
|
816
|
+
}
|
|
817
|
+
rawMessages = t.messages;
|
|
818
|
+
}
|
|
819
|
+
const messages = rawMessages.map(
|
|
820
|
+
(m, i) => parseTriggerMessage(name, i, m, agents, as, !!webhook)
|
|
821
|
+
);
|
|
822
|
+
const entry = { as, messages };
|
|
823
|
+
if (schedule !== void 0) entry.schedule = schedule;
|
|
824
|
+
if (webhook !== void 0) entry.webhook = webhook;
|
|
825
|
+
result[name] = entry;
|
|
826
|
+
}
|
|
827
|
+
return result;
|
|
828
|
+
}
|
|
829
|
+
function parseTriggerMessage(name, index, raw, agents, as, hasWebhook) {
|
|
830
|
+
const label = `triggers.${name}.messages[${index}]`;
|
|
831
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
832
|
+
throw new Error(`${label}: must be a mapping`);
|
|
833
|
+
}
|
|
834
|
+
const m = raw;
|
|
835
|
+
if (typeof m.room !== "string" || m.room.length === 0) {
|
|
836
|
+
throw new Error(`${label}.room: must be a non-empty string`);
|
|
837
|
+
}
|
|
838
|
+
if (!MATRIX_ROOM_IDENT_RE.test(m.room)) {
|
|
839
|
+
throw new Error(`${label}.room: must start with '#' or '!' (got ${JSON.stringify(m.room)})`);
|
|
840
|
+
}
|
|
841
|
+
if (typeof m.mention !== "string" || m.mention.length === 0) {
|
|
842
|
+
throw new Error(`${label}.mention: must be a non-empty string`);
|
|
843
|
+
}
|
|
844
|
+
const mentionedAgent = agents[m.mention];
|
|
845
|
+
if (!mentionedAgent) {
|
|
846
|
+
throw new Error(`${label}.mention: unknown agent "${m.mention}"`);
|
|
847
|
+
}
|
|
848
|
+
if (!mentionedAgent.matrix) {
|
|
849
|
+
throw new Error(
|
|
850
|
+
`${label}.mention: agent "${m.mention}" has no matrix: binding \u2014 a trigger posts through Matrix, so the mentioned agent must be matrix-bound.`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if (as === mentionedAgent.matrix.user_id) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`triggers.${name}.as: must not equal the mentioned agent's own MXID (${as}) \u2014 a message an agent sends never routes back to itself, so this trigger would silently never wake "${m.mention}". Post as a different identity (a dedicated bot, or another agent's).`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (typeof m.text !== "string" || m.text.length === 0) {
|
|
859
|
+
throw new Error(`${label}.text: must be a non-empty string`);
|
|
860
|
+
}
|
|
861
|
+
const message = { room: m.room, mention: m.mention, text: m.text };
|
|
862
|
+
if (m.match !== void 0) {
|
|
863
|
+
if (typeof m.match !== "string" || m.match.length === 0) {
|
|
864
|
+
throw new Error(`${label}.match: must be a non-empty string`);
|
|
865
|
+
}
|
|
866
|
+
if (!hasWebhook) {
|
|
867
|
+
throw new Error(`${label}.match: only applies to a webhook: trigger`);
|
|
868
|
+
}
|
|
869
|
+
try {
|
|
870
|
+
message.match = compileMatch(m.match);
|
|
871
|
+
} catch (err) {
|
|
872
|
+
throw new Error(`${label}.match: ${err.message}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return message;
|
|
876
|
+
}
|
|
877
|
+
function parseWebhookTrigger(name, raw, processEnv, configDir) {
|
|
878
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
879
|
+
throw new Error(`triggers.${name}.webhook: must be a mapping`);
|
|
880
|
+
}
|
|
881
|
+
const w = raw;
|
|
882
|
+
if (typeof w.provider !== "string" || w.provider.length === 0) {
|
|
883
|
+
throw new Error(`triggers.${name}.webhook.provider: must be a non-empty string`);
|
|
884
|
+
}
|
|
885
|
+
if (!WEBHOOK_PROVIDERS.includes(w.provider)) {
|
|
886
|
+
throw new Error(
|
|
887
|
+
`triggers.${name}.webhook.provider: unknown provider ${JSON.stringify(w.provider)} (expected one of: ${WEBHOOK_PROVIDERS.join(", ")})`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
const provider = w.provider;
|
|
891
|
+
if (w.event !== void 0) {
|
|
892
|
+
throw new Error(`triggers.${name}.webhook.event: no longer supported \u2014 use match:`);
|
|
893
|
+
}
|
|
894
|
+
if (typeof w.secret !== "string" || w.secret.length === 0) {
|
|
895
|
+
throw new Error(`triggers.${name}.webhook.secret: is required`);
|
|
896
|
+
}
|
|
897
|
+
const secret = interpolateString(w.secret, processEnv);
|
|
898
|
+
const config = { provider, secret };
|
|
899
|
+
if (provider !== "custom") {
|
|
900
|
+
if (w.verify !== void 0) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
`triggers.${name}.webhook.verify: only applies to provider: custom (this trigger uses provider: ${provider}, whose signing scheme is built in)`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
return config;
|
|
906
|
+
}
|
|
907
|
+
if (typeof w.verify !== "string" || w.verify.length === 0) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
`triggers.${name}.webhook.verify: is required when provider: custom (path to a module exporting the verifier function)`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
if (isAbsolute(w.verify)) {
|
|
913
|
+
config.verify = w.verify;
|
|
914
|
+
} else if (!configDir) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`triggers.${name}.webhook.verify: relative path ${JSON.stringify(w.verify)} requires configDir (zooid.yaml directory) \u2014 pass it via loadZooidConfig(yaml, { configDir })`
|
|
917
|
+
);
|
|
918
|
+
} else {
|
|
919
|
+
config.verify = pathResolve(configDir, w.verify);
|
|
920
|
+
}
|
|
921
|
+
return config;
|
|
922
|
+
}
|
|
710
923
|
function parseRuntime(raw) {
|
|
711
924
|
const runtime = raw ?? "docker";
|
|
712
925
|
if (runtime !== "local" && runtime !== "docker" && runtime !== "podman") {
|
|
@@ -724,7 +937,7 @@ function zooidHooks(raw) {
|
|
|
724
937
|
return out;
|
|
725
938
|
}
|
|
726
939
|
function loadZooidConfig(yamlText, opts = {}) {
|
|
727
|
-
const raw =
|
|
940
|
+
const raw = parse2(yamlText) ?? {};
|
|
728
941
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
729
942
|
throw new Error("zooid.yaml must be a YAML object");
|
|
730
943
|
}
|
|
@@ -767,11 +980,20 @@ function loadZooidConfig(yamlText, opts = {}) {
|
|
|
767
980
|
const transports = parseTransports(r.transports, processEnv, workstation);
|
|
768
981
|
const hooks = zooidHooks(r);
|
|
769
982
|
const agents = parseAgents(r.agents, runtime, transports, hooks, processEnv, opts.configDir);
|
|
983
|
+
const triggers = parseTriggers(
|
|
984
|
+
r.triggers,
|
|
985
|
+
agents,
|
|
986
|
+
transports,
|
|
987
|
+
opts.validateCron ?? defaultValidateCron,
|
|
988
|
+
processEnv,
|
|
989
|
+
opts.configDir
|
|
990
|
+
);
|
|
770
991
|
const cfg = {
|
|
771
992
|
runtime,
|
|
772
993
|
transports,
|
|
773
994
|
agents,
|
|
774
|
-
hooks
|
|
995
|
+
hooks,
|
|
996
|
+
triggers
|
|
775
997
|
};
|
|
776
998
|
if (workstation !== void 0) cfg.workstation = workstation;
|
|
777
999
|
if (r.container !== void 0 && r.container !== null) {
|
|
@@ -834,7 +1056,8 @@ function mergeCliFlags(base, flags) {
|
|
|
834
1056
|
runtime,
|
|
835
1057
|
transports: base.transports,
|
|
836
1058
|
agents: base.agents,
|
|
837
|
-
hooks: { ...base.hooks }
|
|
1059
|
+
hooks: { ...base.hooks },
|
|
1060
|
+
triggers: base.triggers
|
|
838
1061
|
};
|
|
839
1062
|
if (runtime === "docker" || runtime === "podman") {
|
|
840
1063
|
const image = flags.image ?? base.container?.image;
|
|
@@ -847,6 +1070,22 @@ function mergeCliFlags(base, flags) {
|
|
|
847
1070
|
return merged;
|
|
848
1071
|
}
|
|
849
1072
|
|
|
1073
|
+
// src/render-template.ts
|
|
1074
|
+
import { run as run2, isCelError } from "@bufbuild/cel";
|
|
1075
|
+
var PLACEHOLDER_RE = /\$\{([\s\S]+?)\}/g;
|
|
1076
|
+
function renderTemplate(template, ctx) {
|
|
1077
|
+
return template.replace(PLACEHOLDER_RE, (_match, expr) => {
|
|
1078
|
+
let result;
|
|
1079
|
+
try {
|
|
1080
|
+
result = run2(expr, { ...ctx });
|
|
1081
|
+
} catch {
|
|
1082
|
+
return "";
|
|
1083
|
+
}
|
|
1084
|
+
if (result === void 0 || isCelError(result)) return "";
|
|
1085
|
+
return String(result);
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
850
1089
|
// src/acp-registry.ts
|
|
851
1090
|
import { mkdirSync } from "fs";
|
|
852
1091
|
import { join as join2 } from "path";
|
|
@@ -906,7 +1145,9 @@ var AcpAgentRegistry = class {
|
|
|
906
1145
|
async ensureSession(name, threadId, channelId, contextThreadId) {
|
|
907
1146
|
if (!this.hasAgent(name)) throw new Error(`unknown agent: ${name}`);
|
|
908
1147
|
const client = await this.ensureClient(name);
|
|
909
|
-
|
|
1148
|
+
const sessionId = await client.ensureSession(threadId, channelId, contextThreadId);
|
|
1149
|
+
this.opts.onSessionEstablished?.(name, threadId, sessionId);
|
|
1150
|
+
return sessionId;
|
|
910
1151
|
}
|
|
911
1152
|
endSession(name, threadId) {
|
|
912
1153
|
if (!this.hasAgent(name)) return;
|
|
@@ -930,9 +1171,7 @@ var AcpAgentRegistry = class {
|
|
|
930
1171
|
return client.prompt(input);
|
|
931
1172
|
}
|
|
932
1173
|
async stopAll() {
|
|
933
|
-
await Promise.allSettled(
|
|
934
|
-
[...this.clients.values()].map((c) => c.stop())
|
|
935
|
-
);
|
|
1174
|
+
await Promise.allSettled([...this.clients.values()].map((c) => c.stop()));
|
|
936
1175
|
this.clients.clear();
|
|
937
1176
|
}
|
|
938
1177
|
async ensureClient(name) {
|
|
@@ -1069,15 +1308,30 @@ var ApprovalCorrelator = class extends EventEmitter {
|
|
|
1069
1308
|
};
|
|
1070
1309
|
}
|
|
1071
1310
|
};
|
|
1311
|
+
|
|
1312
|
+
// src/task-actions.ts
|
|
1313
|
+
var NO_PENDING_INPUT = {
|
|
1314
|
+
countFor: () => 0,
|
|
1315
|
+
cancelFor: () => {
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
var THREAD_START_FIELD = "dev.zooid.thread_start";
|
|
1319
|
+
var THREAD_RESULT_FIELD = "dev.zooid.thread_result";
|
|
1072
1320
|
export {
|
|
1073
1321
|
AcpAgentRegistry,
|
|
1074
1322
|
ApprovalCorrelator,
|
|
1323
|
+
NO_PENDING_INPUT,
|
|
1324
|
+
THREAD_RESULT_FIELD,
|
|
1325
|
+
THREAD_START_FIELD,
|
|
1326
|
+
compileMatch,
|
|
1327
|
+
evaluateMatch,
|
|
1075
1328
|
findConfigFile,
|
|
1076
1329
|
findHttpTransport,
|
|
1077
1330
|
findMatrixTransport,
|
|
1078
1331
|
findTransport,
|
|
1079
1332
|
loadZooidConfig,
|
|
1080
1333
|
mergeCliFlags,
|
|
1334
|
+
renderTemplate,
|
|
1081
1335
|
resolveAcpAgentSpec
|
|
1082
1336
|
};
|
|
1083
1337
|
//# sourceMappingURL=index.js.map
|