Skip to content

Commit 4b66ca4

Browse files
dmealingclaude
andcommitted
feat(java): template.toolcall core subtype (ADR-0011)
Brings the Java port to parity with TS rc.5/rc.6 for template.toolcall. Kotlin inherits the Java port automatically. - TemplateConstants: SUBTYPE_TOOLCALL + ATTR_TOOL_NAME (cross-port Tier-1 invariant strings — must match TS / C# / Python exactly) - ToolcallTemplate class (parallels PromptTemplate / OutputTemplate) with its own registerTypes block. Does NOT inheritFrom template.base because the shared @textRef / @Format / @maxchars / @requiredTags attrs do NOT apply to toolcall — binds directly to metadata.base alongside template.base itself. - TemplateTypesMetaDataProvider wires the new subtype next to prompt and output (SPI-discovered). - ValidationPhase: explicit ERR_MISSING_REQUIRED_ATTR checks for @toolname and @payloadRef on toolcall (Java enforces required-ness via per-type validation passes, not auto-driven from the requiredChild constraints). The shared R2 (@payloadRef must resolve to a root-level object.value) already iterates all template.* subtypes — toolcall picks that up automatically. Tests: TemplateToolcallTest adds 6 cases (registry shape, positive load, missing @toolname, missing @payloadRef, bad @payloadRef, plus the constants-match assertion). Full metadata suite 742 -> 748 (same 4 pre-existing ConformanceTest provider-extension failures unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7a06673 commit 4b66ca4

5 files changed

Lines changed: 275 additions & 6 deletions

File tree

server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,26 @@ private static void validateTemplateNode(MetaRoot root, MetaTemplate template) {
10641064
ErrorCode.ERR_MISSING_REQUIRED_ATTR, template.getSource());
10651065
}
10661066

1067+
// R1b — template.toolcall requires @toolName + @payloadRef (ADR-0011).
1068+
// Mirrors the TS toolcallAttrs schema (both marked required: true).
1069+
if (TemplateConstants.SUBTYPE_TOOLCALL.equals(subType)) {
1070+
String toolName = template.hasMetaAttr(TemplateConstants.ATTR_TOOL_NAME, false)
1071+
? template.getMetaAttr(TemplateConstants.ATTR_TOOL_NAME, false).getValueAsString()
1072+
: null;
1073+
if (toolName == null || toolName.isEmpty()) {
1074+
throw new MetaDataException(
1075+
ErrorMessageConstants.ERR_MISSING_REQUIRED_ATTR
1076+
+ ": template.toolcall '" + template.getName() + "' is missing required @toolName",
1077+
ErrorCode.ERR_MISSING_REQUIRED_ATTR, template.getSource());
1078+
}
1079+
if (payloadRef == null || payloadRef.isEmpty()) {
1080+
throw new MetaDataException(
1081+
ErrorMessageConstants.ERR_MISSING_REQUIRED_ATTR
1082+
+ ": template.toolcall '" + template.getName() + "' is missing required @payloadRef",
1083+
ErrorCode.ERR_MISSING_REQUIRED_ATTR, template.getSource());
1084+
}
1085+
}
1086+
10671087
// R2 + R3 only apply if @payloadRef is set
10681088
if (payloadRef == null || payloadRef.isEmpty()) return;
10691089

server/java/metadata/src/main/java/com/metaobjects/template/TemplateConstants.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,16 @@ private TemplateConstants() {}
1818
public static final String SUBTYPE_BASE = "base";
1919
public static final String SUBTYPE_PROMPT = "prompt";
2020
public static final String SUBTYPE_OUTPUT = "output";
21+
/**
22+
* LLM tool-call envelope subtype (ADR-0011). Does NOT inherit the
23+
* generic prompt/output attrs — declares its own minimal set
24+
* ({@code @toolName} required, {@code @payloadRef} required, plus
25+
* governance {@code @owner} / {@code @since}). No {@code @textRef}
26+
* requirement: a tool-call has no renderable text body.
27+
*/
28+
public static final String SUBTYPE_TOOLCALL = "toolcall";
2129

22-
// --- Generic attributes (both subtypes) ---
30+
// --- Generic attributes (prompt + output; NOT inherited by toolcall) ---
2331
public static final String ATTR_PAYLOAD_REF = "payloadRef";
2432
public static final String ATTR_TEXT_REF = "textRef";
2533
public static final String ATTR_FORMAT = "format";
@@ -33,6 +41,18 @@ private TemplateConstants() {}
3341
public static final String ATTR_REQUIRED_SLOTS = "requiredSlots";
3442
public static final String ATTR_MODEL = "model";
3543

44+
// --- Toolcall-specific attributes (template.toolcall only — ADR-0011) ---
45+
//
46+
// Vendor-agnostic in core; vendor wire details (retry semantics, fallback
47+
// shapes, parallel invocation, cache hints) are added by consumer providers
48+
// — the cross-port equivalent of TypeScript's registry.extend.
49+
//
50+
// @description is intentionally NOT a toolcall-specific constant — every type
51+
// gets @description via the documentation common-attrs provider. Tool
52+
// descriptions surfaced to the LLM use the same @description common attr
53+
// doc-gen uses.
54+
public static final String ATTR_TOOL_NAME = "toolName";
55+
3656
// --- @format closed value set ---
3757
public static final String FORMAT_TEXT = "text";
3858
public static final String FORMAT_HTML = "html";

server/java/metadata/src/main/java/com/metaobjects/template/TemplateTypesMetaDataProvider.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
/**
77
* Template Types MetaData provider.
88
*
9-
* <p>Registers the abstract {@code template.base} type plus the two concrete
10-
* subtypes {@code template.prompt} and {@code template.output} (FR-004 fourth
11-
* pillar: cross-language prompt construction). Depends on {@code core-types}
12-
* for {@code metadata.base} inheritance.</p>
9+
* <p>Registers the abstract {@code template.base} type plus the three concrete
10+
* subtypes {@code template.prompt}, {@code template.output} (FR-004 fourth
11+
* pillar: cross-language prompt construction) and {@code template.toolcall}
12+
* (ADR-0011: LLM tool-call envelope). Depends on {@code core-types} for
13+
* {@code metadata.base} inheritance.</p>
1314
*
1415
* <p>Discovered via the standard {@link MetaDataTypeProvider} ServiceLoader
1516
* mechanism — wired through
@@ -25,6 +26,10 @@ public void registerTypes(MetaDataRegistry registry) {
2526
// Register concrete template subtypes.
2627
PromptTemplate.registerTypes(registry);
2728
OutputTemplate.registerTypes(registry);
29+
// ADR-0011 — toolcall is a sibling subtype that does NOT inherit
30+
// template.base's shared attrs (no @textRef requirement; the body IS
31+
// the structured output schema resolved via @payloadRef).
32+
ToolcallTemplate.registerTypes(registry);
2833

2934
// Root-level acceptance for template.* is declared on metadata.root in
3035
// MetaRoot's static initializer alongside object/field/attr/validator/view/
@@ -43,6 +48,6 @@ public String[] getDependencies() {
4348

4449
@Override
4550
public String getDescription() {
46-
return "Template Types (prompt / output — FR-004 cross-language prompt construction)";
51+
return "Template Types (prompt / output / toolcall — FR-004 + ADR-0011 cross-language prompt construction)";
4752
}
4853
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.metaobjects.template;
2+
3+
import com.metaobjects.attr.StringAttribute;
4+
import com.metaobjects.registry.MetaDataRegistry;
5+
6+
import static com.metaobjects.template.TemplateConstants.*;
7+
8+
/**
9+
* LLM tool-call envelope template ({@code template.toolcall}) — ADR-0011.
10+
*
11+
* <p>Vendor-agnostic in core; vendor wire details (Anthropic's retry-with-reminder,
12+
* OpenAI's function-calling envelope, MCP's tool definitions, etc.) are NOT in
13+
* core. Consumers add vendor specifics via a provider that registers additional
14+
* attrs on this subtype (the Java port's equivalent of TypeScript's
15+
* {@code registry.extend(TYPE_TEMPLATE, "toolcall", ...)} mechanism).
16+
*
17+
* <p>Crucially: {@code template.toolcall} does NOT inherit the generic
18+
* {@code @payloadRef}/{@code @textRef}/{@code @format} attrs that
19+
* {@link PromptTemplate} and {@link OutputTemplate} share. It declares its own
20+
* minimal set: required {@code @toolName} + required {@code @payloadRef} +
21+
* governance {@code @owner}/{@code @since}. {@code @textRef} is intentionally
22+
* absent — a tool-call has no renderable text body; the body IS the structured
23+
* output schema resolved via {@code @payloadRef}.
24+
*
25+
* <p>{@code @description} is intentionally NOT declared here — it's added to
26+
* every type by the documentation common-attrs provider. Tool descriptions
27+
* surfaced to the LLM use the same {@code @description} common attr that
28+
* doc-gen uses.
29+
*/
30+
public final class ToolcallTemplate extends MetaTemplate {
31+
32+
public ToolcallTemplate(String name) {
33+
super(SUBTYPE_TOOLCALL, name);
34+
}
35+
36+
public static void registerTypes(MetaDataRegistry registry) {
37+
registry.registerType(ToolcallTemplate.class, def -> {
38+
// ADR-0011: toolcall does NOT inheritsFrom template.base because the
39+
// shared @textRef / @format / @maxChars / @requiredTags attrs do NOT
40+
// apply to toolcall. Bind directly to metadata.base instead — match
41+
// template.base's own inheritance.
42+
def.type(TYPE_TEMPLATE).subType(SUBTYPE_TOOLCALL)
43+
.description("Template (LLM tool-call envelope) — ADR-0011")
44+
.inheritsFrom(com.metaobjects.MetaData.TYPE_METADATA,
45+
com.metaobjects.MetaData.SUBTYPE_BASE)
46+
// Accept any attr child (vendor providers add their own).
47+
.optionalChild(com.metaobjects.attr.MetaAttribute.TYPE_ATTR, "*", "*");
48+
49+
// Required + optional toolcall-specific attrs. Mirrors the TS
50+
// toolcallAttrs list in template-schema.ts.
51+
def.requiredAttributeWithConstraints(ATTR_TOOL_NAME)
52+
.ofType(StringAttribute.SUBTYPE_STRING).asSingle();
53+
def.requiredAttributeWithConstraints(ATTR_PAYLOAD_REF)
54+
.ofType(StringAttribute.SUBTYPE_STRING).asSingle();
55+
def.optionalAttributeWithConstraints(ATTR_OWNER)
56+
.ofType(StringAttribute.SUBTYPE_STRING).asSingle();
57+
def.optionalAttributeWithConstraints(ATTR_SINCE)
58+
.ofType(StringAttribute.SUBTYPE_STRING).asSingle();
59+
});
60+
}
61+
62+
/** Returns the raw value of {@code @toolName}, or {@code null} if absent. */
63+
public String getToolName() {
64+
return hasMetaAttr(ATTR_TOOL_NAME, false)
65+
? getMetaAttr(ATTR_TOOL_NAME, false).getValueAsString()
66+
: null;
67+
}
68+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package com.metaobjects.template;
2+
3+
import com.metaobjects.ErrorCode;
4+
import com.metaobjects.MetaData;
5+
import com.metaobjects.MetaDataException;
6+
import com.metaobjects.loader.InMemoryStringSource;
7+
import com.metaobjects.loader.MetaDataLoader;
8+
import com.metaobjects.registry.MetaDataRegistry;
9+
import com.metaobjects.registry.SharedRegistryTestBase;
10+
import com.metaobjects.registry.TypeDefinition;
11+
import org.junit.Test;
12+
13+
import java.util.Collections;
14+
import java.util.List;
15+
16+
import static org.junit.Assert.*;
17+
18+
/**
19+
* Unit tests for {@code template.toolcall} — the third core template subtype
20+
* shipped in {@code 0.7.0-rc.7} per ADR-0011, mirroring the TS port's rc.5
21+
* delivery.
22+
*
23+
* <p>Toolcall is a sibling of {@link PromptTemplate} and {@link OutputTemplate}
24+
* but does NOT inherit the generic {@code @payloadRef} / {@code @textRef} /
25+
* {@code @format} attrs — it declares its own minimal set ({@code @toolName}
26+
* required + {@code @payloadRef} required + governance {@code @owner} /
27+
* {@code @since}). No {@code @textRef} requirement because a tool-call has no
28+
* renderable text body; the body IS the structured output schema resolved via
29+
* {@code @payloadRef}.
30+
*
31+
* <p>Vendor wire details (Anthropic's retry-with-reminder, OpenAI's function-
32+
* calling envelope, MCP's tool definitions, etc.) are NOT in core. Consumers
33+
* add vendor specifics via a project-local provider that registers additional
34+
* attrs on this subtype.
35+
*/
36+
public class TemplateToolcallTest extends SharedRegistryTestBase {
37+
38+
// ---- registry shape ----------------------------------------------------
39+
40+
@Test
41+
public void coreProviderRegistersTemplateToolcallSubtype() {
42+
MetaDataRegistry registry = MetaDataRegistry.getInstance();
43+
TypeDefinition def = registry.getTypeDefinition(
44+
TemplateConstants.TYPE_TEMPLATE, TemplateConstants.SUBTYPE_TOOLCALL);
45+
assertNotNull("template.toolcall should be registered by the template-types provider", def);
46+
assertEquals(ToolcallTemplate.class, def.getImplementationClass());
47+
}
48+
49+
@Test
50+
public void templateSubtypeToolcallConstantMatches() {
51+
assertEquals("toolcall", TemplateConstants.SUBTYPE_TOOLCALL);
52+
assertEquals("toolName", TemplateConstants.ATTR_TOOL_NAME);
53+
}
54+
55+
// ---- loader: positive + negative paths --------------------------------
56+
57+
private static final String TOOLCALL_OK =
58+
"{ \"metadata.root\": { \"package\": \"acme::tools\", \"children\": [" +
59+
" { \"object.value\": { \"name\": \"WeatherToolOutput\", \"children\": [" +
60+
" { \"field.string\": { \"name\": \"summary\" } }," +
61+
" { \"field.int\": { \"name\": \"tempCelsius\" } }" +
62+
" ] } }," +
63+
" { \"template.toolcall\": {" +
64+
" \"name\": \"lookupWeather\"," +
65+
" \"@toolName\": \"lookup_weather\"," +
66+
" \"@payloadRef\": \"WeatherToolOutput\"," +
67+
" \"@owner\": \"tools-team\"," +
68+
" \"@since\": \"0.7.0\" } }" +
69+
"] } }";
70+
71+
private static final String TOOLCALL_MISSING_TOOL_NAME =
72+
"{ \"metadata.root\": { \"package\": \"acme::tools\", \"children\": [" +
73+
" { \"object.value\": { \"name\": \"Out\", \"children\": [" +
74+
" { \"field.string\": { \"name\": \"x\" } }" +
75+
" ] } }," +
76+
" { \"template.toolcall\": {" +
77+
" \"name\": \"missingToolName\"," +
78+
" \"@payloadRef\": \"Out\" } }" +
79+
"] } }";
80+
81+
private static final String TOOLCALL_MISSING_PAYLOAD_REF =
82+
"{ \"metadata.root\": { \"package\": \"acme::tools\", \"children\": [" +
83+
" { \"template.toolcall\": {" +
84+
" \"name\": \"missingPayloadRef\"," +
85+
" \"@toolName\": \"do_thing\" } }" +
86+
"] } }";
87+
88+
private static final String TOOLCALL_BAD_PAYLOAD_REF =
89+
"{ \"metadata.root\": { \"package\": \"acme::tools\", \"children\": [" +
90+
" { \"template.toolcall\": {" +
91+
" \"name\": \"danglingPayload\"," +
92+
" \"@toolName\": \"do_thing\"," +
93+
" \"@payloadRef\": \"DoesNotExist\" } }" +
94+
"] } }";
95+
96+
private MetaDataLoader newTestLoader() {
97+
return createTestLoader("TemplateToolcallTest", Collections.emptyList());
98+
}
99+
100+
@Test
101+
public void toolcallLoadsWithRequiredAttrs() {
102+
MetaDataLoader loader = newTestLoader();
103+
loader.load(List.of(new InMemoryStringSource(TOOLCALL_OK, "toolcall-ok.json")));
104+
105+
MetaData node = loader.getRoot().getChildOfType(
106+
TemplateConstants.TYPE_TEMPLATE, "acme::tools::lookupWeather");
107+
assertNotNull("template.toolcall node should load", node);
108+
assertEquals(TemplateConstants.SUBTYPE_TOOLCALL, node.getSubType());
109+
assertTrue("expected ToolcallTemplate instance", node instanceof ToolcallTemplate);
110+
111+
ToolcallTemplate tc = (ToolcallTemplate) node;
112+
assertEquals("lookup_weather", tc.getToolName());
113+
assertEquals("WeatherToolOutput", tc.getPayloadRef());
114+
assertEquals("tools-team", tc.getOwner());
115+
assertEquals("0.7.0", tc.getSince());
116+
}
117+
118+
@Test
119+
public void toolcallMissingToolNameRaisesMissingRequired() {
120+
try {
121+
newTestLoader().load(List.of(new InMemoryStringSource(
122+
TOOLCALL_MISSING_TOOL_NAME, "toolcall-missing-tool-name.json")));
123+
fail("expected MetaDataException for missing @toolName");
124+
} catch (MetaDataException ex) {
125+
assertEquals(ErrorCode.ERR_MISSING_REQUIRED_ATTR, ex.getCode().orElse(null));
126+
assertTrue("expected message to mention @toolName: " + ex.getMessage(),
127+
ex.getMessage().contains("@toolName"));
128+
}
129+
}
130+
131+
@Test
132+
public void toolcallMissingPayloadRefRaisesMissingRequired() {
133+
try {
134+
newTestLoader().load(List.of(new InMemoryStringSource(
135+
TOOLCALL_MISSING_PAYLOAD_REF, "toolcall-missing-payload-ref.json")));
136+
fail("expected MetaDataException for missing @payloadRef");
137+
} catch (MetaDataException ex) {
138+
assertEquals(ErrorCode.ERR_MISSING_REQUIRED_ATTR, ex.getCode().orElse(null));
139+
assertTrue("expected message to mention @payloadRef: " + ex.getMessage(),
140+
ex.getMessage().contains("@payloadRef"));
141+
}
142+
}
143+
144+
@Test
145+
public void toolcallBadPayloadRefRaisesInvalidTemplate() {
146+
// The shared @payloadRef must-resolve-to-root-level-object.value rule
147+
// applies to toolcall the same as prompt + output.
148+
try {
149+
newTestLoader().load(List.of(new InMemoryStringSource(
150+
TOOLCALL_BAD_PAYLOAD_REF, "toolcall-bad-payload-ref.json")));
151+
fail("expected MetaDataException for dangling @payloadRef");
152+
} catch (MetaDataException ex) {
153+
assertEquals(ErrorCode.ERR_INVALID_TEMPLATE, ex.getCode().orElse(null));
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)