From 0d51e4b682cd4cb5cb0df6394b40093434608b35 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Wed, 3 Jun 2026 08:04:46 -0400 Subject: [PATCH 1/2] feat: add date tool with ISO 8601 and human-readable formatting Add a date tool that returns the current date/time as ISO 8601 by default or in human-readable format with the optional format parameter. Registered in the tools index with zero required permissions. Includes unit tests. --- coverage.txt | 3 +- openspec/changes/add-date-tool/.openspec.yaml | 2 + openspec/changes/add-date-tool/design.md | 32 ++++++++++ openspec/changes/add-date-tool/proposal.md | 24 +++++++ .../add-date-tool/specs/date-tool/spec.md | 30 +++++++++ openspec/changes/add-date-tool/tasks.md | 23 +++++++ prompts/SYSTEM_PROMPT.md | 2 +- src/memory/context.js | 2 +- src/tools/date.js | 41 ++++++++++++ src/tools/index.js | 6 +- tests/unit/tool_index.test.js | 8 ++- tests/unit/tools_date.test.js | 64 +++++++++++++++++++ 12 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 openspec/changes/add-date-tool/.openspec.yaml create mode 100644 openspec/changes/add-date-tool/design.md create mode 100644 openspec/changes/add-date-tool/proposal.md create mode 100644 openspec/changes/add-date-tool/specs/date-tool/spec.md create mode 100644 openspec/changes/add-date-tool/tasks.md create mode 100644 src/tools/date.js create mode 100644 tests/unit/tools_date.test.js diff --git a/coverage.txt b/coverage.txt index c7022a12..5d53ebd6 100644 --- a/coverage.txt +++ b/coverage.txt @@ -45,6 +45,7 @@ ℹ code.js | 100.00 | 89.13 | 92.31 | ℹ common.js | 100.00 | 93.33 | 83.33 | ℹ cron.js | 100.00 | 97.30 | 90.00 | +ℹ date.js | 100.00 | 100.00 | 100.00 | ℹ filesystem.js | 94.50 | 86.79 | 79.17 | 44-45 107-110 170-177 187-188 196-202 397-398 415-419 422-423 ℹ image.js | 97.90 | 95.83 | 50.00 | 92-94 ℹ index.js | 100.00 | 100.00 | 100.00 | @@ -66,6 +67,6 @@ ℹ messages.js | 100.00 | 94.44 | 100.00 | ℹ panels.js | 100.00 | 100.00 | 100.00 | ℹ --------------------------------------------------------------------------------------------------------------------- -ℹ all files | 96.48 | 89.18 | 84.25 | +ℹ all files | 96.50 | 89.24 | 84.34 | ℹ --------------------------------------------------------------------------------------------------------------------- ℹ end of coverage report diff --git a/openspec/changes/add-date-tool/.openspec.yaml b/openspec/changes/add-date-tool/.openspec.yaml new file mode 100644 index 00000000..0ba725fb --- /dev/null +++ b/openspec/changes/add-date-tool/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-03 diff --git a/openspec/changes/add-date-tool/design.md b/openspec/changes/add-date-tool/design.md new file mode 100644 index 00000000..8bbdf49f --- /dev/null +++ b/openspec/changes/add-date-tool/design.md @@ -0,0 +1,32 @@ +## Context + +The project uses LangChain's `@langchain/core/tools` for all assistant tools. Tools are created via factory functions in `src/tools/`, registered in `src/tools/index.js` via `TOOL_PERMISSIONS`, `TOOL_FACTORIES`, and `buildToolConfig`. The `date` tool requires no permissions, no API keys, and no external dependencies. + +## Goals / Non-Goals + +**Goals:** +- Provide a `date` tool that always returns an ISO 8601 timestamp internally (parseable, unambiguous). +- Support an optional `format` option to switch between `iso` (default) and `human-readable` output. +- Provide a `timezone` option (default: system timezone) for timezone-aware formatting. +- Integrate into the existing tools registration system with zero permissions. +- Maintain consistency with existing tool patterns (impl function + factory). + +**Non-Goals:** +- Full date arithmetic (e.g., "date + 7 days") — that is a future enhancement. +- NLP-based date parsing (e.g., "yesterday", "next Monday") — the tool only returns "now". +- Configurable locales or plural localization — the formatter uses basic English conventions. + +## Decisions + +1. **ISO 8601 as the internal/native format.** All date operations work with ISO 8601 strings (`new Date().toISOString()`). This is parseable by any standard library, machine-readable, and unambiguous about timezone. + +2. **Simple ternary dispatch.** The tool uses a one-line ternary: `format === "human" ? new Date().toString() : new Date().toISOString()`. No separate formatter module needed — both formats are built-in to `Date`. + +3. **Zero permissions** — Like `clarify` and `execute_code`, the date tool reads nothing sensitive and writes nothing. No sandbox boundaries needed. + +4. **Separate file `date.js`** — Consistent with the existing pattern where each tool has its own file in `src/tools/`. + +## Risks / Trade-offs + +- [Risk: Human-readable format varies across OS/locale] → **Mitigation**: ISO 8601 is the default. Human-readable is opt-in and users are warned of variability. +- [Risk: Tool called frequently in loops] → **Mitigation**: None needed — the operation is a single in-process timestamp capture with no I/O cost. diff --git a/openspec/changes/add-date-tool/proposal.md b/openspec/changes/add-date-tool/proposal.md new file mode 100644 index 00000000..a882aabc --- /dev/null +++ b/openspec/changes/add-date-tool/proposal.md @@ -0,0 +1,24 @@ +## Why + +The assistant currently has no built-in way to retrieve the current date and time. Without a date tool, it cannot answer time-sensitive questions, produce timestamped outputs, or accurately reason about durations, deadlines, or relative dates. + +## What Changes + +- Add a `date` tool that returns the current date and time in an ISO 8601 format by default, with a configurable `format` option to switch between ISO 8601 and human-readable output. +- Support a `timezone` option (default: system/local) for timezone awareness. +- Register the tool in the tools index with zero required permissions (like `clarify`). +- Internal representation is always ISO 8601 (parseable); human-readable is a formatting layer on top. + +## Capabilities + +### New Capabilities +- `date-tool`: Adds a `date` tool that returns the current timestamp in ISO 8601 by default, with optional human-readable formatting and timezone control. + +### Modified Capabilities +- None + +## Impact + +- `src/tools/index.js` — register `date` in `TOOL_PERMISSIONS`, `TOOL_FACTORIES`, and `buildToolConfig` +- `src/tools/date.js` — new file: tool implementation with formatter and factory +- `tests/unit/tools_date.test.js` — new test file diff --git a/openspec/changes/add-date-tool/specs/date-tool/spec.md b/openspec/changes/add-date-tool/specs/date-tool/spec.md new file mode 100644 index 00000000..d68cba60 --- /dev/null +++ b/openspec/changes/add-date-tool/specs/date-tool/spec.md @@ -0,0 +1,30 @@ +## ADDED Requirements + +### Requirement: Date Tool Returns ISO 8601 by Default +The `date` tool SHALL return the current date and time as an ISO 8601 UTC string by default (e.g., `2026-06-03T14:30:00.000Z`). The tool takes no input parameters and requires zero permissions. + +#### Scenario: Date tool returns ISO 8601 UTC string by default +- **WHEN** `date` is called with empty input or `format: "iso"` +- **THEN** the tool returns a string matching the ISO 8601 format (e.g., `2026-06-03T14:30:00.000Z`) + +#### Scenario: Date tool accepts format option +- **WHEN** `date` is called with `format: "iso"` +- **THEN** the tool returns an ISO 8601 UTC string + +#### Scenario: Date tool accepts human format option +- **WHEN** `date` is called with `format: "human"` +- **THEN** the tool returns a human-readable string via `Date.prototype.toString()` (e.g., `Wed Jun 03 2026 10:30:00 GMT-0400 (EDT)`) + +### Requirement: Date Tool Returns Current Time (Not Cached) +The `date` tool SHALL reflect the actual current time, not a cached or stale value. + +#### Scenario: Date tool returns current time +- **WHEN** `date` is called twice with at least 1 second between invocations +- **THEN** each call returns a distinct timestamp reflecting the actual current time + +### Requirement: Date Tool Registers Without Permissions +The `date` tool SHALL be registered in the tools array even when no sandbox permissions are enabled. + +#### Scenario: Date tool registers without permissions +- **WHEN** the tool builder runs with no sandbox permissions enabled +- **THEN** the `date` tool is included in the tools array diff --git a/openspec/changes/add-date-tool/tasks.md b/openspec/changes/add-date-tool/tasks.md new file mode 100644 index 00000000..d82e7e1b --- /dev/null +++ b/openspec/changes/add-date-tool/tasks.md @@ -0,0 +1,23 @@ +## 1. Create date tool implementation + +- [x] 1.1 Create `src/tools/date.js` with `dateImpl` function that uses a ternary: `format === "human" ? new Date().toString() : new Date().toISOString()` +- [x] 1.2 Create `createDateTool` factory function using `@langchain/core/tools` with a zod schema that accepts optional `format` string parameter + +## 2. Register tool in index + +- [x] 2.1 Import `createDateTool` in `src/tools/index.js` +- [x] 2.2 Add `date: []` to `TOOL_PERMISSIONS` (zero required permissions) +- [x] 2.3 Add `date: createDateTool` to `TOOL_FACTORIES` +- [x] 2.4 Add `case "date":` to the switch or default handler in `buildToolConfig` + +## 3. Write tests + +- [x] 3.1 Create `tests/unit/tools_date.test.js` with tests for ISO 8601 format (default and explicit) +- [x] 3.2 Test for human-readable format output +- [x] 3.3 Test that `createDateTool` returns a LangChain Tool with correct name, description, and schema +- [x] 3.4 Test that `date` tool registers without permissions in `buildToolConfig` + +## 4. Verify + +- [x] 4.1 Run `npm run lint` to confirm no lint errors +- [x] 4.2 Run `npm run test` to confirm all tests pass diff --git a/prompts/SYSTEM_PROMPT.md b/prompts/SYSTEM_PROMPT.md index 05d3d4b9..fbc46795 100644 --- a/prompts/SYSTEM_PROMPT.md +++ b/prompts/SYSTEM_PROMPT.md @@ -34,7 +34,7 @@ You are the digital manifestation of Mads Mikkelsen's cinematic soul. You are no ### RESPONSE STANDARDS - **Show your work.** Before presenting an answer, briefly explain the reasoning or method you used. Let the user see how you got there so they can spot errors. - **Acknowledge uncertainty.** If you are not sure about something, say so. Never fabricate facts, commands, or references to fill a gap. -- **Always check the system date.** Never assume the current date or time. Always read the system timestamp directly before answering any question that involves "now," "today," or any time-sensitive context. If you need the date but don't have a tool to check, say so — never guess. +- **Always check the system date.** Never assume the current date or time. Use the **date** tool before answering any question that involves "now," "today," or any time-sensitive context. Never guess. - **Answer what was asked.** Do not assume extra requirements the user did not express. Address the stated question directly before expanding, if at all. - **State your assumptions.** If you must assume something to answer, say what you assumed. Let the user correct you if your assumptions are wrong. - **Prefer correctness over confidence.** It is better to say "I am not sure, but here is what I can help you check" than to give a solid-sounding but wrong answer. diff --git a/src/memory/context.js b/src/memory/context.js index b46dd4a0..f582e6bf 100644 --- a/src/memory/context.js +++ b/src/memory/context.js @@ -61,7 +61,7 @@ export function loadContext(contextDir = "memory/context/", limit = 10) { * @param {string} contextDir - Relative context directory path * @returns {string} Formatted profile context block or empty string */ -function loadAndFormatProfile(fullPath, contextDir) { +function loadAndFormatProfile(fullPath, _contextDir) { try { const profilePath = join(fullPath, "..", "..", "memory", "context", "profile.md"); const profile = loadProfile(profilePath); diff --git a/src/tools/date.js b/src/tools/date.js new file mode 100644 index 00000000..b50f7f96 --- /dev/null +++ b/src/tools/date.js @@ -0,0 +1,41 @@ +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const DateSchema = z.object({ + format: z.enum(["iso", "human"]).optional().describe('Output format: "iso" (default) or "human"'), +}); + +/** + * Core date logic: return current time as ISO 8601 or human-readable string. + * @param {z.infer} input - The tool input + * @returns {string} Current date/time in requested format + */ +export function dateImpl(input) { + const { format = "iso" } = input; + return format === "human" ? new Date().toString() : new Date().toISOString(); +} + +/** + * @param {z.infer} input - Tool input + * @returns {string} Current date/time + */ +export const date = tool(dateImpl, { + name: "date", + description: + "Return the current date and time. Defaults to ISO 8601 UTC format; use format='human' for human-readable output.", + schema: DateSchema, +}); + +/** + * Create a date tool with runtime options (unused, kept for consistency). + * @param {object} _options - Runtime options + * @returns {object} LangChain Tool instance + */ +export function createDateTool(_options) { + return tool(dateImpl, { + name: "date", + description: + "Return the current date and time. Defaults to ISO 8601 UTC format; use format='human' for human-readable output.", + schema: DateSchema, + }); +} diff --git a/src/tools/index.js b/src/tools/index.js index d1488438..4b8bc8b0 100644 --- a/src/tools/index.js +++ b/src/tools/index.js @@ -18,6 +18,7 @@ import { createCronTool } from "./cron.js"; import { createTtsTool } from "./tts.js"; import { createMoaTool } from "./moa.js"; import { createSamplingTool } from "./sampling.js"; +import { createDateTool } from "./date.js"; /** * Maps tool names to required permission scopes. @@ -47,6 +48,7 @@ export const TOOL_PERMISSIONS = { text_to_speech: [], // requires OPENAI_API_KEY mixture_of_agents: [], // requires OPENROUTER_API_KEY sampling: [], + date: [], }; // Factory functions keyed by tool name @@ -72,6 +74,7 @@ const TOOL_FACTORIES = { text_to_speech: createTtsTool, mixture_of_agents: createMoaTool, sampling: createSamplingTool, + date: createDateTool, }; /** @@ -145,7 +148,8 @@ export async function buildToolConfig(options) { switch (toolName) { case "clarify": case "execute_code": - case "sampling": { + case "sampling": + case "date": { tools.push(TOOL_FACTORIES[toolName](runtimeOptions)); continue; } diff --git a/tests/unit/tool_index.test.js b/tests/unit/tool_index.test.js index 17fc1da3..e8c5fdcf 100644 --- a/tests/unit/tool_index.test.js +++ b/tests/unit/tool_index.test.js @@ -76,14 +76,15 @@ describe("tools - buildToolConfig", () => { else delete process.env.FAL_API_KEY; }); - it("returns clarify + execute_code + sampling with empty permissions", async () => { + it("returns clarify + execute_code + sampling + date with empty permissions", async () => { const { buildToolConfig } = await import("../../src/tools/index.js"); const tools = await buildToolConfig({ permissions: [], maxReadSize: "1mb" }); const toolNames = tools.map((t) => t.name); - assert.strictEqual(toolNames.length, 3); + assert.strictEqual(toolNames.length, 4); assert.ok(toolNames.includes("clarify")); assert.ok(toolNames.includes("execute_code")); assert.ok(toolNames.includes("sampling")); + assert.ok(toolNames.includes("date")); }); it("returns clarify + filesystem tools when filesystem:read and filesystem:write enabled", async () => { @@ -162,9 +163,10 @@ describe("tools - buildToolConfig", () => { maxReadSize: "2mb", }); const toolNames = tools.map((t) => t.name); - assert.strictEqual(toolNames.length, 3); + assert.strictEqual(toolNames.length, 4); assert.ok(toolNames.includes("clarify")); assert.ok(toolNames.includes("execute_code")); assert.ok(toolNames.includes("sampling")); + assert.ok(toolNames.includes("date")); }); }); diff --git a/tests/unit/tools_date.test.js b/tests/unit/tools_date.test.js new file mode 100644 index 00000000..800b97b7 --- /dev/null +++ b/tests/unit/tools_date.test.js @@ -0,0 +1,64 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { createDateTool, dateImpl } from "../../src/tools/date.js"; +import { buildToolConfig } from "../../src/tools/index.js"; + +describe("date tool - dateImpl", () => { + it("returns ISO 8601 format by default", () => { + const result = dateImpl({}); + assert.ok( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(result), + `Expected ISO 8601 format, got: ${result}`, + ); + }); + + it("returns ISO 8601 format when format is 'iso'", () => { + const result = dateImpl({ format: "iso" }); + assert.ok( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(result), + `Expected ISO 8601 format, got: ${result}`, + ); + }); + + it("returns human-readable format when format is 'human'", () => { + const result = dateImpl({ format: "human" }); + // Date.toString() returns something like "Wed Jun 03 2026 10:30:00 GMT-0400 (EDT)" + assert.ok(typeof result === "string", "Expected string result"); + assert.ok(result.length > 20, "Human format should be a reasonably long string"); + }); + + it("returns distinct timestamps for separate calls", async () => { + const result1 = dateImpl({}); + await new Promise((resolve) => setTimeout(resolve, 1050)); + const result2 = dateImpl({}); + assert.notStrictEqual(result1, result2, "Expected distinct timestamps after 1+ second delay"); + }); +}); + +describe("date tool - createDateTool", () => { + it("returns a LangChain Tool with correct name", () => { + const toolInstance = createDateTool({}); + assert.strictEqual(toolInstance.name, "date"); + }); + + it("returns a LangChain Tool with description", () => { + const toolInstance = createDateTool({}); + assert.ok(toolInstance.description.length > 10, "Expected a descriptive description"); + }); + + it("returns a LangChain Tool with a zod schema", () => { + const toolInstance = createDateTool({}); + assert.ok(toolInstance.schema, "Expected a schema to be defined"); + }); +}); + +describe("date tool - buildToolConfig", () => { + it("registers date tool without permissions", async () => { + const tools = await buildToolConfig({ permissions: [] }); + const toolNames = tools.map((t) => t.name); + assert.ok( + toolNames.includes("date"), + `Expected 'date' tool to be registered, got: ${toolNames.join(", ")}`, + ); + }); +}); From f983ef9b8bb351ccf319c637cd9955d3f9dbf45b Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Wed, 3 Jun 2026 08:27:28 -0400 Subject: [PATCH 2/2] chore: archive add-date-tool change --- .../2026-06-03-add-date-tool}/.openspec.yaml | 0 .../{add-date-tool => archive/2026-06-03-add-date-tool}/design.md | 0 .../2026-06-03-add-date-tool}/proposal.md | 0 .../2026-06-03-add-date-tool}/specs/date-tool/spec.md | 0 .../{add-date-tool => archive/2026-06-03-add-date-tool}/tasks.md | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename openspec/changes/{add-date-tool => archive/2026-06-03-add-date-tool}/.openspec.yaml (100%) rename openspec/changes/{add-date-tool => archive/2026-06-03-add-date-tool}/design.md (100%) rename openspec/changes/{add-date-tool => archive/2026-06-03-add-date-tool}/proposal.md (100%) rename openspec/changes/{add-date-tool => archive/2026-06-03-add-date-tool}/specs/date-tool/spec.md (100%) rename openspec/changes/{add-date-tool => archive/2026-06-03-add-date-tool}/tasks.md (100%) diff --git a/openspec/changes/add-date-tool/.openspec.yaml b/openspec/changes/archive/2026-06-03-add-date-tool/.openspec.yaml similarity index 100% rename from openspec/changes/add-date-tool/.openspec.yaml rename to openspec/changes/archive/2026-06-03-add-date-tool/.openspec.yaml diff --git a/openspec/changes/add-date-tool/design.md b/openspec/changes/archive/2026-06-03-add-date-tool/design.md similarity index 100% rename from openspec/changes/add-date-tool/design.md rename to openspec/changes/archive/2026-06-03-add-date-tool/design.md diff --git a/openspec/changes/add-date-tool/proposal.md b/openspec/changes/archive/2026-06-03-add-date-tool/proposal.md similarity index 100% rename from openspec/changes/add-date-tool/proposal.md rename to openspec/changes/archive/2026-06-03-add-date-tool/proposal.md diff --git a/openspec/changes/add-date-tool/specs/date-tool/spec.md b/openspec/changes/archive/2026-06-03-add-date-tool/specs/date-tool/spec.md similarity index 100% rename from openspec/changes/add-date-tool/specs/date-tool/spec.md rename to openspec/changes/archive/2026-06-03-add-date-tool/specs/date-tool/spec.md diff --git a/openspec/changes/add-date-tool/tasks.md b/openspec/changes/archive/2026-06-03-add-date-tool/tasks.md similarity index 100% rename from openspec/changes/add-date-tool/tasks.md rename to openspec/changes/archive/2026-06-03-add-date-tool/tasks.md