@animalabs/agent-framework 0.6.3 → 0.6.4

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 (51) hide show
  1. package/dist/src/agent.d.ts +3 -1
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +7 -2
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/framework.d.ts +2 -0
  6. package/dist/src/framework.d.ts.map +1 -1
  7. package/dist/src/framework.js +16 -3
  8. package/dist/src/framework.js.map +1 -1
  9. package/dist/src/index.d.ts +1 -0
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +1 -0
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/modules/discord/index.d.ts +1 -0
  14. package/dist/src/modules/discord/index.d.ts.map +1 -1
  15. package/dist/src/modules/discord/index.js +4 -1
  16. package/dist/src/modules/discord/index.js.map +1 -1
  17. package/dist/src/modules/discord/types.d.ts +2 -0
  18. package/dist/src/modules/discord/types.d.ts.map +1 -1
  19. package/dist/src/modules/health/index.d.ts +2 -0
  20. package/dist/src/modules/health/index.d.ts.map +1 -1
  21. package/dist/src/modules/health/index.js +15 -4
  22. package/dist/src/modules/health/index.js.map +1 -1
  23. package/dist/src/timezone.d.ts +14 -0
  24. package/dist/src/timezone.d.ts.map +1 -0
  25. package/dist/src/timezone.js +74 -0
  26. package/dist/src/timezone.js.map +1 -0
  27. package/dist/src/types/agent.d.ts +5 -2
  28. package/dist/src/types/agent.d.ts.map +1 -1
  29. package/dist/src/types/framework.d.ts +6 -0
  30. package/dist/src/types/framework.d.ts.map +1 -1
  31. package/dist/test/agent-cache-ttl.test.js +18 -2
  32. package/dist/test/agent-cache-ttl.test.js.map +1 -1
  33. package/dist/test/health-snapshot.test.d.ts +2 -0
  34. package/dist/test/health-snapshot.test.d.ts.map +1 -0
  35. package/dist/test/health-snapshot.test.js +74 -0
  36. package/dist/test/health-snapshot.test.js.map +1 -0
  37. package/dist/test/timezone.test.d.ts +2 -0
  38. package/dist/test/timezone.test.d.ts.map +1 -0
  39. package/dist/test/timezone.test.js +18 -0
  40. package/dist/test/timezone.test.js.map +1 -0
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +1 -1
  43. package/src/agent.ts +8 -3
  44. package/src/framework.ts +18 -3
  45. package/src/index.ts +1 -0
  46. package/src/modules/discord/index.ts +4 -1
  47. package/src/modules/discord/types.ts +3 -0
  48. package/src/modules/health/index.ts +20 -4
  49. package/src/timezone.ts +90 -0
  50. package/src/types/agent.ts +6 -2
  51. package/src/types/framework.ts +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/agent-framework",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Multi-agent framework with pluggable modules, persistent state, and concurrent tool execution",
5
5
  "repository": {
6
6
  "type": "git",
package/src/agent.ts CHANGED
@@ -47,7 +47,9 @@ export class Agent {
47
47
  /** Refusal auto-rewind policy (see AgentConfig.refusalHandling). */
48
48
  readonly refusalHandling: AgentConfig['refusalHandling'];
49
49
  /** Prompt-cache TTL forwarded to the provider (see AgentConfig.cacheTtl). */
50
- readonly cacheTtl: AgentConfig['cacheTtl'];
50
+ readonly cacheTtl: NonNullable<AgentConfig['cacheTtl']>;
51
+ /** Provider-specific request parameters forwarded unchanged by Membrane. */
52
+ readonly providerParams?: Record<string, unknown>;
51
53
 
52
54
  private _state: AgentState = { status: 'idle' };
53
55
  private _inferenceStartedAt = 0;
@@ -75,7 +77,8 @@ export class Agent {
75
77
  this.temperature = config.temperature;
76
78
  this.thinking = config.thinking;
77
79
  this.refusalHandling = config.refusalHandling;
78
- this.cacheTtl = config.cacheTtl;
80
+ this.cacheTtl = config.cacheTtl ?? '1h';
81
+ this.providerParams = config.providerParams;
79
82
  this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
80
83
  this.contextBudgetTokens = config.contextBudgetTokens;
81
84
  this.contextManager = contextManager;
@@ -217,6 +220,7 @@ export class Agent {
217
220
  ...(this.thinking !== undefined && { thinking: this.thinking }),
218
221
  },
219
222
  tools: tools.length > 0 ? tools : undefined,
223
+ ...(this.providerParams && { providerParams: this.providerParams }),
220
224
  assistantParticipant: this.name,
221
225
  };
222
226
 
@@ -357,7 +361,8 @@ export class Agent {
357
361
  },
358
362
  tools: availableTools.length > 0 ? availableTools : undefined,
359
363
  promptCaching: true,
360
- ...(this.cacheTtl !== undefined && { cacheTtl: this.cacheTtl }),
364
+ cacheTtl: this.cacheTtl,
365
+ ...(this.providerParams && { providerParams: this.providerParams }),
361
366
  assistantParticipant: this.name,
362
367
  };
363
368
  }
package/src/framework.ts CHANGED
@@ -79,6 +79,7 @@ import type {
79
79
  ChannelsIncomingParams,
80
80
  } from './mcpl/types.js';
81
81
  import type { ContextInjection } from '@animalabs/context-manager';
82
+ import { formatZonedDateTime, resolveTimeZone } from './timezone.js';
82
83
 
83
84
  const FRAMEWORK_STATE_ID = 'framework/state';
84
85
  const CONVERSATION_ROUTER_STATE_ID = 'framework/conversation-router';
@@ -374,6 +375,8 @@ export class AgentFramework {
374
375
 
375
376
  // Session-level token usage tracking (always-on)
376
377
  private usageTracker: UsageTracker;
378
+ /** Presentation-only wall-clock zone; persistence remains UTC/epoch. */
379
+ private readonly timeZone: string;
377
380
 
378
381
  private constructor(
379
382
  store: JsStore,
@@ -384,7 +387,8 @@ export class AgentFramework {
384
387
  syncIntervalMs: number,
385
388
  maintenanceIntervalMs: number,
386
389
  processLoggingPersist: boolean,
387
- processLoggingBroadcast: boolean
390
+ processLoggingBroadcast: boolean,
391
+ timeZone: string,
388
392
  ) {
389
393
  this.store = store;
390
394
  this.ownsStore = ownsStore;
@@ -395,6 +399,7 @@ export class AgentFramework {
395
399
  this.maintenanceIntervalMs = maintenanceIntervalMs;
396
400
  this.processLoggingPersist = processLoggingPersist;
397
401
  this.processLoggingBroadcast = processLoggingBroadcast;
402
+ this.timeZone = timeZone;
398
403
  this.queue = new ProcessQueueImpl();
399
404
  this.usageTracker = new UsageTracker({
400
405
  emitTrace: (e: UsageUpdatedEvent) => this.emitTrace({ ...e }),
@@ -485,7 +490,8 @@ export class AgentFramework {
485
490
  config.syncIntervalMs ?? DEFAULT_SYNC_INTERVAL_MS,
486
491
  config.maintenanceIntervalMs ?? DEFAULT_MAINTENANCE_INTERVAL_MS,
487
492
  processLoggingPersist,
488
- processLoggingBroadcast
493
+ processLoggingBroadcast,
494
+ resolveTimeZone(config.timeZone),
489
495
  );
490
496
 
491
497
  // Restore persisted usage data (if any) from prior session
@@ -5764,7 +5770,16 @@ export class AgentFramework {
5764
5770
 
5765
5771
  console.error(`[sleep] agent=${agentName} seconds=${seconds} announce=${announce} until=${new Date(until).toISOString()}`);
5766
5772
  // endTurn: the agent stops here and goes idle for the duration.
5767
- finish({ success: true, data: { sleepingFor: human, until }, endTurn: true });
5773
+ finish({
5774
+ success: true,
5775
+ data: {
5776
+ sleepingFor: human,
5777
+ until,
5778
+ untilLocal: formatZonedDateTime(until, this.timeZone),
5779
+ timeZone: this.timeZone,
5780
+ },
5781
+ endTurn: true,
5782
+ });
5768
5783
  }
5769
5784
 
5770
5785
  /** Aggregate the event-tag vocabulary: reserved chat:* core + each connected
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