@animalabs/agent-framework 0.6.3 → 0.6.5

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.
Files changed (66) hide show
  1. package/README.md +5 -0
  2. package/dist/src/agent.d.ts +3 -1
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +27 -2
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/framework.d.ts +46 -1
  7. package/dist/src/framework.d.ts.map +1 -1
  8. package/dist/src/framework.js +280 -146
  9. package/dist/src/framework.js.map +1 -1
  10. package/dist/src/index.d.ts +1 -0
  11. package/dist/src/index.d.ts.map +1 -1
  12. package/dist/src/index.js +1 -0
  13. package/dist/src/index.js.map +1 -1
  14. package/dist/src/modules/discord/index.d.ts +1 -0
  15. package/dist/src/modules/discord/index.d.ts.map +1 -1
  16. package/dist/src/modules/discord/index.js +4 -1
  17. package/dist/src/modules/discord/index.js.map +1 -1
  18. package/dist/src/modules/discord/types.d.ts +2 -0
  19. package/dist/src/modules/discord/types.d.ts.map +1 -1
  20. package/dist/src/modules/health/index.d.ts +2 -0
  21. package/dist/src/modules/health/index.d.ts.map +1 -1
  22. package/dist/src/modules/health/index.js +15 -4
  23. package/dist/src/modules/health/index.js.map +1 -1
  24. package/dist/src/timezone.d.ts +14 -0
  25. package/dist/src/timezone.d.ts.map +1 -0
  26. package/dist/src/timezone.js +74 -0
  27. package/dist/src/timezone.js.map +1 -0
  28. package/dist/src/types/agent.d.ts +5 -2
  29. package/dist/src/types/agent.d.ts.map +1 -1
  30. package/dist/src/types/framework.d.ts +15 -0
  31. package/dist/src/types/framework.d.ts.map +1 -1
  32. package/dist/test/activation-request-sanitize.test.d.ts +2 -0
  33. package/dist/test/activation-request-sanitize.test.d.ts.map +1 -0
  34. package/dist/test/activation-request-sanitize.test.js +89 -0
  35. package/dist/test/activation-request-sanitize.test.js.map +1 -0
  36. package/dist/test/agent-cache-ttl.test.js +18 -2
  37. package/dist/test/agent-cache-ttl.test.js.map +1 -1
  38. package/dist/test/ephemeral-settle.test.d.ts +2 -0
  39. package/dist/test/ephemeral-settle.test.d.ts.map +1 -0
  40. package/dist/test/ephemeral-settle.test.js +351 -0
  41. package/dist/test/ephemeral-settle.test.js.map +1 -0
  42. package/dist/test/health-snapshot.test.d.ts +2 -0
  43. package/dist/test/health-snapshot.test.d.ts.map +1 -0
  44. package/dist/test/health-snapshot.test.js +74 -0
  45. package/dist/test/health-snapshot.test.js.map +1 -0
  46. package/dist/test/maintenance-timer.test.js +49 -0
  47. package/dist/test/maintenance-timer.test.js.map +1 -1
  48. package/dist/test/overbudget-drain-breaker.test.d.ts +2 -0
  49. package/dist/test/overbudget-drain-breaker.test.d.ts.map +1 -0
  50. package/dist/test/overbudget-drain-breaker.test.js +157 -0
  51. package/dist/test/overbudget-drain-breaker.test.js.map +1 -0
  52. package/dist/test/timezone.test.d.ts +2 -0
  53. package/dist/test/timezone.test.d.ts.map +1 -0
  54. package/dist/test/timezone.test.js +18 -0
  55. package/dist/test/timezone.test.js.map +1 -0
  56. package/dist/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +1 -1
  58. package/src/agent.ts +30 -3
  59. package/src/framework.ts +313 -146
  60. package/src/index.ts +1 -0
  61. package/src/modules/discord/index.ts +4 -1
  62. package/src/modules/discord/types.ts +3 -0
  63. package/src/modules/health/index.ts +20 -4
  64. package/src/timezone.ts +90 -0
  65. package/src/types/agent.ts +6 -2
  66. package/src/types/framework.ts +17 -0
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export { Agent } from './agent.js';
7
7
  export type { StartStreamResult } from './agent.js';
8
8
  export { ProcessQueueImpl } from './queue.js';
9
9
  export { ModuleRegistry } from './module-registry.js';
10
+ export { formatZonedDateTime, formatZonedTime, isValidTimeZone, resolveTimeZone } from './timezone.js';
10
11
 
11
12
  // Built-in modules
12
13
  export * from './modules/index.js';
@@ -41,6 +41,7 @@ import type {
41
41
  ListChannelsInput,
42
42
  FetchHistoryInput,
43
43
  } from './types.js';
44
+ import { formatZonedDateTime, resolveTimeZone } from '../../timezone.js';
44
45
 
45
46
  // Re-export event types for consumers who want to handle them
46
47
  export type { DiscordMessageEvent, DiscordEditEvent, DiscordDeleteEvent } from './types.js';
@@ -99,6 +100,7 @@ export class DiscordModule implements Module {
99
100
  readonly name = 'discord';
100
101
 
101
102
  private config: DiscordModuleConfig;
103
+ private readonly timeZone: string;
102
104
  private ctx: ModuleContext | null = null;
103
105
  private client: DiscordClientInterface;
104
106
  private state: DiscordModuleState = {
@@ -126,6 +128,7 @@ export class DiscordModule implements Module {
126
128
  constructor(client: DiscordClientInterface, config: DiscordModuleConfig) {
127
129
  this.client = client;
128
130
  this.config = { ...DEFAULT_CONFIG, ...config };
131
+ this.timeZone = resolveTimeZone(config.timeZone);
129
132
  }
130
133
 
131
134
  async start(ctx: ModuleContext): Promise<void> {
@@ -1190,7 +1193,7 @@ export class DiscordModule implements Module {
1190
1193
  authorId: m.authorId,
1191
1194
  isBot: m.isBot,
1192
1195
  content: m.content,
1193
- timestamp: m.timestamp.toISOString(),
1196
+ timestamp: formatZonedDateTime(m.timestamp, this.timeZone),
1194
1197
  replyTo: m.replyTo,
1195
1198
  })),
1196
1199
  },
@@ -9,6 +9,9 @@ import type { ContentBlock } from '@animalabs/membrane';
9
9
  // ============================================================================
10
10
 
11
11
  export interface DiscordModuleConfig {
12
+ /** IANA zone for timestamps returned to the agent by fetch_history. */
13
+ timeZone?: string;
14
+
12
15
  /** Discord bot token */
13
16
  token: string;
14
17
 
@@ -24,8 +24,12 @@ import type { Module, ModuleContext } from '../../types/module.js';
24
24
  import type { ToolDefinition, ToolCall, ToolResult, ProcessEvent } from '../../types/events.js';
25
25
  import type { EventResponse, ProcessState } from '../../types/module.js';
26
26
  import type { AgentFramework } from '../../framework.js';
27
+ import { formatZonedDateTime, resolveTimeZone } from '../../timezone.js';
27
28
 
28
29
  export interface HealthModuleConfig {
30
+ /** IANA zone for timestamps returned by the snapshot tool. */
31
+ timeZone?: string;
32
+
29
33
  /**
30
34
  * Default number of recent inferences to summarize when the tool is
31
35
  * called without an explicit limit. Default: 20.
@@ -70,6 +74,7 @@ export class HealthModule implements Module {
70
74
 
71
75
  constructor(config: HealthModuleConfig = {}) {
72
76
  this.config = {
77
+ timeZone: resolveTimeZone(config.timeZone),
73
78
  defaultLookback: config.defaultLookback ?? 20,
74
79
  subagentStateId: config.subagentStateId ?? 'modules/subagent/state',
75
80
  };
@@ -162,10 +167,20 @@ export class HealthModule implements Module {
162
167
  const recent = this.framework.queryInferenceLogs({ limit: lookback });
163
168
  const recentEntries = recent.entries ?? [];
164
169
 
170
+ // Presentation-boundary guard: queryInferenceLogs casts raw persisted
171
+ // JSON without validation, so a legacy/corrupt record can carry a missing
172
+ // or non-numeric timestamp. formatZonedDateTime throws on invalid dates
173
+ // (correctly — misuse should be loud), but one bad record must not sink
174
+ // the whole snapshot: degrade to the raw value (or null) instead.
175
+ const safeZoned = (ts: unknown): unknown =>
176
+ typeof ts === 'number' && Number.isFinite(ts)
177
+ ? formatZonedDateTime(ts, this.config.timeZone)
178
+ : ts ?? null;
179
+
165
180
  const projectSummary = (e: typeof recentEntries[number]) => {
166
181
  const s = e.summary ?? e.entry;
167
182
  return {
168
- timestamp: s.timestamp,
183
+ timestamp: safeZoned(s.timestamp),
169
184
  agentName: s.agentName,
170
185
  success: s.success,
171
186
  error: s.error,
@@ -224,8 +239,8 @@ export class HealthModule implements Module {
224
239
  name: sa.name,
225
240
  type: sa.type,
226
241
  status: sa.status,
227
- startedAt: startedAt > 0 ? new Date(startedAt).toISOString() : null,
228
- lastActivityAt: lastActivityAt > 0 ? new Date(lastActivityAt).toISOString() : null,
242
+ startedAt: startedAt > 0 ? formatZonedDateTime(startedAt, this.config.timeZone) : null,
243
+ lastActivityAt: lastActivityAt > 0 ? formatZonedDateTime(lastActivityAt, this.config.timeZone) : null,
229
244
  runtimeSeconds: startedAt > 0 ? Math.floor((now - startedAt) / 1000) : null,
230
245
  silentSeconds: lastActivityAt > 0 ? Math.floor((now - lastActivityAt) / 1000) : null,
231
246
  toolCallsCount: sa.toolCallsCount,
@@ -244,7 +259,8 @@ export class HealthModule implements Module {
244
259
  const modules = this.ctx ? this.listModulesViaContext() : [];
245
260
 
246
261
  return {
247
- timestamp: new Date().toISOString(),
262
+ timestamp: formatZonedDateTime(Date.now(), this.config.timeZone),
263
+ timeZone: this.config.timeZone,
248
264
  window: {
249
265
  lookback,
250
266
  inferencesInWindow: recentEntries.length,
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Agent-visible wall-clock formatting.
3
+ *
4
+ * Persistence and protocol timestamps should remain epoch milliseconds or UTC
5
+ * ISO strings. These helpers are only for presentation boundaries where a
6
+ * timestamp is rendered into an agent's context or a tool result.
7
+ */
8
+
9
+ export function isValidTimeZone(timeZone: string): boolean {
10
+ try {
11
+ new Intl.DateTimeFormat('en-US', { timeZone }).format(new Date(0));
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ export function resolveTimeZone(configured?: string): string {
19
+ const candidate = configured?.trim()
20
+ || process.env.AGENT_TIMEZONE?.trim()
21
+ || Intl.DateTimeFormat().resolvedOptions().timeZone
22
+ || 'UTC';
23
+ if (!isValidTimeZone(candidate)) {
24
+ throw new Error(`Invalid IANA time zone: ${JSON.stringify(candidate)}`);
25
+ }
26
+ return candidate;
27
+ }
28
+
29
+ type ZonedParts = {
30
+ year: string;
31
+ month: string;
32
+ day: string;
33
+ hour: string;
34
+ minute: string;
35
+ second: string;
36
+ };
37
+
38
+ function zonedParts(date: Date, timeZone: string): ZonedParts {
39
+ const parts = new Intl.DateTimeFormat('en-CA', {
40
+ timeZone,
41
+ year: 'numeric',
42
+ month: '2-digit',
43
+ day: '2-digit',
44
+ hour: '2-digit',
45
+ minute: '2-digit',
46
+ second: '2-digit',
47
+ hourCycle: 'h23',
48
+ }).formatToParts(date);
49
+ const get = (type: Intl.DateTimeFormatPartTypes): string =>
50
+ parts.find((part) => part.type === type)?.value ?? '';
51
+ return {
52
+ year: get('year'),
53
+ month: get('month'),
54
+ day: get('day'),
55
+ hour: get('hour'),
56
+ minute: get('minute'),
57
+ second: get('second'),
58
+ };
59
+ }
60
+
61
+ /** RFC3339-style local time with an explicit numeric offset and IANA zone. */
62
+ export function formatZonedDateTime(value: Date | number | string, timeZone: string): string {
63
+ const date = value instanceof Date ? value : new Date(value);
64
+ if (Number.isNaN(date.getTime())) throw new Error(`Invalid date: ${String(value)}`);
65
+ const zone = resolveTimeZone(timeZone);
66
+ const p = zonedParts(date, zone);
67
+
68
+ // Reinterpret the displayed wall-clock components as UTC to recover the
69
+ // offset that applied at this exact instant (including DST transitions).
70
+ const displayedAsUtc = Date.UTC(
71
+ Number(p.year), Number(p.month) - 1, Number(p.day),
72
+ Number(p.hour), Number(p.minute), Number(p.second),
73
+ );
74
+ const instantAtWholeSecond = Math.floor(date.getTime() / 1000) * 1000;
75
+ const offsetMinutes = Math.round((displayedAsUtc - instantAtWholeSecond) / 60_000);
76
+ const sign = offsetMinutes < 0 ? '-' : '+';
77
+ const absoluteOffset = Math.abs(offsetMinutes);
78
+ const offset = `${sign}${String(Math.floor(absoluteOffset / 60)).padStart(2, '0')}:${String(absoluteOffset % 60).padStart(2, '0')}`;
79
+ const millis = String(date.getUTCMilliseconds()).padStart(3, '0');
80
+
81
+ return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}.${millis}${offset} [${zone}]`;
82
+ }
83
+
84
+ /** Compact wall-clock time for provenance headers. */
85
+ export function formatZonedTime(value: Date | number | string, timeZone: string): string {
86
+ const date = value instanceof Date ? value : new Date(value);
87
+ if (Number.isNaN(date.getTime())) throw new Error(`Invalid date: ${String(value)}`);
88
+ const p = zonedParts(date, resolveTimeZone(timeZone));
89
+ return `${p.hour}:${p.minute}`;
90
+ }
@@ -48,7 +48,7 @@ export interface AgentConfig {
48
48
 
49
49
  /**
50
50
  * Prompt-cache TTL forwarded to the provider (Anthropic `cache_control.ttl`).
51
- * '5m' (provider default) or '1h'. When unset, Membrane's default applies.
51
+ * '5m' or '1h'. Defaults to '1h'.
52
52
  *
53
53
  * Why you'd set '1h': for persistent agents whose conversational cadence is
54
54
  * slower than 5 minutes, the default TTL expires between turns and the full
@@ -57,10 +57,14 @@ export interface AgentConfig {
57
57
  * chatty-but-not-rapid agent pays the write premium over and over. With '1h'
58
58
  * the write happens once per idle-hour and subsequent turns hit cache reads;
59
59
  * in practice cache writes can dominate spend for slow-cadence agents.
60
- * Keep '5m' (or unset) for high-frequency loops with sub-5-minute cadence,
60
+ * Set '5m' explicitly for high-frequency loops with sub-5-minute cadence,
61
61
  * where the cheaper write premium wins.
62
62
  */
63
63
  cacheTtl?: '5m' | '1h';
64
+
65
+ /** Provider-specific request parameters (for example Responses reasoning
66
+ * and server-side compaction settings). */
67
+ providerParams?: Record<string, unknown>;
64
68
  /**
65
69
  * Extended thinking config. When `enabled: true`, the agent runs with
66
70
  * Anthropic's native extended thinking; responses include `thinking` blocks
@@ -32,6 +32,13 @@ export interface ProcessLoggingConfig {
32
32
  * Configuration for the agent framework.
33
33
  */
34
34
  export interface FrameworkConfig {
35
+ /**
36
+ * IANA zone used only when rendering wall-clock times for the agent.
37
+ * Stored/protocol timestamps remain epoch/UTC. Defaults to AGENT_TIMEZONE,
38
+ * then the process zone.
39
+ */
40
+ timeZone?: string;
41
+
35
42
  /** Path to Chronicle store */
36
43
  storePath?: string;
37
44
 
@@ -174,6 +181,16 @@ export type ErrorAction =
174
181
  | { retry: true; delayMs: number }
175
182
  | { retry: false; emit?: ProcessEvent };
176
183
 
184
+ /**
185
+ * Terminal result for a framework-driven ephemeral agent run.
186
+ */
187
+ export interface AgentSettleResult {
188
+ stopReason: 'completed' | 'turn_ended' | 'exhausted';
189
+ speech: string;
190
+ toolCallsCount: number;
191
+ error?: string;
192
+ }
193
+
177
194
  /**
178
195
  * Framework state exposed to policies.
179
196
  */